diff options
1355 files changed, 62423 insertions, 26651 deletions
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index 9a4323a119f0..bded26a8748f 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -27,4 +27,4 @@ hidden_api_txt_exclude_hook = ${REPO_ROOT}/frameworks/base/tools/hiddenapi/exclu ktfmt_hook = ${REPO_ROOT}/external/ktfmt/ktfmt.py --check -i ${REPO_ROOT}/frameworks/base/packages/SystemUI/ktfmt_includes.txt ${PREUPLOAD_FILES} -ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES} +ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES} diff --git a/cmds/idmap2/idmap2d/Idmap2Service.cpp b/cmds/idmap2/idmap2d/Idmap2Service.cpp index 1b2d905aec0a..e2638106994c 100644 --- a/cmds/idmap2/idmap2d/Idmap2Service.cpp +++ b/cmds/idmap2/idmap2d/Idmap2Service.cpp @@ -23,6 +23,7 @@ #include <cstring> #include <filesystem> #include <fstream> +#include <limits> #include <memory> #include <ostream> #include <string> @@ -295,26 +296,42 @@ Status Idmap2Service::createFabricatedOverlay( return ok(); } -Status Idmap2Service::acquireFabricatedOverlayIterator() { +Status Idmap2Service::acquireFabricatedOverlayIterator(int32_t* _aidl_return) { + std::lock_guard l(frro_iter_mutex_); if (frro_iter_.has_value()) { LOG(WARNING) << "active ffro iterator was not previously released"; } frro_iter_ = std::filesystem::directory_iterator(kIdmapCacheDir); + if (frro_iter_id_ == std::numeric_limits<int32_t>::max()) { + frro_iter_id_ = 0; + } else { + ++frro_iter_id_; + } + *_aidl_return = frro_iter_id_; return ok(); } -Status Idmap2Service::releaseFabricatedOverlayIterator() { +Status Idmap2Service::releaseFabricatedOverlayIterator(int32_t iteratorId) { + std::lock_guard l(frro_iter_mutex_); if (!frro_iter_.has_value()) { LOG(WARNING) << "no active ffro iterator to release"; + } else if (frro_iter_id_ != iteratorId) { + LOG(WARNING) << "incorrect iterator id in a call to release"; + } else { + frro_iter_.reset(); } return ok(); } -Status Idmap2Service::nextFabricatedOverlayInfos( +Status Idmap2Service::nextFabricatedOverlayInfos(int32_t iteratorId, std::vector<os::FabricatedOverlayInfo>* _aidl_return) { + std::lock_guard l(frro_iter_mutex_); + constexpr size_t kMaxEntryCount = 100; if (!frro_iter_.has_value()) { return error("no active frro iterator"); + } else if (frro_iter_id_ != iteratorId) { + return error("incorrect iterator id in a call to next"); } size_t count = 0; @@ -322,22 +339,22 @@ Status Idmap2Service::nextFabricatedOverlayInfos( auto entry_iter_end = end(*frro_iter_); for (; entry_iter != entry_iter_end && count < kMaxEntryCount; ++entry_iter) { auto& entry = *entry_iter; - if (!entry.is_regular_file() || !android::IsFabricatedOverlay(entry.path())) { + if (!entry.is_regular_file() || !android::IsFabricatedOverlay(entry.path().native())) { continue; } - const auto overlay = FabricatedOverlayContainer::FromPath(entry.path()); + const auto overlay = FabricatedOverlayContainer::FromPath(entry.path().native()); if (!overlay) { LOG(WARNING) << "Failed to open '" << entry.path() << "': " << overlay.GetErrorMessage(); continue; } - const auto info = (*overlay)->GetManifestInfo(); + auto info = (*overlay)->GetManifestInfo(); os::FabricatedOverlayInfo out_info; - out_info.packageName = info.package_name; - out_info.overlayName = info.name; - out_info.targetPackageName = info.target_package; - out_info.targetOverlayable = info.target_name; + out_info.packageName = std::move(info.package_name); + out_info.overlayName = std::move(info.name); + out_info.targetPackageName = std::move(info.target_package); + out_info.targetOverlayable = std::move(info.target_name); out_info.path = entry.path(); _aidl_return->emplace_back(std::move(out_info)); count++; diff --git a/cmds/idmap2/idmap2d/Idmap2Service.h b/cmds/idmap2/idmap2d/Idmap2Service.h index c61e4bc98a54..cc8cc5f218d2 100644 --- a/cmds/idmap2/idmap2d/Idmap2Service.h +++ b/cmds/idmap2/idmap2d/Idmap2Service.h @@ -26,7 +26,10 @@ #include <filesystem> #include <memory> +#include <mutex> +#include <optional> #include <string> +#include <variant> #include <vector> namespace android::os { @@ -60,11 +63,11 @@ class Idmap2Service : public BinderService<Idmap2Service>, public BnIdmap2 { binder::Status deleteFabricatedOverlay(const std::string& overlay_path, bool* _aidl_return) override; - binder::Status acquireFabricatedOverlayIterator() override; + binder::Status acquireFabricatedOverlayIterator(int32_t* _aidl_return) override; - binder::Status releaseFabricatedOverlayIterator() override; + binder::Status releaseFabricatedOverlayIterator(int32_t iteratorId) override; - binder::Status nextFabricatedOverlayInfos( + binder::Status nextFabricatedOverlayInfos(int32_t iteratorId, std::vector<os::FabricatedOverlayInfo>* _aidl_return) override; binder::Status dumpIdmap(const std::string& overlay_path, std::string* _aidl_return) override; @@ -74,7 +77,9 @@ class Idmap2Service : public BinderService<Idmap2Service>, public BnIdmap2 { // be able to be recalculated if idmap2 dies and restarts. std::unique_ptr<idmap2::TargetResourceContainer> framework_apk_cache_; + int32_t frro_iter_id_ = 0; std::optional<std::filesystem::directory_iterator> frro_iter_; + std::mutex frro_iter_mutex_; template <typename T> using MaybeUniquePtr = std::variant<std::unique_ptr<T>, T*>; diff --git a/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl b/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl index 0059cf293177..2bbfba97a6c6 100644 --- a/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl +++ b/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl @@ -41,9 +41,9 @@ interface IIdmap2 { @nullable FabricatedOverlayInfo createFabricatedOverlay(in FabricatedOverlayInternal overlay); boolean deleteFabricatedOverlay(@utf8InCpp String path); - void acquireFabricatedOverlayIterator(); - void releaseFabricatedOverlayIterator(); - List<FabricatedOverlayInfo> nextFabricatedOverlayInfos(); + int acquireFabricatedOverlayIterator(); + void releaseFabricatedOverlayIterator(int iteratorId); + List<FabricatedOverlayInfo> nextFabricatedOverlayInfos(int iteratorId); @utf8InCpp String dumpIdmap(@utf8InCpp String overlayApkPath); } diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java index 7e814af3451d..1403ba2744b3 100644 --- a/core/java/android/animation/AnimationHandler.java +++ b/core/java/android/animation/AnimationHandler.java @@ -432,8 +432,9 @@ public class AnimationHandler { /** * Callbacks that receives notifications for animation timing and frame commit timing. + * @hide */ - interface AnimationFrameCallback { + public interface AnimationFrameCallback { /** * Run animation based on the frame time. * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 7c1f9c80eec1..855366abae4e 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -8045,8 +8045,9 @@ public class Activity extends ContextThemeWrapper resultData.prepareToLeaveProcess(this); } upIntent.prepareToLeaveProcess(this); - return ActivityClient.getInstance().navigateUpTo(mToken, upIntent, resultCode, - resultData); + String resolvedType = upIntent.resolveTypeIfNeeded(getContentResolver()); + return ActivityClient.getInstance().navigateUpTo(mToken, upIntent, resolvedType, + resultCode, resultData); } else { return mParent.navigateUpToFromChild(this, upIntent); } diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java index 482f456b5d83..d1e6780e3618 100644 --- a/core/java/android/app/ActivityClient.java +++ b/core/java/android/app/ActivityClient.java @@ -141,11 +141,11 @@ public class ActivityClient { } } - boolean navigateUpTo(IBinder token, Intent destIntent, int resultCode, + boolean navigateUpTo(IBinder token, Intent destIntent, String resolvedType, int resultCode, Intent resultData) { try { - return getActivityClientController().navigateUpTo(token, destIntent, resultCode, - resultData); + return getActivityClientController().navigateUpTo(token, destIntent, resolvedType, + resultCode, resultData); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index e3ea1238181f..e00329385ca5 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -266,6 +266,12 @@ public class ActivityOptions extends ComponentOptions { private static final String KEY_LAUNCH_TASK_ID = "android.activity.launchTaskId"; /** + * See {@link #setDisableStartingWindow}. + * @hide + */ + private static final String KEY_DISABLE_STARTING_WINDOW = "android.activity.disableStarting"; + + /** * See {@link #setPendingIntentLaunchFlags(int)} * @hide */ @@ -477,6 +483,7 @@ public class ActivityOptions extends ComponentOptions { private PictureInPictureParams mLaunchIntoPipParams; private boolean mDismissKeyguard; private boolean mIgnorePendingIntentCreatorForegroundState; + private boolean mDisableStartingWindow; /** * Create an ActivityOptions specifying a custom animation to run when @@ -1284,6 +1291,7 @@ public class ActivityOptions extends ComponentOptions { mDismissKeyguard = opts.getBoolean(KEY_DISMISS_KEYGUARD); mIgnorePendingIntentCreatorForegroundState = opts.getBoolean( KEY_IGNORE_PENDING_INTENT_CREATOR_FOREGROUND_STATE); + mDisableStartingWindow = opts.getBoolean(KEY_DISABLE_STARTING_WINDOW); } /** @@ -1700,6 +1708,22 @@ public class ActivityOptions extends ComponentOptions { } /** + * Sets whether recents disable showing starting window when activity launch. + * @hide + */ + @RequiresPermission(START_TASKS_FROM_RECENTS) + public void setDisableStartingWindow(boolean disable) { + mDisableStartingWindow = disable; + } + + /** + * @hide + */ + public boolean getDisableStartingWindow() { + return mDisableStartingWindow; + } + + /** * Specifies intent flags to be applied for any activity started from a PendingIntent. * * @hide @@ -2210,6 +2234,9 @@ public class ActivityOptions extends ComponentOptions { b.putBoolean(KEY_IGNORE_PENDING_INTENT_CREATOR_FOREGROUND_STATE, mIgnorePendingIntentCreatorForegroundState); } + if (mDisableStartingWindow) { + b.putBoolean(KEY_DISABLE_STARTING_WINDOW, mDisableStartingWindow); + } return b; } diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index f0e14483d98a..aa5fa5b19117 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -528,6 +528,28 @@ public class BroadcastOptions extends ComponentOptions { return mIsAlarmBroadcast; } + /** + * Did this broadcast originate from a push message from the server? + * + * @return true if this broadcast is a push message, false otherwise. + * @hide + */ + public boolean isPushMessagingBroadcast() { + return mTemporaryAppAllowlistReasonCode == PowerExemptionManager.REASON_PUSH_MESSAGING; + } + + /** + * Did this broadcast originate from a push message from the server which was over the allowed + * quota? + * + * @return true if this broadcast is a push message over quota, false otherwise. + * @hide + */ + public boolean isPushMessagingOverQuotaBroadcast() { + return mTemporaryAppAllowlistReasonCode + == PowerExemptionManager.REASON_PUSH_MESSAGING_OVER_QUOTA; + } + /** {@hide} */ public long getRequireCompatChangeId() { return mRequireCompatChangeId; diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index f5e5cda9c639..9aa67bc51182 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -60,8 +60,8 @@ interface IActivityClientController { in SizeConfigurationBuckets sizeConfigurations); boolean moveActivityTaskToBack(in IBinder token, boolean nonRoot); boolean shouldUpRecreateTask(in IBinder token, in String destAffinity); - boolean navigateUpTo(in IBinder token, in Intent target, int resultCode, - in Intent resultData); + boolean navigateUpTo(in IBinder token, in Intent target, in String resolvedType, + int resultCode, in Intent resultData); boolean releaseActivityInstance(in IBinder token); boolean finishActivity(in IBinder token, int code, in Intent data, int finishTask); boolean finishActivityAffinity(in IBinder token); diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index da6a551175e3..edf96f7b5583 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -159,6 +159,7 @@ interface INotificationManager void clearRequestedListenerHints(in INotificationListener token); void requestHintsFromListener(in INotificationListener token, int hints); int getHintsFromListener(in INotificationListener token); + int getHintsFromListenerNoToken(); void requestInterruptionFilterFromListener(in INotificationListener token, int interruptionFilter); int getInterruptionFilterFromListener(in INotificationListener token); void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim); diff --git a/core/java/android/app/LocaleManager.java b/core/java/android/app/LocaleManager.java index 794c6946f7a8..c5dedb33f954 100644 --- a/core/java/android/app/LocaleManager.java +++ b/core/java/android/app/LocaleManager.java @@ -173,7 +173,7 @@ public class LocaleManager { @TestApi public void setSystemLocales(@NonNull LocaleList locales) { try { - Configuration conf = ActivityManager.getService().getConfiguration(); + Configuration conf = new Configuration(); conf.setLocales(locales); ActivityManager.getService().updatePersistentConfiguration(conf); } catch (RemoteException e) { diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index f320b742a430..8b41aa4a7e7d 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -8468,8 +8468,8 @@ public class Notification implements Parcelable } int maxAvatarSize = resources.getDimensionPixelSize( - isLowRam ? R.dimen.notification_person_icon_max_size - : R.dimen.notification_person_icon_max_size_low_ram); + isLowRam ? R.dimen.notification_person_icon_max_size_low_ram + : R.dimen.notification_person_icon_max_size); if (mUser != null && mUser.getIcon() != null) { mUser.getIcon().scaleDownIfNecessary(maxAvatarSize, maxAvatarSize); } @@ -9591,21 +9591,16 @@ public class Notification implements Parcelable @NonNull public ArrayList<Action> getActionsListWithSystemActions() { // Define the system actions we expect to see - final Action negativeAction = makeNegativeAction(); - final Action answerAction = makeAnswerAction(); - // Sort the expected actions into the correct order: - // * If there's no answer action, put the hang up / decline action at the end - // * Otherwise put the answer action at the end, and put the decline action at start. - final Action firstAction = answerAction == null ? null : negativeAction; - final Action lastAction = answerAction == null ? negativeAction : answerAction; + final Action firstAction = makeNegativeAction(); + final Action lastAction = makeAnswerAction(); // Start creating the result list. int nonContextualActionSlotsRemaining = MAX_ACTION_BUTTONS; ArrayList<Action> resultActions = new ArrayList<>(MAX_ACTION_BUTTONS); - if (firstAction != null) { - resultActions.add(firstAction); - --nonContextualActionSlotsRemaining; - } + + // Always have a first action. + resultActions.add(firstAction); + --nonContextualActionSlotsRemaining; // Copy actions into the new list, correcting system actions. if (mBuilder.mActions != null) { @@ -9621,14 +9616,14 @@ public class Notification implements Parcelable --nonContextualActionSlotsRemaining; } // If there's exactly one action slot left, fill it with the lastAction. - if (nonContextualActionSlotsRemaining == 1) { + if (lastAction != null && nonContextualActionSlotsRemaining == 1) { resultActions.add(lastAction); --nonContextualActionSlotsRemaining; } } } // If there are any action slots left, the lastAction still needs to be added. - if (nonContextualActionSlotsRemaining >= 1) { + if (lastAction != null && nonContextualActionSlotsRemaining >= 1) { resultActions.add(lastAction); } return resultActions; diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 392f52a08fb5..f6d27ad08b00 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -66,9 +66,9 @@ import java.util.Objects; /** * Class to notify the user of events that happen. This is how you tell - * the user that something has happened in the background. {@more} + * the user that something has happened in the background. * - * Notifications can take different forms: + * <p>Notifications can take different forms: * <ul> * <li>A persistent icon that goes in the status bar and is accessible * through the launcher, (when the user selects it, a designated Intent diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index e022ca306674..0ea53ce52a52 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -2080,6 +2080,21 @@ public class WallpaperManager { } /** + * Set the live wallpaper for the given screen(s). + * + * This can only be called by packages with android.permission.SET_WALLPAPER_COMPONENT + * permission. The caller must hold the INTERACT_ACROSS_USERS_FULL permission to change + * another user's wallpaper. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT) + public boolean setWallpaperComponentWithFlags(@NonNull ComponentName name, + @SetWallpaperFlags int which) { + return setWallpaperComponent(name); + } + + /** * Set the display position of the current wallpaper within any larger space, when * that wallpaper is visible behind the given window. The X and Y offsets * are floating point numbers ranging from 0 to 1, representing where the diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 34c91c360dbe..8e09939f6805 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -12150,6 +12150,15 @@ public class DevicePolicyManager { * Attempts by the admin to grant these permissions, when the admin is restricted from doing * so, will be silently ignored (no exception will be thrown). * + * Control over the following permissions are restricted for managed profile owners: + * <ul> + * <li>Manifest.permission.READ_SMS</li> + * </ul> + * <p> + * A managed profile owner may not grant these permissions (i.e. call this method with any of + * the permissions listed above and {@code grantState} of + * {@code #PERMISSION_GRANT_STATE_GRANTED}), but may deny them. + * * @param admin Which profile or device owner this request is associated with. * @param packageName The application to grant or revoke a permission to. * @param permission The permission to grant or revoke. diff --git a/core/java/android/app/servertransaction/PendingTransactionActions.java b/core/java/android/app/servertransaction/PendingTransactionActions.java index a47fe821cd01..81747782cab2 100644 --- a/core/java/android/app/servertransaction/PendingTransactionActions.java +++ b/core/java/android/app/servertransaction/PendingTransactionActions.java @@ -25,11 +25,12 @@ import android.os.Bundle; import android.os.PersistableBundle; import android.os.TransactionTooLargeException; import android.util.Log; -import android.util.LogWriter; import android.util.Slog; import com.android.internal.util.IndentingPrintWriter; +import java.io.StringWriter; + /** * Container that has data pending to be used at later stages of * {@link android.app.servertransaction.ClientTransaction}. @@ -134,6 +135,16 @@ public class PendingTransactionActions { mDescription = description; } + private String collectBundleStates() { + final StringWriter writer = new StringWriter(); + final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); + pw.println("Bundle stats:"); + Bundle.dumpStats(pw, mState); + pw.println("PersistableBundle stats:"); + Bundle.dumpStats(pw, mPersistentState); + return writer.toString().stripTrailing(); + } + @Override public void run() { // Tell activity manager we have been stopped. @@ -142,19 +153,24 @@ public class PendingTransactionActions { // TODO(lifecycler): Use interface callback instead of AMS. ActivityClient.getInstance().activityStopped( mActivity.token, mState, mPersistentState, mDescription); - } catch (RuntimeException ex) { - // Dump statistics about bundle to help developers debug - final LogWriter writer = new LogWriter(Log.WARN, TAG); - final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); - pw.println("Bundle stats:"); - Bundle.dumpStats(pw, mState); - pw.println("PersistableBundle stats:"); - Bundle.dumpStats(pw, mPersistentState); - - if (ex.getCause() instanceof TransactionTooLargeException - && mActivity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) { - Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex); - return; + } catch (RuntimeException runtimeException) { + // Collect the statistics about bundle + final String bundleStats = collectBundleStates(); + + RuntimeException ex = runtimeException; + if (ex.getCause() instanceof TransactionTooLargeException) { + // Embed the stats into exception message to help developers debug if the + // transaction size is too large. + final String message = ex.getMessage() + "\n" + bundleStats; + ex = new RuntimeException(message, ex.getCause()); + if (mActivity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) { + Log.e(TAG, "App sent too much data in instance state, so it was ignored", + ex); + return; + } + } else { + // Otherwise, dump the stats anyway. + Log.w(TAG, bundleStats); } throw ex; } diff --git a/core/java/android/app/smartspace/SmartspaceTarget.java b/core/java/android/app/smartspace/SmartspaceTarget.java index 79d7b216628f..3c66a15399d3 100644 --- a/core/java/android/app/smartspace/SmartspaceTarget.java +++ b/core/java/android/app/smartspace/SmartspaceTarget.java @@ -245,6 +245,10 @@ public final class SmartspaceTarget implements Parcelable { public static final int UI_TEMPLATE_COMBINED_CARDS = 6; // Sub-card template whose data is represented by {@link SubCardTemplateData} public static final int UI_TEMPLATE_SUB_CARD = 7; + // Reserved: 8 + // Template type used by non-UI template features for sending logging information in the + // base template data. This should not be used for UI template features. + // public static final int UI_TEMPLATE_LOGGING_ONLY = 8; /** * The types of the Smartspace ui templates. diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index cc303fb1f413..2dced96d3583 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -329,6 +329,22 @@ public class AppWidgetHost { } /** + * Set the visibiity of all widgets associated with this host to hidden + * + * @hide + */ + public void setAppWidgetHidden() { + if (sService == null) { + return; + } + try { + sService.setAppWidgetHidden(mContextOpPackageName, mHostId); + } catch (RemoteException e) { + throw new RuntimeException("System server dead?", e); + } + } + + /** * Set the host's interaction handler. * * @hide @@ -418,14 +434,7 @@ public class AppWidgetHost { AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget); view.setInteractionHandler(mInteractionHandler); view.setAppWidget(appWidgetId, appWidget); - addListener(appWidgetId, view); - RemoteViews views; - try { - views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId); - } catch (RemoteException e) { - throw new RuntimeException("system server dead?", e); - } - view.updateAppWidget(views); + setListener(appWidgetId, view); return view; } @@ -513,13 +522,19 @@ public class AppWidgetHost { * The AppWidgetHost retains a pointer to the newly-created listener. * @param appWidgetId The ID of the app widget for which to add the listener * @param listener The listener interface that deals with actions towards the widget view - * * @hide */ - public void addListener(int appWidgetId, @NonNull AppWidgetHostListener listener) { + public void setListener(int appWidgetId, @NonNull AppWidgetHostListener listener) { synchronized (mListeners) { mListeners.put(appWidgetId, listener); } + RemoteViews views = null; + try { + views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId); + } catch (RemoteException e) { + throw new RuntimeException("system server dead?", e); + } + listener.updateAppWidget(views); } /** diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java index 0f6010fffcb6..2a47851764da 100644 --- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java @@ -272,6 +272,9 @@ public final class CameraExtensionCharacteristics { @Override public void onServiceConnected(ComponentName component, IBinder binder) { mProxy = ICameraExtensionsProxyService.Stub.asInterface(binder); + if (mProxy == null) { + throw new IllegalStateException("Camera Proxy service is null"); + } try { mSupportsAdvancedExtensions = mProxy.advancedExtensionsSupported(); } catch (RemoteException e) { diff --git a/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl b/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl new file mode 100644 index 000000000000..dc5a96684606 --- /dev/null +++ b/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +/** @hide */ +oneway interface IInputDeviceBatteryListener { + + /** + * Called when there is a change in battery state for a monitored device. This will be called + * immediately after the listener is successfully registered for a new device via IInputManager. + * The parameters are values exposed through {@link android.hardware.BatteryState}. + */ + void onBatteryStateChanged(int deviceId, boolean isBatteryPresent, int status, float capacity, + long eventTime); +} diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 2da12e6c5c9d..a645ae4a3964 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -20,6 +20,7 @@ import android.graphics.Rect; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.KeyboardLayout; import android.hardware.input.IInputDevicesChangedListener; +import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.TouchCalibration; import android.os.CombinedVibration; @@ -155,4 +156,8 @@ interface IInputManager { void closeLightSession(int deviceId, in IBinder token); void cancelCurrentTouch(); + + void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener); + + void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener); } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index d17a9523ab37..97812cea23e1 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -30,6 +30,7 @@ import android.app.ActivityThread; import android.compat.annotation.ChangeId; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.hardware.BatteryState; import android.hardware.SensorManager; import android.hardware.lights.Light; import android.hardware.lights.LightState; @@ -66,6 +67,7 @@ import android.view.PointerIcon; import android.view.VerifiedInputEvent; import android.view.WindowManager.LayoutParams; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import com.android.internal.util.ArrayUtils; @@ -74,6 +76,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; /** * Provides information about input devices and available key layouts. @@ -111,6 +115,14 @@ public final class InputManager { private TabletModeChangedListener mTabletModeChangedListener; private List<OnTabletModeChangedListenerDelegate> mOnTabletModeChangedListeners; + private final Object mBatteryListenersLock = new Object(); + // Maps a deviceId whose battery is currently being monitored to an entry containing the + // registered listeners for that device. + @GuardedBy("mBatteryListenersLock") + private SparseArray<RegisteredBatteryListeners> mBatteryListeners; + @GuardedBy("mBatteryListenersLock") + private IInputDeviceBatteryListener mInputDeviceBatteryListener; + private InputDeviceSensorManager mInputDeviceSensorManager; /** * Broadcast Action: Query available keyboard layouts. @@ -1752,6 +1764,129 @@ public final class InputManager { } /** + * Adds a battery listener to be notified about {@link BatteryState} changes for an input + * device. The same listener can be registered for multiple input devices. + * The listener will be notified of the initial battery state of the device after it is + * successfully registered. + * @param deviceId the input device that should be monitored + * @param executor an executor on which the callback will be called + * @param listener the {@link InputDeviceBatteryListener} + * @see #removeInputDeviceBatteryListener(int, InputDeviceBatteryListener) + * @hide + */ + public void addInputDeviceBatteryListener(int deviceId, @NonNull Executor executor, + @NonNull InputDeviceBatteryListener listener) { + Objects.requireNonNull(executor, "executor should not be null"); + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mBatteryListenersLock) { + if (mBatteryListeners == null) { + mBatteryListeners = new SparseArray<>(); + mInputDeviceBatteryListener = new LocalInputDeviceBatteryListener(); + } + RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId); + if (listenersForDevice == null) { + // The deviceId is currently not being monitored for battery changes. + // Start monitoring the device. + listenersForDevice = new RegisteredBatteryListeners(); + mBatteryListeners.put(deviceId, listenersForDevice); + try { + mIm.registerBatteryListener(deviceId, mInputDeviceBatteryListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } else { + // The deviceId is already being monitored for battery changes. + // Ensure that the listener is not already registered. + for (InputDeviceBatteryListenerDelegate delegate : listenersForDevice.mDelegates) { + if (Objects.equals(listener, delegate.mListener)) { + throw new IllegalArgumentException( + "Attempting to register an InputDeviceBatteryListener that has " + + "already been registered for deviceId: " + + deviceId); + } + } + } + final InputDeviceBatteryListenerDelegate delegate = + new InputDeviceBatteryListenerDelegate(listener, executor); + listenersForDevice.mDelegates.add(delegate); + + // Notify the listener immediately if we already have the latest battery state. + if (listenersForDevice.mLatestBatteryState != null) { + delegate.notifyBatteryStateChanged(listenersForDevice.mLatestBatteryState); + } + } + } + + /** + * Removes a previously registered battery listener for an input device. + * @see #addInputDeviceBatteryListener(int, Executor, InputDeviceBatteryListener) + * @hide + */ + public void removeInputDeviceBatteryListener(int deviceId, + @NonNull InputDeviceBatteryListener listener) { + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mBatteryListenersLock) { + if (mBatteryListeners == null) { + return; + } + RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId); + if (listenersForDevice == null) { + // The deviceId is not currently being monitored. + return; + } + final List<InputDeviceBatteryListenerDelegate> delegates = + listenersForDevice.mDelegates; + for (int i = 0; i < delegates.size();) { + if (Objects.equals(listener, delegates.get(i).mListener)) { + delegates.remove(i); + continue; + } + i++; + } + if (!delegates.isEmpty()) { + return; + } + + // There are no more battery listeners for this deviceId. Stop monitoring this device. + mBatteryListeners.remove(deviceId); + try { + mIm.unregisterBatteryListener(deviceId, mInputDeviceBatteryListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + if (mBatteryListeners.size() == 0) { + // There are no more devices being monitored, so the registered + // IInputDeviceBatteryListener will be automatically dropped by the server. + mBatteryListeners = null; + mInputDeviceBatteryListener = null; + } + } + } + + /** + * A callback used to be notified about battery state changes for an input device. The + * {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the + * listener is successfully registered to provide the initial battery state of the device. + * @see InputDevice#getBatteryState() + * @see #addInputDeviceBatteryListener(int, Executor, InputDeviceBatteryListener) + * @see #removeInputDeviceBatteryListener(int, InputDeviceBatteryListener) + * @hide + */ + public interface InputDeviceBatteryListener { + /** + * Called when the battery state of an input device changes. + * @param deviceId the input device for which the battery changed. + * @param eventTimeMillis the time (in ms) when the battery change took place. + * This timestamp is in the {@link SystemClock#uptimeMillis()} time base. + * @param batteryState the new battery state, never null. + */ + void onBatteryStateChanged( + int deviceId, long eventTimeMillis, @NonNull BatteryState batteryState); + } + + /** * Listens for changes in input devices. */ public interface InputDeviceListener { @@ -1861,4 +1996,76 @@ public final class InputManager { } } } + + private static final class LocalBatteryState extends BatteryState { + final int mDeviceId; + final boolean mIsPresent; + final int mStatus; + final float mCapacity; + final long mEventTime; + + LocalBatteryState(int deviceId, boolean isPresent, int status, float capacity, + long eventTime) { + mDeviceId = deviceId; + mIsPresent = isPresent; + mStatus = status; + mCapacity = capacity; + mEventTime = eventTime; + } + + @Override + public boolean isPresent() { + return mIsPresent; + } + + @Override + public int getStatus() { + return mStatus; + } + + @Override + public float getCapacity() { + return mCapacity; + } + } + + private static final class RegisteredBatteryListeners { + final List<InputDeviceBatteryListenerDelegate> mDelegates = new ArrayList<>(); + LocalBatteryState mLatestBatteryState; + } + + private static final class InputDeviceBatteryListenerDelegate { + final InputDeviceBatteryListener mListener; + final Executor mExecutor; + + InputDeviceBatteryListenerDelegate(InputDeviceBatteryListener listener, Executor executor) { + mListener = listener; + mExecutor = executor; + } + + void notifyBatteryStateChanged(LocalBatteryState batteryState) { + mExecutor.execute(() -> + mListener.onBatteryStateChanged(batteryState.mDeviceId, batteryState.mEventTime, + batteryState)); + } + } + + private class LocalInputDeviceBatteryListener extends IInputDeviceBatteryListener.Stub { + @Override + public void onBatteryStateChanged(int deviceId, boolean isBatteryPresent, int status, + float capacity, long eventTime) { + synchronized (mBatteryListenersLock) { + if (mBatteryListeners == null) return; + final RegisteredBatteryListeners entry = mBatteryListeners.get(deviceId); + if (entry == null) return; + + entry.mLatestBatteryState = + new LocalBatteryState( + deviceId, isBatteryPresent, status, capacity, eventTime); + for (InputDeviceBatteryListenerDelegate delegate : entry.mDelegates) { + delegate.notifyBatteryStateChanged(entry.mLatestBatteryState); + } + } + } + } } diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 06c35b5bec5d..3d5c34c38431 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -962,7 +962,7 @@ public abstract class BatteryStats implements Parcelable { * also be bumped. */ static final String[] USER_ACTIVITY_TYPES = { - "other", "button", "touch", "accessibility", "attention" + "other", "button", "touch", "accessibility", "attention", "faceDown", "deviceState" }; public static final int NUM_USER_ACTIVITY_TYPES = USER_ACTIVITY_TYPES.length; diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java index 58f93364f686..26e5e956f5b7 100644 --- a/core/java/android/os/BatteryUsageStats.java +++ b/core/java/android/os/BatteryUsageStats.java @@ -34,6 +34,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.Closeable; +import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -449,6 +450,16 @@ public final class BatteryUsageStats implements Parcelable, Closeable { return proto.getBytes(); } + /** + * Writes contents in a binary protobuffer format, using + * the android.os.BatteryUsageStatsAtomsProto proto. + */ + public void dumpToProto(FileDescriptor fd) { + final ProtoOutputStream proto = new ProtoOutputStream(fd); + writeStatsProto(proto, /* max size */ Integer.MAX_VALUE); + proto.flush(); + } + @NonNull private void writeStatsProto(ProtoOutputStream proto, int maxRawSize) { final BatteryConsumer deviceBatteryConsumer = getAggregateBatteryConsumer( diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 7f9f720ce9c3..fc4bb6a41b86 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -4422,6 +4422,9 @@ public final class Parcel { int type = readInt(); if (isLengthPrefixed(type)) { int objectLength = readInt(); + if (objectLength < 0) { + return null; + } int end = MathUtils.addOrThrow(dataPosition(), objectLength); int valueLength = end - start; setDataPosition(end); diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 13ca2c34b27e..a46868e93ab8 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -345,6 +345,39 @@ public final class PowerManager { public static final int USER_ACTIVITY_EVENT_DEVICE_STATE = 6; /** + * @hide + */ + @IntDef(prefix = { "USER_ACTIVITY_EVENT_" }, value = { + USER_ACTIVITY_EVENT_OTHER, + USER_ACTIVITY_EVENT_BUTTON, + USER_ACTIVITY_EVENT_TOUCH, + USER_ACTIVITY_EVENT_ACCESSIBILITY, + USER_ACTIVITY_EVENT_ATTENTION, + USER_ACTIVITY_EVENT_FACE_DOWN, + USER_ACTIVITY_EVENT_DEVICE_STATE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface UserActivityEvent{} + + /** + * + * Convert the user activity event to a string for debugging purposes. + * @hide + */ + public static String userActivityEventToString(@UserActivityEvent int userActivityEvent) { + switch (userActivityEvent) { + case USER_ACTIVITY_EVENT_OTHER: return "other"; + case USER_ACTIVITY_EVENT_BUTTON: return "button"; + case USER_ACTIVITY_EVENT_TOUCH: return "touch"; + case USER_ACTIVITY_EVENT_ACCESSIBILITY: return "accessibility"; + case USER_ACTIVITY_EVENT_ATTENTION: return "attention"; + case USER_ACTIVITY_EVENT_FACE_DOWN: return "faceDown"; + case USER_ACTIVITY_EVENT_DEVICE_STATE: return "deviceState"; + default: return Integer.toString(userActivityEvent); + } + } + + /** * User activity flag: If already dimmed, extend the dim timeout * but do not brighten. This flag is useful for keeping the screen on * a little longer without causing a visible change such as when diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java index 5ca0da2d3f97..8afd6de235a0 100644 --- a/core/java/android/os/PowerManagerInternal.java +++ b/core/java/android/os/PowerManagerInternal.java @@ -335,4 +335,16 @@ public abstract class PowerManagerInternal { /** Allows power button to intercept a power key button press. */ public abstract boolean interceptPowerKeyDown(KeyEvent event); + + /** + * Internal version of {@link android.os.PowerManager#nap} which allows for napping while the + * device is not awake. + */ + public abstract void nap(long eventTime, boolean allowWake); + + /** + * Returns true if ambient display is suppressed by any app with any token. This method will + * return false if ambient display is not available. + */ + public abstract boolean isAmbientDisplaySuppressed(); } diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java index 2c0be870836a..3bf9ca044141 100644 --- a/core/java/android/preference/SeekBarVolumizer.java +++ b/core/java/android/preference/SeekBarVolumizer.java @@ -115,6 +115,7 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba private final int mMaxStreamVolume; private boolean mAffectedByRingerMode; private boolean mNotificationOrRing; + private final boolean mNotifAliasRing; private final Receiver mReceiver = new Receiver(); private Handler mHandler; @@ -179,6 +180,8 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba if (mNotificationOrRing) { mRingerMode = mAudioManager.getRingerModeInternal(); } + mNotifAliasRing = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_alias_ring_notif_stream_types); mZenMode = mNotificationManager.getZenMode(); if (hasAudioProductStrategies()) { @@ -280,7 +283,15 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba if (zenMuted) { mSeekBar.setProgress(mLastAudibleStreamVolume, true); } else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) { - mSeekBar.setProgress(0, true); + /** + * the first variable above is preserved and the conditions below are made explicit + * so that when user attempts to slide the notification seekbar out of vibrate the + * seekbar doesn't wrongly snap back to 0 when the streams aren't aliased + */ + if (mNotifAliasRing || mStreamType == AudioManager.STREAM_RING + || (mStreamType == AudioManager.STREAM_NOTIFICATION && mMuted)) { + mSeekBar.setProgress(0, true); + } } else if (mMuted) { mSeekBar.setProgress(0, true); } else { @@ -354,6 +365,7 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba // set the time of stop volume if ((mStreamType == AudioManager.STREAM_VOICE_CALL || mStreamType == AudioManager.STREAM_RING + || (!mNotifAliasRing && mStreamType == AudioManager.STREAM_NOTIFICATION) || mStreamType == AudioManager.STREAM_ALARM)) { sStopVolumeTime = java.lang.System.currentTimeMillis(); } @@ -632,8 +644,8 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba } private void updateVolumeSlider(int streamType, int streamValue) { - final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType) - : (streamType == mStreamType); + final boolean streamMatch = mNotifAliasRing && mNotificationOrRing + ? isNotificationOrRing(streamType) : streamType == mStreamType; if (mSeekBar != null && streamMatch && streamValue != -1) { final boolean muted = mAudioManager.isStreamMute(mStreamType) || streamValue == 0; diff --git a/core/java/android/service/dreams/DreamActivity.java b/core/java/android/service/dreams/DreamActivity.java index f6a7c8eb8c4b..a2fa1392b079 100644 --- a/core/java/android/service/dreams/DreamActivity.java +++ b/core/java/android/service/dreams/DreamActivity.java @@ -44,6 +44,8 @@ import android.text.TextUtils; public class DreamActivity extends Activity { static final String EXTRA_CALLBACK = "binder"; static final String EXTRA_DREAM_TITLE = "title"; + @Nullable + private DreamService.DreamActivityCallbacks mCallback; public DreamActivity() {} @@ -57,11 +59,19 @@ public class DreamActivity extends Activity { } final Bundle extras = getIntent().getExtras(); - final DreamService.DreamActivityCallback callback = - (DreamService.DreamActivityCallback) extras.getBinder(EXTRA_CALLBACK); + mCallback = (DreamService.DreamActivityCallbacks) extras.getBinder(EXTRA_CALLBACK); - if (callback != null) { - callback.onActivityCreated(this); + if (mCallback != null) { + mCallback.onActivityCreated(this); } } + + @Override + public void onDestroy() { + if (mCallback != null) { + mCallback.onActivityDestroyed(); + } + + super.onDestroy(); + } } diff --git a/core/java/android/service/dreams/DreamManagerInternal.java b/core/java/android/service/dreams/DreamManagerInternal.java index 6956cd4cae6b..295171ca9bbd 100644 --- a/core/java/android/service/dreams/DreamManagerInternal.java +++ b/core/java/android/service/dreams/DreamManagerInternal.java @@ -29,16 +29,18 @@ public abstract class DreamManagerInternal { * * @param doze If true, starts the doze dream component if one has been configured, * otherwise starts the user-specified dream. + * @param reason The reason to start dreaming, which is logged to help debugging. */ - public abstract void startDream(boolean doze); + public abstract void startDream(boolean doze, String reason); /** * Called by the power manager to stop a dream. * * @param immediate If true, ends the dream summarily, otherwise gives it some time * to perform a proper exit transition. + * @param reason The reason to stop dreaming, which is logged to help debugging. */ - public abstract void stopDream(boolean immediate); + public abstract void stopDream(boolean immediate, String reason); /** * Called by the power manager to determine whether a dream is running. diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java index 432444211bce..aa45c20a8e13 100644 --- a/core/java/android/service/dreams/DreamOverlayService.java +++ b/core/java/android/service/dreams/DreamOverlayService.java @@ -42,8 +42,11 @@ public abstract class DreamOverlayService extends Service { private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() { @Override public void startDream(WindowManager.LayoutParams layoutParams, - IDreamOverlayCallback callback) { + IDreamOverlayCallback callback, String dreamComponent, + boolean shouldShowComplications) { mDreamOverlayCallback = callback; + mDreamComponent = ComponentName.unflattenFromString(dreamComponent); + mShowComplications = shouldShowComplications; onStartDream(layoutParams); } }; @@ -56,10 +59,6 @@ public abstract class DreamOverlayService extends Service { @Nullable @Override public final IBinder onBind(@NonNull Intent intent) { - mShowComplications = intent.getBooleanExtra(DreamService.EXTRA_SHOW_COMPLICATIONS, - DreamService.DEFAULT_SHOW_COMPLICATIONS); - mDreamComponent = intent.getParcelableExtra(DreamService.EXTRA_DREAM_COMPONENT, - ComponentName.class); return mDreamOverlay.asBinder(); } diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index d066ee773006..32bdf7962273 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -31,9 +31,9 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; +import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; @@ -68,6 +68,8 @@ import android.view.accessibility.AccessibilityEvent; import com.android.internal.R; import com.android.internal.util.DumpUtils; +import com.android.internal.util.ObservableServiceConnection; +import com.android.internal.util.PersistentServiceConnection; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -75,7 +77,8 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; -import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -211,20 +214,8 @@ public class DreamService extends Service implements Window.Callback { private static final String DREAM_META_DATA_ROOT_TAG = "dream"; /** - * Extra containing a boolean for whether to show complications on the overlay. - * @hide - */ - public static final String EXTRA_SHOW_COMPLICATIONS = - "android.service.dreams.SHOW_COMPLICATIONS"; - - /** - * Extra containing the component name for the active dream. - * @hide - */ - public static final String EXTRA_DREAM_COMPONENT = "android.service.dreams.DREAM_COMPONENT"; - - /** * The default value for whether to show complications on the overlay. + * * @hide */ public static final boolean DEFAULT_SHOW_COMPLICATIONS = false; @@ -248,80 +239,72 @@ public class DreamService extends Service implements Window.Callback { private boolean mDebug = false; + private ComponentName mDreamComponent; + private boolean mShouldShowComplications; + private DreamServiceWrapper mDreamServiceWrapper; private Runnable mDispatchAfterOnAttachedToWindow; - private final OverlayConnection mOverlayConnection; + private OverlayConnection mOverlayConnection; - private static class OverlayConnection implements ServiceConnection { + private static class OverlayConnection extends PersistentServiceConnection<IDreamOverlay> { // Overlay set during onBind. private IDreamOverlay mOverlay; - // A Queue of pending requests to execute on the overlay. - private final ArrayDeque<Consumer<IDreamOverlay>> mRequests; - - private boolean mBound; - - OverlayConnection() { - mRequests = new ArrayDeque<>(); - } - - public void bind(Context context, @Nullable ComponentName overlayService, - ComponentName dreamService) { - if (overlayService == null) { - return; + // A list of pending requests to execute on the overlay. + private final ArrayList<Consumer<IDreamOverlay>> mConsumers = new ArrayList<>(); + + private final Callback<IDreamOverlay> mCallback = new Callback<IDreamOverlay>() { + @Override + public void onConnected(ObservableServiceConnection<IDreamOverlay> connection, + IDreamOverlay service) { + mOverlay = service; + for (Consumer<IDreamOverlay> consumer : mConsumers) { + consumer.accept(mOverlay); + } } - final ServiceInfo serviceInfo = fetchServiceInfo(context, dreamService); - - final Intent overlayIntent = new Intent(); - overlayIntent.setComponent(overlayService); - overlayIntent.putExtra(EXTRA_SHOW_COMPLICATIONS, - fetchShouldShowComplications(context, serviceInfo)); - overlayIntent.putExtra(EXTRA_DREAM_COMPONENT, dreamService); - - context.bindService(overlayIntent, - this, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE); - mBound = true; - } - - public void unbind(Context context) { - if (!mBound) { - return; + @Override + public void onDisconnected(ObservableServiceConnection<IDreamOverlay> connection, + int reason) { + mOverlay = null; } + }; - context.unbindService(this); - mBound = false; + OverlayConnection(Context context, + Executor executor, + Handler handler, + ServiceTransformer<IDreamOverlay> transformer, + Intent serviceIntent, + int flags, + int minConnectionDurationMs, + int maxReconnectAttempts, + int baseReconnectDelayMs) { + super(context, executor, handler, transformer, serviceIntent, flags, + minConnectionDurationMs, + maxReconnectAttempts, baseReconnectDelayMs); } - public void request(Consumer<IDreamOverlay> request) { - mRequests.push(request); - evaluate(); + @Override + public boolean bind() { + addCallback(mCallback); + return super.bind(); } - private void evaluate() { - if (mOverlay == null) { - return; - } - - // Any new requests that arrive during this loop will be processed synchronously after - // the loop exits. - while (!mRequests.isEmpty()) { - final Consumer<IDreamOverlay> request = mRequests.pop(); - request.accept(mOverlay); - } + @Override + public void unbind() { + removeCallback(mCallback); + super.unbind(); } - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - // Store Overlay and execute pending requests. - mOverlay = IDreamOverlay.Stub.asInterface(service); - evaluate(); + public void addConsumer(Consumer<IDreamOverlay> consumer) { + mConsumers.add(consumer); + if (mOverlay != null) { + consumer.accept(mOverlay); + } } - @Override - public void onServiceDisconnected(ComponentName name) { - // Clear Overlay binder to prevent further request processing. - mOverlay = null; + public void removeConsumer(Consumer<IDreamOverlay> consumer) { + mConsumers.remove(consumer); } } @@ -336,7 +319,6 @@ public class DreamService extends Service implements Window.Callback { public DreamService() { mDreamManager = IDreamManager.Stub.asInterface(ServiceManager.getService(DREAM_SERVICE)); - mOverlayConnection = new OverlayConnection(); } /** @@ -532,7 +514,7 @@ public class DreamService extends Service implements Window.Callback { return mWindow; } - /** + /** * Inflates a layout resource and set it to be the content view for this Dream. * Behaves similarly to {@link android.app.Activity#setContentView(int)}. * @@ -955,6 +937,11 @@ public class DreamService extends Service implements Window.Callback { @Override public void onCreate() { if (mDebug) Slog.v(mTag, "onCreate()"); + + mDreamComponent = new ComponentName(this, getClass()); + mShouldShowComplications = fetchShouldShowComplications(this /*context*/, + fetchServiceInfo(this /*context*/, mDreamComponent)); + super.onCreate(); } @@ -996,13 +983,26 @@ public class DreamService extends Service implements Window.Callback { public final IBinder onBind(Intent intent) { if (mDebug) Slog.v(mTag, "onBind() intent = " + intent); mDreamServiceWrapper = new DreamServiceWrapper(); + final ComponentName overlayComponent = intent.getParcelableExtra( + EXTRA_DREAM_OVERLAY_COMPONENT, ComponentName.class); // Connect to the overlay service if present. - if (!mWindowless) { - mOverlayConnection.bind( + if (!mWindowless && overlayComponent != null) { + final Resources resources = getResources(); + final Intent overlayIntent = new Intent().setComponent(overlayComponent); + + mOverlayConnection = new OverlayConnection( /* context= */ this, - intent.getParcelableExtra(EXTRA_DREAM_OVERLAY_COMPONENT), - new ComponentName(this, getClass())); + getMainExecutor(), + mHandler, + IDreamOverlay.Stub::asInterface, + overlayIntent, + /* flags= */ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, + resources.getInteger(R.integer.config_minDreamOverlayDurationMs), + resources.getInteger(R.integer.config_dreamOverlayMaxReconnectAttempts), + resources.getInteger(R.integer.config_dreamOverlayReconnectTimeoutMs)); + + mOverlayConnection.bind(); } return mDreamServiceWrapper; @@ -1011,7 +1011,9 @@ public class DreamService extends Service implements Window.Callback { @Override public boolean onUnbind(Intent intent) { // We must unbind from any overlay connection if we are unbound before finishing. - mOverlayConnection.unbind(this); + if (mOverlayConnection != null) { + mOverlayConnection.unbind(); + } return super.onUnbind(intent); } @@ -1040,10 +1042,12 @@ public class DreamService extends Service implements Window.Callback { } mFinished = true; - mOverlayConnection.unbind(this); + if (mOverlayConnection != null) { + mOverlayConnection.unbind(); + } if (mDreamToken == null) { - Slog.w(mTag, "Finish was called before the dream was attached."); + if (mDebug) Slog.v(mTag, "finish() called when not attached."); stopSelf(); return; } @@ -1291,7 +1295,7 @@ public class DreamService extends Service implements Window.Callback { Intent i = new Intent(this, DreamActivity.class); i.setPackage(getApplicationContext().getPackageName()); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallback(mDreamToken)); + i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallbacks(mDreamToken)); final ServiceInfo serviceInfo = fetchServiceInfo(this, new ComponentName(this, getClass())); i.putExtra(DreamActivity.EXTRA_DREAM_TITLE, fetchDreamLabel(this, serviceInfo)); @@ -1337,19 +1341,26 @@ public class DreamService extends Service implements Window.Callback { mWindow.getDecorView().addOnAttachStateChangeListener( new View.OnAttachStateChangeListener() { + private Consumer<IDreamOverlay> mDreamStartOverlayConsumer; + @Override public void onViewAttachedToWindow(View v) { mDispatchAfterOnAttachedToWindow.run(); - // Request the DreamOverlay be told to dream with dream's window parameters - // once the window has been attached. - mOverlayConnection.request(overlay -> { - try { - overlay.startDream(mWindow.getAttributes(), mOverlayCallback); - } catch (RemoteException e) { - Log.e(mTag, "could not send window attributes:" + e); - } - }); + if (mOverlayConnection != null) { + // Request the DreamOverlay be told to dream with dream's window + // parameters once the window has been attached. + mDreamStartOverlayConsumer = overlay -> { + try { + overlay.startDream(mWindow.getAttributes(), mOverlayCallback, + mDreamComponent.flattenToString(), + mShouldShowComplications); + } catch (RemoteException e) { + Log.e(mTag, "could not send window attributes:" + e); + } + }; + mOverlayConnection.addConsumer(mDreamStartOverlayConsumer); + } } @Override @@ -1362,6 +1373,9 @@ public class DreamService extends Service implements Window.Callback { mActivity = null; finish(); } + if (mOverlayConnection != null && mDreamStartOverlayConsumer != null) { + mOverlayConnection.removeConsumer(mDreamStartOverlayConsumer); + } } }); } @@ -1474,10 +1488,10 @@ public class DreamService extends Service implements Window.Callback { } /** @hide */ - final class DreamActivityCallback extends Binder { + final class DreamActivityCallbacks extends Binder { private final IBinder mActivityDreamToken; - DreamActivityCallback(IBinder token) { + DreamActivityCallbacks(IBinder token) { mActivityDreamToken = token; } @@ -1502,6 +1516,12 @@ public class DreamService extends Service implements Window.Callback { mActivity = activity; onWindowCreated(activity.getWindow()); } + + // If DreamActivity is destroyed, wake up from Dream. + void onActivityDestroyed() { + mActivity = null; + onDestroy(); + } } /** diff --git a/core/java/android/service/dreams/IDreamOverlay.aidl b/core/java/android/service/dreams/IDreamOverlay.aidl index 2b6633d93dc5..05ebbfe98c9f 100644 --- a/core/java/android/service/dreams/IDreamOverlay.aidl +++ b/core/java/android/service/dreams/IDreamOverlay.aidl @@ -31,7 +31,11 @@ interface IDreamOverlay { * @param params The {@link LayoutParams} for the associated DreamWindow, including the window token of the Dream Activity. * @param callback The {@link IDreamOverlayCallback} for requesting actions such as exiting the - * dream. + * dream. + * @param dreamComponent The component name of the dream service requesting overlay. + * @param shouldShowComplications Whether the dream overlay should show complications, e.g. clock + * and weather. */ - void startDream(in LayoutParams params, in IDreamOverlayCallback callback); + void startDream(in LayoutParams params, in IDreamOverlayCallback callback, + in String dreamComponent, in boolean shouldShowComplications); } diff --git a/core/java/android/service/dreams/Sandman.java b/core/java/android/service/dreams/Sandman.java index fae72a2e91a8..ced2a01cb1d0 100644 --- a/core/java/android/service/dreams/Sandman.java +++ b/core/java/android/service/dreams/Sandman.java @@ -20,13 +20,13 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.PowerManager; +import android.os.RemoteException; +import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.util.Slog; -import com.android.server.LocalServices; - /** * Internal helper for launching dreams to ensure consistency between the * <code>UiModeManagerService</code> system service and the <code>Somnambulator</code> activity. @@ -75,28 +75,32 @@ public final class Sandman { } private static void startDream(Context context, boolean docked) { - DreamManagerInternal dreamManagerService = - LocalServices.getService(DreamManagerInternal.class); - if (dreamManagerService != null && !dreamManagerService.isDreaming()) { - if (docked) { - Slog.i(TAG, "Activating dream while docked."); + try { + IDreamManager dreamManagerService = IDreamManager.Stub.asInterface( + ServiceManager.getService(DreamService.DREAM_SERVICE)); + if (dreamManagerService != null && !dreamManagerService.isDreaming()) { + if (docked) { + Slog.i(TAG, "Activating dream while docked."); - // Wake up. - // The power manager will wake up the system automatically when it starts - // receiving power from a dock but there is a race between that happening - // and the UI mode manager starting a dream. We want the system to already - // be awake by the time this happens. Otherwise the dream may not start. - PowerManager powerManager = - context.getSystemService(PowerManager.class); - powerManager.wakeUp(SystemClock.uptimeMillis(), - PowerManager.WAKE_REASON_PLUGGED_IN, - "android.service.dreams:DREAM"); - } else { - Slog.i(TAG, "Activating dream by user request."); - } + // Wake up. + // The power manager will wake up the system automatically when it starts + // receiving power from a dock but there is a race between that happening + // and the UI mode manager starting a dream. We want the system to already + // be awake by the time this happens. Otherwise the dream may not start. + PowerManager powerManager = + context.getSystemService(PowerManager.class); + powerManager.wakeUp(SystemClock.uptimeMillis(), + PowerManager.WAKE_REASON_PLUGGED_IN, + "android.service.dreams:DREAM"); + } else { + Slog.i(TAG, "Activating dream by user request."); + } - // Dream. - dreamManagerService.requestDream(); + // Dream. + dreamManagerService.dream(); + } + } catch (RemoteException ex) { + Slog.e(TAG, "Could not start dream when docked.", ex); } } diff --git a/core/java/android/service/notification/NotificationStats.java b/core/java/android/service/notification/NotificationStats.java index 206e4fa4fb11..e5ad85cb526f 100644 --- a/core/java/android/service/notification/NotificationStats.java +++ b/core/java/android/service/notification/NotificationStats.java @@ -42,7 +42,8 @@ public final class NotificationStats implements Parcelable { /** @hide */ @IntDef(prefix = { "DISMISSAL_SURFACE_" }, value = { - DISMISSAL_NOT_DISMISSED, DISMISSAL_OTHER, DISMISSAL_PEEK, DISMISSAL_AOD, DISMISSAL_SHADE + DISMISSAL_NOT_DISMISSED, DISMISSAL_OTHER, DISMISSAL_PEEK, DISMISSAL_AOD, + DISMISSAL_SHADE, DISMISSAL_BUBBLE, DISMISSAL_LOCKSCREEN }) @Retention(RetentionPolicy.SOURCE) public @interface DismissalSurface {} @@ -75,7 +76,12 @@ public final class NotificationStats implements Parcelable { * Notification has been dismissed as a bubble. * @hide */ - public static final int DISMISSAL_BUBBLE = 3; + public static final int DISMISSAL_BUBBLE = 4; + /** + * Notification has been dismissed from the lock screen. + * @hide + */ + public static final int DISMISSAL_LOCKSCREEN = 5; /** @hide */ @IntDef(prefix = { "DISMISS_SENTIMENT_" }, value = { diff --git a/core/java/android/service/wallpaper/IWallpaperService.aidl b/core/java/android/service/wallpaper/IWallpaperService.aidl index 56e2486dd626..f46c60fc4f7a 100644 --- a/core/java/android/service/wallpaper/IWallpaperService.aidl +++ b/core/java/android/service/wallpaper/IWallpaperService.aidl @@ -25,6 +25,6 @@ import android.service.wallpaper.IWallpaperConnection; oneway interface IWallpaperService { void attach(IWallpaperConnection connection, IBinder windowToken, int windowType, boolean isPreview, - int reqWidth, int reqHeight, in Rect padding, int displayId); + int reqWidth, int reqHeight, in Rect padding, int displayId, int which); void detach(); } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 01749c0bebb9..e5792a9ef4e2 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -18,6 +18,7 @@ package android.service.wallpaper; import static android.app.WallpaperManager.COMMAND_FREEZE; import static android.app.WallpaperManager.COMMAND_UNFREEZE; +import static android.app.WallpaperManager.SetWallpaperFlags; import static android.graphics.Matrix.MSCALE_X; import static android.graphics.Matrix.MSCALE_Y; import static android.graphics.Matrix.MSKEW_X; @@ -2427,7 +2428,7 @@ public abstract class WallpaperService extends Service { @Override public void attach(IWallpaperConnection conn, IBinder windowToken, int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding, - int displayId) { + int displayId, @SetWallpaperFlags int which) { mEngineWrapper = new IWallpaperEngineWrapper(mTarget, conn, windowToken, windowType, isPreview, reqWidth, reqHeight, padding, displayId); } diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 7d5603994efa..71644120cb43 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -1022,6 +1022,15 @@ public final class InputDevice implements Parcelable { } /** + * Reports whether the device has a battery. + * @return true if the device has a battery, false otherwise. + * @hide + */ + public boolean hasBattery() { + return mHasBattery; + } + + /** * Provides information about the range of values for a particular {@link MotionEvent} axis. * * @see InputDevice#getMotionRange(int) diff --git a/core/java/android/view/RemoteAnimationTarget.java b/core/java/android/view/RemoteAnimationTarget.java index 44f419a8097d..e407707231ca 100644 --- a/core/java/android/view/RemoteAnimationTarget.java +++ b/core/java/android/view/RemoteAnimationTarget.java @@ -241,6 +241,8 @@ public class RemoteAnimationTarget implements Parcelable { */ public boolean willShowImeOnTarget; + public int rotationChange; + public RemoteAnimationTarget(int taskId, int mode, SurfaceControl leash, boolean isTranslucent, Rect clipRect, Rect contentInsets, int prefixOrderIndex, Point position, Rect localBounds, Rect screenSpaceBounds, @@ -302,6 +304,7 @@ public class RemoteAnimationTarget implements Parcelable { backgroundColor = in.readInt(); showBackdrop = in.readBoolean(); willShowImeOnTarget = in.readBoolean(); + rotationChange = in.readInt(); } public void setShowBackdrop(boolean shouldShowBackdrop) { @@ -316,6 +319,14 @@ public class RemoteAnimationTarget implements Parcelable { return willShowImeOnTarget; } + public void setRotationChange(int rotationChange) { + this.rotationChange = rotationChange; + } + + public int getRotationChange() { + return rotationChange; + } + @Override public int describeContents() { return 0; @@ -345,6 +356,7 @@ public class RemoteAnimationTarget implements Parcelable { dest.writeInt(backgroundColor); dest.writeBoolean(showBackdrop); dest.writeBoolean(willShowImeOnTarget); + dest.writeInt(rotationChange); } public void dump(PrintWriter pw, String prefix) { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 3c33358bf192..e9f2ddff340e 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -284,7 +284,7 @@ public final class ViewRootImpl implements ViewParent, * @hide */ public static final boolean LOCAL_LAYOUT = - SystemProperties.getBoolean("persist.debug.local_layout", false); + SystemProperties.getBoolean("persist.debug.local_layout", true); /** * Set this system property to true to force the view hierarchy to render @@ -1099,6 +1099,10 @@ public final class ViewRootImpl implements ViewParent, mInputQueueCallback.onInputQueueCreated(mInputQueue); } } + + // Update the last resource config in case the resource configuration was changed while + // activity relaunched. + updateLastConfigurationFromResources(getConfiguration()); } private Configuration getConfiguration() { @@ -5394,13 +5398,7 @@ public final class ViewRootImpl implements ViewParent, // Update the display with new DisplayAdjustments. updateInternalDisplay(mDisplay.getDisplayId(), localResources); - final int lastLayoutDirection = mLastConfigurationFromResources.getLayoutDirection(); - final int currentLayoutDirection = config.getLayoutDirection(); - mLastConfigurationFromResources.setTo(config); - if (lastLayoutDirection != currentLayoutDirection - && mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) { - mView.setLayoutDirection(currentLayoutDirection); - } + updateLastConfigurationFromResources(config); mView.dispatchConfigurationChanged(config); // We could have gotten this {@link Configuration} update after we called @@ -5414,6 +5412,17 @@ public final class ViewRootImpl implements ViewParent, updateForceDarkMode(); } + private void updateLastConfigurationFromResources(Configuration resConfig) { + final int lastLayoutDirection = mLastConfigurationFromResources.getLayoutDirection(); + final int currentLayoutDirection = resConfig.getLayoutDirection(); + mLastConfigurationFromResources.setTo(resConfig); + // Update layout direction in case the language or screen layout is changed. + if (lastLayoutDirection != currentLayoutDirection && mView != null + && mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) { + mView.setLayoutDirection(currentLayoutDirection); + } + } + /** * Return true if child is an ancestor of parent, (or equal to the parent). */ @@ -8156,6 +8165,7 @@ public final class ViewRootImpl implements ViewParent, mLastSyncSeqId, mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets, mTempControls, mRelayoutBundle); mRelayoutRequested = true; + final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid"); if (maybeSyncSeqId > 0) { mSyncSeqId = maybeSyncSeqId; @@ -8195,6 +8205,12 @@ public final class ViewRootImpl implements ViewParent, } } + if (mSurfaceControl.isValid() && !HardwareRenderer.isDrawingEnabled()) { + // When drawing is disabled the window layer won't have a valid buffer. + // Set a window crop so input can get delivered to the window. + mTransaction.setWindowCrop(mSurfaceControl, mSurfaceSize.x, mSurfaceSize.y).apply(); + } + mLastTransformHint = transformHint; mSurfaceControl.setTransformHint(transformHint); @@ -8484,6 +8500,10 @@ public final class ViewRootImpl implements ViewParent, if (mLocalSyncState != LOCAL_SYNC_NONE) { writer.println(innerPrefix + "mLocalSyncState=" + mLocalSyncState); } + writer.println(innerPrefix + "mLastReportedMergedConfiguration=" + + mLastReportedMergedConfiguration); + writer.println(innerPrefix + "mLastConfigurationFromResources=" + + mLastConfigurationFromResources); writer.println(innerPrefix + "mIsAmbientMode=" + mIsAmbientMode); writer.println(innerPrefix + "mUnbufferedInputSource=" + Integer.toHexString(mUnbufferedInputSource)); diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 218ca58dee54..2f779010be0e 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -777,12 +777,6 @@ public interface WindowManager extends ViewManager { int TAKE_SCREENSHOT_FULLSCREEN = 1; /** - * Invoke screenshot flow allowing the user to select a region. - * @hide - */ - int TAKE_SCREENSHOT_SELECTED_REGION = 2; - - /** * Invoke screenshot flow with an image provided by the caller. * @hide */ @@ -794,7 +788,6 @@ public interface WindowManager extends ViewManager { * @hide */ @IntDef({TAKE_SCREENSHOT_FULLSCREEN, - TAKE_SCREENSHOT_SELECTED_REGION, TAKE_SCREENSHOT_PROVIDED_IMAGE}) @interface ScreenshotType {} diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java index 6bf2474beb17..514df59f1989 100644 --- a/core/java/android/view/translation/UiTranslationController.java +++ b/core/java/android/view/translation/UiTranslationController.java @@ -175,10 +175,7 @@ public class UiTranslationController implements Dumpable { */ public void onActivityDestroyed() { synchronized (mLock) { - if (DEBUG) { - Log.i(TAG, - "onActivityDestroyed(): mCurrentState is " + stateToString(mCurrentState)); - } + Log.i(TAG, "onActivityDestroyed(): mCurrentState is " + stateToString(mCurrentState)); if (mCurrentState != STATE_UI_TRANSLATION_FINISHED) { notifyTranslationFinished(/* activityDestroyed= */ true); } diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java new file mode 100644 index 000000000000..dd4385c8f50c --- /dev/null +++ b/core/java/android/window/BackProgressAnimator.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +import android.util.FloatProperty; + +import com.android.internal.dynamicanimation.animation.SpringAnimation; +import com.android.internal.dynamicanimation.animation.SpringForce; + +/** + * An animator that drives the predictive back progress with a spring. + * + * The back gesture's latest touch point and committal state determines the final position of + * the spring. The continuous movement of the spring is used to produce {@link BackEvent}s with + * smoothly transitioning progress values. + * + * @hide + */ +public class BackProgressAnimator { + /** + * A factor to scale the input progress by, so that it works better with the spring. + * We divide the output progress by this value before sending it to apps, so that apps + * always receive progress values in [0, 1]. + */ + private static final float SCALE_FACTOR = 100f; + private final SpringAnimation mSpring; + private ProgressCallback mCallback; + private float mProgress = 0; + private BackEvent mLastBackEvent; + private boolean mStarted = false; + + private void setProgress(float progress) { + mProgress = progress; + } + + private float getProgress() { + return mProgress; + } + + private static final FloatProperty<BackProgressAnimator> PROGRESS_PROP = + new FloatProperty<BackProgressAnimator>("progress") { + @Override + public void setValue(BackProgressAnimator animator, float value) { + animator.setProgress(value); + animator.updateProgressValue(value); + } + + @Override + public Float get(BackProgressAnimator object) { + return object.getProgress(); + } + }; + + + /** A callback to be invoked when there's a progress value update from the animator. */ + public interface ProgressCallback { + /** Called when there's a progress value update. */ + void onProgressUpdate(BackEvent event); + } + + public BackProgressAnimator() { + mSpring = new SpringAnimation(this, PROGRESS_PROP); + mSpring.setSpring(new SpringForce() + .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); + } + + /** + * Sets a new target position for the back progress. + * + * @param event the {@link BackEvent} containing the latest target progress. + */ + public void onBackProgressed(BackEvent event) { + if (!mStarted) { + return; + } + mLastBackEvent = event; + mSpring.animateToFinalPosition(event.getProgress() * SCALE_FACTOR); + } + + /** + * Starts the back progress animation. + * + * @param event the {@link BackEvent} that started the gesture. + * @param callback the back callback to invoke for the gesture. It will receive back progress + * dispatches as the progress animation updates. + */ + public void onBackStarted(BackEvent event, ProgressCallback callback) { + reset(); + mLastBackEvent = event; + mCallback = callback; + mStarted = true; + } + + /** + * Resets the back progress animation. This should be called when back is invoked or cancelled. + */ + public void reset() { + mSpring.animateToFinalPosition(0); + if (mSpring.canSkipToEnd()) { + mSpring.skipToEnd(); + } else { + // Should never happen. + mSpring.cancel(); + } + mStarted = false; + mLastBackEvent = null; + mCallback = null; + mProgress = 0; + } + + private void updateProgressValue(float progress) { + if (mLastBackEvent == null || mCallback == null || !mStarted) { + return; + } + mCallback.onProgressUpdate( + new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(), + progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge(), + mLastBackEvent.getDepartingAnimationTarget())); + } + +} diff --git a/core/java/android/window/IOnBackInvokedCallback.aidl b/core/java/android/window/IOnBackInvokedCallback.aidl index 47796de11dd5..6af8ddda3a62 100644 --- a/core/java/android/window/IOnBackInvokedCallback.aidl +++ b/core/java/android/window/IOnBackInvokedCallback.aidl @@ -28,17 +28,18 @@ import android.window.BackEvent; oneway interface IOnBackInvokedCallback { /** * Called when a back gesture has been started, or back button has been pressed down. - * Wraps {@link OnBackInvokedCallback#onBackStarted()}. + * Wraps {@link OnBackInvokedCallback#onBackStarted(BackEvent)}. + * + * @param backEvent The {@link BackEvent} containing information about the touch or button press. */ - void onBackStarted(); + void onBackStarted(in BackEvent backEvent); /** * Called on back gesture progress. - * Wraps {@link OnBackInvokedCallback#onBackProgressed()}. + * Wraps {@link OnBackInvokedCallback#onBackProgressed(BackEvent)}. * - * @param touchX Absolute X location of the touch point. - * @param touchY Absolute Y location of the touch point. - * @param progress Value between 0 and 1 on how far along the back gesture is. + * @param backEvent The {@link BackEvent} containing information about the latest touch point + * and the progress that the back animation should seek to. */ void onBackProgressed(in BackEvent backEvent); diff --git a/core/java/android/window/IWindowOrganizerController.aidl b/core/java/android/window/IWindowOrganizerController.aidl index 3c7cd0254e78..36eaf4966165 100644 --- a/core/java/android/window/IWindowOrganizerController.aidl +++ b/core/java/android/window/IWindowOrganizerController.aidl @@ -51,16 +51,19 @@ interface IWindowOrganizerController { in IWindowContainerTransactionCallback callback); /** - * Starts a transition. + * Starts a new transition. * @param type The transition type. - * @param transitionToken A token associated with the transition to start. If null, a new - * transition will be created of the provided type. * @param t Operations that are part of the transition. - * @return a token representing the transition. This will just be transitionToken if it was - * non-null. + * @return a token representing the transition. */ - IBinder startTransition(int type, in @nullable IBinder transitionToken, - in @nullable WindowContainerTransaction t); + IBinder startNewTransition(int type, in @nullable WindowContainerTransaction t); + + /** + * Starts the given transition. + * @param transitionToken A token associated with the transition to start. + * @param t Operations that are part of the transition. + */ + oneway void startTransition(IBinder transitionToken, in @nullable WindowContainerTransaction t); /** * Starts a legacy transition. diff --git a/core/java/android/window/OnBackAnimationCallback.java b/core/java/android/window/OnBackAnimationCallback.java index 1a37e57df403..582308436b02 100644 --- a/core/java/android/window/OnBackAnimationCallback.java +++ b/core/java/android/window/OnBackAnimationCallback.java @@ -40,10 +40,12 @@ import android.view.View; * @hide */ public interface OnBackAnimationCallback extends OnBackInvokedCallback { - /** - * Called when a back gesture has been started, or back button has been pressed down. - */ - default void onBackStarted() { } + /** + * Called when a back gesture has been started, or back button has been pressed down. + * + * @param backEvent An {@link BackEvent} object describing the progress event. + */ + default void onBackStarted(@NonNull BackEvent backEvent) {} /** * Called on back gesture progress. diff --git a/core/java/android/window/OnBackInvokedCallback.java b/core/java/android/window/OnBackInvokedCallback.java index 6e2d4f9edbc1..62c41bfb0681 100644 --- a/core/java/android/window/OnBackInvokedCallback.java +++ b/core/java/android/window/OnBackInvokedCallback.java @@ -16,6 +16,7 @@ package android.window; +import android.annotation.NonNull; import android.app.Activity; import android.app.Dialog; import android.view.Window; @@ -41,8 +42,35 @@ import android.view.Window; @SuppressWarnings("deprecation") public interface OnBackInvokedCallback { /** + * Called when a back gesture has been started, or back button has been pressed down. + * + * @param backEvent The {@link BackEvent} containing information about the touch or + * button press. + * + * @hide + */ + default void onBackStarted(@NonNull BackEvent backEvent) {} + + /** + * Called when a back gesture has been progressed. + * + * @param backEvent The {@link BackEvent} containing information about the latest touch point + * and the progress that the back animation should seek to. + * + * @hide + */ + default void onBackProgressed(@NonNull BackEvent backEvent) {} + + /** * Called when a back gesture has been completed and committed, or back button pressed * has been released and committed. */ void onBackInvoked(); + + /** + * Called when a back gesture or button press has been cancelled. + * + * @hide + */ + default void onBackCancelled() {} } diff --git a/core/java/android/window/TaskFragmentParentInfo.java b/core/java/android/window/TaskFragmentParentInfo.java index 64b2638407df..841354a92192 100644 --- a/core/java/android/window/TaskFragmentParentInfo.java +++ b/core/java/android/window/TaskFragmentParentInfo.java @@ -33,19 +33,19 @@ public class TaskFragmentParentInfo implements Parcelable { private final int mDisplayId; - private final boolean mVisibleRequested; + private final boolean mVisible; public TaskFragmentParentInfo(@NonNull Configuration configuration, int displayId, - boolean visibleRequested) { + boolean visible) { mConfiguration.setTo(configuration); mDisplayId = displayId; - mVisibleRequested = visibleRequested; + mVisible = visible; } public TaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { mConfiguration.setTo(info.getConfiguration()); mDisplayId = info.mDisplayId; - mVisibleRequested = info.mVisibleRequested; + mVisible = info.mVisible; } /** The {@link Configuration} of the parent Task */ @@ -62,9 +62,9 @@ public class TaskFragmentParentInfo implements Parcelable { return mDisplayId; } - /** Whether the parent Task is requested to be visible or not */ - public boolean isVisibleRequested() { - return mVisibleRequested; + /** Whether the parent Task is visible or not */ + public boolean isVisible() { + return mVisible; } /** @@ -80,7 +80,7 @@ public class TaskFragmentParentInfo implements Parcelable { return false; } return getWindowingMode() == that.getWindowingMode() && mDisplayId == that.mDisplayId - && mVisibleRequested == that.mVisibleRequested; + && mVisible == that.mVisible; } @WindowConfiguration.WindowingMode @@ -93,7 +93,7 @@ public class TaskFragmentParentInfo implements Parcelable { return TaskFragmentParentInfo.class.getSimpleName() + ":{" + "config=" + mConfiguration + ", displayId=" + mDisplayId - + ", visibleRequested=" + mVisibleRequested + + ", visible=" + mVisible + "}"; } @@ -114,14 +114,14 @@ public class TaskFragmentParentInfo implements Parcelable { final TaskFragmentParentInfo that = (TaskFragmentParentInfo) obj; return mConfiguration.equals(that.mConfiguration) && mDisplayId == that.mDisplayId - && mVisibleRequested == that.mVisibleRequested; + && mVisible == that.mVisible; } @Override public int hashCode() { int result = mConfiguration.hashCode(); result = 31 * result + mDisplayId; - result = 31 * result + (mVisibleRequested ? 1 : 0); + result = 31 * result + (mVisible ? 1 : 0); return result; } @@ -129,13 +129,13 @@ public class TaskFragmentParentInfo implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { mConfiguration.writeToParcel(dest, flags); dest.writeInt(mDisplayId); - dest.writeBoolean(mVisibleRequested); + dest.writeBoolean(mVisible); } private TaskFragmentParentInfo(Parcel in) { mConfiguration.readFromParcel(in); mDisplayId = in.readInt(); - mVisibleRequested = in.readBoolean(); + mVisible = in.readBoolean(); } public static final Creator<TaskFragmentParentInfo> CREATOR = diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 641d1a189711..8815ab35b671 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -132,8 +132,14 @@ public final class TransitionInfo implements Parcelable { */ public static final int FLAG_IS_BEHIND_STARTING_WINDOW = 1 << 14; + /** This change happened underneath something else. */ + public static final int FLAG_IS_OCCLUDED = 1 << 15; + + /** The container is a system window, excluding wallpaper and input-method. */ + public static final int FLAG_IS_SYSTEM_WINDOW = 1 << 16; + /** The first unused bit. This can be used by remotes to attach custom flags to this change. */ - public static final int FLAG_FIRST_CUSTOM = 1 << 15; + public static final int FLAG_FIRST_CUSTOM = 1 << 17; /** @hide */ @IntDef(prefix = { "FLAG_" }, value = { @@ -153,6 +159,8 @@ public final class TransitionInfo implements Parcelable { FLAG_CROSS_PROFILE_OWNER_THUMBNAIL, FLAG_CROSS_PROFILE_WORK_THUMBNAIL, FLAG_IS_BEHIND_STARTING_WINDOW, + FLAG_IS_OCCLUDED, + FLAG_IS_SYSTEM_WINDOW, FLAG_FIRST_CUSTOM }) public @interface ChangeFlags {} @@ -362,6 +370,12 @@ public final class TransitionInfo implements Parcelable { if ((flags & FLAG_IS_BEHIND_STARTING_WINDOW) != 0) { sb.append(sb.length() == 0 ? "" : "|").append("IS_BEHIND_STARTING_WINDOW"); } + if ((flags & FLAG_IS_OCCLUDED) != 0) { + sb.append(sb.length() == 0 ? "" : "|").append("IS_OCCLUDED"); + } + if ((flags & FLAG_IS_SYSTEM_WINDOW) != 0) { + sb.append(sb.length() == 0 ? "" : "|").append("FLAG_IS_SYSTEM_WINDOW"); + } if ((flags & FLAG_FIRST_CUSTOM) != 0) { sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM"); } @@ -400,6 +414,7 @@ public final class TransitionInfo implements Parcelable { public static final class Change implements Parcelable { private final WindowContainerToken mContainer; private WindowContainerToken mParent; + private WindowContainerToken mLastParent; private final SurfaceControl mLeash; private @TransitionMode int mMode = TRANSIT_NONE; private @ChangeFlags int mFlags = FLAG_NONE; @@ -428,6 +443,7 @@ public final class TransitionInfo implements Parcelable { private Change(Parcel in) { mContainer = in.readTypedObject(WindowContainerToken.CREATOR); mParent = in.readTypedObject(WindowContainerToken.CREATOR); + mLastParent = in.readTypedObject(WindowContainerToken.CREATOR); mLeash = new SurfaceControl(); mLeash.readFromParcel(in); mMode = in.readInt(); @@ -451,6 +467,14 @@ public final class TransitionInfo implements Parcelable { mParent = parent; } + /** + * Sets the parent of this change's container before the transition if this change's + * container is reparented in the transition. + */ + public void setLastParent(@Nullable WindowContainerToken lastParent) { + mLastParent = lastParent; + } + /** Sets the transition mode for this change */ public void setMode(@TransitionMode int mode) { mMode = mode; @@ -534,6 +558,17 @@ public final class TransitionInfo implements Parcelable { return mParent; } + /** + * @return the parent of the changing container before the transition if it is reparented + * in the transition. The parent window may not be collected in the transition as a + * participant, and it may have been detached from the display. {@code null} if the changing + * container has not been reparented in the transition, or if the parent is not organizable. + */ + @Nullable + public WindowContainerToken getLastParent() { + return mLastParent; + } + /** @return which action this change represents. */ public @TransitionMode int getMode() { return mMode; @@ -633,6 +668,7 @@ public final class TransitionInfo implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeTypedObject(mContainer, flags); dest.writeTypedObject(mParent, flags); + dest.writeTypedObject(mLastParent, flags); mLeash.writeToParcel(dest, flags); dest.writeInt(mMode); dest.writeInt(mFlags); @@ -672,13 +708,37 @@ public final class TransitionInfo implements Parcelable { @Override public String toString() { - String out = "{" + mContainer + "(" + mParent + ") leash=" + mLeash - + " m=" + modeToString(mMode) + " f=" + flagsToString(mFlags) + " sb=" - + mStartAbsBounds + " eb=" + mEndAbsBounds + " eo=" + mEndRelOffset + " r=" - + mStartRotation + "->" + mEndRotation + ":" + mRotationAnimation - + " endFixedRotation=" + mEndFixedRotation; - if (mSnapshot != null) out += " snapshot=" + mSnapshot; - return out + "}"; + final StringBuilder sb = new StringBuilder(); + sb.append('{'); sb.append(mContainer); + sb.append(" m="); sb.append(modeToString(mMode)); + sb.append(" f="); sb.append(flagsToString(mFlags)); + if (mParent != null) { + sb.append(" p="); sb.append(mParent); + } + if (mLeash != null) { + sb.append(" leash="); sb.append(mLeash); + } + sb.append(" sb="); sb.append(mStartAbsBounds); + sb.append(" eb="); sb.append(mEndAbsBounds); + if (mEndRelOffset.x != 0 || mEndRelOffset.y != 0) { + sb.append(" eo="); sb.append(mEndRelOffset); + } + if (mStartRotation != mEndRotation) { + sb.append(" r="); sb.append(mStartRotation); + sb.append("->"); sb.append(mEndRotation); + sb.append(':'); sb.append(mRotationAnimation); + } + if (mEndFixedRotation != ROTATION_UNDEFINED) { + sb.append(" endFixedRotation="); sb.append(mEndFixedRotation); + } + if (mSnapshot != null) { + sb.append(" snapshot="); sb.append(mSnapshot); + } + if (mLastParent != null) { + sb.append(" lastParent="); sb.append(mLastParent); + } + sb.append('}'); + return sb.toString(); } } diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 0730f3ddf8ac..fda39c14dac7 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -218,19 +218,24 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { public Checker getChecker() { return mChecker; } + @NonNull + private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { private final WeakReference<OnBackInvokedCallback> mCallback; + OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) { mCallback = new WeakReference<>(callback); } @Override - public void onBackStarted() { + public void onBackStarted(BackEvent backEvent) { Handler.getMain().post(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { - callback.onBackStarted(); + mProgressAnimator.onBackStarted(backEvent, event -> + callback.onBackProgressed(event)); + callback.onBackStarted(backEvent); } }); } @@ -240,7 +245,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { Handler.getMain().post(() -> { final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { - callback.onBackProgressed(backEvent); + mProgressAnimator.onBackProgressed(backEvent); } }); } @@ -248,6 +253,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Override public void onBackCancelled() { Handler.getMain().post(() -> { + mProgressAnimator.reset(); final OnBackAnimationCallback callback = getBackAnimationCallback(); if (callback != null) { callback.onBackCancelled(); @@ -258,6 +264,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Override public void onBackInvoked() throws RemoteException { Handler.getMain().post(() -> { + mProgressAnimator.reset(); final OnBackInvokedCallback callback = mCallback.get(); if (callback == null) { return; diff --git a/core/java/android/window/WindowOrganizer.java b/core/java/android/window/WindowOrganizer.java index 4ea5ea5694fa..2a80d021abd6 100644 --- a/core/java/android/window/WindowOrganizer.java +++ b/core/java/android/window/WindowOrganizer.java @@ -84,9 +84,8 @@ public class WindowOrganizer { } /** - * Start a transition. + * Starts a new transition, don't use this to start an already created one. * @param type The type of the transition. This is ignored if a transitionToken is provided. - * @param transitionToken An existing transition to start. If null, a new transition is created. * @param t The set of window operations that are part of this transition. * @return A token identifying the transition. This will be the same as transitionToken if it * was provided. @@ -94,10 +93,24 @@ public class WindowOrganizer { */ @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) @NonNull - public IBinder startTransition(int type, @Nullable IBinder transitionToken, + public IBinder startNewTransition(int type, @Nullable WindowContainerTransaction t) { + try { + return getWindowOrganizerController().startNewTransition(type, t); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Starts an already created transition. + * @param transitionToken An existing transition to start. + * @hide + */ + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) + public void startTransition(@NonNull IBinder transitionToken, @Nullable WindowContainerTransaction t) { try { - return getWindowOrganizerController().startTransition(type, transitionToken, t); + getWindowOrganizerController().startTransition(transitionToken, t); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/com/android/internal/app/AppLocaleCollector.java b/core/java/com/android/internal/app/AppLocaleCollector.java new file mode 100644 index 000000000000..65e8c646e17d --- /dev/null +++ b/core/java/com/android/internal/app/AppLocaleCollector.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET; +import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG; + +import android.content.Context; +import android.os.Build; +import android.os.LocaleList; +import android.util.Log; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +/** The Locale data collector for per-app language. */ +class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { + private static final String TAG = AppLocaleCollector.class.getSimpleName(); + private final Context mContext; + private final String mAppPackageName; + private final LocaleStore.LocaleInfo mAppCurrentLocale; + + AppLocaleCollector(Context context, String appPackageName) { + mContext = context; + mAppPackageName = appPackageName; + mAppCurrentLocale = LocaleStore.getAppCurrentLocaleInfo( + mContext, mAppPackageName); + } + + @Override + public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) { + HashSet<String> langTagsToIgnore = new HashSet<>(); + + LocaleList systemLangList = LocaleList.getDefault(); + for(int i = 0; i < systemLangList.size(); i++) { + langTagsToIgnore.add(systemLangList.get(i).toLanguageTag()); + } + + if (mAppCurrentLocale != null) { + langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag()); + } + return langTagsToIgnore; + } + + @Override + public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, + boolean translatedOnly, boolean isForCountryMode) { + AppLocaleStore.AppLocaleResult result = + AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName); + Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly); + Set<LocaleStore.LocaleInfo> appLocaleList = new HashSet<>(); + Set<LocaleStore.LocaleInfo> systemLocaleList; + boolean shouldShowList = + result.mLocaleStatus == GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG + || result.mLocaleStatus == GET_SUPPORTED_LANGUAGE_FROM_ASSET; + + // Get system supported locale list + if (isForCountryMode) { + systemLocaleList = LocaleStore.getLevelLocales(mContext, + langTagsToIgnore, parent, translatedOnly); + } else { + systemLocaleList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore, + null /* no parent */, translatedOnly); + } + + // Add current app locale + if (mAppCurrentLocale != null && !isForCountryMode) { + appLocaleList.add(mAppCurrentLocale); + } + + // Add current system language into suggestion list + for(LocaleStore.LocaleInfo localeInfo: + LocaleStore.getSystemCurrentLocaleInfo()) { + boolean isNotCurrentLocale = mAppCurrentLocale == null + || !localeInfo.getLocale().equals(mAppCurrentLocale.getLocale()); + if (!isForCountryMode && isNotCurrentLocale) { + appLocaleList.add(localeInfo); + } + } + + // Add the languages that included in system supported locale + if (shouldShowList) { + appLocaleList.addAll(filterTheLanguagesNotIncludedInSystemLocale( + systemLocaleList, result.mAppSupportedLocales)); + } + + // Add "system language" option + if (!isForCountryMode && shouldShowList) { + appLocaleList.add(LocaleStore.getSystemDefaultLocaleInfo( + mAppCurrentLocale == null)); + } + + if (Build.isDebuggable()) { + Log.d(TAG, "App locale list: " + appLocaleList); + } + + return appLocaleList; + } + + @Override + public boolean hasSpecificPackageName() { + return true; + } + + private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotIncludedInSystemLocale( + Set<LocaleStore.LocaleInfo> systemLocaleList, + HashSet<Locale> appSupportedLocales) { + Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>(); + + for(LocaleStore.LocaleInfo li: systemLocaleList) { + if (appSupportedLocales.contains(li.getLocale())) { + filteredList.add(li); + } else { + for(Locale l: appSupportedLocales) { + if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) { + filteredList.add(li); + break; + } + } + } + } + return filteredList; + } +} diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java index 1ec5325623ec..2ae2c09680bf 100644 --- a/core/java/com/android/internal/app/ChooserListAdapter.java +++ b/core/java/com/android/internal/app/ChooserListAdapter.java @@ -43,6 +43,7 @@ import android.view.ViewGroup; import android.widget.TextView; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; import com.android.internal.app.chooser.ChooserTargetInfo; import com.android.internal.app.chooser.DisplayResolveInfo; @@ -86,7 +87,7 @@ public class ChooserListAdapter extends ResolverListAdapter { private final ChooserActivityLogger mChooserActivityLogger; private int mNumShortcutResults = 0; - private Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>(); + private final Map<SelectableTargetInfo, LoadDirectShareIconTask> mIconLoaders = new HashMap<>(); private boolean mApplySharingAppLimits; // Reserve spots for incoming direct share targets by adding placeholders @@ -240,7 +241,6 @@ public class ChooserListAdapter extends ResolverListAdapter { mListViewDataChanged = false; } - private void createPlaceHolders() { mNumShortcutResults = 0; mServiceTargets.clear(); @@ -265,31 +265,24 @@ public class ChooserListAdapter extends ResolverListAdapter { return; } - if (!(info instanceof DisplayResolveInfo)) { - holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel()); - holder.bindIcon(info); - - if (info instanceof SelectableTargetInfo) { - // direct share targets should append the application name for a better readout - DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo(); - CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : ""; - CharSequence extendedInfo = info.getExtendedInfo(); - String contentDescription = String.join(" ", info.getDisplayLabel(), - extendedInfo != null ? extendedInfo : "", appName); - holder.updateContentDescription(contentDescription); + holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel()); + holder.bindIcon(info); + if (info instanceof SelectableTargetInfo) { + // direct share targets should append the application name for a better readout + SelectableTargetInfo sti = (SelectableTargetInfo) info; + DisplayResolveInfo rInfo = sti.getDisplayResolveInfo(); + CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : ""; + CharSequence extendedInfo = info.getExtendedInfo(); + String contentDescription = String.join(" ", info.getDisplayLabel(), + extendedInfo != null ? extendedInfo : "", appName); + holder.updateContentDescription(contentDescription); + if (!sti.hasDisplayIcon()) { + loadDirectShareIcon(sti); } - } else { + } else if (info instanceof DisplayResolveInfo) { DisplayResolveInfo dri = (DisplayResolveInfo) info; - holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel()); - LoadIconTask task = mIconLoaders.get(dri); - if (task == null) { - task = new LoadIconTask(dri, holder); - mIconLoaders.put(dri, task); - task.execute(); - } else { - // The holder was potentially changed as the underlying items were - // reshuffled, so reset the target holder - task.setViewHolder(holder); + if (!dri.hasDisplayIcon()) { + loadIcon(dri); } } @@ -330,6 +323,20 @@ public class ChooserListAdapter extends ResolverListAdapter { } } + private void loadDirectShareIcon(SelectableTargetInfo info) { + LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info); + if (task == null) { + task = createLoadDirectShareIconTask(info); + mIconLoaders.put(info, task); + task.loadIcon(); + } + } + + @VisibleForTesting + protected LoadDirectShareIconTask createLoadDirectShareIconTask(SelectableTargetInfo info) { + return new LoadDirectShareIconTask(info); + } + void updateAlphabeticalList() { new AsyncTask<Void, Void, List<DisplayResolveInfo>>() { @Override @@ -344,7 +351,7 @@ public class ChooserListAdapter extends ResolverListAdapter { Map<String, DisplayResolveInfo> consolidated = new HashMap<>(); for (DisplayResolveInfo info : allTargets) { String resolvedTarget = info.getResolvedComponentName().getPackageName() - + '#' + info.getDisplayLabel(); + + '#' + info.getDisplayLabel(); DisplayResolveInfo multiDri = consolidated.get(resolvedTarget); if (multiDri == null) { consolidated.put(resolvedTarget, info); @@ -353,7 +360,7 @@ public class ChooserListAdapter extends ResolverListAdapter { } else { // create consolidated target from the single DisplayResolveInfo MultiDisplayResolveInfo multiDisplayResolveInfo = - new MultiDisplayResolveInfo(resolvedTarget, multiDri); + new MultiDisplayResolveInfo(resolvedTarget, multiDri); multiDisplayResolveInfo.addTarget(info); consolidated.put(resolvedTarget, multiDisplayResolveInfo); } @@ -743,7 +750,8 @@ public class ChooserListAdapter extends ResolverListAdapter { * Necessary methods to communicate between {@link ChooserListAdapter} * and {@link ChooserActivity}. */ - interface ChooserListCommunicator extends ResolverListCommunicator { + @VisibleForTesting + public interface ChooserListCommunicator extends ResolverListCommunicator { int getMaxRankedTargets(); @@ -751,4 +759,35 @@ public class ChooserListAdapter extends ResolverListAdapter { boolean isSendAction(Intent targetIntent); } + + /** + * Loads direct share targets icons. + */ + @VisibleForTesting + public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Boolean> { + private final SelectableTargetInfo mTargetInfo; + + private LoadDirectShareIconTask(SelectableTargetInfo targetInfo) { + mTargetInfo = targetInfo; + } + + @Override + protected Boolean doInBackground(Void... voids) { + return mTargetInfo.loadIcon(); + } + + @Override + protected void onPostExecute(Boolean isLoaded) { + if (isLoaded) { + notifyDataSetChanged(); + } + } + + /** + * An alias for execute to use with unit tests. + */ + public void loadIcon() { + execute(); + } + } } diff --git a/core/java/com/android/internal/app/LocalePicker.java b/core/java/com/android/internal/app/LocalePicker.java index 3c53d07b6180..7dd1d2607149 100644 --- a/core/java/com/android/internal/app/LocalePicker.java +++ b/core/java/com/android/internal/app/LocalePicker.java @@ -311,8 +311,7 @@ public class LocalePicker extends ListFragment { try { final IActivityManager am = ActivityManager.getService(); - final Configuration config = am.getConfiguration(); - + final Configuration config = new Configuration(); config.setLocales(locales); config.userSetLocale = true; diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java index 965895f08d6e..3efd279c2639 100644 --- a/core/java/com/android/internal/app/LocalePickerWithRegion.java +++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java @@ -16,16 +16,12 @@ package com.android.internal.app; -import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus; - import android.app.FragmentManager; import android.app.FragmentTransaction; import android.app.ListFragment; import android.content.Context; import android.os.Bundle; -import android.os.LocaleList; import android.text.TextUtils; -import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -36,7 +32,6 @@ import android.widget.SearchView; import com.android.internal.R; -import java.util.Collections; import java.util.HashSet; import java.util.Locale; import java.util.Set; @@ -54,6 +49,7 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O private SuggestedLocaleAdapter mAdapter; private LocaleSelectedListener mListener; + private LocaleCollectorBase mLocalePickerCollector; private Set<LocaleStore.LocaleInfo> mLocaleList; private LocaleStore.LocaleInfo mParentLocale; private boolean mTranslatedOnly = false; @@ -62,7 +58,6 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O private boolean mPreviousSearchHadFocus = false; private int mFirstVisiblePosition = 0; private int mTopDistance = 0; - private String mAppPackageName; private CharSequence mTitle = null; private OnActionExpandListener mOnActionExpandListener; @@ -79,31 +74,50 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O void onLocaleSelected(LocaleStore.LocaleInfo locale); } - private static LocalePickerWithRegion createCountryPicker(Context context, + /** + * The interface which provides the locale list. + */ + interface LocaleCollectorBase { + /** Gets the ignored locale list. */ + HashSet<String> getIgnoredLocaleList(boolean translatedOnly); + + /** Gets the supported locale list. */ + Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, + boolean translatedOnly, boolean isForCountryMode); + + /** Indicates if the class work for specific package. */ + boolean hasSpecificPackageName(); + } + + private static LocalePickerWithRegion createCountryPicker( LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, - boolean translatedOnly, String appPackageName, - OnActionExpandListener onActionExpandListener) { + boolean translatedOnly, OnActionExpandListener onActionExpandListener, + LocaleCollectorBase localePickerCollector) { LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); localePicker.setOnActionExpandListener(onActionExpandListener); - boolean shouldShowTheList = localePicker.setListener(context, listener, parent, - translatedOnly, appPackageName); + boolean shouldShowTheList = localePicker.setListener(listener, parent, + translatedOnly, localePickerCollector); return shouldShowTheList ? localePicker : null; } public static LocalePickerWithRegion createLanguagePicker(Context context, LocaleSelectedListener listener, boolean translatedOnly) { - LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); - localePicker.setListener(context, listener, /* parent */ null, translatedOnly, null); - return localePicker; + return createLanguagePicker(context, listener, translatedOnly, null, null); } public static LocalePickerWithRegion createLanguagePicker(Context context, LocaleSelectedListener listener, boolean translatedOnly, String appPackageName, OnActionExpandListener onActionExpandListener) { + LocaleCollectorBase localePickerController; + if (TextUtils.isEmpty(appPackageName)) { + localePickerController = new SystemLocaleCollector(context); + } else { + localePickerController = new AppLocaleCollector(context, appPackageName); + } LocalePickerWithRegion localePicker = new LocalePickerWithRegion(); localePicker.setOnActionExpandListener(onActionExpandListener); - localePicker.setListener( - context, listener, /* parent */ null, translatedOnly, appPackageName); + localePicker.setListener(listener, /* parent */ null, translatedOnly, + localePickerController); return localePicker; } @@ -120,109 +134,23 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O * In this case we don't even show the list, we call the listener with that locale, * "pretending" it was selected, and return false.</p> */ - private boolean setListener(Context context, LocaleSelectedListener listener, - LocaleStore.LocaleInfo parent, boolean translatedOnly, String appPackageName) { + private boolean setListener(LocaleSelectedListener listener, LocaleStore.LocaleInfo parent, + boolean translatedOnly, LocaleCollectorBase localePickerController) { this.mParentLocale = parent; this.mListener = listener; this.mTranslatedOnly = translatedOnly; - this.mAppPackageName = appPackageName; + this.mLocalePickerCollector = localePickerController; setRetainInstance(true); - final HashSet<String> langTagsToIgnore = new HashSet<>(); - LocaleStore.LocaleInfo appCurrentLocale = - LocaleStore.getAppCurrentLocaleInfo(context, appPackageName); - boolean isForCountryMode = parent != null; - - if (!TextUtils.isEmpty(appPackageName) && !isForCountryMode) { - // Filter current system locale to add them into suggestion - LocaleList systemLangList = LocaleList.getDefault(); - for(int i = 0; i < systemLangList.size(); i++) { - langTagsToIgnore.add(systemLangList.get(i).toLanguageTag()); - } - - if (appCurrentLocale != null) { - Log.d(TAG, "appCurrentLocale: " + appCurrentLocale.getLocale().toLanguageTag()); - langTagsToIgnore.add(appCurrentLocale.getLocale().toLanguageTag()); - } else { - Log.d(TAG, "appCurrentLocale is null"); - } - } else if (!translatedOnly) { - final LocaleList userLocales = LocalePicker.getLocales(); - final String[] langTags = userLocales.toLanguageTags().split(","); - Collections.addAll(langTagsToIgnore, langTags); - } + mLocaleList = localePickerController.getSupportedLocaleList( + parent, translatedOnly, parent != null); - if (isForCountryMode) { - mLocaleList = LocaleStore.getLevelLocales(context, - langTagsToIgnore, parent, translatedOnly); - if (mLocaleList.size() <= 1) { - if (listener != null && (mLocaleList.size() == 1)) { - listener.onLocaleSelected(mLocaleList.iterator().next()); - } - return false; - } + if (parent != null && listener != null && mLocaleList.size() == 1) { + listener.onLocaleSelected(mLocaleList.iterator().next()); + return false; } else { - mLocaleList = LocaleStore.getLevelLocales(context, langTagsToIgnore, - null /* no parent */, translatedOnly); + return true; } - Log.d(TAG, "mLocaleList size: " + mLocaleList.size()); - - // Adding current locale and system default option into suggestion list - if(!TextUtils.isEmpty(appPackageName)) { - if (appCurrentLocale != null && !isForCountryMode) { - mLocaleList.add(appCurrentLocale); - } - - AppLocaleStore.AppLocaleResult result = - AppLocaleStore.getAppSupportedLocales(context, appPackageName); - boolean shouldShowList = - result.mLocaleStatus == LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG - || result.mLocaleStatus == LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET; - - // Add current system language into suggestion list - for(LocaleStore.LocaleInfo localeInfo: LocaleStore.getSystemCurrentLocaleInfo()) { - boolean isNotCurrentLocale = appCurrentLocale == null - || !localeInfo.getLocale().equals(appCurrentLocale.getLocale()); - if (!isForCountryMode && isNotCurrentLocale) { - mLocaleList.add(localeInfo); - } - } - - // Filter the language not support in app - mLocaleList = filterTheLanguagesNotSupportedInApp( - shouldShowList, result.mAppSupportedLocales); - - Log.d(TAG, "mLocaleList after app-supported filter: " + mLocaleList.size()); - - // Add "system language" - if (!isForCountryMode && shouldShowList) { - mLocaleList.add(LocaleStore.getSystemDefaultLocaleInfo(appCurrentLocale == null)); - } - } - return true; - } - - private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotSupportedInApp( - boolean shouldShowList, HashSet<Locale> supportedLocales) { - Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>(); - if (!shouldShowList) { - return filteredList; - } - - for(LocaleStore.LocaleInfo li: mLocaleList) { - if (supportedLocales.contains(li.getLocale())) { - filteredList.add(li); - } else { - for(Locale l: supportedLocales) { - if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) { - filteredList.add(li); - break; - } - } - } - } - - return filteredList; } private void returnToParentFrame() { @@ -246,7 +174,9 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O mTitle = getActivity().getTitle(); final boolean countryMode = mParentLocale != null; final Locale sortingLocale = countryMode ? mParentLocale.getLocale() : Locale.getDefault(); - mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, mAppPackageName); + final boolean hasSpecificPackageName = + mLocalePickerCollector != null && mLocalePickerCollector.hasSpecificPackageName(); + mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, hasSpecificPackageName); final LocaleHelper.LocaleInfoComparator comp = new LocaleHelper.LocaleInfoComparator(sortingLocale, countryMode); mAdapter.sort(comp); @@ -321,8 +251,8 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O returnToParentFrame(); } else { LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker( - getContext(), mListener, locale, mTranslatedOnly /* translate only */, - mAppPackageName, mOnActionExpandListener); + mListener, locale, mTranslatedOnly /* translate only */, + mOnActionExpandListener, this.mLocalePickerCollector); if (selector != null) { getFragmentManager().beginTransaction() .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) @@ -340,7 +270,8 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O inflater.inflate(R.menu.language_selection_list, menu); final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu); - if (!TextUtils.isEmpty(mAppPackageName) && mOnActionExpandListener != null) { + if (mLocalePickerCollector.hasSpecificPackageName() + && mOnActionExpandListener != null) { searchMenuItem.setOnActionExpandListener(mOnActionExpandListener); } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 0e1ed7bd0550..c70e26f65829 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -55,6 +55,7 @@ import android.content.pm.UserInfo; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Insets; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -1475,14 +1476,21 @@ public class ResolverActivity extends Activity implements mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList.get(0); boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK; - ResolverListAdapter inactiveAdapter = mMultiProfilePagerAdapter.getInactiveListAdapter(); - DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0); + final ResolverListAdapter inactiveAdapter = + mMultiProfilePagerAdapter.getInactiveListAdapter(); + final DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0); // Load the icon asynchronously ImageView icon = findViewById(R.id.icon); - ResolverListAdapter.LoadIconTask iconTask = inactiveAdapter.new LoadIconTask( - otherProfileResolveInfo, new ResolverListAdapter.ViewHolder(icon)); - iconTask.execute(); + inactiveAdapter.new LoadIconTask(otherProfileResolveInfo) { + @Override + protected void onPostExecute(Drawable drawable) { + if (!isDestroyed()) { + otherProfileResolveInfo.setDisplayIcon(drawable); + new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo); + } + } + }.execute(); ((TextView) findViewById(R.id.open_cross_profile)).setText( getResources().getString( diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java index 66fff5c13ab7..42b46cda6ba3 100644 --- a/core/java/com/android/internal/app/ResolverListAdapter.java +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -58,7 +58,10 @@ import com.android.internal.app.chooser.DisplayResolveInfo; import com.android.internal.app.chooser.TargetInfo; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class ResolverListAdapter extends BaseAdapter { private static final String TAG = "ResolverListAdapter"; @@ -87,6 +90,8 @@ public class ResolverListAdapter extends BaseAdapter { private Runnable mPostListReadyRunnable; private final boolean mIsAudioCaptureDevice; private boolean mIsTabLoaded; + private final Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>(); + private final Map<DisplayResolveInfo, LoadLabelTask> mLabelLoaders = new HashMap<>(); public ResolverListAdapter(Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, @@ -636,26 +641,48 @@ public class ResolverListAdapter extends BaseAdapter { if (info == null) { holder.icon.setImageDrawable( mContext.getDrawable(R.drawable.resolver_icon_placeholder)); + holder.bindLabel("", "", false); return; } - if (info instanceof DisplayResolveInfo - && !((DisplayResolveInfo) info).hasDisplayLabel()) { - getLoadLabelTask((DisplayResolveInfo) info, holder).execute(); - } else { - holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel()); + if (info instanceof DisplayResolveInfo) { + DisplayResolveInfo dri = (DisplayResolveInfo) info; + if (dri.hasDisplayLabel()) { + holder.bindLabel( + dri.getDisplayLabel(), + dri.getExtendedInfo(), + alwaysShowSubLabel()); + } else { + holder.bindLabel("", "", false); + loadLabel(dri); + } + holder.bindIcon(info); + if (!dri.hasDisplayIcon()) { + loadIcon(dri); + } } + } - if (info instanceof DisplayResolveInfo - && !((DisplayResolveInfo) info).hasDisplayIcon()) { - new LoadIconTask((DisplayResolveInfo) info, holder).execute(); - } else { - holder.bindIcon(info); + protected final void loadIcon(DisplayResolveInfo info) { + LoadIconTask task = mIconLoaders.get(info); + if (task == null) { + task = new LoadIconTask((DisplayResolveInfo) info); + mIconLoaders.put(info, task); + task.execute(); + } + } + + private void loadLabel(DisplayResolveInfo info) { + LoadLabelTask task = mLabelLoaders.get(info); + if (task == null) { + task = createLoadLabelTask(info); + mLabelLoaders.put(info, task); + task.execute(); } } - protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) { - return new LoadLabelTask(info, holder); + protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) { + return new LoadLabelTask(info); } public void onDestroy() { @@ -666,6 +693,16 @@ public class ResolverListAdapter extends BaseAdapter { if (mResolverListController != null) { mResolverListController.destroy(); } + cancelTasks(mIconLoaders.values()); + cancelTasks(mLabelLoaders.values()); + mIconLoaders.clear(); + mLabelLoaders.clear(); + } + + private <T extends AsyncTask> void cancelTasks(Collection<T> tasks) { + for (T task: tasks) { + task.cancel(false); + } } private static ColorMatrixColorFilter getSuspendedColorMatrix() { @@ -834,7 +871,12 @@ public class ResolverListAdapter extends BaseAdapter { void onHandlePackagesChanged(ResolverListAdapter listAdapter); } - static class ViewHolder { + /** + * A view holder keeps a reference to a list view and provides functionality for managing its + * state. + */ + @VisibleForTesting + public static class ViewHolder { public View itemView; public Drawable defaultItemViewBackground; @@ -842,7 +884,8 @@ public class ResolverListAdapter extends BaseAdapter { public TextView text2; public ImageView icon; - ViewHolder(View view) { + @VisibleForTesting + public ViewHolder(View view) { itemView = view; defaultItemViewBackground = view.getBackground(); text = (TextView) view.findViewById(com.android.internal.R.id.text1); @@ -883,11 +926,9 @@ public class ResolverListAdapter extends BaseAdapter { protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> { private final DisplayResolveInfo mDisplayResolveInfo; - private final ViewHolder mHolder; - protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) { + protected LoadLabelTask(DisplayResolveInfo dri) { mDisplayResolveInfo = dri; - mHolder = holder; } @Override @@ -925,21 +966,22 @@ public class ResolverListAdapter extends BaseAdapter { @Override protected void onPostExecute(CharSequence[] result) { + if (mDisplayResolveInfo.hasDisplayLabel()) { + return; + } mDisplayResolveInfo.setDisplayLabel(result[0]); mDisplayResolveInfo.setExtendedInfo(result[1]); - mHolder.bindLabel(result[0], result[1], alwaysShowSubLabel()); + notifyDataSetChanged(); } } class LoadIconTask extends AsyncTask<Void, Void, Drawable> { protected final DisplayResolveInfo mDisplayResolveInfo; private final ResolveInfo mResolveInfo; - private ViewHolder mHolder; - LoadIconTask(DisplayResolveInfo dri, ViewHolder holder) { + LoadIconTask(DisplayResolveInfo dri) { mDisplayResolveInfo = dri; mResolveInfo = dri.getResolveInfo(); - mHolder = holder; } @Override @@ -953,17 +995,9 @@ public class ResolverListAdapter extends BaseAdapter { mResolverListCommunicator.updateProfileViewButton(); } else if (!mDisplayResolveInfo.hasDisplayIcon()) { mDisplayResolveInfo.setDisplayIcon(d); - mHolder.bindIcon(mDisplayResolveInfo); - // Notify in case view is already bound to resolve the race conditions on - // low end devices notifyDataSetChanged(); } } - - public void setViewHolder(ViewHolder holder) { - mHolder = holder; - mHolder.bindIcon(mDisplayResolveInfo); - } } /** diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java index 1be1247b7cc0..5ed0e8bc7883 100644 --- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java +++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java @@ -70,17 +70,17 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { private Locale mDisplayLocale = null; // used to potentially cache a modified Context that uses mDisplayLocale private Context mContextOverride = null; - private String mAppPackageName; + private boolean mHasSpecificAppPackageName; public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode) { - this(localeOptions, countryMode, null); + this(localeOptions, countryMode, false); } public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode, - String appPackageName) { + boolean hasSpecificAppPackageName) { mCountryMode = countryMode; mLocaleOptions = new ArrayList<>(localeOptions.size()); - mAppPackageName = appPackageName; + mHasSpecificAppPackageName = hasSpecificAppPackageName; for (LocaleStore.LocaleInfo li : localeOptions) { if (li.isSuggested()) { @@ -134,7 +134,7 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { @Override public int getViewTypeCount() { - if (!TextUtils.isEmpty(mAppPackageName) && showHeaders()) { + if (mHasSpecificAppPackageName && showHeaders()) { // Two headers, 1 "System language", 1 current locale return APP_LANGUAGE_PICKER_TYPE_COUNT; } else if (showHeaders()) { diff --git a/core/java/com/android/internal/app/SystemLocaleCollector.java b/core/java/com/android/internal/app/SystemLocaleCollector.java new file mode 100644 index 000000000000..9a6d4c192fdc --- /dev/null +++ b/core/java/com/android/internal/app/SystemLocaleCollector.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import android.content.Context; +import android.os.LocaleList; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** The Locale data collector for System language. */ +class SystemLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { + private final Context mContext; + + SystemLocaleCollector(Context context) { + mContext = context; + } + + @Override + public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) { + HashSet<String> ignoreList = new HashSet<>(); + if (!translatedOnly) { + final LocaleList userLocales = LocalePicker.getLocales(); + final String[] langTags = userLocales.toLanguageTags().split(","); + Collections.addAll(ignoreList, langTags); + } + return ignoreList; + } + + @Override + public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, + boolean translatedOnly, boolean isForCountryMode) { + Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly); + Set<LocaleStore.LocaleInfo> localeList; + + if (isForCountryMode) { + localeList = LocaleStore.getLevelLocales(mContext, + langTagsToIgnore, parent, translatedOnly); + } else { + localeList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore, + null /* no parent */, translatedOnly); + } + return localeList; + } + + + @Override + public boolean hasSpecificPackageName() { + return false; + } +}
\ No newline at end of file diff --git a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java index 5f4a9cd5141e..473134ea46f3 100644 --- a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java +++ b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java @@ -172,14 +172,14 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable { @Override public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { - prepareIntentForCrossProfileLaunch(mResolvedIntent, userId); + TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, userId); activity.startActivityAsCaller(mResolvedIntent, options, false, userId); return true; } @Override public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { - prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier()); + TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier()); activity.startActivityAsUser(mResolvedIntent, options, user); return false; } @@ -224,13 +224,6 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable { } }; - private static void prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId) { - final int currentUserId = UserHandle.myUserId(); - if (targetUserId != currentUserId) { - intent.fixUris(currentUserId); - } - } - private DisplayResolveInfo(Parcel in) { mDisplayLabel = in.readCharSequence(); mExtendedInfo = in.readCharSequence(); diff --git a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java index 264e4f76d35d..d7f3a76c61e0 100644 --- a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java +++ b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java @@ -37,6 +37,7 @@ import android.service.chooser.ChooserTarget; import android.text.SpannableStringBuilder; import android.util.Log; +import com.android.internal.annotations.GuardedBy; import com.android.internal.app.ChooserActivity; import com.android.internal.app.ResolverActivity; import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter; @@ -59,8 +60,11 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { private final String mDisplayLabel; private final PackageManager mPm; private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator; + @GuardedBy("this") + private ShortcutInfo mShortcutInfo; private Drawable mBadgeIcon = null; private CharSequence mBadgeContentDescription; + @GuardedBy("this") private Drawable mDisplayIcon; private final Intent mFillInIntent; private final int mFillInFlags; @@ -78,6 +82,7 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { mModifiedScore = modifiedScore; mPm = mContext.getPackageManager(); mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator; + mShortcutInfo = shortcutInfo; mIsPinned = shortcutInfo != null && shortcutInfo.isPinned(); if (sourceInfo != null) { final ResolveInfo ri = sourceInfo.getResolveInfo(); @@ -92,8 +97,6 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { } } } - // TODO(b/121287224): do this in the background thread, and only for selected targets - mDisplayIcon = getChooserTargetIconDrawable(chooserTarget, shortcutInfo); if (sourceInfo != null) { mBackupResolveInfo = null; @@ -118,7 +121,10 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { mChooserTarget = other.mChooserTarget; mBadgeIcon = other.mBadgeIcon; mBadgeContentDescription = other.mBadgeContentDescription; - mDisplayIcon = other.mDisplayIcon; + synchronized (other) { + mShortcutInfo = other.mShortcutInfo; + mDisplayIcon = other.mDisplayIcon; + } mFillInIntent = fillInIntent; mFillInFlags = flags; mModifiedScore = other.mModifiedScore; @@ -141,6 +147,27 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { return mSourceInfo; } + /** + * Load display icon, if needed. + */ + public boolean loadIcon() { + ShortcutInfo shortcutInfo; + Drawable icon; + synchronized (this) { + shortcutInfo = mShortcutInfo; + icon = mDisplayIcon; + } + boolean shouldLoadIcon = icon == null && shortcutInfo != null; + if (shouldLoadIcon) { + icon = getChooserTargetIconDrawable(mChooserTarget, shortcutInfo); + synchronized (this) { + mDisplayIcon = icon; + mShortcutInfo = null; + } + } + return shouldLoadIcon; + } + private Drawable getChooserTargetIconDrawable(ChooserTarget target, @Nullable ShortcutInfo shortcutInfo) { Drawable directShareIcon = null; @@ -232,6 +259,7 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { } intent.setComponent(mChooserTarget.getComponentName()); intent.putExtras(mChooserTarget.getIntentExtras()); + TargetInfo.prepareIntentForCrossProfileLaunch(intent, userId); // Important: we will ignore the target security checks in ActivityManager // if and only if the ChooserTarget's target package is the same package @@ -270,10 +298,17 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { } @Override - public Drawable getDisplayIcon(Context context) { + public synchronized Drawable getDisplayIcon(Context context) { return mDisplayIcon; } + /** + * @return true if display icon is available + */ + public synchronized boolean hasDisplayIcon() { + return mDisplayIcon != null; + } + public ChooserTarget getChooserTarget() { return mChooserTarget; } diff --git a/core/java/com/android/internal/app/chooser/TargetInfo.java b/core/java/com/android/internal/app/chooser/TargetInfo.java index f56ab17cb059..7bb7ddc65c6d 100644 --- a/core/java/com/android/internal/app/chooser/TargetInfo.java +++ b/core/java/com/android/internal/app/chooser/TargetInfo.java @@ -130,4 +130,15 @@ public interface TargetInfo { * @return true if this target should be pinned to the front by the request of the user */ boolean isPinned(); + + /** + * Fix the URIs in {@code intent} if cross-profile sharing is required. This should be called + * before launching the intent as another user. + */ + static void prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId) { + final int currentUserId = UserHandle.myUserId(); + if (targetUserId != currentUserId) { + intent.fixUris(currentUserId); + } + } } diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl index 2d68cb472fa3..51b56dbf582b 100644 --- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl +++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl @@ -45,6 +45,7 @@ interface IAppWidgetService { @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) RemoteViews getAppWidgetViews(String callingPackage, int appWidgetId); int[] getAppWidgetIdsForHost(String callingPackage, int hostId); + void setAppWidgetHidden(in String callingPackage, int hostId); IntentSender createAppWidgetConfigIntentSender(String callingPackage, int appWidgetId, int intentFlags); diff --git a/core/java/com/android/internal/display/BrightnessSynchronizer.java b/core/java/com/android/internal/display/BrightnessSynchronizer.java index 62c7966bb2d6..d503904c2e3c 100644 --- a/core/java/com/android/internal/display/BrightnessSynchronizer.java +++ b/core/java/com/android/internal/display/BrightnessSynchronizer.java @@ -508,6 +508,5 @@ public class BrightnessSynchronizer { DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS); mIsObserving = true; } - } } diff --git a/core/java/com/android/internal/dynamicanimation/animation/DynamicAnimation.java b/core/java/com/android/internal/dynamicanimation/animation/DynamicAnimation.java new file mode 100644 index 000000000000..d4fe7c8d7f36 --- /dev/null +++ b/core/java/com/android/internal/dynamicanimation/animation/DynamicAnimation.java @@ -0,0 +1,815 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.dynamicanimation.animation; + +import android.animation.AnimationHandler; +import android.animation.ValueAnimator; +import android.annotation.FloatRange; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.os.Looper; +import android.util.AndroidRuntimeException; +import android.util.FloatProperty; +import android.view.View; + +import java.util.ArrayList; + +/** + * This class is the base class of physics-based animations. It manages the animation's + * lifecycle such as {@link #start()} and {@link #cancel()}. This base class also handles the common + * setup for all the subclass animations. For example, DynamicAnimation supports adding + * {@link OnAnimationEndListener} and {@link OnAnimationUpdateListener} so that the important + * animation events can be observed through the callbacks. The start conditions for any subclass of + * DynamicAnimation can be set using {@link #setStartValue(float)} and + * {@link #setStartVelocity(float)}. + * + * @param <T> subclass of DynamicAnimation + */ +public abstract class DynamicAnimation<T extends DynamicAnimation<T>> + implements AnimationHandler.AnimationFrameCallback { + + /** + * ViewProperty holds the access of a property of a {@link View}. When an animation is + * created with a {@link ViewProperty} instance, the corresponding property value of the view + * will be updated through this ViewProperty instance. + */ + public abstract static class ViewProperty extends FloatProperty<View> { + private ViewProperty(String name) { + super(name); + } + } + + /** + * View's translationX property. + */ + public static final ViewProperty TRANSLATION_X = new ViewProperty("translationX") { + @Override + public void setValue(View view, float value) { + view.setTranslationX(value); + } + + @Override + public Float get(View view) { + return view.getTranslationX(); + } + }; + + /** + * View's translationY property. + */ + public static final ViewProperty TRANSLATION_Y = new ViewProperty("translationY") { + @Override + public void setValue(View view, float value) { + view.setTranslationY(value); + } + + @Override + public Float get(View view) { + return view.getTranslationY(); + } + }; + + /** + * View's translationZ property. + */ + public static final ViewProperty TRANSLATION_Z = new ViewProperty("translationZ") { + @Override + public void setValue(View view, float value) { + view.setTranslationZ(value); + } + + @Override + public Float get(View view) { + return view.getTranslationZ(); + } + }; + + /** + * View's scaleX property. + */ + public static final ViewProperty SCALE_X = new ViewProperty("scaleX") { + @Override + public void setValue(View view, float value) { + view.setScaleX(value); + } + + @Override + public Float get(View view) { + return view.getScaleX(); + } + }; + + /** + * View's scaleY property. + */ + public static final ViewProperty SCALE_Y = new ViewProperty("scaleY") { + @Override + public void setValue(View view, float value) { + view.setScaleY(value); + } + + @Override + public Float get(View view) { + return view.getScaleY(); + } + }; + + /** + * View's rotation property. + */ + public static final ViewProperty ROTATION = new ViewProperty("rotation") { + @Override + public void setValue(View view, float value) { + view.setRotation(value); + } + + @Override + public Float get(View view) { + return view.getRotation(); + } + }; + + /** + * View's rotationX property. + */ + public static final ViewProperty ROTATION_X = new ViewProperty("rotationX") { + @Override + public void setValue(View view, float value) { + view.setRotationX(value); + } + + @Override + public Float get(View view) { + return view.getRotationX(); + } + }; + + /** + * View's rotationY property. + */ + public static final ViewProperty ROTATION_Y = new ViewProperty("rotationY") { + @Override + public void setValue(View view, float value) { + view.setRotationY(value); + } + + @Override + public Float get(View view) { + return view.getRotationY(); + } + }; + + /** + * View's x property. + */ + public static final ViewProperty X = new ViewProperty("x") { + @Override + public void setValue(View view, float value) { + view.setX(value); + } + + @Override + public Float get(View view) { + return view.getX(); + } + }; + + /** + * View's y property. + */ + public static final ViewProperty Y = new ViewProperty("y") { + @Override + public void setValue(View view, float value) { + view.setY(value); + } + + @Override + public Float get(View view) { + return view.getY(); + } + }; + + /** + * View's z property. + */ + public static final ViewProperty Z = new ViewProperty("z") { + @Override + public void setValue(View view, float value) { + view.setZ(value); + } + + @Override + public Float get(View view) { + return view.getZ(); + } + }; + + /** + * View's alpha property. + */ + public static final ViewProperty ALPHA = new ViewProperty("alpha") { + @Override + public void setValue(View view, float value) { + view.setAlpha(value); + } + + @Override + public Float get(View view) { + return view.getAlpha(); + } + }; + + // Properties below are not RenderThread compatible + /** + * View's scrollX property. + */ + public static final ViewProperty SCROLL_X = new ViewProperty("scrollX") { + @Override + public void setValue(View view, float value) { + view.setScrollX((int) value); + } + + @Override + public Float get(View view) { + return (float) view.getScrollX(); + } + }; + + /** + * View's scrollY property. + */ + public static final ViewProperty SCROLL_Y = new ViewProperty("scrollY") { + @Override + public void setValue(View view, float value) { + view.setScrollY((int) value); + } + + @Override + public Float get(View view) { + return (float) view.getScrollY(); + } + }; + + /** + * The minimum visible change in pixels that can be visible to users. + */ + @SuppressLint("MinMaxConstant") + public static final float MIN_VISIBLE_CHANGE_PIXELS = 1f; + /** + * The minimum visible change in degrees that can be visible to users. + */ + @SuppressLint("MinMaxConstant") + public static final float MIN_VISIBLE_CHANGE_ROTATION_DEGREES = 1f / 10f; + /** + * The minimum visible change in alpha that can be visible to users. + */ + @SuppressLint("MinMaxConstant") + public static final float MIN_VISIBLE_CHANGE_ALPHA = 1f / 256f; + /** + * The minimum visible change in scale that can be visible to users. + */ + @SuppressLint("MinMaxConstant") + public static final float MIN_VISIBLE_CHANGE_SCALE = 1f / 500f; + + // Use the max value of float to indicate an unset state. + private static final float UNSET = Float.MAX_VALUE; + + // Multiplier to the min visible change value for value threshold + private static final float THRESHOLD_MULTIPLIER = 0.75f; + + // Internal tracking for velocity. + float mVelocity = 0; + + // Internal tracking for value. + float mValue = UNSET; + + // Tracks whether start value is set. If not, the animation will obtain the value at the time + // of starting through the getter and use that as the starting value of the animation. + boolean mStartValueIsSet = false; + + // Target to be animated. + final Object mTarget; + + // View property id. + final FloatProperty mProperty; + + // Package private tracking of animation lifecycle state. Visible to subclass animations. + boolean mRunning = false; + + // Min and max values that defines the range of the animation values. + float mMaxValue = Float.MAX_VALUE; + float mMinValue = -mMaxValue; + + // Last frame time. Always gets reset to -1 at the end of the animation. + private long mLastFrameTime = 0; + + private float mMinVisibleChange; + + // List of end listeners + private final ArrayList<OnAnimationEndListener> mEndListeners = new ArrayList<>(); + + // List of update listeners + private final ArrayList<OnAnimationUpdateListener> mUpdateListeners = new ArrayList<>(); + + // Animation handler used to schedule updates for this animation. + private AnimationHandler mAnimationHandler; + + // Internal state for value/velocity pair. + static class MassState { + float mValue; + float mVelocity; + } + + /** + * Creates a dynamic animation with the given FloatValueHolder instance. + * + * @param floatValueHolder the FloatValueHolder instance to be animated. + */ + DynamicAnimation(final FloatValueHolder floatValueHolder) { + mTarget = null; + mProperty = new FloatProperty("FloatValueHolder") { + @Override + public Float get(Object object) { + return floatValueHolder.getValue(); + } + + @Override + public void setValue(Object object, float value) { + floatValueHolder.setValue(value); + } + }; + mMinVisibleChange = MIN_VISIBLE_CHANGE_PIXELS; + } + + /** + * Creates a dynamic animation to animate the given property for the given {@link View} + * + * @param object the Object whose property is to be animated + * @param property the property to be animated + */ + + <K> DynamicAnimation(K object, FloatProperty<K> property) { + mTarget = object; + mProperty = property; + if (mProperty == ROTATION || mProperty == ROTATION_X + || mProperty == ROTATION_Y) { + mMinVisibleChange = MIN_VISIBLE_CHANGE_ROTATION_DEGREES; + } else if (mProperty == ALPHA) { + mMinVisibleChange = MIN_VISIBLE_CHANGE_ALPHA; + } else if (mProperty == SCALE_X || mProperty == SCALE_Y) { + mMinVisibleChange = MIN_VISIBLE_CHANGE_SCALE; + } else { + mMinVisibleChange = MIN_VISIBLE_CHANGE_PIXELS; + } + } + + /** + * Sets the start value of the animation. If start value is not set, the animation will get + * the current value for the view's property, and use that as the start value. + * + * @param startValue start value for the animation + * @return the Animation whose start value is being set + */ + @SuppressWarnings("unchecked") + public T setStartValue(float startValue) { + mValue = startValue; + mStartValueIsSet = true; + return (T) this; + } + + /** + * Start velocity of the animation. Default velocity is 0. Unit: change in property per + * second (e.g. pixels per second, scale/alpha value change per second). + * + * <p>Note when using a fixed value as the start velocity (as opposed to getting the velocity + * through touch events), it is recommended to define such a value in dp/second and convert it + * to pixel/second based on the density of the screen to achieve a consistent look across + * different screens. + * + * <p>To convert from dp/second to pixel/second: + * <pre class="prettyprint"> + * float pixelPerSecond = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, + * getResources().getDisplayMetrics()); + * </pre> + * + * @param startVelocity start velocity of the animation + * @return the Animation whose start velocity is being set + */ + @SuppressWarnings("unchecked") + public T setStartVelocity(float startVelocity) { + mVelocity = startVelocity; + return (T) this; + } + + /** + * Sets the max value of the animation. Animations will not animate beyond their max value. + * Whether or not animation will come to an end when max value is reached is dependent on the + * child animation's implementation. + * + * @param max maximum value of the property to be animated + * @return the Animation whose max value is being set + */ + @SuppressWarnings("unchecked") + public T setMaxValue(float max) { + // This max value should be checked and handled in the subclass animations, instead of + // assuming the end of the animations when the max/min value is hit in the base class. + // The reason is that hitting max/min value may just be a transient state, such as during + // the spring oscillation. + mMaxValue = max; + return (T) this; + } + + /** + * Sets the min value of the animation. Animations will not animate beyond their min value. + * Whether or not animation will come to an end when min value is reached is dependent on the + * child animation's implementation. + * + * @param min minimum value of the property to be animated + * @return the Animation whose min value is being set + */ + @SuppressWarnings("unchecked") + public T setMinValue(float min) { + mMinValue = min; + return (T) this; + } + + /** + * Adds an end listener to the animation for receiving onAnimationEnd callbacks. If the listener + * is {@code null} or has already been added to the list of listeners for the animation, no op. + * + * @param listener the listener to be added + * @return the animation to which the listener is added + */ + @SuppressWarnings("unchecked") + public T addEndListener(OnAnimationEndListener listener) { + if (!mEndListeners.contains(listener)) { + mEndListeners.add(listener); + } + return (T) this; + } + + /** + * Removes the end listener from the animation, so as to stop receiving animation end callbacks. + * + * @param listener the listener to be removed + */ + public void removeEndListener(OnAnimationEndListener listener) { + removeEntry(mEndListeners, listener); + } + + /** + * Adds an update listener to the animation for receiving per-frame animation update callbacks. + * If the listener is {@code null} or has already been added to the list of listeners for the + * animation, no op. + * + * <p>Note that update listener should only be added before the start of the animation. + * + * @param listener the listener to be added + * @return the animation to which the listener is added + * @throws UnsupportedOperationException if the update listener is added after the animation has + * started + */ + @SuppressWarnings("unchecked") + public T addUpdateListener(OnAnimationUpdateListener listener) { + if (isRunning()) { + // Require update listener to be added before the animation, such as when we start + // the animation, we know whether the animation is RenderThread compatible. + throw new UnsupportedOperationException("Error: Update listeners must be added before" + + "the animation."); + } + if (!mUpdateListeners.contains(listener)) { + mUpdateListeners.add(listener); + } + return (T) this; + } + + /** + * Removes the update listener from the animation, so as to stop receiving animation update + * callbacks. + * + * @param listener the listener to be removed + */ + public void removeUpdateListener(OnAnimationUpdateListener listener) { + removeEntry(mUpdateListeners, listener); + } + + + /** + * This method sets the minimal change of animation value that is visible to users, which helps + * determine a reasonable threshold for the animation's termination condition. It is critical + * to set the minimal visible change for custom properties (i.e. non-<code>ViewProperty</code>s) + * unless the custom property is in pixels. + * + * <p>For custom properties, this minimum visible change defaults to change in pixel + * (i.e. {@link #MIN_VISIBLE_CHANGE_PIXELS}. It is recommended to adjust this value that is + * reasonable for the property to be animated. A general rule of thumb to calculate such a value + * is: minimum visible change = range of custom property value / corresponding pixel range. For + * example, if the property to be animated is a progress (from 0 to 100) that corresponds to a + * 200-pixel change. Then the min visible change should be 100 / 200. (i.e. 0.5). + * + * <p>It's not necessary to call this method when animating {@link ViewProperty}s, as the + * minimum visible change will be derived from the property. For example, if the property to be + * animated is in pixels (i.e. {@link #TRANSLATION_X}, {@link #TRANSLATION_Y}, + * {@link #TRANSLATION_Z}, @{@link #SCROLL_X} or {@link #SCROLL_Y}), the default minimum visible + * change is 1 (pixel). For {@link #ROTATION}, {@link #ROTATION_X} or {@link #ROTATION_Y}, the + * animation will use {@link #MIN_VISIBLE_CHANGE_ROTATION_DEGREES} as the min visible change, + * which is 1/10. Similarly, the minimum visible change for alpha ( + * i.e. {@link #MIN_VISIBLE_CHANGE_ALPHA} is defined as 1 / 256. + * + * @param minimumVisibleChange minimum change in property value that is visible to users + * @return the animation whose min visible change is being set + * @throws IllegalArgumentException if the given threshold is not positive + */ + @SuppressWarnings("unchecked") + public T setMinimumVisibleChange(@FloatRange(from = 0.0, fromInclusive = false) + float minimumVisibleChange) { + if (minimumVisibleChange <= 0) { + throw new IllegalArgumentException("Minimum visible change must be positive."); + } + mMinVisibleChange = minimumVisibleChange; + setValueThreshold(minimumVisibleChange * THRESHOLD_MULTIPLIER); + return (T) this; + } + + /** + * Returns the minimum change in the animation property that could be visibly different to + * users. + * + * @return minimum change in property value that is visible to users + */ + public float getMinimumVisibleChange() { + return mMinVisibleChange; + } + + /** + * Remove {@code null} entries from the list. + */ + private static <T> void removeNullEntries(ArrayList<T> list) { + // Clean up null entries + for (int i = list.size() - 1; i >= 0; i--) { + if (list.get(i) == null) { + list.remove(i); + } + } + } + + /** + * Remove an entry from the list by marking it {@code null} and clean up later. + */ + private static <T> void removeEntry(ArrayList<T> list, T entry) { + int id = list.indexOf(entry); + if (id >= 0) { + list.set(id, null); + } + } + + /****************Animation Lifecycle Management***************/ + + /** + * Starts an animation. If the animation has already been started, no op. Note that calling + * {@link #start()} will not immediately set the property value to start value of the animation. + * The property values will be changed at each animation pulse, which happens before the draw + * pass. As a result, the changes will be reflected in the next frame, the same as if the values + * were set immediately. This method should only be called on main thread. + * + * Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler + * is created on the same thread as the first call to start/cancel an animation. All the + * subsequent animation lifecycle manipulations need to be on that same thread, until the + * AnimationHandler is reset (using [setAnimationHandler]). + * + * @throws AndroidRuntimeException if this method is not called on the same thread as the + * animation handler + */ + @MainThread + public void start() { + if (!isCurrentThread()) { + throw new AndroidRuntimeException("Animations may only be started on the same thread " + + "as the animation handler"); + } + if (!mRunning) { + startAnimationInternal(); + } + } + + boolean isCurrentThread() { + return Thread.currentThread() == Looper.myLooper().getThread(); + } + + /** + * Cancels the on-going animation. If the animation hasn't started, no op. + * + * Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler + * is created on the same thread as the first call to start/cancel an animation. All the + * subsequent animation lifecycle manipulations need to be on that same thread, until the + * AnimationHandler is reset (using [setAnimationHandler]). + * + * @throws AndroidRuntimeException if this method is not called on the same thread as the + * animation handler + */ + @MainThread + public void cancel() { + if (!isCurrentThread()) { + throw new AndroidRuntimeException("Animations may only be canceled from the same " + + "thread as the animation handler"); + } + if (mRunning) { + endAnimationInternal(true); + } + } + + /** + * Returns whether the animation is currently running. + * + * @return {@code true} if the animation is currently running, {@code false} otherwise + */ + public boolean isRunning() { + return mRunning; + } + + /************************** Private APIs below ********************************/ + + // This gets called when the animation is started, to finish the setup of the animation + // before the animation pulsing starts. + private void startAnimationInternal() { + if (!mRunning) { + mRunning = true; + if (!mStartValueIsSet) { + mValue = getPropertyValue(); + } + // Sanity check: + if (mValue > mMaxValue || mValue < mMinValue) { + throw new IllegalArgumentException("Starting value need to be in between min" + + " value and max value"); + } + getAnimationHandler().addAnimationFrameCallback(this, 0); + } + } + + /** + * This gets call on each frame of the animation. Animation value and velocity are updated + * in this method based on the new frame time. The property value of the view being animated + * is then updated. The animation's ending conditions are also checked in this method. Once + * the animation reaches equilibrium, the animation will come to its end, and end listeners + * will be notified, if any. + */ + @Override + public boolean doAnimationFrame(long frameTime) { + if (mLastFrameTime == 0) { + // First frame. + mLastFrameTime = frameTime; + setPropertyValue(mValue); + return false; + } + long deltaT = frameTime - mLastFrameTime; + mLastFrameTime = frameTime; + float durationScale = ValueAnimator.getDurationScale(); + deltaT = durationScale == 0.0f ? Integer.MAX_VALUE : (long) (deltaT / durationScale); + boolean finished = updateValueAndVelocity(deltaT); + // Clamp value & velocity. + mValue = Math.min(mValue, mMaxValue); + mValue = Math.max(mValue, mMinValue); + + setPropertyValue(mValue); + + if (finished) { + endAnimationInternal(false); + } + return finished; + } + + @Override + public void commitAnimationFrame(long frameTime) { + doAnimationFrame(frameTime); + } + + /** + * Updates the animation state (i.e. value and velocity). This method is package private, so + * subclasses can override this method to calculate the new value and velocity in their custom + * way. + * + * @param deltaT time elapsed in millisecond since last frame + * @return whether the animation has finished + */ + abstract boolean updateValueAndVelocity(long deltaT); + + /** + * Internal method to reset the animation states when animation is finished/canceled. + */ + private void endAnimationInternal(boolean canceled) { + mRunning = false; + getAnimationHandler().removeCallback(this); + mLastFrameTime = 0; + mStartValueIsSet = false; + for (int i = 0; i < mEndListeners.size(); i++) { + if (mEndListeners.get(i) != null) { + mEndListeners.get(i).onAnimationEnd(this, canceled, mValue, mVelocity); + } + } + removeNullEntries(mEndListeners); + } + + /** + * Updates the property value through the corresponding setter. + */ + @SuppressWarnings("unchecked") + void setPropertyValue(float value) { + mProperty.setValue(mTarget, value); + for (int i = 0; i < mUpdateListeners.size(); i++) { + if (mUpdateListeners.get(i) != null) { + mUpdateListeners.get(i).onAnimationUpdate(this, mValue, mVelocity); + } + } + removeNullEntries(mUpdateListeners); + } + + /** + * Returns the default threshold. + */ + float getValueThreshold() { + return mMinVisibleChange * THRESHOLD_MULTIPLIER; + } + + /** + * Obtain the property value through the corresponding getter. + */ + @SuppressWarnings("unchecked") + private float getPropertyValue() { + return (Float) mProperty.get(mTarget); + } + + /** + * Returns the {@link AnimationHandler} used to schedule updates for this animator. + * + * @return the {@link AnimationHandler} for this animator. + */ + @NonNull + public AnimationHandler getAnimationHandler() { + return mAnimationHandler != null ? mAnimationHandler : AnimationHandler.getInstance(); + } + + /****************Sub class animations**************/ + /** + * Returns the acceleration at the given value with the given velocity. + **/ + abstract float getAcceleration(float value, float velocity); + + /** + * Returns whether the animation has reached equilibrium. + */ + abstract boolean isAtEquilibrium(float value, float velocity); + + /** + * Updates the default value threshold for the animation based on the property to be animated. + */ + abstract void setValueThreshold(float threshold); + + /** + * An animation listener that receives end notifications from an animation. + */ + public interface OnAnimationEndListener { + /** + * Notifies the end of an animation. Note that this callback will be invoked not only when + * an animation reach equilibrium, but also when the animation is canceled. + * + * @param animation animation that has ended or was canceled + * @param canceled whether the animation has been canceled + * @param value the final value when the animation stopped + * @param velocity the final velocity when the animation stopped + */ + void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, + float velocity); + } + + /** + * Implementors of this interface can add themselves as update listeners + * to an <code>DynamicAnimation</code> instance to receive callbacks on every animation + * frame, after the current frame's values have been calculated for that + * <code>DynamicAnimation</code>. + */ + public interface OnAnimationUpdateListener { + + /** + * Notifies the occurrence of another frame of the animation. + * + * @param animation animation that the update listener is added to + * @param value the current value of the animation + * @param velocity the current velocity of the animation + */ + void onAnimationUpdate(DynamicAnimation animation, float value, float velocity); + } +} diff --git a/core/java/com/android/internal/dynamicanimation/animation/FloatValueHolder.java b/core/java/com/android/internal/dynamicanimation/animation/FloatValueHolder.java new file mode 100644 index 000000000000..c3a2cacd16ec --- /dev/null +++ b/core/java/com/android/internal/dynamicanimation/animation/FloatValueHolder.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.dynamicanimation.animation; + +/** + * <p>FloatValueHolder holds a float value. FloatValueHolder provides a setter and a getter ( + * i.e. {@link #setValue(float)} and {@link #getValue()}) to access this float value. Animations can + * be performed on a FloatValueHolder instance. During each frame of the animation, the + * FloatValueHolder will have its value updated via {@link #setValue(float)}. The caller can + * obtain the up-to-date animation value via {@link FloatValueHolder#getValue()}. + * + * @see SpringAnimation#SpringAnimation(FloatValueHolder) + */ + +public class FloatValueHolder { + private float mValue = 0.0f; + + /** + * Constructs a holder for a float value that is initialized to 0. + */ + public FloatValueHolder() { + } + + /** + * Constructs a holder for a float value that is initialized to the input value. + * + * @param value the value to initialize the value held in the FloatValueHolder + */ + public FloatValueHolder(float value) { + setValue(value); + } + + /** + * Sets the value held in the FloatValueHolder instance. + * + * @param value float value held in the FloatValueHolder instance + */ + public void setValue(float value) { + mValue = value; + } + + /** + * Returns the float value held in the FloatValueHolder instance. + * + * @return float value held in the FloatValueHolder instance + */ + public float getValue() { + return mValue; + } +} diff --git a/core/java/com/android/internal/dynamicanimation/animation/Force.java b/core/java/com/android/internal/dynamicanimation/animation/Force.java new file mode 100644 index 000000000000..fcb9c459fff3 --- /dev/null +++ b/core/java/com/android/internal/dynamicanimation/animation/Force.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.dynamicanimation.animation; + +/** + * Hide this for now, in case we want to change the API. + */ +interface Force { + // Acceleration based on position. + float getAcceleration(float position, float velocity); + + boolean isAtEquilibrium(float value, float velocity); +} diff --git a/core/java/com/android/internal/dynamicanimation/animation/SpringAnimation.java b/core/java/com/android/internal/dynamicanimation/animation/SpringAnimation.java new file mode 100644 index 000000000000..2f3b72c4f97d --- /dev/null +++ b/core/java/com/android/internal/dynamicanimation/animation/SpringAnimation.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.dynamicanimation.animation; + +import android.util.AndroidRuntimeException; +import android.util.FloatProperty; + +/** + * SpringAnimation is an animation that is driven by a {@link SpringForce}. The spring force defines + * the spring's stiffness, damping ratio, as well as the rest position. Once the SpringAnimation is + * started, on each frame the spring force will update the animation's value and velocity. + * The animation will continue to run until the spring force reaches equilibrium. If the spring used + * in the animation is undamped, the animation will never reach equilibrium. Instead, it will + * oscillate forever. + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * </div> + * + * <p>To create a simple {@link SpringAnimation} that uses the default {@link SpringForce}:</p> + * <pre class="prettyprint"> + * // Create an animation to animate view's X property, set the rest position of the + * // default spring to 0, and start the animation with a starting velocity of 5000 (pixel/s). + * final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.X, 0) + * .setStartVelocity(5000); + * anim.start(); + * </pre> + * + * <p>Alternatively, a {@link SpringAnimation} can take a pre-configured {@link SpringForce}, and + * use that to drive the animation. </p> + * <pre class="prettyprint"> + * // Create a low stiffness, low bounce spring at position 0. + * SpringForce spring = new SpringForce(0) + * .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) + * .setStiffness(SpringForce.STIFFNESS_LOW); + * // Create an animation to animate view's scaleY property, and start the animation using + * // the spring above and a starting value of 0.5. Additionally, constrain the range of value for + * // the animation to be non-negative, effectively preventing any spring overshoot. + * final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.SCALE_Y) + * .setMinValue(0).setSpring(spring).setStartValue(1); + * anim.start(); + * </pre> + */ +public final class SpringAnimation extends DynamicAnimation<SpringAnimation> { + + private SpringForce mSpring = null; + private float mPendingPosition = UNSET; + private static final float UNSET = Float.MAX_VALUE; + private boolean mEndRequested = false; + + /** + * <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During + * the animation, the {@link FloatValueHolder} instance will be updated via + * {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date + * animation value via {@link FloatValueHolder#getValue()}. + * + * <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via + * {@link FloatValueHolder#setValue(float)} outside of the animation during an + * animation run will not have any effect on the on-going animation. + * + * @param floatValueHolder the property to be animated + */ + public SpringAnimation(FloatValueHolder floatValueHolder) { + super(floatValueHolder); + } + + /** + * <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During + * the animation, the {@link FloatValueHolder} instance will be updated via + * {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date + * animation value via {@link FloatValueHolder#getValue()}. + * + * A Spring will be created with the given final position and default stiffness and damping + * ratio. This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}. + * + * <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via + * {@link FloatValueHolder#setValue(float)} outside of the animation during an + * animation run will not have any effect on the on-going animation. + * + * @param floatValueHolder the property to be animated + * @param finalPosition the final position of the spring to be created. + */ + public SpringAnimation(FloatValueHolder floatValueHolder, float finalPosition) { + super(floatValueHolder); + mSpring = new SpringForce(finalPosition); + } + + /** + * This creates a SpringAnimation that animates the property of the given object. + * Note, a spring will need to setup through {@link #setSpring(SpringForce)} before + * the animation starts. + * + * @param object the Object whose property will be animated + * @param property the property to be animated + * @param <K> the class on which the Property is declared + */ + public <K> SpringAnimation(K object, FloatProperty<K> property) { + super(object, property); + } + + /** + * This creates a SpringAnimation that animates the property of the given object. A Spring will + * be created with the given final position and default stiffness and damping ratio. + * This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}. + * + * @param object the Object whose property will be animated + * @param property the property to be animated + * @param finalPosition the final position of the spring to be created. + * @param <K> the class on which the Property is declared + */ + public <K> SpringAnimation(K object, FloatProperty<K> property, + float finalPosition) { + super(object, property); + mSpring = new SpringForce(finalPosition); + } + + /** + * Returns the spring that the animation uses for animations. + * + * @return the spring that the animation uses for animations + */ + public SpringForce getSpring() { + return mSpring; + } + + /** + * Uses the given spring as the force that drives this animation. If this spring force has its + * parameters re-configured during the animation, the new configuration will be reflected in the + * animation immediately. + * + * @param force a pre-defined spring force that drives the animation + * @return the animation that the spring force is set on + */ + public SpringAnimation setSpring(SpringForce force) { + mSpring = force; + return this; + } + + @Override + public void start() { + sanityCheck(); + mSpring.setValueThreshold(getValueThreshold()); + super.start(); + } + + /** + * Updates the final position of the spring. + * <p/> + * When the animation is running, calling this method would assume the position change of the + * spring as a continuous movement since last frame, which yields more accurate results than + * changing the spring position directly through {@link SpringForce#setFinalPosition(float)}. + * <p/> + * If the animation hasn't started, calling this method will change the spring position, and + * immediately start the animation. + * + * @param finalPosition rest position of the spring + */ + public void animateToFinalPosition(float finalPosition) { + if (isRunning()) { + mPendingPosition = finalPosition; + } else { + if (mSpring == null) { + mSpring = new SpringForce(finalPosition); + } + mSpring.setFinalPosition(finalPosition); + start(); + } + } + + /** + * Cancels the on-going animation. If the animation hasn't started, no op. Note that this method + * should only be called on main thread. + * + * @throws AndroidRuntimeException if this method is not called on the main thread + */ + @Override + public void cancel() { + super.cancel(); + if (mPendingPosition != UNSET) { + if (mSpring == null) { + mSpring = new SpringForce(mPendingPosition); + } else { + mSpring.setFinalPosition(mPendingPosition); + } + mPendingPosition = UNSET; + } + } + + /** + * Skips to the end of the animation. If the spring is undamped, an + * {@link IllegalStateException} will be thrown, as the animation would never reach to an end. + * It is recommended to check {@link #canSkipToEnd()} before calling this method. If animation + * is not running, no-op. + * + * Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler + * is created on the same thread as the first call to start/cancel an animation. All the + * subsequent animation lifecycle manipulations need to be on that same thread, until the + * AnimationHandler is reset (using [setAnimationHandler]). + * + * @throws IllegalStateException if the spring is undamped (i.e. damping ratio = 0) + * @throws AndroidRuntimeException if this method is not called on the same thread as the + * animation handler + */ + public void skipToEnd() { + if (!canSkipToEnd()) { + throw new UnsupportedOperationException("Spring animations can only come to an end" + + " when there is damping"); + } + if (!isCurrentThread()) { + throw new AndroidRuntimeException("Animations may only be started on the same thread " + + "as the animation handler"); + } + if (mRunning) { + mEndRequested = true; + } + } + + /** + * Queries whether the spring can eventually come to the rest position. + * + * @return {@code true} if the spring is damped, otherwise {@code false} + */ + public boolean canSkipToEnd() { + return mSpring.mDampingRatio > 0; + } + + /************************ Below are private APIs *************************/ + + private void sanityCheck() { + if (mSpring == null) { + throw new UnsupportedOperationException("Incomplete SpringAnimation: Either final" + + " position or a spring force needs to be set."); + } + double finalPosition = mSpring.getFinalPosition(); + if (finalPosition > mMaxValue) { + throw new UnsupportedOperationException("Final position of the spring cannot be greater" + + " than the max value."); + } else if (finalPosition < mMinValue) { + throw new UnsupportedOperationException("Final position of the spring cannot be less" + + " than the min value."); + } + } + + @Override + boolean updateValueAndVelocity(long deltaT) { + // If user had requested end, then update the value and velocity to end state and consider + // animation done. + if (mEndRequested) { + if (mPendingPosition != UNSET) { + mSpring.setFinalPosition(mPendingPosition); + mPendingPosition = UNSET; + } + mValue = mSpring.getFinalPosition(); + mVelocity = 0; + mEndRequested = false; + return true; + } + + if (mPendingPosition != UNSET) { + // Approximate by considering half of the time spring position stayed at the old + // position, half of the time it's at the new position. + MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT / 2); + mSpring.setFinalPosition(mPendingPosition); + mPendingPosition = UNSET; + + massState = mSpring.updateValues(massState.mValue, massState.mVelocity, deltaT / 2); + mValue = massState.mValue; + mVelocity = massState.mVelocity; + + } else { + MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT); + mValue = massState.mValue; + mVelocity = massState.mVelocity; + } + + mValue = Math.max(mValue, mMinValue); + mValue = Math.min(mValue, mMaxValue); + + if (isAtEquilibrium(mValue, mVelocity)) { + mValue = mSpring.getFinalPosition(); + mVelocity = 0f; + return true; + } + return false; + } + + @Override + float getAcceleration(float value, float velocity) { + return mSpring.getAcceleration(value, velocity); + } + + @Override + boolean isAtEquilibrium(float value, float velocity) { + return mSpring.isAtEquilibrium(value, velocity); + } + + @Override + void setValueThreshold(float threshold) { + } +} diff --git a/core/java/com/android/internal/dynamicanimation/animation/SpringForce.java b/core/java/com/android/internal/dynamicanimation/animation/SpringForce.java new file mode 100644 index 000000000000..36242ae2cf3d --- /dev/null +++ b/core/java/com/android/internal/dynamicanimation/animation/SpringForce.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.dynamicanimation.animation; + +import android.annotation.FloatRange; + +/** + * Spring Force defines the characteristics of the spring being used in the animation. + * <p> + * By configuring the stiffness and damping ratio, callers can create a spring with the look and + * feel suits their use case. Stiffness corresponds to the spring constant. The stiffer the spring + * is, the harder it is to stretch it, the faster it undergoes dampening. + * <p> + * Spring damping ratio describes how oscillations in a system decay after a disturbance. + * When damping ratio > 1* (i.e. over-damped), the object will quickly return to the rest position + * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will + * return to equilibrium within the shortest amount of time. When damping ratio is less than 1 + * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without any + * damping (i.e. damping ratio = 0), the mass will oscillate forever. + */ +public final class SpringForce implements Force { + /** + * Stiffness constant for extremely stiff spring. + */ + public static final float STIFFNESS_HIGH = 10_000f; + /** + * Stiffness constant for medium stiff spring. This is the default stiffness for spring force. + */ + public static final float STIFFNESS_MEDIUM = 1500f; + /** + * Stiffness constant for a spring with low stiffness. + */ + public static final float STIFFNESS_LOW = 200f; + /** + * Stiffness constant for a spring with very low stiffness. + */ + public static final float STIFFNESS_VERY_LOW = 50f; + + /** + * Damping ratio for a very bouncy spring. Note for under-damped springs + * (i.e. damping ratio < 1), the lower the damping ratio, the more bouncy the spring. + */ + public static final float DAMPING_RATIO_HIGH_BOUNCY = 0.2f; + /** + * Damping ratio for a medium bouncy spring. This is also the default damping ratio for spring + * force. Note for under-damped springs (i.e. damping ratio < 1), the lower the damping ratio, + * the more bouncy the spring. + */ + public static final float DAMPING_RATIO_MEDIUM_BOUNCY = 0.5f; + /** + * Damping ratio for a spring with low bounciness. Note for under-damped springs + * (i.e. damping ratio < 1), the lower the damping ratio, the higher the bounciness. + */ + public static final float DAMPING_RATIO_LOW_BOUNCY = 0.75f; + /** + * Damping ratio for a spring with no bounciness. This damping ratio will create a critically + * damped spring that returns to equilibrium within the shortest amount of time without + * oscillating. + */ + public static final float DAMPING_RATIO_NO_BOUNCY = 1f; + + // This multiplier is used to calculate the velocity threshold given a certain value threshold. + // The idea is that if it takes >= 1 frame to move the value threshold amount, then the velocity + // is a reasonable threshold. + private static final double VELOCITY_THRESHOLD_MULTIPLIER = 1000.0 / 16.0; + + // Natural frequency + double mNaturalFreq = Math.sqrt(STIFFNESS_MEDIUM); + // Damping ratio. + double mDampingRatio = DAMPING_RATIO_MEDIUM_BOUNCY; + + // Value to indicate an unset state. + private static final double UNSET = Double.MAX_VALUE; + + // Indicates whether the spring has been initialized + private boolean mInitialized = false; + + // Threshold for velocity and value to determine when it's reasonable to assume that the spring + // is approximately at rest. + private double mValueThreshold; + private double mVelocityThreshold; + + // Intermediate values to simplify the spring function calculation per frame. + private double mGammaPlus; + private double mGammaMinus; + private double mDampedFreq; + + // Final position of the spring. This must be set before the start of the animation. + private double mFinalPosition = UNSET; + + // Internal state to hold a value/velocity pair. + private final DynamicAnimation.MassState mMassState = new DynamicAnimation.MassState(); + + /** + * Creates a spring force. Note that final position of the spring must be set through + * {@link #setFinalPosition(float)} before the spring animation starts. + */ + public SpringForce() { + // No op. + } + + /** + * Creates a spring with a given final rest position. + * + * @param finalPosition final position of the spring when it reaches equilibrium + */ + public SpringForce(float finalPosition) { + mFinalPosition = finalPosition; + } + + /** + * Sets the stiffness of a spring. The more stiff a spring is, the more force it applies to + * the object attached when the spring is not at the final position. Default stiffness is + * {@link #STIFFNESS_MEDIUM}. + * + * @param stiffness non-negative stiffness constant of a spring + * @return the spring force that the given stiffness is set on + * @throws IllegalArgumentException if the given spring stiffness is not positive + */ + public SpringForce setStiffness( + @FloatRange(from = 0.0, fromInclusive = false) float stiffness) { + if (stiffness <= 0) { + throw new IllegalArgumentException("Spring stiffness constant must be positive."); + } + mNaturalFreq = Math.sqrt(stiffness); + // All the intermediate values need to be recalculated. + mInitialized = false; + return this; + } + + /** + * Gets the stiffness of the spring. + * + * @return the stiffness of the spring + */ + public float getStiffness() { + return (float) (mNaturalFreq * mNaturalFreq); + } + + /** + * Spring damping ratio describes how oscillations in a system decay after a disturbance. + * <p> + * When damping ratio > 1 (over-damped), the object will quickly return to the rest position + * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will + * return to equilibrium within the shortest amount of time. When damping ratio is less than 1 + * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without + * any damping (i.e. damping ratio = 0), the mass will oscillate forever. + * <p> + * Default damping ratio is {@link #DAMPING_RATIO_MEDIUM_BOUNCY}. + * + * @param dampingRatio damping ratio of the spring, it should be non-negative + * @return the spring force that the given damping ratio is set on + * @throws IllegalArgumentException if the {@param dampingRatio} is negative. + */ + public SpringForce setDampingRatio(@FloatRange(from = 0.0) float dampingRatio) { + if (dampingRatio < 0) { + throw new IllegalArgumentException("Damping ratio must be non-negative"); + } + mDampingRatio = dampingRatio; + // All the intermediate values need to be recalculated. + mInitialized = false; + return this; + } + + /** + * Returns the damping ratio of the spring. + * + * @return damping ratio of the spring + */ + public float getDampingRatio() { + return (float) mDampingRatio; + } + + /** + * Sets the rest position of the spring. + * + * @param finalPosition rest position of the spring + * @return the spring force that the given final position is set on + */ + public SpringForce setFinalPosition(float finalPosition) { + mFinalPosition = finalPosition; + return this; + } + + /** + * Returns the rest position of the spring. + * + * @return rest position of the spring + */ + public float getFinalPosition() { + return (float) mFinalPosition; + } + + /*********************** Below are private APIs *********************/ + + @Override + public float getAcceleration(float lastDisplacement, float lastVelocity) { + + lastDisplacement -= getFinalPosition(); + + double k = mNaturalFreq * mNaturalFreq; + double c = 2 * mNaturalFreq * mDampingRatio; + + return (float) (-k * lastDisplacement - c * lastVelocity); + } + + @Override + public boolean isAtEquilibrium(float value, float velocity) { + if (Math.abs(velocity) < mVelocityThreshold + && Math.abs(value - getFinalPosition()) < mValueThreshold) { + return true; + } + return false; + } + + /** + * Initialize the string by doing the necessary pre-calculation as well as some sanity check + * on the setup. + * + * @throws IllegalStateException if the final position is not yet set by the time the spring + * animation has started + */ + private void init() { + if (mInitialized) { + return; + } + + if (mFinalPosition == UNSET) { + throw new IllegalStateException("Error: Final position of the spring must be" + + " set before the animation starts"); + } + + if (mDampingRatio > 1) { + // Over damping + mGammaPlus = -mDampingRatio * mNaturalFreq + + mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1); + mGammaMinus = -mDampingRatio * mNaturalFreq + - mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1); + } else if (mDampingRatio >= 0 && mDampingRatio < 1) { + // Under damping + mDampedFreq = mNaturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio); + } + + mInitialized = true; + } + + /** + * Internal only call for Spring to calculate the spring position/velocity using + * an analytical approach. + */ + DynamicAnimation.MassState updateValues(double lastDisplacement, double lastVelocity, + long timeElapsed) { + init(); + + double deltaT = timeElapsed / 1000d; // unit: seconds + lastDisplacement -= mFinalPosition; + double displacement; + double currentVelocity; + if (mDampingRatio > 1) { + // Overdamped + double coeffA = lastDisplacement - (mGammaMinus * lastDisplacement - lastVelocity) + / (mGammaMinus - mGammaPlus); + double coeffB = (mGammaMinus * lastDisplacement - lastVelocity) + / (mGammaMinus - mGammaPlus); + displacement = coeffA * Math.pow(Math.E, mGammaMinus * deltaT) + + coeffB * Math.pow(Math.E, mGammaPlus * deltaT); + currentVelocity = coeffA * mGammaMinus * Math.pow(Math.E, mGammaMinus * deltaT) + + coeffB * mGammaPlus * Math.pow(Math.E, mGammaPlus * deltaT); + } else if (mDampingRatio == 1) { + // Critically damped + double coeffA = lastDisplacement; + double coeffB = lastVelocity + mNaturalFreq * lastDisplacement; + displacement = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT); + currentVelocity = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT) + * -mNaturalFreq + coeffB * Math.pow(Math.E, -mNaturalFreq * deltaT); + } else { + // Underdamped + double cosCoeff = lastDisplacement; + double sinCoeff = (1 / mDampedFreq) * (mDampingRatio * mNaturalFreq + * lastDisplacement + lastVelocity); + displacement = Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT) + * (cosCoeff * Math.cos(mDampedFreq * deltaT) + + sinCoeff * Math.sin(mDampedFreq * deltaT)); + currentVelocity = displacement * -mNaturalFreq * mDampingRatio + + Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT) + * (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT) + + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT)); + } + + mMassState.mValue = (float) (displacement + mFinalPosition); + mMassState.mVelocity = (float) currentVelocity; + return mMassState; + } + + /** + * This threshold defines how close the animation value needs to be before the animation can + * finish. This default value is based on the property being animated, e.g. animations on alpha, + * scale, translation or rotation would have different thresholds. This value should be small + * enough to avoid visual glitch of "jumping to the end". But it shouldn't be so small that + * animations take seconds to finish. + * + * @param threshold the difference between the animation value and final spring position that + * is allowed to end the animation when velocity is very low + */ + void setValueThreshold(double threshold) { + mValueThreshold = Math.abs(threshold); + mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER; + } +} diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 5de72409133d..a05062b43d60 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -45,6 +45,7 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_ENTER_TRANSITION; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_EXIT_TRANSITION; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF_SHOW_AOD; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_PAGE_SCROLL; @@ -223,6 +224,7 @@ public class InteractionJankMonitor { public static final int CUJ_SHADE_CLEAR_ALL = 62; public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = 63; public static final int CUJ_LOCKSCREEN_OCCLUSION = 64; + public static final int CUJ_RECENTS_SCROLLING = 65; private static final int NO_STATSD_LOGGING = -1; @@ -296,6 +298,7 @@ public class InteractionJankMonitor { UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING, }; private static volatile InteractionJankMonitor sInstance; @@ -380,7 +383,8 @@ public class InteractionJankMonitor { CUJ_TASKBAR_COLLAPSE, CUJ_SHADE_CLEAR_ALL, CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, - CUJ_LOCKSCREEN_OCCLUSION + CUJ_LOCKSCREEN_OCCLUSION, + CUJ_RECENTS_SCROLLING }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -893,6 +897,8 @@ public class InteractionJankMonitor { return "LAUNCHER_UNLOCK_ENTRANCE_ANIMATION"; case CUJ_LOCKSCREEN_OCCLUSION: return "LOCKSCREEN_OCCLUSION"; + case CUJ_RECENTS_SCROLLING: + return "RECENTS_SCROLLING"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index cc7150be57b5..f44b848cdeba 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -167,7 +167,7 @@ public class BatteryStatsImpl extends BatteryStats { private static final int MAGIC = 0xBA757475; // 'BATSTATS' // Current on-disk Parcel version. Must be updated when the format of the parcelable changes - public static final int VERSION = 209; + public static final int VERSION = 210; // The maximum number of names wakelocks we will keep track of // per uid; once the limit is reached, we batch the remaining wakelocks @@ -1348,6 +1348,13 @@ public class BatteryStatsImpl extends BatteryStats { LongSamplingCounter mMobileRadioActiveUnknownTime; LongSamplingCounter mMobileRadioActiveUnknownCount; + /** + * The soonest the Mobile Radio stats can be updated due to a mobile radio power state change + * after it was last updated. + */ + @VisibleForTesting + protected static final long MOBILE_RADIO_POWER_STATE_UPDATE_FREQ_MS = 1000 * 60 * 10; + int mWifiRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW; @GuardedBy("this") @@ -6167,12 +6174,13 @@ public class BatteryStatsImpl extends BatteryStats { @UnsupportedAppUsage @GuardedBy("this") - public void noteUserActivityLocked(int uid, int event) { + public void noteUserActivityLocked(int uid, @PowerManager.UserActivityEvent int event) { noteUserActivityLocked(uid, event, mClock.elapsedRealtime(), mClock.uptimeMillis()); } @GuardedBy("this") - public void noteUserActivityLocked(int uid, int event, long elapsedRealtimeMs, long uptimeMs) { + public void noteUserActivityLocked(int uid, @PowerManager.UserActivityEvent int event, + long elapsedRealtimeMs, long uptimeMs) { if (mOnBatteryInternal) { uid = mapUid(uid); getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs).noteUserActivityLocked(event); @@ -6281,6 +6289,15 @@ public class BatteryStatsImpl extends BatteryStats { } else { mMobileRadioActiveTimer.stopRunningLocked(realElapsedRealtimeMs); mMobileRadioActivePerAppTimer.stopRunningLocked(realElapsedRealtimeMs); + + if (mLastModemActivityInfo != null) { + if (elapsedRealtimeMs < mLastModemActivityInfo.getTimestampMillis() + + MOBILE_RADIO_POWER_STATE_UPDATE_FREQ_MS) { + // Modem Activity info has been collected recently, don't bother + // triggering another update. + return false; + } + } // Tell the caller to collect radio network/power stats. return true; } @@ -9995,14 +10012,14 @@ public class BatteryStatsImpl extends BatteryStats { } @Override - public void noteUserActivityLocked(int type) { + public void noteUserActivityLocked(@PowerManager.UserActivityEvent int event) { if (mUserActivityCounters == null) { initUserActivityLocked(); } - if (type >= 0 && type < NUM_USER_ACTIVITY_TYPES) { - mUserActivityCounters[type].stepAtomic(); + if (event >= 0 && event < NUM_USER_ACTIVITY_TYPES) { + mUserActivityCounters[event].stepAtomic(); } else { - Slog.w(TAG, "Unknown user activity type " + type + " was specified.", + Slog.w(TAG, "Unknown user activity type " + event + " was specified.", new Throwable()); } } diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 44cfe1aa4a79..1d4b246de5c8 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -322,4 +322,7 @@ oneway interface IStatusBar /** Unregisters a nearby media devices provider. */ void unregisterNearbyMediaDevicesProvider(in INearbyMediaDevicesProvider provider); + + /** Dump protos from SystemUI. The proto definition is defined there */ + void dumpProto(in String[] args, in ParcelFileDescriptor pfd); } diff --git a/core/java/com/android/internal/util/ObservableServiceConnection.java b/core/java/com/android/internal/util/ObservableServiceConnection.java new file mode 100644 index 000000000000..3165d293bd91 --- /dev/null +++ b/core/java/com/android/internal/util/ObservableServiceConnection.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util; + +import android.annotation.CallbackExecutor; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.CallbackRegistry.NotifierCallback; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; + +/** + * {@link ObservableServiceConnection} is a concrete implementation of {@link ServiceConnection} + * that enables monitoring the status of a binder connection. It also aides in automatically + * converting a proxy into an internal wrapper type. + * + * @param <T> The type of the wrapper over the resulting service. + */ +public class ObservableServiceConnection<T> implements ServiceConnection { + /** + * An interface for converting the service proxy into a given internal wrapper type. + * + * @param <T> The type of the wrapper over the resulting service. + */ + public interface ServiceTransformer<T> { + /** + * Called to convert the service proxy to the wrapper type. + * + * @param service The service proxy to create the wrapper type from. + * @return The wrapper type. + */ + T convert(IBinder service); + } + + /** + * An interface for listening to the connection status. + * + * @param <T> The wrapper type. + */ + public interface Callback<T> { + /** + * Invoked when the service has been successfully connected to. + * + * @param connection The {@link ObservableServiceConnection} instance that is now connected + * @param service The service proxy converted into the typed wrapper. + */ + void onConnected(ObservableServiceConnection<T> connection, T service); + + /** + * Invoked when the service has been disconnected. + * + * @param connection The {@link ObservableServiceConnection} that is now disconnected. + * @param reason The reason for the disconnection. + */ + void onDisconnected(ObservableServiceConnection<T> connection, + @DisconnectReason int reason); + } + + /** + * Default state, service has not yet disconnected. + */ + public static final int DISCONNECT_REASON_NONE = 0; + /** + * Disconnection was due to the resulting binding being {@code null}. + */ + public static final int DISCONNECT_REASON_NULL_BINDING = 1; + /** + * Disconnection was due to the remote end disconnecting. + */ + public static final int DISCONNECT_REASON_DISCONNECTED = 2; + /** + * Disconnection due to the binder dying. + */ + public static final int DISCONNECT_REASON_BINDING_DIED = 3; + /** + * Disconnection from an explicit unbinding. + */ + public static final int DISCONNECT_REASON_UNBIND = 4; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DISCONNECT_REASON_NONE, + DISCONNECT_REASON_NULL_BINDING, + DISCONNECT_REASON_DISCONNECTED, + DISCONNECT_REASON_BINDING_DIED, + DISCONNECT_REASON_UNBIND + }) + public @interface DisconnectReason { + } + + private final Object mLock = new Object(); + private final Context mContext; + private final Executor mExecutor; + private final ServiceTransformer<T> mTransformer; + private final Intent mServiceIntent; + private final int mFlags; + + @GuardedBy("mLock") + private T mService; + @GuardedBy("mLock") + private boolean mBoundCalled = false; + @GuardedBy("mLock") + private int mLastDisconnectReason = DISCONNECT_REASON_NONE; + + private final CallbackRegistry<Callback<T>, ObservableServiceConnection<T>, T> + mCallbackRegistry = new CallbackRegistry<>( + new NotifierCallback<Callback<T>, ObservableServiceConnection<T>, T>() { + @Override + public void onNotifyCallback(Callback<T> callback, + ObservableServiceConnection<T> sender, + int disconnectReason, T service) { + mExecutor.execute(() -> { + synchronized (mLock) { + if (service != null) { + callback.onConnected(sender, service); + } else if (mLastDisconnectReason != DISCONNECT_REASON_NONE) { + callback.onDisconnected(sender, disconnectReason); + } + } + }); + } + }); + + /** + * Default constructor for {@link ObservableServiceConnection}. + * + * @param context The context from which the service will be bound with. + * @param executor The executor for connection callbacks to be delivered on + * @param transformer A {@link ObservableServiceConnection.ServiceTransformer} for transforming + * the resulting service into a desired type. + */ + public ObservableServiceConnection(@NonNull Context context, + @NonNull @CallbackExecutor Executor executor, + @NonNull ServiceTransformer<T> transformer, + Intent serviceIntent, + int flags) { + mContext = context; + mExecutor = executor; + mTransformer = transformer; + mServiceIntent = serviceIntent; + mFlags = flags; + } + + /** + * Initiate binding to the service. + * + * @return {@code true} if initiating binding succeed, {@code false} if the binding failed or + * if this service is already bound. Regardless of the return value, you should later call + * {@link #unbind()} to release the connection. + */ + public boolean bind() { + synchronized (mLock) { + if (mBoundCalled) { + return false; + } + final boolean bindResult = + mContext.bindService(mServiceIntent, mFlags, mExecutor, this); + mBoundCalled = true; + return bindResult; + } + } + + /** + * Disconnect from the service if bound. + */ + public void unbind() { + onDisconnected(DISCONNECT_REASON_UNBIND); + } + + /** + * Adds a callback for receiving connection updates. + * + * @param callback The {@link Callback} to receive future updates. + */ + public void addCallback(Callback<T> callback) { + mCallbackRegistry.add(callback); + mExecutor.execute(() -> { + synchronized (mLock) { + if (mService != null) { + callback.onConnected(this, mService); + } else if (mLastDisconnectReason != DISCONNECT_REASON_NONE) { + callback.onDisconnected(this, mLastDisconnectReason); + } + } + }); + } + + /** + * Removes previously added callback from receiving future connection updates. + * + * @param callback The {@link Callback} to be removed. + */ + public void removeCallback(Callback<T> callback) { + synchronized (mLock) { + mCallbackRegistry.remove(callback); + } + } + + private void onDisconnected(@DisconnectReason int reason) { + synchronized (mLock) { + if (!mBoundCalled) { + return; + } + mBoundCalled = false; + mLastDisconnectReason = reason; + mContext.unbindService(this); + mService = null; + mCallbackRegistry.notifyCallbacks(this, reason, null); + } + } + + @Override + public final void onServiceConnected(ComponentName name, IBinder service) { + synchronized (mLock) { + mService = mTransformer.convert(service); + mLastDisconnectReason = DISCONNECT_REASON_NONE; + mCallbackRegistry.notifyCallbacks(this, mLastDisconnectReason, mService); + } + } + + @Override + public final void onServiceDisconnected(ComponentName name) { + onDisconnected(DISCONNECT_REASON_DISCONNECTED); + } + + @Override + public final void onBindingDied(ComponentName name) { + onDisconnected(DISCONNECT_REASON_BINDING_DIED); + } + + @Override + public final void onNullBinding(ComponentName name) { + onDisconnected(DISCONNECT_REASON_NULL_BINDING); + } +} diff --git a/core/java/com/android/internal/util/PersistentServiceConnection.java b/core/java/com/android/internal/util/PersistentServiceConnection.java new file mode 100644 index 000000000000..d2017347bd64 --- /dev/null +++ b/core/java/com/android/internal/util/PersistentServiceConnection.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util; + +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.SystemClock; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.concurrent.Executor; + +/** + * {@link PersistentServiceConnection} is a concrete implementation of {@link ServiceConnection} + * that maintains the binder connection by handling reconnection when a failure occurs. + * + * @param <T> The transformed connection type handled by the service. + * + * When the target process is killed (by OOM-killer, force-stopped, crash, etc..) then this class + * will trigger a reconnection to the target. This should be used carefully. + * + * NOTE: This class does *not* handle package-updates -- i.e. even if the binding dies due to + * the target package being updated, this class won't reconnect. This is because this class doesn't + * know what to do when the service component has gone missing, for example. If the user of this + * class wants to restore the connection, then it should call {@link #unbind()} and {@link #bind} + * explicitly. + */ +public class PersistentServiceConnection<T> extends ObservableServiceConnection<T> { + private final Callback<T> mConnectionCallback = new Callback<T>() { + private long mConnectedTime; + + @Override + public void onConnected(ObservableServiceConnection<T> connection, T service) { + mConnectedTime = mInjector.uptimeMillis(); + } + + @Override + public void onDisconnected(ObservableServiceConnection<T> connection, + @DisconnectReason int reason) { + if (reason == DISCONNECT_REASON_UNBIND) return; + synchronized (mLock) { + if ((mInjector.uptimeMillis() - mConnectedTime) > mMinConnectionDurationMs) { + mReconnectAttempts = 0; + bindInternalLocked(); + } else { + scheduleConnectionAttemptLocked(); + } + } + } + }; + + private final Object mLock = new Object(); + private final Injector mInjector; + private final Handler mHandler; + private final int mMinConnectionDurationMs; + private final int mMaxReconnectAttempts; + private final int mBaseReconnectDelayMs; + @GuardedBy("mLock") + private int mReconnectAttempts; + @GuardedBy("mLock") + private Object mCancelToken; + + private final Runnable mConnectRunnable = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + mCancelToken = null; + bindInternalLocked(); + } + } + }; + + /** + * Default constructor for {@link PersistentServiceConnection}. + * + * @param context The context from which the service will be bound with. + * @param executor The executor for connection callbacks to be delivered on + * @param transformer A {@link ServiceTransformer} for transforming + */ + public PersistentServiceConnection(Context context, + Executor executor, + Handler handler, + ServiceTransformer<T> transformer, + Intent serviceIntent, + int flags, + int minConnectionDurationMs, + int maxReconnectAttempts, + int baseReconnectDelayMs) { + this(context, + executor, + handler, + transformer, + serviceIntent, + flags, + minConnectionDurationMs, + maxReconnectAttempts, + baseReconnectDelayMs, + new Injector()); + } + + @VisibleForTesting + public PersistentServiceConnection( + Context context, + Executor executor, + Handler handler, + ServiceTransformer<T> transformer, + Intent serviceIntent, + int flags, + int minConnectionDurationMs, + int maxReconnectAttempts, + int baseReconnectDelayMs, + Injector injector) { + super(context, executor, transformer, serviceIntent, flags); + mHandler = handler; + mMinConnectionDurationMs = minConnectionDurationMs; + mMaxReconnectAttempts = maxReconnectAttempts; + mBaseReconnectDelayMs = baseReconnectDelayMs; + mInjector = injector; + } + + /** {@inheritDoc} */ + @Override + public boolean bind() { + synchronized (mLock) { + addCallback(mConnectionCallback); + mReconnectAttempts = 0; + return bindInternalLocked(); + } + } + + @GuardedBy("mLock") + private boolean bindInternalLocked() { + return super.bind(); + } + + /** {@inheritDoc} */ + @Override + public void unbind() { + synchronized (mLock) { + removeCallback(mConnectionCallback); + cancelPendingConnectionAttemptLocked(); + super.unbind(); + } + } + + @GuardedBy("mLock") + private void cancelPendingConnectionAttemptLocked() { + if (mCancelToken != null) { + mHandler.removeCallbacksAndMessages(mCancelToken); + mCancelToken = null; + } + } + + @GuardedBy("mLock") + private void scheduleConnectionAttemptLocked() { + cancelPendingConnectionAttemptLocked(); + + if (mReconnectAttempts >= mMaxReconnectAttempts) { + return; + } + + final long reconnectDelayMs = + (long) Math.scalb(mBaseReconnectDelayMs, mReconnectAttempts); + + mCancelToken = new Object(); + mHandler.postDelayed(mConnectRunnable, mCancelToken, reconnectDelayMs); + mReconnectAttempts++; + } + + /** + * Injector for testing + */ + @VisibleForTesting + public static class Injector { + /** + * Returns milliseconds since boot, not counting time spent in deep sleep. Can be overridden + * in tests with a fake clock. + */ + public long uptimeMillis() { + return SystemClock.uptimeMillis(); + } + } +} diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index a4da8de44880..1235b602cde9 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -1522,8 +1522,7 @@ public class LockPatternUtils { STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, STRONG_AUTH_REQUIRED_AFTER_TIMEOUT, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, - STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT, - SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED}) + STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT}) @Retention(RetentionPolicy.SOURCE) public @interface StrongAuthFlags {} @@ -1576,12 +1575,6 @@ public class LockPatternUtils { public static final int STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT = 0x80; /** - * Some authentication is required because the trustagent either timed out or was disabled - * manually. - */ - public static final int SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED = 0x100; - - /** * Strong auth flags that do not prevent biometric methods from being accepted as auth. * If any other flags are set, biometric authentication is disabled. */ diff --git a/core/java/com/android/internal/widget/ResolverDrawerLayout.java b/core/java/com/android/internal/widget/ResolverDrawerLayout.java index af9c5a5cc0d5..52ffc984c41e 100644 --- a/core/java/com/android/internal/widget/ResolverDrawerLayout.java +++ b/core/java/com/android/internal/widget/ResolverDrawerLayout.java @@ -17,6 +17,9 @@ package com.android.internal.widget; +import static android.content.res.Resources.ID_NULL; + +import android.annotation.IdRes; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -96,6 +99,8 @@ public class ResolverDrawerLayout extends ViewGroup { private int mTopOffset; private boolean mShowAtTop; + @IdRes + private int mIgnoreOffsetTopLimitViewId = ID_NULL; private boolean mIsDragging; private boolean mOpenOnClick; @@ -156,6 +161,10 @@ public class ResolverDrawerLayout extends ViewGroup { mIsMaxCollapsedHeightSmallExplicit = a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall); mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false); + if (a.hasValue(R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit)) { + mIgnoreOffsetTopLimitViewId = a.getResourceId( + R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL); + } a.recycle(); mScrollIndicatorDrawable = mContext.getDrawable(R.drawable.scroll_indicator_material); @@ -577,12 +586,32 @@ public class ResolverDrawerLayout extends ViewGroup { dy -= 1.0f; } + boolean isIgnoreOffsetLimitSet = false; + int ignoreOffsetLimit = 0; + View ignoreOffsetLimitView = findIgnoreOffsetLimitView(); + if (ignoreOffsetLimitView != null) { + LayoutParams lp = (LayoutParams) ignoreOffsetLimitView.getLayoutParams(); + ignoreOffsetLimit = ignoreOffsetLimitView.getBottom() + lp.bottomMargin; + isIgnoreOffsetLimitSet = true; + } final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.ignoreOffset) { child.offsetTopAndBottom((int) dy); + } else if (isIgnoreOffsetLimitSet) { + int top = child.getTop(); + int targetTop = Math.max( + (int) (ignoreOffsetLimit + lp.topMargin + dy), + lp.mFixedTop); + if (top != targetTop) { + child.offsetTopAndBottom(targetTop - top); + } + ignoreOffsetLimit = child.getBottom() + lp.bottomMargin; } } final boolean isCollapsedOld = mCollapseOffset != 0; @@ -1024,6 +1053,8 @@ public class ResolverDrawerLayout extends ViewGroup { final int rightEdge = width - getPaddingRight(); final int widthAvailable = rightEdge - leftEdge; + boolean isIgnoreOffsetLimitSet = false; + int ignoreOffsetLimit = 0; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); @@ -1036,9 +1067,24 @@ public class ResolverDrawerLayout extends ViewGroup { continue; } + if (mIgnoreOffsetTopLimitViewId != ID_NULL && !isIgnoreOffsetLimitSet) { + if (mIgnoreOffsetTopLimitViewId == child.getId()) { + ignoreOffsetLimit = child.getBottom() + lp.bottomMargin; + isIgnoreOffsetLimitSet = true; + } + } + int top = ypos + lp.topMargin; if (lp.ignoreOffset) { - top -= mCollapseOffset; + if (!isDragging()) { + lp.mFixedTop = (int) (top - mCollapseOffset); + } + if (isIgnoreOffsetLimitSet) { + top = Math.max(ignoreOffsetLimit + lp.topMargin, (int) (top - mCollapseOffset)); + ignoreOffsetLimit = top + child.getMeasuredHeight() + lp.bottomMargin; + } else { + top -= mCollapseOffset; + } } final int bottom = top + child.getMeasuredHeight(); @@ -1102,11 +1148,23 @@ public class ResolverDrawerLayout extends ViewGroup { mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved; } + private View findIgnoreOffsetLimitView() { + if (mIgnoreOffsetTopLimitViewId == ID_NULL) { + return null; + } + View v = findViewById(mIgnoreOffsetTopLimitViewId); + if (v != null && v != this && v.getParent() == this && v.getVisibility() != View.GONE) { + return v; + } + return null; + } + public static class LayoutParams extends MarginLayoutParams { public boolean alwaysShow; public boolean ignoreOffset; public boolean hasNestedScrollIndicator; public int maxHeight; + int mFixedTop; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); diff --git a/core/proto/android/os/processstarttime.proto b/core/proto/android/os/processstarttime.proto deleted file mode 100644 index d0f8baee7da2..000000000000 --- a/core/proto/android/os/processstarttime.proto +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -syntax = "proto2"; -package android.os; - -option java_multiple_files = true; - -// This message is used for statsd logging and should be kept in sync with -// frameworks/proto_logging/stats/atoms.proto -/** - * Logs information about process start time. - * - * Logged from: - * frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java - */ -message ProcessStartTime { - // The uid of the ProcessRecord. - optional int32 uid = 1; - - // The process pid. - optional int32 pid = 2; - - // The process name. - // Usually package name, "system" for system server. - // Provided by ActivityManagerService. - optional string process_name = 3; - - enum StartType { - UNKNOWN = 0; - WARM = 1; - HOT = 2; - COLD = 3; - } - - // The start type. - optional StartType type = 4; - - // The elapsed realtime at the start of the process. - optional int64 process_start_time_millis = 5; - - // Number of milliseconds it takes to reach bind application. - optional int32 bind_application_delay_millis = 6; - - // Number of milliseconds it takes to finish start of the process. - optional int32 process_start_delay_millis = 7; - - // hostingType field in ProcessRecord, the component type such as "activity", - // "service", "content provider", "broadcast" or other strings. - optional string hosting_type = 8; - - // hostingNameStr field in ProcessRecord. The component class name that runs - // in this process. - optional string hosting_name = 9; - - // Broadcast action name. - optional string broadcast_action_name = 10; - - enum HostingTypeId { - HOSTING_TYPE_UNKNOWN = 0; - HOSTING_TYPE_ACTIVITY = 1; - HOSTING_TYPE_ADDED_APPLICATION = 2; - HOSTING_TYPE_BACKUP = 3; - HOSTING_TYPE_BROADCAST = 4; - HOSTING_TYPE_CONTENT_PROVIDER = 5; - HOSTING_TYPE_LINK_FAIL = 6; - HOSTING_TYPE_ON_HOLD = 7; - HOSTING_TYPE_NEXT_ACTIVITY = 8; - HOSTING_TYPE_NEXT_TOP_ACTIVITY = 9; - HOSTING_TYPE_RESTART = 10; - HOSTING_TYPE_SERVICE = 11; - HOSTING_TYPE_SYSTEM = 12; - HOSTING_TYPE_TOP_ACTIVITY = 13; - HOSTING_TYPE_EMPTY = 14; - } - - optional HostingTypeId hosting_type_id = 11; -} - diff --git a/core/res/Android.bp b/core/res/Android.bp index 93ce7832824b..7e17840445ab 100644 --- a/core/res/Android.bp +++ b/core/res/Android.bp @@ -130,6 +130,10 @@ android_app { // Allow overlay to add resource "--auto-add-overlay", + + // Framework resources benefit tremendously from enabling sparse encoding, saving tens + // of MBs in size and RAM use. + "--enable-sparse-encoding", ], resource_zips: [ diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 83bf4f057264..26ba816e556b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1035,25 +1035,38 @@ android:priority="900" /> <!-- Allows an application to read from external storage. - <p>Any app that declares the {@link #WRITE_EXTERNAL_STORAGE} permission is implicitly - granted this permission.</p> + <p class="note"><strong>Note: </strong>Starting in API level 33, this permission has no + effect. If your app accesses other apps' media files, request one or more of these permissions + instead: <a href="#READ_MEDIA_IMAGES"><code>READ_MEDIA_IMAGES</code></a>, + <a href="#READ_MEDIA_VIDEO"><code>READ_MEDIA_VIDEO</code></a>, + <a href="#READ_MEDIA_AUDIO"><code>READ_MEDIA_AUDIO</code></a>. Learn more about the + <a href="{@docRoot}training/data-storage/shared/media#storage-permission">storage + permissions</a> that are associated with media files.</p> + <p>This permission is enforced starting in API level 19. Before API level 19, this permission is not enforced and all apps still have access to read from external storage. You can test your app with the permission enforced by enabling <em>Protect USB - storage</em> under Developer options in the Settings app on a device running Android 4.1 or - higher.</p> + storage</em> under <b>Developer options</b> in the Settings app on a device running Android + 4.1 or higher.</p> <p>Also starting in API level 19, this permission is <em>not</em> required to - read/write files in your application-specific directories returned by + read or write files in your application-specific directories returned by {@link android.content.Context#getExternalFilesDir} and - {@link android.content.Context#getExternalCacheDir}. - <p class="note"><strong>Note:</strong> If <em>both</em> your <a + {@link android.content.Context#getExternalCacheDir}.</p> + <p>Starting in API level 29, apps don't need to request this permission to access files in + their app-specific directory on external storage, or their own files in the + <a href="{@docRoot}reference/android/provider/MediaStore"><code>MediaStore</code></a>. Apps + shouldn't request this permission unless they need to access other apps' files in the + <code>MediaStore</code>. Read more about these changes in the + <a href="{@docRoot}training/data-storage#scoped-storage">scoped storage</a> section of the + developer documentation.</p> + <p>If <em>both</em> your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code minSdkVersion}</a> and <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code targetSdkVersion}</a> values are set to 3 or lower, the system implicitly grants your app this permission. If you don't need this permission, be sure your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code - targetSdkVersion}</a> is 4 or higher. + targetSdkVersion}</a> is 4 or higher.</p> <p> This is a soft restricted permission which cannot be held by an app it its full form until the installer on record allowlists the permission. @@ -1134,7 +1147,28 @@ android:protectionLevel="dangerous" /> <!-- Allows an application to write to external storage. - <p class="note"><strong>Note:</strong> If <em>both</em> your <a + <p><strong>Note: </strong>If your app targets {@link android.os.Build.VERSION_CODES#R} or + higher, this permission has no effect. + + <p>If your app is on a device that runs API level 19 or higher, you don't need to declare + this permission to read and write files in your application-specific directories returned + by {@link android.content.Context#getExternalFilesDir} and + {@link android.content.Context#getExternalCacheDir}. + + <p>Learn more about how to + <a href="{@docRoot}training/data-storage/shared/media#update-other-apps-files">modify media + files</a> that your app doesn't own, and how to + <a href="{@docRoot}training/data-storage/shared/documents-files">modify non-media files</a> + that your app doesn't own. + + <p>If your app is a file manager and needs broad access to external storage files, then + the system must place your app on an allowlist so that you can successfully request the + <a href="#MANAGE_EXTERNAL_STORAGE><code>MANAGE_EXTERNAL_STORAGE</code></a> permission. + Learn more about the appropriate use cases for + <a href="{@docRoot}training/data-storage/manage-all-files>managing all files on a storage + device</a>. + + <p>If <em>both</em> your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code minSdkVersion}</a> and <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code @@ -1142,12 +1176,6 @@ grants your app this permission. If you don't need this permission, be sure your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code targetSdkVersion}</a> is 4 or higher. - <p>Starting in API level 19, this permission is <em>not</em> required to - read/write files in your application-specific directories returned by - {@link android.content.Context#getExternalFilesDir} and - {@link android.content.Context#getExternalCacheDir}. - <p>If this permission is not allowlisted for an app that targets an API level before - {@link android.os.Build.VERSION_CODES#Q} this permission cannot be granted to apps.</p> <p>Protection level: dangerous</p> --> <permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" diff --git a/core/res/res/anim/dream_activity_close_exit.xml b/core/res/res/anim/dream_activity_close_exit.xml index c4599dad31a0..8df624fdd2e5 100644 --- a/core/res/res/anim/dream_activity_close_exit.xml +++ b/core/res/res/anim/dream_activity_close_exit.xml @@ -19,5 +19,5 @@ <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="1.0" android:toAlpha="0.0" - android:duration="100" /> + android:duration="@integer/config_dreamCloseAnimationDuration" /> diff --git a/core/res/res/anim/dream_activity_open_enter.xml b/core/res/res/anim/dream_activity_open_enter.xml index 9e1c6e2ee0d7..d6d9c5c990f8 100644 --- a/core/res/res/anim/dream_activity_open_enter.xml +++ b/core/res/res/anim/dream_activity_open_enter.xml @@ -22,5 +22,5 @@ those two has to be the same. --> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="0.0" android:toAlpha="1.0" - android:duration="1000" /> + android:duration="@integer/config_dreamOpenAnimationDuration" /> diff --git a/core/res/res/anim/dream_activity_open_exit.xml b/core/res/res/anim/dream_activity_open_exit.xml index 740f52856b7f..2c2e501eda69 100644 --- a/core/res/res/anim/dream_activity_open_exit.xml +++ b/core/res/res/anim/dream_activity_open_exit.xml @@ -22,4 +22,4 @@ dream_activity_open_enter animation. --> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="1.0" android:toAlpha="1.0" - android:duration="1000" /> + android:duration="@integer/config_dreamOpenAnimationDuration" /> diff --git a/core/res/res/color/letterbox_background.xml b/core/res/res/color/letterbox_background.xml new file mode 100644 index 000000000000..955948ad2b6a --- /dev/null +++ b/core/res/res/color/letterbox_background.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@color/system_neutral1_500" android:lStar="5" /> +</selector> diff --git a/core/res/res/layout/miniresolver.xml b/core/res/res/layout/miniresolver.xml index ded23feaca8f..38a71f0e17f6 100644 --- a/core/res/res/layout/miniresolver.xml +++ b/core/res/res/layout/miniresolver.xml @@ -65,8 +65,7 @@ android:paddingTop="32dp" android:paddingBottom="@dimen/resolver_button_bar_spacing" android:orientation="vertical" - android:background="?attr/colorBackground" - android:layout_ignoreOffset="true"> + android:background="?attr/colorBackground"> <RelativeLayout style="?attr/buttonBarStyle" android:layout_width="match_parent" diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml index a7f2aa7cba69..be1c939f0ff8 100644 --- a/core/res/res/layout/notification_template_header.xml +++ b/core/res/res/layout/notification_template_header.xml @@ -24,6 +24,7 @@ android:gravity="center_vertical" android:orientation="horizontal" android:theme="@style/Theme.DeviceDefault.Notification" + android:importantForAccessibility="no" > <ImageView diff --git a/core/res/res/layout/resolver_list.xml b/core/res/res/layout/resolver_list.xml index 6a200d05c2d7..a06e8a4ea4a0 100644 --- a/core/res/res/layout/resolver_list.xml +++ b/core/res/res/layout/resolver_list.xml @@ -23,6 +23,7 @@ android:maxWidth="@dimen/resolver_max_width" android:maxCollapsedHeight="@dimen/resolver_max_collapsed_height" android:maxCollapsedHeightSmall="56dp" + android:ignoreOffsetTopLimit="@id/title_container" android:id="@id/contentPanel"> <RelativeLayout diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 004b5f6a3ea4..d86aa1122d3a 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -9560,6 +9560,12 @@ <attr name="maxCollapsedHeightSmall" format="dimension" /> <!-- Whether the Drawer should be positioned at the top rather than at the bottom. --> <attr name="showAtTop" format="boolean" /> + <!-- By default `ResolverDrawerLayout`’s children views with `layout_ignoreOffset` property + set to true have a fixed position in the layout that won’t be affected by the drawer’s + movements. This property alternates that behavior. It specifies a child view’s id that + will push all ignoreOffset siblings below it when the drawer is moved i.e. setting the + top limit the ignoreOffset elements. --> + <attr name="ignoreOffsetTopLimit" format="reference" /> </declare-styleable> <declare-styleable name="MessagingLinearLayout"> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1fed8e45d328..6acf980dc05b 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -553,6 +553,14 @@ <!-- If this is true, then keep dreaming when undocking. --> <bool name="config_keepDreamingWhenUndocking">false</bool> + <!-- The timeout (in ms) to wait before attempting to reconnect to the dream overlay service if + it becomes disconnected --> + <integer name="config_dreamOverlayReconnectTimeoutMs">1000</integer> <!-- 1 second --> + <!-- The maximum number of times to attempt reconnecting to the dream overlay service --> + <integer name="config_dreamOverlayMaxReconnectAttempts">3</integer> + <!-- The duration after which the dream overlay connection should be considered stable --> + <integer name="config_minDreamOverlayDurationMs">10000</integer> <!-- 10 seconds --> + <!-- Auto-rotation behavior --> <!-- If true, enables auto-rotation features using the accelerometer. @@ -650,6 +658,20 @@ --> </integer-array> + <!-- The device states (supplied by DeviceStateManager) that should be treated as half-folded by + the display fold controller. Default is empty. --> + <integer-array name="config_halfFoldedDeviceStates"> + <!-- Example: + <item>0</item> + <item>1</item> + <item>2</item> + --> + </integer-array> + + <!-- Indicates whether the window manager reacts to half-fold device states by overriding + rotation. --> + <bool name="config_windowManagerHalfFoldAutoRotateOverride">false</bool> + <!-- When a device enters any of these states, it should be woken up. States are defined in device_state_configuration.xml. --> <integer-array name="config_deviceStatesOnWhichToWakeUp"> @@ -679,7 +701,7 @@ <!-- Indicates the time needed to time out the fold animation if the device stops in half folded mode. --> - <integer name="config_unfoldTransitionHalfFoldedTimeout">600</integer> + <integer name="config_unfoldTransitionHalfFoldedTimeout">1000</integer> <!-- Indicates that the device supports having more than one internal display on at the same time. Only applicable to devices with more than one internal display. If this option is @@ -2414,6 +2436,13 @@ <integer name="config_attentionMaximumExtension">900000</integer> <!-- 15 minutes. --> <!-- Is the system user the only user allowed to dream. --> <bool name="config_dreamsOnlyEnabledForSystemUser">false</bool> + <!-- Whether dreams are disabled when ambient mode is suppressed. --> + <bool name="config_dreamsDisabledByAmbientModeSuppressionConfig">false</bool> + + <!-- The duration in milliseconds of the dream opening animation. --> + <integer name="config_dreamOpenAnimationDuration">250</integer> + <!-- The duration in milliseconds of the dream closing animation. --> + <integer name="config_dreamCloseAnimationDuration">100</integer> <!-- Whether to dismiss the active dream when an activity is started. Doesn't apply to assistant activities (ACTIVITY_TYPE_ASSISTANT) --> @@ -3510,9 +3539,9 @@ config_sidefpsSkipWaitForPowerVendorAcquireMessage --> <integer name="config_sidefpsSkipWaitForPowerAcquireMessage">6</integer> - <!-- This vendor acquired message that will cause the sidefpsKgPowerPress window to be skipped. - config_sidefpsSkipWaitForPowerOnFingerUp must be true and - config_sidefpsSkipWaitForPowerAcquireMessage must be BIOMETRIC_ACQUIRED_VENDOR == 6. --> + <!-- This vendor acquired message will cause the sidefpsKgPowerPress window to be skipped + when config_sidefpsSkipWaitForPowerAcquireMessage == 6 (VENDOR) and the vendor acquire + message equals this constant --> <integer name="config_sidefpsSkipWaitForPowerVendorAcquireMessage">2</integer> <!-- This config is used to force VoiceInteractionService to start on certain low ram devices. @@ -5167,7 +5196,7 @@ but isn't supported on the device or both dark scrim alpha and blur radius aren't provided. --> - <color name="config_letterboxBackgroundColor">@android:color/system_neutral2_900</color> + <color name="config_letterboxBackgroundColor">@color/letterbox_background</color> <!-- Horizontal position of a center of the letterboxed app window. 0 corresponds to the left side of the screen and 1 to the right side. If given value < 0 @@ -5891,4 +5920,8 @@ TODO(b/236022708) Move rear display state to device state config file --> <integer name="config_deviceStateRearDisplay">-1</integer> + + <!-- Whether the lock screen is allowed to run its own live wallpaper, + different from the home screen wallpaper. --> + <bool name="config_independentLockscreenLiveWallpaper">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 7e9a2d46277b..1046113109fa 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2224,10 +2224,16 @@ <java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenNotPowered" /> <java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" /> <java-symbol type="string" name="config_dreamsDefaultComponent" /> + <java-symbol type="bool" name="config_dreamsDisabledByAmbientModeSuppressionConfig" /> <java-symbol type="bool" name="config_dreamsOnlyEnabledForSystemUser" /> + <java-symbol type="integer" name="config_dreamOpenAnimationDuration" /> + <java-symbol type="integer" name="config_dreamCloseAnimationDuration" /> <java-symbol type="array" name="config_supportedDreamComplications" /> <java-symbol type="array" name="config_disabledDreamComponents" /> <java-symbol type="bool" name="config_dismissDreamOnActivityStart" /> + <java-symbol type="integer" name="config_dreamOverlayReconnectTimeoutMs" /> + <java-symbol type="integer" name="config_dreamOverlayMaxReconnectAttempts" /> + <java-symbol type="integer" name="config_minDreamOverlayDurationMs" /> <java-symbol type="string" name="config_loggable_dream_prefix" /> <java-symbol type="string" name="config_dozeComponent" /> <java-symbol type="string" name="enable_explore_by_touch_warning_title" /> @@ -3991,6 +3997,8 @@ <!-- For Foldables --> <java-symbol type="array" name="config_foldedDeviceStates" /> + <java-symbol type="array" name="config_halfFoldedDeviceStates" /> + <java-symbol type="bool" name="config_windowManagerHalfFoldAutoRotateOverride" /> <java-symbol type="array" name="config_deviceStatesOnWhichToWakeUp" /> <java-symbol type="array" name="config_deviceStatesOnWhichToSleep" /> <java-symbol type="string" name="config_foldedArea" /> @@ -4864,6 +4872,7 @@ <java-symbol type="array" name="config_deviceStatesAvailableForAppRequests" /> <java-symbol type="array" name="config_serviceStateLocationAllowedPackages" /> <java-symbol type="integer" name="config_deviceStateRearDisplay"/> + <java-symbol type="bool" name="config_independentLockscreenLiveWallpaper"/> <!-- For app language picker --> <java-symbol type="string" name="system_locale_title" /> diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index f9f3b4c8ead1..bcb13d2108b8 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -48,6 +48,7 @@ import static com.android.internal.util.ContrastColorUtilTest.assertContrastIsWi import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.fail; import static org.junit.Assert.assertEquals; @@ -56,9 +57,12 @@ import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import android.annotation.Nullable; +import android.app.Notification.CallStyle; import android.content.Context; import android.content.Intent; import android.content.LocusId; @@ -67,6 +71,7 @@ import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; +import android.graphics.Typeface; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Build; @@ -78,7 +83,9 @@ import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; +import android.util.Pair; import android.widget.RemoteViews; import androidx.test.InstrumentationRegistry; @@ -88,10 +95,13 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; import com.android.internal.util.ContrastColorUtil; +import junit.framework.Assert; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.List; import java.util.function.Consumer; @RunWith(AndroidJUnit4.class) @@ -216,8 +226,10 @@ public class NotificationTest { @Test public void allPendingIntents_recollectedAfterReusingBuilder() { - PendingIntent intent1 = PendingIntent.getActivity(mContext, 0, new Intent("test1"), PendingIntent.FLAG_MUTABLE_UNAUDITED); - PendingIntent intent2 = PendingIntent.getActivity(mContext, 0, new Intent("test2"), PendingIntent.FLAG_MUTABLE_UNAUDITED); + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + PendingIntent intent2 = PendingIntent.getActivity( + mContext, 0, new Intent("test2"), PendingIntent.FLAG_IMMUTABLE); Notification.Builder builder = new Notification.Builder(mContext, "channel"); builder.setContentIntent(intent1); @@ -531,6 +543,108 @@ public class NotificationTest { } @Test + public void testCallStyle_getSystemActions_forIncomingCall() { + PendingIntent answerIntent = createPendingIntent("answer"); + PendingIntent declineIntent = createPendingIntent("decline"); + Notification.CallStyle style = Notification.CallStyle.forIncomingCall( + new Person.Builder().setName("A Caller").build(), + declineIntent, + answerIntent + ); + style.setBuilder(new Notification.Builder(mContext, "Channel")); + + List<Notification.Action> actions = style.getActionsListWithSystemActions(); + + assertEquals(2, actions.size()); + assertEquals(declineIntent, actions.get(0).actionIntent); + assertEquals(answerIntent, actions.get(1).actionIntent); + } + + @Test + public void testCallStyle_getSystemActions_forOngoingCall() { + PendingIntent hangUpIntent = createPendingIntent("hangUp"); + Notification.CallStyle style = Notification.CallStyle.forOngoingCall( + new Person.Builder().setName("A Caller").build(), + hangUpIntent + ); + style.setBuilder(new Notification.Builder(mContext, "Channel")); + + List<Notification.Action> actions = style.getActionsListWithSystemActions(); + + assertEquals(1, actions.size()); + assertEquals(hangUpIntent, actions.get(0).actionIntent); + } + + @Test + public void testCallStyle_getSystemActions_forIncomingCallWithOtherActions() { + PendingIntent answerIntent = createPendingIntent("answer"); + PendingIntent declineIntent = createPendingIntent("decline"); + Notification.CallStyle style = Notification.CallStyle.forIncomingCall( + new Person.Builder().setName("A Caller").build(), + declineIntent, + answerIntent + ); + Notification.Action actionToKeep = makeNotificationAction(null); + Notification.Action actionToDrop = makeNotificationAction(null); + Notification.Builder notifBuilder = new Notification.Builder(mContext, "Channel") + .addAction(actionToKeep) + .addAction(actionToDrop); //expect to move this action to the end + style.setBuilder(notifBuilder); //add a builder with actions + + List<Notification.Action> actions = style.getActionsListWithSystemActions(); + + assertEquals(4, actions.size()); + assertEquals(declineIntent, actions.get(0).actionIntent); + assertEquals(actionToKeep, actions.get(1)); + assertEquals(answerIntent, actions.get(2).actionIntent); + assertEquals(actionToDrop, actions.get(3)); + } + + @Test + public void testCallStyle_getSystemActions_forOngoingCallWithOtherActions() { + PendingIntent hangUpIntent = createPendingIntent("hangUp"); + Notification.CallStyle style = Notification.CallStyle.forOngoingCall( + new Person.Builder().setName("A Caller").build(), + hangUpIntent + ); + Notification.Action firstAction = makeNotificationAction(null); + Notification.Action secondAction = makeNotificationAction(null); + Notification.Builder notifBuilder = new Notification.Builder(mContext, "Channel") + .addAction(firstAction) + .addAction(secondAction); + style.setBuilder(notifBuilder); //add a builder with actions + + List<Notification.Action> actions = style.getActionsListWithSystemActions(); + + assertEquals(3, actions.size()); + assertEquals(hangUpIntent, actions.get(0).actionIntent); + assertEquals(firstAction, actions.get(1)); + assertEquals(secondAction, actions.get(2)); + } + + @Test + public void testCallStyle_getSystemActions_dropsOldSystemActions() { + PendingIntent hangUpIntent = createPendingIntent("decline"); + Notification.CallStyle style = Notification.CallStyle.forOngoingCall( + new Person.Builder().setName("A Caller").build(), + hangUpIntent + ); + Bundle actionExtras = new Bundle(); + actionExtras.putBoolean("key_action_priority", true); + Notification.Action oldSystemAction = makeNotificationAction( + builder -> builder.addExtras(actionExtras) + ); + Notification.Builder notifBuilder = new Notification.Builder(mContext, "Channel") + .addAction(oldSystemAction); + style.setBuilder(notifBuilder); //add a builder with actions + + List<Notification.Action> actions = style.getActionsListWithSystemActions(); + + assertFalse("Old versions of system actions should be dropped.", + actions.contains(oldSystemAction)); + } + + @Test public void testBuild_ensureSmallIconIsNotTooBig_resizesIcon() { Icon hugeIcon = Icon.createWithBitmap( Bitmap.createBitmap(3000, 3000, Bitmap.Config.ARGB_8888)); @@ -565,30 +679,23 @@ public class NotificationTest { Notification notification = new Notification.Builder(mContext, "Channel").setStyle( style).build(); + int targetSize = mContext.getResources().getDimensionPixelSize( + ActivityManager.isLowRamDeviceStatic() + ? R.dimen.notification_person_icon_max_size_low_ram + : R.dimen.notification_person_icon_max_size); + Bitmap personIcon = style.getUser().getIcon().getBitmap(); - assertThat(personIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(personIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(personIcon.getWidth()).isEqualTo(targetSize); + assertThat(personIcon.getHeight()).isEqualTo(targetSize); Bitmap avatarIcon = style.getMessages().get(0).getSenderPerson().getIcon().getBitmap(); - assertThat(avatarIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(avatarIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(avatarIcon.getWidth()).isEqualTo(targetSize); + assertThat(avatarIcon.getHeight()).isEqualTo(targetSize); Bitmap historicAvatarIcon = style.getHistoricMessages().get( 0).getSenderPerson().getIcon().getBitmap(); - assertThat(historicAvatarIcon.getWidth()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); - assertThat(historicAvatarIcon.getHeight()).isEqualTo( - mContext.getResources().getDimensionPixelSize( - R.dimen.notification_person_icon_max_size)); + assertThat(historicAvatarIcon.getWidth()).isEqualTo(targetSize); + assertThat(historicAvatarIcon.getHeight()).isEqualTo(targetSize); } @Test @@ -676,7 +783,6 @@ public class NotificationTest { assertFalse(notification.isMediaNotification()); } - @Test public void validateColorizedPaletteForColor(int rawColor) { Notification.Colors cDay = new Notification.Colors(); Notification.Colors cNight = new Notification.Colors(); @@ -757,19 +863,22 @@ public class NotificationTest { Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_LARGE_ICON_BIG, new Bundle()); - style.restoreFromExtras(fakeTypes); // no crash, good } @Test public void testRestoreFromExtras_Messaging_invalidExtra_noCrash() { - Notification.Style style = new Notification.MessagingStyle(); + Notification.Style style = new Notification.MessagingStyle("test"); Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_MESSAGING_PERSON, new Bundle()); fakeTypes.putParcelable(EXTRA_CONVERSATION_ICON, new Bundle()); - style.restoreFromExtras(fakeTypes); + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @@ -781,22 +890,33 @@ public class NotificationTest { fakeTypes.putParcelable(EXTRA_MEDIA_SESSION, new Bundle()); fakeTypes.putParcelable(EXTRA_MEDIA_REMOTE_INTENT, new Bundle()); - style.restoreFromExtras(fakeTypes); + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @Test public void testRestoreFromExtras_Call_invalidExtra_noCrash() { - Notification.Style style = new Notification.CallStyle(); + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Notification.Style style = Notification.CallStyle.forIncomingCall( + new Person.Builder().setName("hi").build(), intent1, intent1); + Bundle fakeTypes = new Bundle(); fakeTypes.putParcelable(EXTRA_CALL_PERSON, new Bundle()); fakeTypes.putParcelable(EXTRA_ANSWER_INTENT, new Bundle()); fakeTypes.putParcelable(EXTRA_DECLINE_INTENT, new Bundle()); fakeTypes.putParcelable(EXTRA_HANG_UP_INTENT, new Bundle()); - style.restoreFromExtras(fakeTypes); - + Notification n = new Notification.Builder(mContext, "test") + .setStyle(style) + .setExtras(fakeTypes) + .build(); + Notification.Builder.recoverBuilder(mContext, n); // no crash, good } @@ -858,7 +978,11 @@ public class NotificationTest { fakeTypes.putParcelable(KEY_ON_READ, new Bundle()); fakeTypes.putParcelable(KEY_ON_REPLY, new Bundle()); fakeTypes.putParcelable(KEY_REMOTE_INPUT, new Bundle()); - Notification.CarExtender.UnreadConversation.getUnreadConversationFromBundle(fakeTypes); + + Notification n = new Notification.Builder(mContext, "test") + .setExtras(fakeTypes) + .build(); + Notification.CarExtender extender = new Notification.CarExtender(n); // no crash, good } @@ -876,6 +1000,493 @@ public class NotificationTest { // no crash, good } + + @Test + public void testDoesNotStripsExtenders() { + Notification.Builder nb = new Notification.Builder(mContext, "channel"); + nb.extend(new Notification.CarExtender().setColor(Color.RED)); + nb.extend(new Notification.TvExtender().setChannelId("different channel")); + nb.extend(new Notification.WearableExtender().setDismissalId("dismiss")); + Notification before = nb.build(); + Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before); + + assertTrue(before == after); + + Assert.assertEquals("different channel", + new Notification.TvExtender(before).getChannelId()); + Assert.assertEquals(Color.RED, new Notification.CarExtender(before).getColor()); + Assert.assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId()); + } + + @Test + public void testStyleChangeVisiblyDifferent_noStyles() { + Notification.Builder n1 = new Notification.Builder(mContext, "test"); + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + + assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_noStyleToStyle() { + Notification.Builder n1 = new Notification.Builder(mContext, "test"); + Notification.Builder n2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_styleToNoStyle() { + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testStyleChangeVisiblyDifferent_changeStyle() { + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle()); + Notification.Builder n2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle()); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); + } + + @Test + public void testInboxTextChange() { + Notification.Builder nInbox1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle().addLine("a").addLine("b")); + Notification.Builder nInbox2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.InboxStyle().addLine("b").addLine("c")); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2)); + } + + @Test + public void testBigTextTextChange() { + Notification.Builder nBigText1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle().bigText("something")); + Notification.Builder nBigText2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigTextStyle().bigText("else")); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2)); + } + + @Test + public void testBigPictureChange() { + Bitmap bitA = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + Bitmap bitB = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888); + + Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigPictureStyle().bigPicture(bitA)); + Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.BigPictureStyle().bigPicture(bitB)); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2)); + } + + @Test + public void testMessagingChange_text() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build())) + .addMessage(new Notification.MessagingStyle.Message( + "b", 100, new Person.Builder().setName("hi").build())) + ); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_data() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()) + .setData("text", mock(Uri.class)))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_sender() { + Person a = new Person.Builder().setName("A").build(); + Person b = new Person.Builder().setName("b").build(); + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_key() { + Person a = new Person.Builder().setName("hi").setKey("A").build(); + Person b = new Person.Builder().setName("hi").setKey("b").build(); + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); + + assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testMessagingChange_ignoreTimeChange() { + Notification.Builder nM1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 100, new Person.Builder().setName("hi").build()))); + Notification.Builder nM2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("") + .addMessage(new Notification.MessagingStyle.Message( + "a", 1000, new Person.Builder().setName("hi").build())) + ); + + assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); + } + + @Test + public void testRemoteViews_nullChange() { + Notification.Builder n1 = new Notification.Builder(mContext, "test") + .setContent(mock(RemoteViews.class)); + Notification.Builder n2 = new Notification.Builder(mContext, "test"); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test") + .setContent(mock(RemoteViews.class)); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test") + .setCustomBigContentView(mock(RemoteViews.class)); + n2 = new Notification.Builder(mContext, "test"); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test") + .setCustomBigContentView(mock(RemoteViews.class)); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test"); + n2 = new Notification.Builder(mContext, "test"); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_layoutChange() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(189); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_layoutSame() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_sequenceChange() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + when(a.getSequenceNumber()).thenReturn(1); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + when(b.getSequenceNumber()).thenReturn(2); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertTrue(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testRemoteViews_sequenceSame() { + RemoteViews a = mock(RemoteViews.class); + when(a.getLayoutId()).thenReturn(234); + when(a.getSequenceNumber()).thenReturn(1); + RemoteViews b = mock(RemoteViews.class); + when(b.getLayoutId()).thenReturn(234); + when(b.getSequenceNumber()).thenReturn(1); + + Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); + Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + + n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); + n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); + assertFalse(Notification.areRemoteViewsChanged(n1, n2)); + } + + @Test + public void testActionsDifferent_null() { + Notification n1 = new Notification.Builder(mContext, "test") + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentSame() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentText() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) + .build(); + + assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentSpannables() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, + new SpannableStringBuilder().append("test1", + new StyleSpan(Typeface.BOLD), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE), + intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "test1", intent).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentNumber() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) + .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) + .build(); + + assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsDifferentIntent() { + PendingIntent intent1 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + PendingIntent intent2 = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE); + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testActionsIgnoresRemoteInputs() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + Notification n1 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(new RemoteInput.Builder("a") + .setChoices(new CharSequence[] {"i", "m"}) + .build()) + .build()) + .build(); + Notification n2 = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(new RemoteInput.Builder("a") + .setChoices(new CharSequence[] {"t", "m"}) + .build()) + .build()) + .build(); + + assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); + } + + @Test + public void testFreeformRemoteInputActionPair_noRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + Notification notification = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .build()) + .build(); + Assert.assertNull(notification.findRemoteInputActionPair(false)); + } + + @Test + public void testFreeformRemoteInputActionPair_hasRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + RemoteInput remoteInput = new RemoteInput.Builder("a").build(); + + Notification.Action actionWithRemoteInput = + new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(remoteInput) + .addRemoteInput(remoteInput) + .build(); + + Notification.Action actionWithoutRemoteInput = + new Notification.Action.Builder(icon, "TEXT 2", intent) + .build(); + + Notification notification = new Notification.Builder(mContext, "test") + .addAction(actionWithoutRemoteInput) + .addAction(actionWithRemoteInput) + .build(); + + Pair<RemoteInput, Notification.Action> remoteInputActionPair = + notification.findRemoteInputActionPair(false); + + assertNotNull(remoteInputActionPair); + Assert.assertEquals(remoteInput, remoteInputActionPair.first); + Assert.assertEquals(actionWithRemoteInput, remoteInputActionPair.second); + } + + @Test + public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + Notification notification = new Notification.Builder(mContext, "test") + .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput( + new RemoteInput.Builder("a") + .setAllowFreeFormInput(false).build()) + .build()) + .build(); + Assert.assertNull(notification.findRemoteInputActionPair(true)); + } + + @Test + public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() { + PendingIntent intent = PendingIntent.getActivity( + mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);; + Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + RemoteInput remoteInput = + new RemoteInput.Builder("a").setAllowFreeFormInput(false).build(); + RemoteInput freeformRemoteInput = + new RemoteInput.Builder("b").setAllowFreeFormInput(true).build(); + + Notification.Action actionWithFreeformRemoteInput = + new Notification.Action.Builder(icon, "TEXT 1", intent) + .addRemoteInput(remoteInput) + .addRemoteInput(freeformRemoteInput) + .build(); + + Notification.Action actionWithoutFreeformRemoteInput = + new Notification.Action.Builder(icon, "TEXT 2", intent) + .addRemoteInput(remoteInput) + .build(); + + Notification notification = new Notification.Builder(mContext, "test") + .addAction(actionWithoutFreeformRemoteInput) + .addAction(actionWithFreeformRemoteInput) + .build(); + + Pair<RemoteInput, Notification.Action> remoteInputActionPair = + notification.findRemoteInputActionPair(true); + + assertNotNull(remoteInputActionPair); + Assert.assertEquals(freeformRemoteInput, remoteInputActionPair.first); + Assert.assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second); + } + private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); @@ -962,4 +1573,12 @@ public class NotificationTest { } return actionBuilder.build(); } + + /** + * Creates a PendingIntent with the given action. + */ + private PendingIntent createPendingIntent(String action) { + return PendingIntent.getActivity(mContext, 0, new Intent(action), + PendingIntent.FLAG_MUTABLE); + } } diff --git a/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt b/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt new file mode 100644 index 000000000000..e3b3ea7492c5 --- /dev/null +++ b/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.hardware.input + +import android.hardware.BatteryState +import android.os.Handler +import android.os.HandlerExecutor +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import com.android.server.testutils.any +import java.util.concurrent.Executor +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.doAnswer +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoJUnitRunner + +/** + * Tests for [InputManager.InputDeviceBatteryListener]. + * + * Build/Install/Run: + * atest FrameworksCoreTests:InputDeviceBatteryListenerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner::class) +class InputDeviceBatteryListenerTest { + @get:Rule + val rule = MockitoJUnit.rule()!! + + private lateinit var testLooper: TestLooper + private var registeredListener: IInputDeviceBatteryListener? = null + private val monitoredDevices = mutableListOf<Int>() + private lateinit var executor: Executor + private lateinit var inputManager: InputManager + + @Mock + private lateinit var iInputManagerMock: IInputManager + + @Before + fun setUp() { + testLooper = TestLooper() + executor = HandlerExecutor(Handler(testLooper.looper)) + registeredListener = null + monitoredDevices.clear() + inputManager = InputManager.resetInstance(iInputManagerMock) + + // Handle battery listener registration. + doAnswer { + val deviceId = it.getArgument(0) as Int + val listener = it.getArgument(1) as IInputDeviceBatteryListener + if (registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder()) { + // There can only be one registered battery listener per process. + fail("Trying to register a new listener when one already exists") + } + if (monitoredDevices.contains(deviceId)) { + fail("Trying to start monitoring a device that was already being monitored") + } + monitoredDevices.add(deviceId) + registeredListener = listener + null + }.`when`(iInputManagerMock).registerBatteryListener(anyInt(), any()) + + // Handle battery listener being unregistered. + doAnswer { + val deviceId = it.getArgument(0) as Int + val listener = it.getArgument(1) as IInputDeviceBatteryListener + if (registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder()) { + fail("Trying to unregister a listener that is not registered") + } + if (!monitoredDevices.remove(deviceId)) { + fail("Trying to stop monitoring a device that is not being monitored") + } + if (monitoredDevices.isEmpty()) { + registeredListener = null + } + }.`when`(iInputManagerMock).unregisterBatteryListener(anyInt(), any()) + } + + @After + fun tearDown() { + InputManager.clearInstance() + } + + private fun notifyBatteryStateChanged( + deviceId: Int, + isPresent: Boolean = true, + status: Int = BatteryState.STATUS_FULL, + capacity: Float = 1.0f, + eventTime: Long = 12345L + ) { + registeredListener!!.onBatteryStateChanged(deviceId, isPresent, status, capacity, eventTime) + } + + @Test + fun testListenerIsNotifiedCorrectly() { + var callbackCount = 0 + + // Add a battery listener to monitor battery changes. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor) { + deviceId: Int, eventTime: Long, batteryState: BatteryState -> + callbackCount++ + assertEquals(1, deviceId) + assertEquals(true, batteryState.isPresent) + assertEquals(BatteryState.STATUS_DISCHARGING, batteryState.status) + assertEquals(0.5f, batteryState.capacity) + assertEquals(8675309L, eventTime) + } + + // Adding the listener should register the callback with InputManagerService. + assertNotNull(registeredListener) + assertTrue(monitoredDevices.contains(1)) + + // Notifying battery change for a different device should not trigger the listener. + notifyBatteryStateChanged(deviceId = 2) + testLooper.dispatchAll() + assertEquals(0, callbackCount) + + // Notifying battery change for the registered device will notify the listener. + notifyBatteryStateChanged(1 /*deviceId*/, true /*isPresent*/, + BatteryState.STATUS_DISCHARGING, 0.5f /*capacity*/, 8675309L /*eventTime*/) + testLooper.dispatchNext() + assertEquals(1, callbackCount) + } + + @Test + fun testMultipleListeners() { + // Set up two callbacks. + var callbackCount1 = 0 + var callbackCount2 = 0 + val callback1 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount1++ } + val callback2 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount2++ } + + // Monitor battery changes for three devices. The first callback monitors devices 1 and 3, + // while the second callback monitors devices 2 and 3. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback1) + assertEquals(1, monitoredDevices.size) + inputManager.addInputDeviceBatteryListener(2 /*deviceId*/, executor, callback2) + assertEquals(2, monitoredDevices.size) + inputManager.addInputDeviceBatteryListener(3 /*deviceId*/, executor, callback1) + assertEquals(3, monitoredDevices.size) + inputManager.addInputDeviceBatteryListener(3 /*deviceId*/, executor, callback2) + assertEquals(3, monitoredDevices.size) + + // Notifying battery change for each of the devices should trigger the registered callbacks. + notifyBatteryStateChanged(deviceId = 1) + testLooper.dispatchNext() + assertEquals(1, callbackCount1) + assertEquals(0, callbackCount2) + + notifyBatteryStateChanged(deviceId = 2) + testLooper.dispatchNext() + assertEquals(1, callbackCount1) + assertEquals(1, callbackCount2) + + notifyBatteryStateChanged(deviceId = 3) + testLooper.dispatchNext() + testLooper.dispatchNext() + assertEquals(2, callbackCount1) + assertEquals(2, callbackCount2) + + // Stop monitoring devices 1 and 2. + inputManager.removeInputDeviceBatteryListener(1 /*deviceId*/, callback1) + assertEquals(2, monitoredDevices.size) + inputManager.removeInputDeviceBatteryListener(2 /*deviceId*/, callback2) + assertEquals(1, monitoredDevices.size) + + // Ensure device 3 continues to be monitored. + notifyBatteryStateChanged(deviceId = 3) + testLooper.dispatchNext() + testLooper.dispatchNext() + assertEquals(3, callbackCount1) + assertEquals(3, callbackCount2) + + // Stop monitoring all devices. + inputManager.removeInputDeviceBatteryListener(3 /*deviceId*/, callback1) + assertEquals(1, monitoredDevices.size) + inputManager.removeInputDeviceBatteryListener(3 /*deviceId*/, callback2) + assertEquals(0, monitoredDevices.size) + } + + @Test + fun testAdditionalListenersNotifiedImmediately() { + var callbackCount1 = 0 + var callbackCount2 = 0 + val callback1 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount1++ } + val callback2 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount2++ } + + // Add a battery listener and send the latest battery state. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback1) + assertEquals(1, monitoredDevices.size) + notifyBatteryStateChanged(deviceId = 1) + testLooper.dispatchNext() + assertEquals(1, callbackCount1) + + // Add a second listener for the same device that already has the latest battery state. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback2) + assertEquals(1, monitoredDevices.size) + + // Ensure that this listener is notified immediately. + testLooper.dispatchNext() + assertEquals(1, callbackCount2) + } +} diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index f448cb3091e7..f370ebd94545 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -60,6 +60,8 @@ public class WindowOnBackInvokedDispatcherTest { private OnBackAnimationCallback mCallback1; @Mock private OnBackAnimationCallback mCallback2; + @Mock + private BackEvent mBackEvent; @Before public void setUp() throws Exception { @@ -85,14 +87,14 @@ public class WindowOnBackInvokedDispatcherTest { verify(mWindowSession, times(2)).setOnBackInvokedCallbackInfo( Mockito.eq(mWindow), captor.capture()); - captor.getAllValues().get(0).getCallback().onBackStarted(); + captor.getAllValues().get(0).getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback1).onBackStarted(); + verify(mCallback1).onBackStarted(mBackEvent); verifyZeroInteractions(mCallback2); - captor.getAllValues().get(1).getCallback().onBackStarted(); + captor.getAllValues().get(1).getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback2).onBackStarted(); + verify(mCallback2).onBackStarted(mBackEvent); verifyNoMoreInteractions(mCallback1); } @@ -110,9 +112,9 @@ public class WindowOnBackInvokedDispatcherTest { Mockito.eq(mWindow), captor.capture()); verifyNoMoreInteractions(mWindowSession); assertEquals(captor.getValue().getPriority(), OnBackInvokedDispatcher.PRIORITY_OVERLAY); - captor.getValue().getCallback().onBackStarted(); + captor.getValue().getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback1).onBackStarted(); + verify(mCallback1).onBackStarted(mBackEvent); } @Test @@ -148,8 +150,8 @@ public class WindowOnBackInvokedDispatcherTest { mDispatcher.registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_OVERLAY, mCallback2); verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), captor.capture()); - captor.getValue().getCallback().onBackStarted(); + captor.getValue().getCallback().onBackStarted(mBackEvent); waitForIdle(); - verify(mCallback2).onBackStarted(); + verify(mCallback2).onBackStarted(mBackEvent); } } diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt new file mode 100644 index 000000000000..8218b9869b5d --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/ChooserListAdapterTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.os.Bundle +import android.service.chooser.ChooserTarget +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.R +import com.android.internal.app.ChooserListAdapter.LoadDirectShareIconTask +import com.android.internal.app.chooser.SelectableTargetInfo +import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator +import com.android.internal.app.chooser.TargetInfo +import com.android.server.testutils.any +import com.android.server.testutils.mock +import com.android.server.testutils.whenever +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class ChooserListAdapterTest { + private val packageManager = mock<PackageManager> { + whenever(resolveActivity(any(), anyInt())).thenReturn(mock()) + } + private val context = InstrumentationRegistry.getInstrumentation().getContext() + private val resolverListController = mock<ResolverListController>() + private val chooserListCommunicator = mock<ChooserListAdapter.ChooserListCommunicator> { + whenever(maxRankedTargets).thenReturn(0) + } + private val selectableTargetInfoCommunicator = + mock<SelectableTargetInfoCommunicator> { + whenever(targetIntent).thenReturn(mock()) + } + private val chooserActivityLogger = mock<ChooserActivityLogger>() + + private fun createChooserListAdapter( + taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask + ) = + ChooserListAdapterOverride( + context, + emptyList(), + emptyArray(), + emptyList(), + false, + resolverListController, + chooserListCommunicator, + selectableTargetInfoCommunicator, + packageManager, + chooserActivityLogger, + taskProvider + ) + + @Test + fun testDirectShareTargetLoadingIconIsStarted() { + val view = createView() + val viewHolder = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolder + val targetInfo = createSelectableTargetInfo() + val iconTask = mock<LoadDirectShareIconTask>() + val testSubject = createChooserListAdapter { iconTask } + testSubject.testViewBind(view, targetInfo, 0) + + verify(iconTask, times(1)).loadIcon() + } + + @Test + fun testOnlyOneTaskPerTarget() { + val view = createView() + val viewHolderOne = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolderOne + val targetInfo = createSelectableTargetInfo() + val iconTaskOne = mock<LoadDirectShareIconTask>() + val testTaskProvider = mock<() -> LoadDirectShareIconTask> { + whenever(invoke()).thenReturn(iconTaskOne) + } + val testSubject = createChooserListAdapter { testTaskProvider.invoke() } + testSubject.testViewBind(view, targetInfo, 0) + + val viewHolderTwo = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolderTwo + whenever(testTaskProvider()).thenReturn(mock()) + + testSubject.testViewBind(view, targetInfo, 0) + + verify(iconTaskOne, times(1)).loadIcon() + verify(testTaskProvider, times(1)).invoke() + } + + private fun createSelectableTargetInfo(): SelectableTargetInfo = + SelectableTargetInfo( + context, + null, + createChooserTarget(), + 1f, + selectableTargetInfoCommunicator, + null + ) + + private fun createChooserTarget(): ChooserTarget = + ChooserTarget( + "Title", + null, + 1f, + ComponentName("package", "package.Class"), + Bundle() + ) + + private fun createView(): View { + val view = FrameLayout(context) + TextView(context).apply { + id = R.id.text1 + view.addView(this) + } + TextView(context).apply { + id = R.id.text2 + view.addView(this) + } + ImageView(context).apply { + id = R.id.icon + view.addView(this) + } + return view + } +} + +private class ChooserListAdapterOverride( + context: Context?, + payloadIntents: List<Intent>?, + initialIntents: Array<out Intent>?, + rList: List<ResolveInfo>?, + filterLastUsed: Boolean, + resolverListController: ResolverListController?, + chooserListCommunicator: ChooserListCommunicator?, + selectableTargetInfoCommunicator: SelectableTargetInfoCommunicator?, + packageManager: PackageManager?, + chooserActivityLogger: ChooserActivityLogger?, + private val taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask +) : ChooserListAdapter( + context, + payloadIntents, + initialIntents, + rList, + filterLastUsed, + resolverListController, + chooserListCommunicator, + selectableTargetInfoCommunicator, + packageManager, + chooserActivityLogger +) { + override fun createLoadDirectShareIconTask( + info: SelectableTargetInfo? + ): LoadDirectShareIconTask = + taskProvider.invoke(info) + + fun testViewBind(view: View?, info: TargetInfo?, position: Int) { + onBindView(view, info, position) + } +} diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java index 56a70708b6df..2861428ece4d 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java @@ -46,14 +46,14 @@ public class ResolverWrapperAdapter extends ResolverListAdapter { } @Override - protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) { - return new LoadLabelWrapperTask(info, holder); + protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) { + return new LoadLabelWrapperTask(info); } class LoadLabelWrapperTask extends LoadLabelTask { - protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) { - super(dri, holder); + protected LoadLabelWrapperTask(DisplayResolveInfo dri) { + super(dri); } @Override diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java index d19f9f5ea58f..52feac5a585a 100644 --- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java +++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java @@ -47,6 +47,7 @@ import android.telephony.DataConnectionRealTimeInfo; import android.telephony.ModemActivityInfo; import android.telephony.ServiceState; import android.telephony.TelephonyManager; +import android.util.Log; import android.util.MutableInt; import android.util.SparseIntArray; import android.util.SparseLongArray; @@ -82,6 +83,7 @@ import java.util.function.IntConsumer; * com.android.frameworks.coretests/androidx.test.runner.AndroidJUnitRunner */ public class BatteryStatsNoteTest extends TestCase { + private static final String TAG = BatteryStatsNoteTest.class.getSimpleName(); private static final int UID = 10500; private static final int ISOLATED_APP_ID = Process.FIRST_ISOLATED_UID + 23; @@ -2031,6 +2033,115 @@ public class BatteryStatsNoteTest extends TestCase { noRadioProcFlags, lastProcStateChangeFlags.value); } + + + @SmallTest + public void testNoteMobileRadioPowerStateLocked() { + long curr; + boolean update; + final MockClock clocks = new MockClock(); // holds realtime and uptime in ms + final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); + bi.setOnBatteryInternal(true); + + // Note mobile radio is on. + curr = 1000L * (clocks.realtime = clocks.uptime = 1001); + bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr, + UID); + + // Note mobile radio is still on. + curr = 1000L * (clocks.realtime = clocks.uptime = 2001); + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, + curr, UID); + assertFalse( + "noteMobileRadioPowerStateLocked should not request an update when the power " + + "state does not change from HIGH.", + update); + + // Note mobile radio is off. + curr = 1000L * (clocks.realtime = clocks.uptime = 3001); + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, + curr, UID); + assertTrue( + "noteMobileRadioPowerStateLocked should request an update when the power state " + + "changes from HIGH to LOW.", + update); + + // Note mobile radio is still off. + curr = 1000L * (clocks.realtime = clocks.uptime = 4001); + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, + curr, UID); + assertFalse( + "noteMobileRadioPowerStateLocked should not request an update when the power " + + "state does not change from LOW.", + update); + + // Note mobile radio is on. + curr = 1000L * (clocks.realtime = clocks.uptime = 5001); + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, + curr, UID); + assertFalse( + "noteMobileRadioPowerStateLocked should not request an update when the power " + + "state changes from LOW to HIGH.", + update); + } + + @SmallTest + public void testNoteMobileRadioPowerStateLocked_rateLimited() { + long curr; + boolean update; + final MockClock clocks = new MockClock(); // holds realtime and uptime in ms + final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks); + bi.setPowerProfile(mock(PowerProfile.class)); + + final int txLevelCount = CellSignalStrength.getNumSignalStrengthLevels(); + final ModemActivityInfo mai = new ModemActivityInfo(0L, 0L, 0L, new int[txLevelCount], 0L); + + final long rateLimit = bi.getMobileRadioPowerStateUpdateRateLimit(); + if (rateLimit < 0) { + Log.w(TAG, "Skipping testNoteMobileRadioPowerStateLocked_rateLimited, rateLimit = " + + rateLimit); + return; + } + bi.setOnBatteryInternal(true); + + // Note mobile radio is on. + curr = 1000L * (clocks.realtime = clocks.uptime = 1001); + bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr, + UID); + + clocks.realtime = clocks.uptime = 2001; + mai.setTimestamp(clocks.realtime); + bi.noteModemControllerActivity(mai, POWER_DATA_UNAVAILABLE, + clocks.realtime, clocks.uptime, mNetworkStatsManager); + + // Note mobile radio is off within the rate limit duration. + clocks.realtime = clocks.uptime = clocks.realtime + (long) (rateLimit * 0.7); + curr = 1000L * clocks.realtime; + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, + curr, UID); + assertFalse( + "noteMobileRadioPowerStateLocked should not request an update when the power " + + "state so soon after a noteModemControllerActivity", + update); + + // Note mobile radio is on. + clocks.realtime = clocks.uptime = clocks.realtime + (long) (rateLimit * 0.7); + curr = 1000L * clocks.realtime; + bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr, + UID); + + // Note mobile radio is off much later + clocks.realtime = clocks.uptime = clocks.realtime + rateLimit; + curr = 1000L * clocks.realtime; + update = bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, + curr, UID); + assertTrue( + "noteMobileRadioPowerStateLocked should request an update when the power state " + + "changes from HIGH to LOW much later after a " + + "noteModemControllerActivity.", + update); + } + private void setFgState(int uid, boolean fgOn, MockBatteryStatsImpl bi) { // Note that noteUidProcessStateLocked uses ActivityManager process states. if (fgOn) { diff --git a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java index edeb5e9f4834..5ea4f069f026 100644 --- a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java +++ b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java @@ -114,6 +114,10 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { return getUidStatsLocked(uid).mOnBatteryScreenOffBackgroundTimeBase; } + public long getMobileRadioPowerStateUpdateRateLimit() { + return MOBILE_RADIO_POWER_STATE_UPDATE_FREQ_MS; + } + public MockBatteryStatsImpl setNetworkStats(NetworkStats networkStats) { mNetworkStats = networkStats; return this; diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java index fd4fb133ef12..2719431a536e 100644 --- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java +++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java @@ -17,7 +17,6 @@ package com.android.internal.util; import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; -import static android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.fail; @@ -85,12 +84,6 @@ public final class ScreenshotHelperTest { } @Test - public void testSelectedRegionScreenshot() { - mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_SELECTED_REGION, - WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null); - } - - @Test public void testProvidedImageScreenshot() { mScreenshotHelper.provideScreenshot( new Bundle(), new Rect(), Insets.of(0, 0, 0, 0), 1, 1, new ComponentName("", ""), diff --git a/core/tests/utiltests/Android.bp b/core/tests/utiltests/Android.bp index adc3676f7b93..3798da592cd5 100644 --- a/core/tests/utiltests/Android.bp +++ b/core/tests/utiltests/Android.bp @@ -34,6 +34,7 @@ android_test { "mockito-target-minus-junit4", "androidx.test.ext.junit", "truth-prebuilt", + "servicestests-utils", ], libs: [ diff --git a/core/tests/utiltests/src/com/android/internal/util/ObservableServiceConnectionTest.java b/core/tests/utiltests/src/com/android/internal/util/ObservableServiceConnectionTest.java new file mode 100644 index 000000000000..d124ad9ddfb0 --- /dev/null +++ b/core/tests/utiltests/src/com/android/internal/util/ObservableServiceConnectionTest.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; + +import androidx.test.filters.SmallTest; + +import com.android.internal.util.ObservableServiceConnection.ServiceTransformer; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayDeque; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.Executor; + +@SmallTest +public class ObservableServiceConnectionTest { + private static final ComponentName COMPONENT_NAME = + new ComponentName("test.package", "component"); + + public static class Foo { + int mValue; + + Foo(int value) { + mValue = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Foo)) return false; + Foo foo = (Foo) o; + return mValue == foo.mValue; + } + + @Override + public int hashCode() { + return Objects.hash(mValue); + } + } + + + @Mock + private Context mContext; + @Mock + private Intent mIntent; + @Mock + private Foo mResult; + @Mock + private IBinder mBinder; + @Mock + private ServiceTransformer<Foo> mTransformer; + @Mock + private ObservableServiceConnection.Callback<Foo> mCallback; + private final FakeExecutor mExecutor = new FakeExecutor(); + private ObservableServiceConnection<Foo> mConnection; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mConnection = new ObservableServiceConnection<>( + mContext, + mExecutor, + mTransformer, + mIntent, + /* flags= */ Context.BIND_AUTO_CREATE); + } + + @After + public void tearDown() { + mExecutor.clearAll(); + } + + @Test + public void testConnect() { + // Register twice to ensure only one callback occurs. + mConnection.addCallback(mCallback); + mConnection.addCallback(mCallback); + + mExecutor.runAll(); + mConnection.bind(); + + // Ensure that no callbacks happen before connection. + verify(mCallback, never()).onConnected(any(), any()); + verify(mCallback, never()).onDisconnected(any(), anyInt()); + + when(mTransformer.convert(mBinder)).thenReturn(mResult); + mConnection.onServiceConnected(COMPONENT_NAME, mBinder); + + mExecutor.runAll(); + verify(mCallback, times(1)).onConnected(mConnection, mResult); + } + + @Test + public void testDisconnectBeforeBind() { + mConnection.addCallback(mCallback); + mExecutor.runAll(); + mConnection.onServiceDisconnected(COMPONENT_NAME); + mExecutor.runAll(); + // Disconnects before binds should be ignored. + verify(mCallback, never()).onDisconnected(eq(mConnection), anyInt()); + } + + @Test + public void testDisconnect() { + mConnection.addCallback(mCallback); + mExecutor.runAll(); + mConnection.bind(); + mConnection.onServiceDisconnected(COMPONENT_NAME); + + // Ensure the callback doesn't get triggered until the executor runs. + verify(mCallback, never()).onDisconnected(eq(mConnection), anyInt()); + mExecutor.runAll(); + // Ensure proper disconnect reason reported. + verify(mCallback, times(1)).onDisconnected(mConnection, + ObservableServiceConnection.DISCONNECT_REASON_DISCONNECTED); + // Verify unbound from service. + verify(mContext, times(1)).unbindService(mConnection); + + clearInvocations(mContext); + // Ensure unbind after disconnect has no effect on the connection + mConnection.unbind(); + verify(mContext, never()).unbindService(mConnection); + } + + @Test + public void testBindingDied() { + mConnection.addCallback(mCallback); + mExecutor.runAll(); + mConnection.bind(); + mConnection.onBindingDied(COMPONENT_NAME); + + // Ensure the callback doesn't get triggered until the executor runs. + verify(mCallback, never()).onDisconnected(eq(mConnection), anyInt()); + mExecutor.runAll(); + // Ensure proper disconnect reason reported. + verify(mCallback, times(1)).onDisconnected(mConnection, + ObservableServiceConnection.DISCONNECT_REASON_BINDING_DIED); + // Verify unbound from service. + verify(mContext, times(1)).unbindService(mConnection); + } + + @Test + public void testNullBinding() { + mConnection.addCallback(mCallback); + mExecutor.runAll(); + mConnection.bind(); + mConnection.onNullBinding(COMPONENT_NAME); + + // Ensure the callback doesn't get triggered until the executor runs. + verify(mCallback, never()).onDisconnected(eq(mConnection), anyInt()); + mExecutor.runAll(); + // Ensure proper disconnect reason reported. + verify(mCallback, times(1)).onDisconnected(mConnection, + ObservableServiceConnection.DISCONNECT_REASON_NULL_BINDING); + // Verify unbound from service. + verify(mContext, times(1)).unbindService(mConnection); + } + + @Test + public void testUnbind() { + mConnection.addCallback(mCallback); + mExecutor.runAll(); + mConnection.bind(); + mConnection.unbind(); + + // Ensure the callback doesn't get triggered until the executor runs. + verify(mCallback, never()).onDisconnected(eq(mConnection), anyInt()); + mExecutor.runAll(); + verify(mCallback).onDisconnected(mConnection, + ObservableServiceConnection.DISCONNECT_REASON_UNBIND); + } + + static class FakeExecutor implements Executor { + private final Queue<Runnable> mQueue = new ArrayDeque<>(); + + @Override + public void execute(Runnable command) { + mQueue.add(command); + } + + public void runAll() { + while (!mQueue.isEmpty()) { + mQueue.remove().run(); + } + } + + public void clearAll() { + while (!mQueue.isEmpty()) { + mQueue.remove(); + } + } + } +} diff --git a/core/tests/utiltests/src/com/android/internal/util/PersistentServiceConnectionTest.java b/core/tests/utiltests/src/com/android/internal/util/PersistentServiceConnectionTest.java new file mode 100644 index 000000000000..fee46545ac62 --- /dev/null +++ b/core/tests/utiltests/src/com/android/internal/util/PersistentServiceConnectionTest.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; + +import com.android.internal.util.ObservableServiceConnection.ServiceTransformer; +import com.android.server.testutils.OffsettableClock; +import com.android.server.testutils.TestHandler; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.Executor; + +public class PersistentServiceConnectionTest { + private static final ComponentName COMPONENT_NAME = + new ComponentName("test.package", "component"); + private static final int MAX_RETRIES = 2; + private static final int RETRY_DELAY_MS = 1000; + private static final int CONNECTION_MIN_DURATION_MS = 5000; + private PersistentServiceConnection<Proxy> mConnection; + + public static class Proxy { + } + + @Mock + private Context mContext; + @Mock + private Intent mIntent; + @Mock + private Proxy mResult; + @Mock + private IBinder mBinder; + @Mock + private ServiceTransformer<Proxy> mTransformer; + @Mock + private ObservableServiceConnection.Callback<Proxy> mCallback; + private TestHandler mHandler; + private final FakeExecutor mFakeExecutor = new FakeExecutor(); + private OffsettableClock mClock; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mClock = new OffsettableClock.Stopped(); + mHandler = spy(new TestHandler(null, mClock)); + + mConnection = new PersistentServiceConnection<>( + mContext, + mFakeExecutor, + mHandler, + mTransformer, + mIntent, + /* flags= */ Context.BIND_AUTO_CREATE, + CONNECTION_MIN_DURATION_MS, + MAX_RETRIES, + RETRY_DELAY_MS, + new TestInjector(mClock)); + + mClock.fastForward(1000); + mConnection.addCallback(mCallback); + when(mTransformer.convert(mBinder)).thenReturn(mResult); + } + + @After + public void tearDown() { + mFakeExecutor.clearAll(); + } + + @Test + public void testConnect() { + mConnection.bind(); + mConnection.onServiceConnected(COMPONENT_NAME, mBinder); + mFakeExecutor.runAll(); + // Ensure that we did not schedule a retry + verify(mHandler, never()).postDelayed(any(), anyLong()); + } + + @Test + public void testRetryOnBindFailure() { + mConnection.bind(); + + verify(mContext, times(1)).bindService( + eq(mIntent), + anyInt(), + eq(mFakeExecutor), + eq(mConnection)); + + // After disconnect, a reconnection should be attempted after the RETRY_DELAY_MS + mConnection.onServiceDisconnected(COMPONENT_NAME); + mFakeExecutor.runAll(); + advanceTime(RETRY_DELAY_MS); + verify(mContext, times(2)).bindService( + eq(mIntent), + anyInt(), + eq(mFakeExecutor), + eq(mConnection)); + + // Reconnect attempt #2 + mConnection.onServiceDisconnected(COMPONENT_NAME); + mFakeExecutor.runAll(); + advanceTime(RETRY_DELAY_MS * 2); + verify(mContext, times(3)).bindService( + eq(mIntent), + anyInt(), + eq(mFakeExecutor), + eq(mConnection)); + + // There should be no more reconnect attempts, since the maximum is 2 + mConnection.onServiceDisconnected(COMPONENT_NAME); + mFakeExecutor.runAll(); + advanceTime(RETRY_DELAY_MS * 4); + verify(mContext, times(3)).bindService( + eq(mIntent), + anyInt(), + eq(mFakeExecutor), + eq(mConnection)); + } + + @Test + public void testManualUnbindDoesNotReconnect() { + mConnection.bind(); + + verify(mContext, times(1)).bindService( + eq(mIntent), + anyInt(), + eq(mFakeExecutor), + eq(mConnection)); + + mConnection.unbind(); + // Ensure that disconnection after unbind does not reconnect. + mConnection.onServiceDisconnected(COMPONENT_NAME); + mFakeExecutor.runAll(); + advanceTime(RETRY_DELAY_MS); + + verify(mContext, times(1)).bindService( + eq(mIntent), + anyInt(), + eq(mFakeExecutor), + eq(mConnection)); + } + + private void advanceTime(long millis) { + mClock.fastForward(millis); + mHandler.timeAdvance(); + } + + static class TestInjector extends PersistentServiceConnection.Injector { + private final OffsettableClock mClock; + + TestInjector(OffsettableClock clock) { + mClock = clock; + } + + @Override + public long uptimeMillis() { + return mClock.now(); + } + } + + static class FakeExecutor implements Executor { + private final Queue<Runnable> mQueue = new ArrayDeque<>(); + + @Override + public void execute(Runnable command) { + mQueue.add(command); + } + + public void runAll() { + while (!mQueue.isEmpty()) { + mQueue.remove().run(); + } + } + + public void clearAll() { + while (!mQueue.isEmpty()) { + mQueue.remove(); + } + } + } +} diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index b1ecb43f9a23..31e2abe0bb85 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1087,6 +1087,12 @@ "group": "WM_DEBUG_FOCUS", "at": "com\/android\/server\/wm\/WindowState.java" }, + "-1043981272": { + "message": "Reverting orientation. Rotating to %s from %s rather than %s.", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotation.java" + }, "-1042574499": { "message": "Attempted to add Accessibility overlay window with unknown token %s. Aborting.", "level": "WARN", @@ -4285,6 +4291,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "2066210760": { + "message": "foldStateChanged: displayId %d, halfFoldStateChanged %s, saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, mLastOrientation: %d, mRotation: %d", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotation.java" + }, "2070726247": { "message": "InsetsSource updateVisibility for %s, serverVisible: %s clientVisible: %s", "level": "DEBUG", diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index fb0a9db6a20b..7e9c4189dabb 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -41,7 +41,7 @@ public class WindowExtensionsImpl implements WindowExtensions { // TODO(b/241126279) Introduce constants to better version functionality @Override public int getVendorApiLevel() { - return 1; + return 2; } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java index 00be5a6e3416..77284c4166bd 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -109,6 +109,12 @@ class SplitContainer { return (mSplitRule instanceof SplitPlaceholderRule); } + @NonNull + SplitInfo toSplitInfo() { + return new SplitInfo(mPrimaryContainer.toActivityStack(), + mSecondaryContainer.toActivityStack(), mSplitAttributes); + } + static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) { final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule; final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule) diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 203ece091e46..1d513e444050 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -20,10 +20,11 @@ import static android.app.ActivityManager.START_SUCCESS; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE; -import static android.window.TaskFragmentOrganizer.getTransitionType; import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; @@ -76,6 +77,7 @@ import androidx.annotation.Nullable; import androidx.window.common.CommonFoldingFeature; import androidx.window.common.EmptyLifecycleCallbacksAdapter; import androidx.window.extensions.WindowExtensionsProvider; +import androidx.window.extensions.embedding.TransactionManager.TransactionRecord; import androidx.window.extensions.layout.WindowLayoutComponentImpl; import com.android.internal.annotations.VisibleForTesting; @@ -100,6 +102,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") final SplitPresenter mPresenter; + @VisibleForTesting + @GuardedBy("mLock") + final TransactionManager mTransactionManager; + // Currently applied split configuration. @GuardedBy("mLock") private final List<EmbeddingRule> mSplitRules = new ArrayList<>(); @@ -150,6 +156,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final MainThreadExecutor executor = new MainThreadExecutor(); mHandler = executor.mHandler; mPresenter = new SplitPresenter(executor, this); + mTransactionManager = new TransactionManager(mPresenter); final ActivityThread activityThread = ActivityThread.currentActivityThread(); final Application application = activityThread.getApplication(); // Register a callback to be notified about activities being created. @@ -167,7 +174,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void accept(List<CommonFoldingFeature> foldingFeatures) { synchronized (mLock) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); for (int i = 0; i < mTaskContainers.size(); i++) { final TaskContainer taskContainer = mTaskContainers.valueAt(i); if (!taskContainer.isVisible()) { @@ -186,7 +195,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen updateContainersInTask(wct, taskContainer); updateAnimationOverride(taskContainer); } - mPresenter.applyTransaction(wct); + // The WCT should be applied and merged to the device state change transition if + // there is one. + transactionRecord.apply(false /* shouldApplyIndependently */); } } } @@ -240,13 +251,25 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** + * Clears the listener set in {@link SplitController#setSplitInfoListener}. + */ + @Override + public void clearSplitInfoCallback() { + synchronized (mLock) { + mEmbeddingCallback = null; + } + } + + /** * Called when the transaction is ready so that the organizer can update the TaskFragments based * on the changes in transaction. */ @Override public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) { synchronized (mLock) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction( + transaction.getTransactionToken()); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); for (TaskFragmentTransaction.Change change : changes) { final int taskId = change.getTaskId(); @@ -297,8 +320,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Notify the server, and the server should apply and merge the // WindowContainerTransaction to the active sync to finish the TaskFragmentTransaction. - mPresenter.onTransactionHandled(transaction.getTransactionToken(), wct, - getTransitionType(wct), false /* shouldApplyIndependently */); + transactionRecord.apply(false /* shouldApplyIndependently */); updateCallbackIfNecessary(); } } @@ -323,6 +345,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen container.setInfo(wct, taskFragmentInfo); if (container.isFinished()) { + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } else { // Update with the latest Task configuration. @@ -358,15 +381,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Do not finish the dependents if the last activity is reparented to PiP. // Instead, the original split should be cleanup, and the dependent may be // expanded to fullscreen. + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); cleanupForEnterPip(wct, container); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } else if (taskFragmentInfo.isTaskClearedForReuse()) { // Do not finish the dependents if this TaskFragment was cleared due to // launching activity in the Task. + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } else if (!container.isWaitingActivityAppear()) { // Do not finish the container before the expected activity appear until // timeout. + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */); } } else if (wasInPip && isInPip) { @@ -561,6 +587,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen container.setInfo(wct, taskFragmentInfo); container.clearPendingAppearedActivities(); if (container.isEmpty()) { + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } break; @@ -999,11 +1026,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @GuardedBy("mLock") void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - onTaskFragmentAppearEmptyTimeout(wct, container); + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + onTaskFragmentAppearEmptyTimeout(transactionRecord.getTransaction(), container); // Can be applied independently as a timeout callback. - mPresenter.applyTransaction(wct, getTransitionType(wct), - true /* shouldApplyIndependently */); + transactionRecord.apply(true /* shouldApplyIndependently */); } /** @@ -1013,6 +1039,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") void onTaskFragmentAppearEmptyTimeout(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } @@ -1395,6 +1422,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") void updateContainer(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { + if (!container.getTaskContainer().isVisible()) { + // Wait until the Task is visible to avoid unnecessary update when the Task is still in + // background. + return; + } if (launchPlaceholderIfNecessary(wct, container)) { // Placeholder was launched, the positions will be updated when the activity is added // to the secondary container. @@ -1552,6 +1584,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * @param isOnCreated whether this happens during the primary activity onCreated. */ @VisibleForTesting + @GuardedBy("mLock") @Nullable Bundle getPlaceholderOptions(@NonNull Activity primaryActivity, boolean isOnCreated) { // Setting avoid move to front will also skip the animation. We only want to do that when @@ -1559,6 +1592,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Check if the primary is resumed or if this is called when the primary is onCreated // (not resumed yet). if (isOnCreated || primaryActivity.isResumed()) { + // Only set trigger type if the launch happens in foreground. + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_OPEN); return null; } final ActivityOptions options = ActivityOptions.makeBasic(); @@ -1585,6 +1620,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (SplitPresenter.shouldShowSplit(splitAttributes)) { return false; } + + mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(), false /* shouldFinishDependent */); return true; @@ -1611,16 +1648,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Notifies listeners about changes to split states if necessary. */ + @VisibleForTesting @GuardedBy("mLock") - private void updateCallbackIfNecessary() { - if (mEmbeddingCallback == null) { - return; - } - if (!allActivitiesCreated()) { + void updateCallbackIfNecessary() { + if (mEmbeddingCallback == null || !readyToReportToClient()) { return; } - List<SplitInfo> currentSplitStates = getActiveSplitStates(); - if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) { + final List<SplitInfo> currentSplitStates = getActiveSplitStates(); + if (mLastReportedSplitStates.equals(currentSplitStates)) { return; } mLastReportedSplitStates.clear(); @@ -1629,48 +1664,27 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** - * @return a list of descriptors for currently active split states. If the value returned is - * null, that indicates that the active split states are in an intermediate state and should - * not be reported. + * Returns a list of descriptors for currently active split states. */ @GuardedBy("mLock") - @Nullable + @NonNull private List<SplitInfo> getActiveSplitStates() { - List<SplitInfo> splitStates = new ArrayList<>(); + final List<SplitInfo> splitStates = new ArrayList<>(); for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - final List<SplitContainer> splitContainers = mTaskContainers.valueAt(i) - .mSplitContainers; - for (SplitContainer container : splitContainers) { - if (container.getPrimaryContainer().isEmpty() - || container.getSecondaryContainer().isEmpty()) { - // We are in an intermediate state because either the split container is about - // to be removed or the primary or secondary container are about to receive an - // activity. - return null; - } - final ActivityStack primaryContainer = container.getPrimaryContainer() - .toActivityStack(); - final ActivityStack secondaryContainer = container.getSecondaryContainer() - .toActivityStack(); - final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer, - container.getSplitAttributes()); - splitStates.add(splitState); - } + mTaskContainers.valueAt(i).getSplitStates(splitStates); } return splitStates; } /** - * Checks if all activities that are registered with the containers have already appeared in - * the client. + * Whether we can now report the split states to the client. */ - private boolean allActivitiesCreated() { + @GuardedBy("mLock") + private boolean readyToReportToClient() { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { - final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; - for (TaskFragmentContainer container : containers) { - if (!container.taskInfoActivityCountMatchesCreated()) { - return false; - } + if (mTaskContainers.valueAt(i).isInIntermediateState()) { + // If any Task is in an intermediate state, wait for the server update. + return false; } } return true; @@ -1895,23 +1909,26 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // that we don't launch it if an activity itself already requested something to be // launched to side. synchronized (mLock) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - SplitController.this.onActivityCreated(wct, activity); + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + transactionRecord.setOriginType(TRANSIT_OPEN); + SplitController.this.onActivityCreated(transactionRecord.getTransaction(), + activity); // The WCT should be applied and merged to the activity launch transition. - mPresenter.applyTransaction(wct, getTransitionType(wct), - false /* shouldApplyIndependently */); + transactionRecord.apply(false /* shouldApplyIndependently */); } } @Override public void onActivityConfigurationChanged(@NonNull Activity activity) { synchronized (mLock) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - SplitController.this.onActivityConfigurationChanged(wct, activity); + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + SplitController.this.onActivityConfigurationChanged( + transactionRecord.getTransaction(), activity); // The WCT should be applied and merged to the Task change transition so that the // placeholder is launched in the same transition. - mPresenter.applyTransaction(wct, getTransitionType(wct), - false /* shouldApplyIndependently */); + transactionRecord.apply(false /* shouldApplyIndependently */); } } @@ -1967,7 +1984,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } synchronized (mLock) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); + final TransactionRecord transactionRecord = mTransactionManager + .startNewTransaction(); + transactionRecord.setOriginType(TRANSIT_OPEN); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); final TaskFragmentContainer launchedInTaskFragment; if (launchingActivity != null) { final int taskId = getTaskId(launchingActivity); @@ -1980,13 +2000,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (launchedInTaskFragment != null) { // Make sure the WCT is applied immediately instead of being queued so that the // TaskFragment will be ready before activity attachment. - mPresenter.applyTransaction(wct, getTransitionType(wct), - false /* shouldApplyIndependently */); + transactionRecord.apply(false /* shouldApplyIndependently */); // Amend the request to let the WM know that the activity should be placed in // the dedicated container. options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, launchedInTaskFragment.getTaskFragmentToken()); mCurrentIntent = intent; + } else { + transactionRecord.abort(); } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 91573ffef568..231da0542e95 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -140,7 +140,7 @@ class TaskContainer { void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { mConfiguration.setTo(info.getConfiguration()); mDisplayId = info.getDisplayId(); - mIsVisible = info.isVisibleRequested(); + mIsVisible = info.isVisible(); } /** @@ -221,6 +221,24 @@ class TaskContainer { return mContainers.indexOf(child); } + /** Whether the Task is in an intermediate state waiting for the server update.*/ + boolean isInIntermediateState() { + for (TaskFragmentContainer container : mContainers) { + if (container.isInIntermediateState()) { + // We are in an intermediate state to wait for server update on this TaskFragment. + return true; + } + } + return false; + } + + /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */ + void getSplitStates(@NonNull List<SplitInfo> outSplitStates) { + for (SplitContainer container : mSplitContainers) { + outSplitStates.add(container.toSplitInfo()); + } + } + /** * A wrapper class which contains the display ID and {@link Configuration} of a * {@link TaskContainer} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java index ef5ea563de12..a7d47ef81687 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java @@ -161,7 +161,7 @@ class TaskFragmentAnimationSpec { // The position should be 0-based as we will post translate in // TaskFragmentAnimationAdapter#onAnimationUpdate final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0, - 0, 0); + startBounds.top - endBounds.top, 0); endTranslate.setDuration(CHANGE_ANIMATION_DURATION); endSet.addAnimation(endTranslate); // The end leash is resizing, we should update the window crop based on the clip rect. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 18712aed1be6..71b884018bdb 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -166,16 +166,34 @@ class TaskFragmentContainer { return allActivities; } - /** - * Checks if the count of activities from the same process in task fragment info corresponds to - * the ones created and available on the client side. - */ - boolean taskInfoActivityCountMatchesCreated() { + /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/ + boolean isInIntermediateState() { if (mInfo == null) { - return false; + // Haven't received onTaskFragmentAppeared event. + return true; + } + if (mInfo.isEmpty()) { + // Empty TaskFragment will be removed or will have activity launched into it soon. + return true; + } + if (!mPendingAppearedActivities.isEmpty()) { + // Reparented activity hasn't appeared. + return true; } - return mPendingAppearedActivities.isEmpty() - && mInfo.getActivities().size() == collectNonFinishingActivities().size(); + // Check if there is any reported activity that is no longer alive. + for (IBinder token : mInfo.getActivities()) { + final Activity activity = mController.getActivity(token); + if (activity == null && !mTaskContainer.isVisible()) { + // Activity can be null if the activity is not attached to process yet. That can + // happen when the activity is started in background. + continue; + } + if (activity == null || activity.isFinishing()) { + // One of the reported activity is no longer alive, wait for the server update. + return true; + } + } + return false; } @NonNull diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java new file mode 100644 index 000000000000..0071fea41aa8 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TransactionManager.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_NONE; + +import android.os.IBinder; +import android.view.WindowManager.TransitionType; +import android.window.TaskFragmentOrganizer; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Responsible for managing the current {@link WindowContainerTransaction} as a response to device + * state changes and app interactions. + * + * A typical use flow: + * 1. Call {@link #startNewTransaction} to start tracking the changes. + * 2. Use {@link TransactionRecord#setOriginType(int)} (int)} to record the type of operation that + * will start a new transition on system server. + * 3. Use {@link #getCurrentTransactionRecord()} to get current {@link TransactionRecord} for + * changes. + * 4. Call {@link TransactionRecord#apply(boolean)} to request the system server to apply changes in + * the current {@link WindowContainerTransaction}, or call {@link TransactionRecord#abort()} to + * dispose the current one. + * + * Note: + * There should be only one transaction at a time. The caller should not call + * {@link #startNewTransaction} again before calling {@link TransactionRecord#apply(boolean)} or + * {@link TransactionRecord#abort()} to the previous transaction. + */ +class TransactionManager { + + @NonNull + private final TaskFragmentOrganizer mOrganizer; + + @Nullable + private TransactionRecord mCurrentTransaction; + + TransactionManager(@NonNull TaskFragmentOrganizer organizer) { + mOrganizer = organizer; + } + + @NonNull + TransactionRecord startNewTransaction() { + return startNewTransaction(null /* taskFragmentTransactionToken */); + } + + /** + * Starts tracking the changes in a new {@link WindowContainerTransaction}. Caller can call + * {@link #getCurrentTransactionRecord()} later to continue adding change to the current + * transaction until {@link TransactionRecord#apply(boolean)} or + * {@link TransactionRecord#abort()} is called. + * @param taskFragmentTransactionToken {@link android.window.TaskFragmentTransaction + * #getTransactionToken()} if this is a response to a + * {@link android.window.TaskFragmentTransaction}. + */ + @NonNull + TransactionRecord startNewTransaction(@Nullable IBinder taskFragmentTransactionToken) { + if (mCurrentTransaction != null) { + mCurrentTransaction = null; + throw new IllegalStateException( + "The previous transaction has not been applied or aborted,"); + } + mCurrentTransaction = new TransactionRecord(taskFragmentTransactionToken); + return mCurrentTransaction; + } + + /** + * Gets the current {@link TransactionRecord} started from {@link #startNewTransaction}. + */ + @NonNull + TransactionRecord getCurrentTransactionRecord() { + if (mCurrentTransaction == null) { + throw new IllegalStateException("startNewTransaction() is not invoked before calling" + + " getCurrentTransactionRecord()."); + } + return mCurrentTransaction; + } + + /** The current transaction. The manager should only handle one transaction at a time. */ + class TransactionRecord { + /** + * {@link WindowContainerTransaction} containing the current change. + * @see #startNewTransaction(IBinder) + * @see #apply (boolean) + */ + @NonNull + private final WindowContainerTransaction mTransaction = new WindowContainerTransaction(); + + /** + * If the current transaction is a response to a + * {@link android.window.TaskFragmentTransaction}, this is the + * {@link android.window.TaskFragmentTransaction#getTransactionToken()}. + * @see #startNewTransaction(IBinder) + */ + @Nullable + private final IBinder mTaskFragmentTransactionToken; + + /** + * To track of the origin type of the current {@link #mTransaction}. When + * {@link #apply (boolean)} to start a new transition, this is the type to request. + * @see #setOriginType(int) + * @see #getTransactionTransitionType() + */ + @TransitionType + private int mOriginType = TRANSIT_NONE; + + TransactionRecord(@Nullable IBinder taskFragmentTransactionToken) { + mTaskFragmentTransactionToken = taskFragmentTransactionToken; + } + + @NonNull + WindowContainerTransaction getTransaction() { + ensureCurrentTransaction(); + return mTransaction; + } + + /** + * Sets the {@link TransitionType} that triggers this transaction. If there are multiple + * calls, only the first call will be respected as the "origin" type. + */ + void setOriginType(@TransitionType int type) { + ensureCurrentTransaction(); + if (mOriginType != TRANSIT_NONE) { + // Skip if the origin type has already been set. + return; + } + mOriginType = type; + } + + /** + * Requests the system server to apply the current transaction started from + * {@link #startNewTransaction}. + * @param shouldApplyIndependently If {@code true}, the {@link #mCurrentTransaction} will + * request a new transition, which will be queued until the + * sync engine is free if there is any other active sync. + * If {@code false}, the {@link #startNewTransaction} will + * be directly applied to the active sync. + */ + void apply(boolean shouldApplyIndependently) { + ensureCurrentTransaction(); + if (mTaskFragmentTransactionToken != null) { + // If this is a response to a TaskFragmentTransaction. + mOrganizer.onTransactionHandled(mTaskFragmentTransactionToken, mTransaction, + getTransactionTransitionType(), shouldApplyIndependently); + } else { + mOrganizer.applyTransaction(mTransaction, getTransactionTransitionType(), + shouldApplyIndependently); + } + dispose(); + } + + /** Called when there is no need to {@link #apply(boolean)} the current transaction. */ + void abort() { + ensureCurrentTransaction(); + dispose(); + } + + private void dispose() { + TransactionManager.this.mCurrentTransaction = null; + } + + private void ensureCurrentTransaction() { + if (TransactionManager.this.mCurrentTransaction != this) { + throw new IllegalStateException( + "This transaction has already been apply() or abort()."); + } + } + + /** + * Gets the {@link TransitionType} that we will request transition with for the + * current {@link WindowContainerTransaction}. + */ + @VisibleForTesting + @TransitionType + int getTransactionTransitionType() { + // Use TRANSIT_CHANGE as default if there is not opening/closing window. + return mOriginType != TRANSIT_NONE ? mOriginType : TRANSIT_CHANGE; + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index c76f568e117f..b516e1407b11 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -103,13 +103,20 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { /** * Similar to {@link #addWindowLayoutInfoListener(Activity, Consumer)}, but takes a UI Context * as a parameter. + * + * Jetpack {@link androidx.window.layout.ExtensionWindowLayoutInfoBackend} makes sure all + * consumers related to the same {@link Context} gets updated {@link WindowLayoutInfo} + * together. However only the first registered consumer of a {@link Context} will actually + * invoke {@link #addWindowLayoutInfoListener(Context, Consumer)}. + * Here we enforce that {@link #addWindowLayoutInfoListener(Context, Consumer)} can only be + * called once for each {@link Context}. */ - // TODO(b/204073440): Add @Override to hook the API in WM extensions library. + @Override public void addWindowLayoutInfoListener(@NonNull @UiContext Context context, @NonNull Consumer<WindowLayoutInfo> consumer) { if (mWindowLayoutChangeListeners.containsKey(context) + // In theory this method can be called on the same consumer with different context. || mWindowLayoutChangeListeners.containsValue(consumer)) { - // Early return if the listener or consumer has been registered. return; } if (!context.isUiContext()) { diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 25d034756265..87d027899eb4 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -102,6 +102,7 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; /** * Test class for {@link SplitController}. @@ -132,6 +133,9 @@ public class SplitControllerTest { private SplitController mSplitController; private SplitPresenter mSplitPresenter; + private Consumer<List<SplitInfo>> mEmbeddingCallback; + private List<SplitInfo> mSplitInfos; + private TransactionManager mTransactionManager; @Before public void setUp() { @@ -140,8 +144,17 @@ public class SplitControllerTest { .getCurrentWindowLayoutInfo(anyInt(), any()); mSplitController = new SplitController(mWindowLayoutComponent); mSplitPresenter = mSplitController.mPresenter; + mSplitInfos = new ArrayList<>(); + mEmbeddingCallback = splitInfos -> { + mSplitInfos.clear(); + mSplitInfos.addAll(splitInfos); + }; + mSplitController.setSplitInfoCallback(mEmbeddingCallback); + mTransactionManager = mSplitController.mTransactionManager; spyOn(mSplitController); spyOn(mSplitPresenter); + spyOn(mEmbeddingCallback); + spyOn(mTransactionManager); doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean()); final Configuration activityConfig = new Configuration(); activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); @@ -212,6 +225,8 @@ public class SplitControllerTest { @Test public void testOnTaskFragmentAppearEmptyTimeout() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID); doCallRealMethod().when(mSplitController).onTaskFragmentAppearEmptyTimeout(any(), any()); mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf); @@ -324,6 +339,30 @@ public class SplitControllerTest { } @Test + public void testUpdateContainer_skipIfTaskIsInvisible() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); + final TaskFragmentContainer taskFragmentContainer = taskContainer.mContainers.get(0); + spyOn(taskContainer); + + // No update when the Task is invisible. + clearInvocations(mSplitPresenter); + doReturn(false).when(taskContainer).isVisible(); + mSplitController.updateContainer(mTransaction, taskFragmentContainer); + + verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any()); + + // Update the split when the Task is visible. + doReturn(true).when(taskContainer).isVisible(); + mSplitController.updateContainer(mTransaction, taskFragmentContainer); + + verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0), + taskFragmentContainer, mTransaction); + } + + @Test public void testOnStartActivityResultError() { final Intent intent = new Intent(); final TaskContainer taskContainer = createTestTaskContainer(); @@ -615,6 +654,8 @@ public class SplitControllerTest { @Test public void testResolveActivityToContainer_placeholderRule_notInTaskFragment() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); setupPlaceholderRule(mActivity); final SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); @@ -647,6 +688,8 @@ public class SplitControllerTest { @Test public void testResolveActivityToContainer_placeholderRule_inTopMostTaskFragment() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); setupPlaceholderRule(mActivity); final SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); @@ -679,6 +722,8 @@ public class SplitControllerTest { @Test public void testResolveActivityToContainer_placeholderRule_inSecondarySplit() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); setupPlaceholderRule(mActivity); final SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); @@ -961,6 +1006,8 @@ public class SplitControllerTest { @Test public void testGetPlaceholderOptions() { + // Setup to make sure a transaction record is started. + mTransactionManager.startNewTransaction(); doReturn(true).when(mActivity).isResumed(); assertNull(mSplitController.getPlaceholderOptions(mActivity, false /* isOnCreated */)); @@ -1147,18 +1194,71 @@ public class SplitControllerTest { + "of other properties", SplitController.haveSamePresentation(splitRule1, splitRule2, new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); + } + + @Test + public void testSplitInfoCallback_reportSplit() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + + mSplitController.updateCallbackIfNecessary(); + assertEquals(1, mSplitInfos.size()); + final SplitInfo splitInfo = mSplitInfos.get(0); + assertEquals(1, splitInfo.getPrimaryActivityStack().getActivities().size()); + assertEquals(1, splitInfo.getSecondaryActivityStack().getActivities().size()); + assertEquals(r0, splitInfo.getPrimaryActivityStack().getActivities().get(0)); + assertEquals(r1, splitInfo.getSecondaryActivityStack().getActivities().get(0)); + } + + @Test + public void testSplitInfoCallback_reportSplitInMultipleTasks() { + final int taskId0 = 1; + final int taskId1 = 2; + final Activity r0 = createMockActivity(taskId0); + final Activity r1 = createMockActivity(taskId0); + final Activity r2 = createMockActivity(taskId1); + final Activity r3 = createMockActivity(taskId1); + addSplitTaskFragments(r0, r1); + addSplitTaskFragments(r2, r3); + + mSplitController.updateCallbackIfNecessary(); + assertEquals(2, mSplitInfos.size()); + } + + @Test + public void testSplitInfoCallback_doNotReportIfInIntermediateState() { + final Activity r0 = createMockActivity(); + final Activity r1 = createMockActivity(); + addSplitTaskFragments(r0, r1); + final TaskFragmentContainer tf0 = mSplitController.getContainerWithActivity(r0); + final TaskFragmentContainer tf1 = mSplitController.getContainerWithActivity(r1); + spyOn(tf0); + spyOn(tf1); + // Do not report if activity has not appeared in the TaskFragmentContainer in split. + doReturn(true).when(tf0).isInIntermediateState(); + mSplitController.updateCallbackIfNecessary(); + verify(mEmbeddingCallback, never()).accept(any()); + doReturn(false).when(tf0).isInIntermediateState(); + mSplitController.updateCallbackIfNecessary(); + verify(mEmbeddingCallback).accept(any()); } /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { + return createMockActivity(TASK_ID); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity(int taskId) { final Activity activity = mock(Activity.class); doReturn(mActivityResources).when(activity).getResources(); final IBinder activityToken = new Binder(); doReturn(activityToken).when(activity).getActivityToken(); doReturn(activity).when(mSplitController).getActivity(activityToken); - doReturn(TASK_ID).when(activity).getTaskId(); + doReturn(taskId).when(activity).getTaskId(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); return activity; @@ -1166,7 +1266,8 @@ public class SplitControllerTest { /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { - final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID); + final TaskFragmentContainer container = mSplitController.newContainer(activity, + activity.getTaskId()); setupTaskFragmentInfo(container, activity); return container; } @@ -1257,7 +1358,7 @@ public class SplitControllerTest { // We need to set those in case we are not respecting clear top. // TODO(b/231845476) we should always respect clearTop. - final int windowingMode = mSplitController.getTaskContainer(TASK_ID) + final int windowingMode = mSplitController.getTaskContainer(primaryContainer.getTaskId()) .getWindowingModeForSplitTaskFragment(TASK_BOUNDS); primaryContainer.setLastRequestedWindowingMode(windowingMode); secondaryContainer.setLastRequestedWindowingMode(windowingMode); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 35415d816d8b..d43c471fb8ae 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -334,6 +334,70 @@ public class TaskFragmentContainerTest { assertFalse(container.hasActivity(mActivity.getActivityToken())); } + @Test + public void testIsInIntermediateState() { + // True if no info set. + final TaskContainer taskContainer = createTestTaskContainer(); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + mIntent, taskContainer, mController); + spyOn(taskContainer); + doReturn(true).when(taskContainer).isVisible(); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // True if empty info set. + final List<IBinder> activities = new ArrayList<>(); + doReturn(activities).when(mInfo).getActivities(); + doReturn(true).when(mInfo).isEmpty(); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if info is not empty. + doReturn(false).when(mInfo).isEmpty(); + container.setInfo(mTransaction, mInfo); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + + // True if there is pending appeared activity. + container.addPendingAppearedActivity(mActivity); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // True if the activity is finishing. + activities.add(mActivity.getActivityToken()); + doReturn(true).when(mActivity).isFinishing(); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if the activity is not finishing. + doReturn(false).when(mActivity).isFinishing(); + container.setInfo(mTransaction, mInfo); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + + // True if there is a token that can't find associated activity. + activities.clear(); + activities.add(new Binder()); + container.setInfo(mTransaction, mInfo); + + assertTrue(container.isInIntermediateState()); + assertTrue(taskContainer.isInIntermediateState()); + + // False if there is a token that can't find associated activity when the Task is invisible. + doReturn(false).when(taskContainer).isVisible(); + + assertFalse(container.isInIntermediateState()); + assertFalse(taskContainer.isInIntermediateState()); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { final Activity activity = mock(Activity.class); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java new file mode 100644 index 000000000000..62006bd51399 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.clearInvocations; + +import android.os.Binder; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.window.TaskFragmentOrganizer; +import android.window.WindowContainerTransaction; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.window.extensions.embedding.TransactionManager.TransactionRecord; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test class for {@link TransactionManager}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:TransactionManagerTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TransactionManagerTest { + + @Mock + private TaskFragmentOrganizer mOrganizer; + private TransactionManager mTransactionManager; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mTransactionManager = new TransactionManager(mOrganizer); + } + + @Test + public void testStartNewTransaction() { + mTransactionManager.startNewTransaction(); + + // Throw exception if #startNewTransaction is called twice without #apply() or #abort(). + assertThrows(IllegalStateException.class, mTransactionManager::startNewTransaction); + + // Allow to start new after #apply() the last transaction. + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + transactionRecord.apply(false /* shouldApplyIndependently */); + transactionRecord = mTransactionManager.startNewTransaction(); + + // Allow to start new after #abort() the last transaction. + transactionRecord.abort(); + mTransactionManager.startNewTransaction(); + } + + @Test + public void testSetTransactionOriginType() { + // Return TRANSIT_CHANGE if there is no trigger type set. + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + + assertEquals(TRANSIT_CHANGE, transactionRecord.getTransactionTransitionType()); + + // Return the first set type. + mTransactionManager.getCurrentTransactionRecord().abort(); + transactionRecord = mTransactionManager.startNewTransaction(); + transactionRecord.setOriginType(TRANSIT_OPEN); + + assertEquals(TRANSIT_OPEN, transactionRecord.getTransactionTransitionType()); + + transactionRecord.setOriginType(TRANSIT_CLOSE); + + assertEquals(TRANSIT_OPEN, transactionRecord.getTransactionTransitionType()); + + // Reset when #startNewTransaction(). + transactionRecord.abort(); + transactionRecord = mTransactionManager.startNewTransaction(); + + assertEquals(TRANSIT_CHANGE, transactionRecord.getTransactionTransitionType()); + } + + @Test + public void testGetCurrentTransactionRecord() { + // Throw exception if #getTransaction is called without calling #startNewTransaction(). + assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord); + + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + assertNotNull(transactionRecord); + + // Same WindowContainerTransaction should be returned. + assertSame(transactionRecord, mTransactionManager.getCurrentTransactionRecord()); + + // Reset after #abort(). + transactionRecord.abort(); + assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord); + + // New WindowContainerTransaction after #startNewTransaction(). + mTransactionManager.startNewTransaction(); + assertNotEquals(transactionRecord, mTransactionManager.getCurrentTransactionRecord()); + + // Reset after #apply(). + mTransactionManager.getCurrentTransactionRecord().apply( + false /* shouldApplyIndependently */); + assertThrows(IllegalStateException.class, mTransactionManager::getCurrentTransactionRecord); + } + + @Test + public void testApply() { + // #applyTransaction(false) + TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + int transitionType = transactionRecord.getTransactionTransitionType(); + WindowContainerTransaction wct = transactionRecord.getTransaction(); + transactionRecord.apply(false /* shouldApplyIndependently */); + + verify(mOrganizer).applyTransaction(wct, transitionType, + false /* shouldApplyIndependently */); + + // #applyTransaction(true) + clearInvocations(mOrganizer); + transactionRecord = mTransactionManager.startNewTransaction(); + transitionType = transactionRecord.getTransactionTransitionType(); + wct = transactionRecord.getTransaction(); + transactionRecord.apply(true /* shouldApplyIndependently */); + + verify(mOrganizer).applyTransaction(wct, transitionType, + true /* shouldApplyIndependently */); + + // #onTransactionHandled(false) + clearInvocations(mOrganizer); + IBinder token = new Binder(); + transactionRecord = mTransactionManager.startNewTransaction(token); + transitionType = transactionRecord.getTransactionTransitionType(); + wct = transactionRecord.getTransaction(); + transactionRecord.apply(false /* shouldApplyIndependently */); + + verify(mOrganizer).onTransactionHandled(token, wct, transitionType, + false /* shouldApplyIndependently */); + + // #onTransactionHandled(true) + clearInvocations(mOrganizer); + token = new Binder(); + transactionRecord = mTransactionManager.startNewTransaction(token); + transitionType = transactionRecord.getTransactionTransitionType(); + wct = transactionRecord.getTransaction(); + transactionRecord.apply(true /* shouldApplyIndependently */); + + verify(mOrganizer).onTransactionHandled(token, wct, transitionType, + true /* shouldApplyIndependently */); + + // Throw exception if there is any more interaction. + final TransactionRecord record = transactionRecord; + assertThrows(IllegalStateException.class, + () -> record.apply(false /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + () -> record.apply(true /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + record::abort); + } + + @Test + public void testAbort() { + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + transactionRecord.abort(); + + // Throw exception if there is any more interaction. + verifyNoMoreInteractions(mOrganizer); + assertThrows(IllegalStateException.class, + () -> transactionRecord.apply(false /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + () -> transactionRecord.apply(true /* shouldApplyIndependently */)); + assertThrows(IllegalStateException.class, + transactionRecord::abort); + } +} diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar Binary files differindex 2c766d81d611..4978e04e0115 100644 --- a/libs/WindowManager/Jetpack/window-extensions-release.aar +++ b/libs/WindowManager/Jetpack/window-extensions-release.aar diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 7960dec5080b..f615ad6e671b 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -44,6 +44,10 @@ filegroup { srcs: [ "src/com/android/wm/shell/util/**/*.java", "src/com/android/wm/shell/common/split/SplitScreenConstants.java", + "src/com/android/wm/shell/sysui/ShellSharedConstants.java", + "src/com/android/wm/shell/common/TransactionPool.java", + "src/com/android/wm/shell/animation/Interpolators.java", + "src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java", ], path: "src", } @@ -100,6 +104,21 @@ genrule { out: ["wm_shell_protolog.json"], } +genrule { + name: "protolog.json.gz", + srcs: [":generate-wm_shell_protolog.json"], + out: ["wmshell.protolog.json.gz"], + cmd: "$(location minigzip) -c < $(in) > $(out)", + tools: ["minigzip"], +} + +prebuilt_etc { + name: "wmshell.protolog.json.gz", + system_ext_specific: true, + src: ":protolog.json.gz", + filename_from_src: true, +} + // End ProtoLog java_library { @@ -123,9 +142,6 @@ android_library { resource_dirs: [ "res", ], - java_resources: [ - ":generate-wm_shell_protolog.json", - ], static_libs: [ "androidx.appcompat_appcompat", "androidx.arch.core_core-runtime", diff --git a/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml new file mode 100644 index 000000000000..5ecba380fb60 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/decor_back_button_dark.xml @@ -0,0 +1,32 @@ +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32.0dp" + android:height="32.0dp" + android:viewportWidth="32.0" + android:viewportHeight="32.0" + > + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="4.0" + android:translateY="4.0" > + <path + android:fillColor="@android:color/black" + android:pathData="MM24,40.3 L7.7,24 24,7.7 26.8,10.45 15.3,22H40.3V26H15.3L26.8,37.5Z"/> + + </group> +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml b/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml index 8207365a737d..416287d2cbb3 100644 --- a/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml +++ b/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml @@ -15,8 +15,7 @@ ~ limitations under the License. --> <shape android:shape="rectangle" - android:tintMode="multiply" - android:tint="@color/decor_caption_title_color" xmlns:android="http://schemas.android.com/apk/res/android"> - <solid android:color="?android:attr/colorPrimary" /> + <solid android:color="@android:color/white" /> + <corners android:radius="20dp" /> </shape> diff --git a/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml index f2f1a1d55dee..cf9e632f6941 100644 --- a/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml +++ b/libs/WindowManager/Shell/res/drawable/decor_close_button_dark.xml @@ -18,15 +18,13 @@ android:width="32.0dp" android:height="32.0dp" android:viewportWidth="32.0" - android:viewportHeight="32.0" - android:tint="@color/decor_button_dark_color" - > + android:viewportHeight="32.0"> <group android:scaleX="0.5" android:scaleY="0.5" - android:translateX="8.0" - android:translateY="8.0" > + android:translateX="4.0" + android:translateY="4.0" > <path - android:fillColor="@android:color/white" - android:pathData="M6.9,4.0l-2.9,2.9 9.1,9.1 -9.1,9.200001 2.9,2.799999 9.1,-9.1 9.1,9.1 2.9,-2.799999 -9.1,-9.200001 9.1,-9.1 -2.9,-2.9 -9.1,9.2z"/> + android:fillColor="@android:color/black" + android:pathData="M12.45,38.35 L9.65,35.55 21.2,24 9.65,12.45 12.45,9.65 24,21.2 35.55,9.65 38.35,12.45 26.8,24 38.35,35.55 35.55,38.35 24,26.8Z"/> </group> </vector> diff --git a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml new file mode 100644 index 000000000000..c9f262398f68 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <group android:translateY="8.0"> + <path + android:fillColor="@android:color/black" android:pathData="M3,5V3H21V5Z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml index d183e42c173b..38cd5702f134 100644 --- a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml +++ b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml @@ -17,39 +17,33 @@ <com.android.wm.shell.windowdecor.WindowDecorLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/caption" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="end" + android:gravity="center_horizontal" android:background="@drawable/decor_caption_title"> <Button - android:id="@+id/minimize_window" - android:visibility="gone" + android:id="@+id/back_button" android:layout_width="32dp" android:layout_height="32dp" android:layout_margin="5dp" android:padding="4dp" - android:layout_gravity="top|end" - android:contentDescription="@string/maximize_button_text" - android:background="@drawable/decor_minimize_button_dark" - android:duplicateParentState="true"/> + android:contentDescription="@string/back_button_text" + android:background="@drawable/decor_back_button_dark" + /> <Button - android:id="@+id/maximize_window" - android:layout_width="32dp" + android:id="@+id/caption_handle" + android:layout_width="128dp" android:layout_height="32dp" android:layout_margin="5dp" android:padding="4dp" - android:layout_gravity="center_vertical|end" - android:contentDescription="@string/maximize_button_text" - android:background="@drawable/decor_maximize_button_dark" - android:duplicateParentState="true"/> + android:contentDescription="@string/handle_text" + android:background="@drawable/decor_handle_dark"/> <Button android:id="@+id/close_window" android:layout_width="32dp" android:layout_height="32dp" android:layout_margin="5dp" android:padding="4dp" - android:layout_gravity="center_vertical|end" android:contentDescription="@string/close_button_text" - android:background="@drawable/decor_close_button_dark" - android:duplicateParentState="true"/> -</com.android.wm.shell.windowdecor.WindowDecorLinearLayout> + android:background="@drawable/decor_close_button_dark"/> +</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 0bc70857a113..3ee20ea95ee5 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -321,4 +321,21 @@ <!-- The smaller size of the dismiss target (shrinks when something is in the target). --> <dimen name="floating_dismiss_circle_small">120dp</dimen> + + <!-- The thickness of shadows of a window that has focus in DIP. --> + <dimen name="freeform_decor_shadow_focused_thickness">20dp</dimen> + + <!-- The thickness of shadows of a window that doesn't have focus in DIP. --> + <dimen name="freeform_decor_shadow_unfocused_thickness">5dp</dimen> + + <!-- Height of button (32dp) + 2 * margin (5dp each). --> + <dimen name="freeform_decor_caption_height">42dp</dimen> + + <!-- Width of buttons (64dp) + handle (128dp) + padding (24dp total). --> + <dimen name="freeform_decor_caption_width">216dp</dimen> + + <dimen name="freeform_resize_handle">30dp</dimen> + + <dimen name="freeform_resize_corner">44dp</dimen> + </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 1d1162daf249..d8a507469722 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -193,4 +193,8 @@ <string name="minimize_button_text">Minimize</string> <!-- Accessibility text for the close window button [CHAR LIMIT=NONE] --> <string name="close_button_text">Close</string> + <!-- Accessibility text for the caption back button [CHAR LIMIT=NONE] --> + <string name="back_button_text">Back</string> + <!-- Accessibility text for the caption handle [CHAR LIMIT=NONE] --> + <string name="handle_text">Handle</string> </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java index 591e3476ecd9..215308d9e96e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java @@ -130,6 +130,10 @@ class ActivityEmbeddingAnimationAdapter { if (!cropRect.intersect(mWholeAnimationBounds)) { // Hide the surface when it is outside of the animation area. t.setAlpha(mLeash, 0); + } else if (mAnimation.hasExtension()) { + // Allow the surface to be shown in its original bounds in case we want to use edge + // extensions. + cropRect.union(mChange.getEndAbsBounds()); } // cropRect is in absolute coordinate, so we need to translate it to surface top left. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index 756d80204833..921861ae0913 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -21,6 +21,7 @@ import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; +import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; import android.animation.Animator; @@ -45,6 +46,7 @@ import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.function.Consumer; /** To run the ActivityEmbedding animations. */ class ActivityEmbeddingAnimationRunner { @@ -65,10 +67,31 @@ class ActivityEmbeddingAnimationRunner { void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction) { + // There may be some surface change that we want to apply after the start transaction is + // applied to make sure the surface is ready. + final List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks = + new ArrayList<>(); final Animator animator = createAnimator(info, startTransaction, finishTransaction, - () -> mController.onAnimationFinished(transition)); - startTransaction.apply(); - animator.start(); + () -> mController.onAnimationFinished(transition), postStartTransactionCallbacks); + + // Start the animation. + if (!postStartTransactionCallbacks.isEmpty()) { + // postStartTransactionCallbacks require that the start transaction is already + // applied to run otherwise they may result in flickers and UI inconsistencies. + startTransaction.apply(true /* sync */); + + // Run tasks that require startTransaction to already be applied + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (Consumer<SurfaceControl.Transaction> postStartTransactionCallback : + postStartTransactionCallbacks) { + postStartTransactionCallback.accept(t); + } + t.apply(); + animator.start(); + } else { + startTransaction.apply(); + animator.start(); + } } /** @@ -85,9 +108,13 @@ class ActivityEmbeddingAnimationRunner { Animator createAnimator(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Runnable animationFinishCallback) { - final List<ActivityEmbeddingAnimationAdapter> adapters = - createAnimationAdapters(info, startTransaction, finishTransaction); + @NonNull Runnable animationFinishCallback, + @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks) { + final List<ActivityEmbeddingAnimationAdapter> adapters = createAnimationAdapters(info, + startTransaction); + addEdgeExtensionIfNeeded(startTransaction, finishTransaction, postStartTransactionCallbacks, + adapters); + addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters); long duration = 0; for (ActivityEmbeddingAnimationAdapter adapter : adapters) { duration = Math.max(duration, adapter.getDurationHint()); @@ -131,8 +158,7 @@ class ActivityEmbeddingAnimationRunner { */ @NonNull private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters( - @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction) { + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) { boolean isChangeTransition = false; for (TransitionInfo.Change change : info.getChanges()) { if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) { @@ -148,25 +174,23 @@ class ActivityEmbeddingAnimationRunner { return createChangeAnimationAdapters(info, startTransaction); } if (Transitions.isClosingType(info.getType())) { - return createCloseAnimationAdapters(info, startTransaction, finishTransaction); + return createCloseAnimationAdapters(info); } - return createOpenAnimationAdapters(info, startTransaction, finishTransaction); + return createOpenAnimationAdapters(info); } @NonNull private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters( - @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction) { - return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction, - true /* isOpening */, mAnimationSpec::loadOpenAnimation); + @NonNull TransitionInfo info) { + return createOpenCloseAnimationAdapters(info, true /* isOpening */, + mAnimationSpec::loadOpenAnimation); } @NonNull private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters( - @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction) { - return createOpenCloseAnimationAdapters(info, startTransaction, finishTransaction, - false /* isOpening */, mAnimationSpec::loadCloseAnimation); + @NonNull TransitionInfo info) { + return createOpenCloseAnimationAdapters(info, false /* isOpening */, + mAnimationSpec::loadCloseAnimation); } /** @@ -175,8 +199,7 @@ class ActivityEmbeddingAnimationRunner { */ @NonNull private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters( - @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, boolean isOpening, + @NonNull TransitionInfo info, boolean isOpening, @NonNull AnimationProvider animationProvider) { // We need to know if the change window is only a partial of the whole animation screen. // If so, we will need to adjust it to make the whole animation screen looks like one. @@ -200,8 +223,7 @@ class ActivityEmbeddingAnimationRunner { final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>(); for (TransitionInfo.Change change : openingChanges) { final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter( - info, change, startTransaction, finishTransaction, animationProvider, - openingWholeScreenBounds); + info, change, animationProvider, openingWholeScreenBounds); if (isOpening) { adapter.overrideLayer(offsetLayer++); } @@ -209,8 +231,7 @@ class ActivityEmbeddingAnimationRunner { } for (TransitionInfo.Change change : closingChanges) { final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter( - info, change, startTransaction, finishTransaction, animationProvider, - closingWholeScreenBounds); + info, change, animationProvider, closingWholeScreenBounds); if (!isOpening) { adapter.overrideLayer(offsetLayer++); } @@ -219,20 +240,51 @@ class ActivityEmbeddingAnimationRunner { return adapters; } + /** Adds edge extension to the surfaces that have such an animation property. */ + private void addEdgeExtensionIfNeeded(@NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks, + @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) { + for (ActivityEmbeddingAnimationAdapter adapter : adapters) { + final Animation animation = adapter.mAnimation; + if (!animation.hasExtension()) { + continue; + } + final TransitionInfo.Change change = adapter.mChange; + if (Transitions.isOpeningType(adapter.mChange.getMode())) { + // Need to screenshot after startTransaction is applied otherwise activity + // may not be visible or ready yet. + postStartTransactionCallbacks.add( + t -> edgeExtendWindow(change, animation, t, finishTransaction)); + } else { + // Can screenshot now (before startTransaction is applied) + edgeExtendWindow(change, animation, startTransaction, finishTransaction); + } + } + } + + /** Adds background color to the transition if any animation has such a property. */ + private void addBackgroundColorIfNeeded(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) { + for (ActivityEmbeddingAnimationAdapter adapter : adapters) { + final int backgroundColor = getTransitionBackgroundColorIfSet(info, adapter.mChange, + adapter.mAnimation, 0 /* defaultColor */); + if (backgroundColor != 0) { + // We only need to show one color. + addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction, + finishTransaction); + return; + } + } + } + @NonNull private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter( @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, @NonNull AnimationProvider animationProvider, @NonNull Rect wholeAnimationBounds) { final Animation animation = animationProvider.get(info, change, wholeAnimationBounds); - // We may want to show a background color for open/close transition. - final int backgroundColor = getTransitionBackgroundColorIfSet(info, change, animation, - 0 /* defaultColor */); - if (backgroundColor != 0) { - addBackgroundToTransition(info.getRootLeash(), backgroundColor, startTransaction, - finishTransaction); - } return new ActivityEmbeddingAnimationAdapter(animation, change, change.getLeash(), wholeAnimationBounds); } @@ -251,6 +303,7 @@ class ActivityEmbeddingAnimationRunner { // 3. Animate the TaskFragment using Activity Change info (start/end bounds). // This is because the TaskFragment surface/change won't contain the Activity's before its // reparent. + Animation changeAnimation = null; for (TransitionInfo.Change change : info.getChanges()) { if (change.getMode() != TRANSIT_CHANGE || change.getStartAbsBounds().equals(change.getEndAbsBounds())) { @@ -273,8 +326,14 @@ class ActivityEmbeddingAnimationRunner { } } + // There are two animations in the array. The first one is for the start leash + // (snapshot), and the second one is for the end leash (TaskFragment). final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(change, boundsAnimationChange.getEndAbsBounds()); + // Keep track as we might need to add background color for the animation. + // Although there may be multiple change animation, record one of them is sufficient + // because the background color will be added to the root leash for the whole animation. + changeAnimation = animations[1]; // Create a screenshot based on change, but attach it to the top of the // boundsAnimationChange. @@ -293,6 +352,9 @@ class ActivityEmbeddingAnimationRunner { animations[1], boundsAnimationChange)); } + // If there is no corresponding open/close window with the change, we should show background + // color to cover the empty part of the screen. + boolean shouldShouldBackgroundColor = true; // Handle the other windows that don't have bounds change in the same transition. for (TransitionInfo.Change change : info.getChanges()) { if (handledChanges.contains(change)) { @@ -307,11 +369,20 @@ class ActivityEmbeddingAnimationRunner { animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change); } else if (Transitions.isClosingType(change.getMode())) { animation = mAnimationSpec.createChangeBoundsCloseAnimation(change); + shouldShouldBackgroundColor = false; } else { animation = mAnimationSpec.createChangeBoundsOpenAnimation(change); + shouldShouldBackgroundColor = false; } adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change)); } + + if (shouldShouldBackgroundColor && changeAnimation != null) { + // Change animation may leave part of the screen empty. Show background color to cover + // that. + changeAnimation.setShowBackdrop(true); + } + return adapters; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java index eb6ac7615266..2bb73692b457 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -158,7 +158,7 @@ class ActivityEmbeddingAnimationSpec { // The position should be 0-based as we will post translate in // ActivityEmbeddingAnimationAdapter#onAnimationUpdate final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0, - 0, 0); + startBounds.top - endBounds.top, 0); endTranslate.setDuration(CHANGE_ANIMATION_DURATION); endSet.addAnimation(endTranslate); // The end leash is resizing, we should update the window crop based on the clip rect. @@ -181,15 +181,15 @@ class ActivityEmbeddingAnimationSpec { @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = Transitions.isOpeningType(change.getMode()); final Animation animation; - // TODO(b/207070762): Implement edgeExtension version if (shouldShowBackdrop(info, change)) { animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter ? com.android.internal.R.anim.task_fragment_clear_top_open_enter : com.android.internal.R.anim.task_fragment_clear_top_open_exit); } else { + // Use the same edge extension animation as regular activity open. animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter - ? com.android.internal.R.anim.task_fragment_open_enter - : com.android.internal.R.anim.task_fragment_open_exit); + ? com.android.internal.R.anim.activity_open_enter + : com.android.internal.R.anim.activity_open_exit); } // Use the whole animation bounds instead of the change bounds, so that when multiple change // targets are opening at the same time, the animation applied to each will be the same. @@ -205,15 +205,15 @@ class ActivityEmbeddingAnimationSpec { @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = Transitions.isOpeningType(change.getMode()); final Animation animation; - // TODO(b/207070762): Implement edgeExtension version if (shouldShowBackdrop(info, change)) { animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter ? com.android.internal.R.anim.task_fragment_clear_top_close_enter : com.android.internal.R.anim.task_fragment_clear_top_close_exit); } else { + // Use the same edge extension animation as regular activity close. animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter - ? com.android.internal.R.anim.task_fragment_close_enter - : com.android.internal.R.anim.task_fragment_close_exit); + ? com.android.internal.R.anim.activity_close_enter + : com.android.internal.R.anim.activity_close_exit); } // Use the whole animation bounds instead of the change bounds, so that when multiple change // targets are closing at the same time, the animation applied to each will be the same. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java index 86f9d5b534f4..8cbe44b15e42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java @@ -29,13 +29,6 @@ import com.android.wm.shell.common.annotations.ExternalThread; public interface BackAnimation { /** - * Returns a binder that can be passed to an external process to update back animations. - */ - default IBackAnimation createExternalInterface() { - return null; - } - - /** * Called when a {@link MotionEvent} is generated by a back gesture. * * @param touchX the X touch position of the {@link MotionEvent}. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 33ecdd88fad3..cbcd9498fe55 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -21,6 +21,7 @@ import static android.view.RemoteAnimationTarget.MODE_OPENING; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION; import android.annotation.NonNull; import android.annotation.Nullable; @@ -57,10 +58,12 @@ import android.window.IOnBackInvokedCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import java.util.concurrent.atomic.AtomicBoolean; @@ -72,13 +75,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private static final String TAG = "BackAnimationController"; private static final int SETTING_VALUE_OFF = 0; private static final int SETTING_VALUE_ON = 1; - private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP = - "persist.wm.debug.predictive_back_progress_threshold"; public static final boolean IS_ENABLED = SystemProperties.getInt("persist.wm.debug.predictive_back", - SETTING_VALUE_ON) != SETTING_VALUE_OFF; - private static final int PROGRESS_THRESHOLD = SystemProperties - .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); + SETTING_VALUE_ON) == SETTING_VALUE_ON; + /** Flag for U animation features */ + public static boolean IS_U_ANIMATION_ENABLED = + SystemProperties.getInt("persist.wm.debug.predictive_back_anim", + SETTING_VALUE_OFF) == SETTING_VALUE_ON; + /** Predictive back animation developer option */ private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); // TODO (b/241808055) Find a appropriate time to remove during refactor private static final boolean USE_TRANSITION = @@ -105,12 +109,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final IActivityTaskManager mActivityTaskManager; private final Context mContext; private final ContentResolver mContentResolver; + private final ShellController mShellController; private final ShellExecutor mShellExecutor; private final Handler mBgHandler; @Nullable private IOnBackInvokedCallback mBackToLauncherCallback; private float mTriggerThreshold; - private float mProgressThreshold; private final Runnable mResetTransitionRunnable = () -> { finishAnimation(); mTransitionInProgress = false; @@ -121,7 +125,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private IBackNaviAnimationController mBackAnimationController; private BackAnimationAdaptor mBackAnimationAdaptor; - private boolean mWaitingAnimationStart; private final TouchTracker mTouchTracker = new TouchTracker(); private final CachingBackDispatcher mCachingBackDispatcher = new CachingBackDispatcher(); @@ -144,44 +147,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont }; /** - * Helper class to record the touch location for gesture start and latest. - */ - private static class TouchTracker { - /** - * Location of the latest touch event - */ - private float mLatestTouchX; - private float mLatestTouchY; - private int mSwipeEdge; - - /** - * Location of the initial touch event of the back gesture. - */ - private float mInitTouchX; - private float mInitTouchY; - - void update(float touchX, float touchY, int swipeEdge) { - mLatestTouchX = touchX; - mLatestTouchY = touchY; - mSwipeEdge = swipeEdge; - } - - void setGestureStartLocation(float touchX, float touchY) { - mInitTouchX = touchX; - mInitTouchY = touchY; - } - - int getDeltaFromGestureStart(float touchX) { - return Math.round(touchX - mInitTouchX); - } - - void reset() { - mInitTouchX = 0; - mInitTouchY = 0; - } - } - - /** * Cache the temporary callback and trigger result if gesture was finish before received * BackAnimationRunner#onAnimationStart/cancel, so there can continue play the animation. */ @@ -208,15 +173,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont boolean consumed = false; if (mWaitingAnimation && mOnBackCallback != null) { if (mTriggerBack) { - final BackEvent backFinish = new BackEvent( - mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 1, - mTouchTracker.mSwipeEdge, mAnimationTarget); + final BackEvent backFinish = mTouchTracker.createProgressEvent(1); dispatchOnBackProgressed(mBackToLauncherCallback, backFinish); dispatchOnBackInvoked(mOnBackCallback); } else { - final BackEvent backFinish = new BackEvent( - mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 0, - mTouchTracker.mSwipeEdge, mAnimationTarget); + final BackEvent backFinish = mTouchTracker.createProgressEvent(0); dispatchOnBackProgressed(mBackToLauncherCallback, backFinish); dispatchOnBackCancelled(mOnBackCallback); } @@ -231,21 +192,25 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont public BackAnimationController( @NonNull ShellInit shellInit, + @NonNull ShellController shellController, @NonNull @ShellMainThread ShellExecutor shellExecutor, @NonNull @ShellBackgroundThread Handler backgroundHandler, Context context) { - this(shellInit, shellExecutor, backgroundHandler, new SurfaceControl.Transaction(), - ActivityTaskManager.getService(), context, context.getContentResolver()); + this(shellInit, shellController, shellExecutor, backgroundHandler, + new SurfaceControl.Transaction(), ActivityTaskManager.getService(), + context, context.getContentResolver()); } @VisibleForTesting BackAnimationController( @NonNull ShellInit shellInit, + @NonNull ShellController shellController, @NonNull @ShellMainThread ShellExecutor shellExecutor, @NonNull @ShellBackgroundThread Handler bgHandler, @NonNull SurfaceControl.Transaction transaction, @NonNull IActivityTaskManager activityTaskManager, Context context, ContentResolver contentResolver) { + mShellController = shellController; mShellExecutor = shellExecutor; mTransaction = transaction; mActivityTaskManager = activityTaskManager; @@ -255,8 +220,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont shellInit.addInitCallback(this::onInit, this); } + @VisibleForTesting + void setEnableUAnimation(boolean enable) { + IS_U_ANIMATION_ENABLED = enable; + } + private void onInit() { setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION, + this::createExternalInterface, this); } private void setupAnimationDeveloperSettingsObserver( @@ -289,7 +261,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return mBackAnimation; } - private final BackAnimation mBackAnimation = new BackAnimationImpl(); + private ExternalInterfaceBinder createExternalInterface() { + return new IBackAnimationImpl(this); + } + + private final BackAnimationImpl mBackAnimation = new BackAnimationImpl(); @Override public Context getContext() { @@ -302,17 +278,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private class BackAnimationImpl implements BackAnimation { - private IBackAnimationImpl mBackAnimation; - - @Override - public IBackAnimation createExternalInterface() { - if (mBackAnimation != null) { - mBackAnimation.invalidate(); - } - mBackAnimation = new IBackAnimationImpl(BackAnimationController.this); - return mBackAnimation; - } - @Override public void onBackMotion( float touchX, float touchY, int keyAction, @BackEvent.SwipeEdge int swipeEdge) { @@ -331,7 +296,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private static class IBackAnimationImpl extends IBackAnimation.Stub { + private static class IBackAnimationImpl extends IBackAnimation.Stub + implements ExternalInterfaceBinder { private BackAnimationController mController; IBackAnimationImpl(BackAnimationController controller) { @@ -356,7 +322,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont (controller) -> controller.onBackToLauncherAnimationFinished()); } - void invalidate() { + @Override + public void invalidate() { mController = null; } } @@ -399,7 +366,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } - mTouchTracker.update(touchX, touchY, swipeEdge); + mTouchTracker.update(touchX, touchY); if (keyAction == MotionEvent.ACTION_DOWN) { if (!mBackGestureStarted) { mShouldStartOnNextMoveEvent = true; @@ -409,7 +376,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // Let the animation initialized here to make sure the onPointerDownOutsideFocus // could be happened when ACTION_DOWN, it may change the current focus that we // would access it when startBackNavigation. - onGestureStarted(touchX, touchY); + onGestureStarted(touchX, touchY, swipeEdge); mShouldStartOnNextMoveEvent = false; } onMove(touchX, touchY, swipeEdge); @@ -423,14 +390,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private void onGestureStarted(float touchX, float touchY) { + private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted); if (mBackGestureStarted || mBackNavigationInfo != null) { Log.e(TAG, "Animation is being initialized but is already started."); finishAnimation(); } - mTouchTracker.setGestureStartLocation(touchX, touchY); + mTouchTracker.setGestureStartLocation(touchX, touchY, swipeEdge); mBackGestureStarted = true; try { @@ -459,6 +426,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont displayTargetScreenshot(hardwareBuffer, backNavigationInfo.getTaskWindowConfiguration()); } + targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); mTransaction.apply(); } else if (dispatchToLauncher) { targetCallback = mBackToLauncherCallback; @@ -469,7 +437,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); } if (!USE_TRANSITION || !dispatchToLauncher) { - dispatchOnBackStarted(targetCallback); + dispatchOnBackStarted( + targetCallback, + mTouchTracker.createStartEvent( + mBackNavigationInfo.getDepartingAnimationTarget())); } } @@ -509,29 +480,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (!mBackGestureStarted || mBackNavigationInfo == null) { return; } - int deltaX = mTouchTracker.getDeltaFromGestureStart(touchX); - float progressThreshold = PROGRESS_THRESHOLD >= 0 ? PROGRESS_THRESHOLD : mProgressThreshold; - float progress = Math.min(Math.max(Math.abs(deltaX) / progressThreshold, 0), 1); - if (USE_TRANSITION) { - if (mBackAnimationController != null && mAnimationTarget != null) { - final BackEvent backEvent = new BackEvent( - touchX, touchY, progress, swipeEdge, mAnimationTarget); + final BackEvent backEvent = mTouchTracker.createProgressEvent(); + if (USE_TRANSITION && mBackAnimationController != null && mAnimationTarget != null) { dispatchOnBackProgressed(mBackToLauncherCallback, backEvent); - } - } else { + } else if (mEnableAnimations.get()) { int backType = mBackNavigationInfo.getType(); - RemoteAnimationTarget animationTarget = - mBackNavigationInfo.getDepartingAnimationTarget(); - - BackEvent backEvent = new BackEvent( - touchX, touchY, progress, swipeEdge, animationTarget); - IOnBackInvokedCallback targetCallback = null; + IOnBackInvokedCallback targetCallback; if (shouldDispatchToLauncher(backType)) { targetCallback = mBackToLauncherCallback; - } else if (backType == BackNavigationInfo.TYPE_CROSS_TASK - || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) { - // TODO(208427216) Run the actual animation - } else if (backType == BackNavigationInfo.TYPE_CALLBACK) { + } else { targetCallback = mBackNavigationInfo.getOnBackInvokedCallback(); } dispatchOnBackProgressed(targetCallback, backEvent); @@ -615,18 +572,21 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont || mBackNavigationInfo.getDepartingAnimationTarget() != null); } - private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) { + private void dispatchOnBackStarted(IOnBackInvokedCallback callback, + BackEvent backEvent) { if (callback == null) { return; } try { - callback.onBackStarted(); + if (shouldDispatchAnimation(callback)) { + callback.onBackStarted(backEvent); + } } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackStarted error: ", e); } } - private static void dispatchOnBackInvoked(IOnBackInvokedCallback callback) { + private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) { if (callback == null) { return; } @@ -637,29 +597,38 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - private static void dispatchOnBackCancelled(IOnBackInvokedCallback callback) { + private void dispatchOnBackCancelled(IOnBackInvokedCallback callback) { if (callback == null) { return; } try { - callback.onBackCancelled(); + if (shouldDispatchAnimation(callback)) { + callback.onBackCancelled(); + } } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackCancelled error: ", e); } } - private static void dispatchOnBackProgressed(IOnBackInvokedCallback callback, + private void dispatchOnBackProgressed(IOnBackInvokedCallback callback, BackEvent backEvent) { if (callback == null) { return; } try { - callback.onBackProgressed(backEvent); + if (shouldDispatchAnimation(callback)) { + callback.onBackProgressed(backEvent); + } } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackProgressed error: ", e); } } + private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) { + return (IS_U_ANIMATION_ENABLED || callback == mBackToLauncherCallback) + && mEnableAnimations.get(); + } + /** * Sets to true when the back gesture has passed the triggering threshold, false otherwise. */ @@ -668,10 +637,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } mTriggerBack = triggerBack; + mTouchTracker.setTriggerBack(triggerBack); } private void setSwipeThresholds(float triggerThreshold, float progressThreshold) { - mProgressThreshold = progressThreshold; + mTouchTracker.setProgressThreshold(progressThreshold); mTriggerThreshold = triggerThreshold; } @@ -681,6 +651,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont BackNavigationInfo backNavigationInfo = mBackNavigationInfo; boolean triggerBack = mTriggerBack; mBackNavigationInfo = null; + mAnimationTarget = null; mTriggerBack = false; mShouldStartOnNextMoveEvent = false; if (backNavigationInfo == null) { @@ -757,17 +728,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont tx.apply(); } } - // TODO animation target should be passed at onBackStarted - dispatchOnBackStarted(mBackToLauncherCallback); - // TODO This is Workaround for LauncherBackAnimationController, there will need - // to dispatch onBackProgressed twice(startBack & updateBackProgress) to - // initialize the animation data, for now that would happen when onMove - // called, but there will no expected animation if the down -> up gesture - // happen very fast which ACTION_MOVE only happen once. - final BackEvent backInit = new BackEvent( - mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 0, - mTouchTracker.mSwipeEdge, mAnimationTarget); - dispatchOnBackProgressed(mBackToLauncherCallback, backInit); + dispatchOnBackStarted(mBackToLauncherCallback, + mTouchTracker.createStartEvent(mAnimationTarget)); + final BackEvent backInit = mTouchTracker.createProgressEvent(); if (!mCachingBackDispatcher.consume()) { dispatchOnBackProgressed(mBackToLauncherCallback, backInit); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java new file mode 100644 index 000000000000..ccfac65d6342 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.back; + +import android.os.SystemProperties; +import android.view.RemoteAnimationTarget; +import android.window.BackEvent; + +/** + * Helper class to record the touch location for gesture and generate back events. + */ +class TouchTracker { + private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP = + "persist.wm.debug.predictive_back_progress_threshold"; + private static final int PROGRESS_THRESHOLD = SystemProperties + .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1); + private float mProgressThreshold; + /** + * Location of the latest touch event + */ + private float mLatestTouchX; + private float mLatestTouchY; + private boolean mTriggerBack; + + /** + * Location of the initial touch event of the back gesture. + */ + private float mInitTouchX; + private float mInitTouchY; + private float mStartThresholdX; + private int mSwipeEdge; + private boolean mCancelled; + + void update(float touchX, float touchY) { + /** + * If back was previously cancelled but the user has started swiping in the forward + * direction again, restart back. + */ + if (mCancelled && ((touchX > mLatestTouchX && mSwipeEdge == BackEvent.EDGE_LEFT) + || touchX < mLatestTouchX && mSwipeEdge == BackEvent.EDGE_RIGHT)) { + mCancelled = false; + mStartThresholdX = touchX; + } + mLatestTouchX = touchX; + mLatestTouchY = touchY; + } + + void setTriggerBack(boolean triggerBack) { + if (mTriggerBack != triggerBack && !triggerBack) { + mCancelled = true; + } + mTriggerBack = triggerBack; + } + + void setGestureStartLocation(float touchX, float touchY, int swipeEdge) { + mInitTouchX = touchX; + mInitTouchY = touchY; + mSwipeEdge = swipeEdge; + mStartThresholdX = mInitTouchX; + } + + void reset() { + mInitTouchX = 0; + mInitTouchY = 0; + mStartThresholdX = 0; + mCancelled = false; + mTriggerBack = false; + mSwipeEdge = BackEvent.EDGE_LEFT; + } + + BackEvent createStartEvent(RemoteAnimationTarget target) { + return new BackEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target); + } + + BackEvent createProgressEvent() { + float progressThreshold = PROGRESS_THRESHOLD >= 0 + ? PROGRESS_THRESHOLD : mProgressThreshold; + progressThreshold = progressThreshold == 0 ? 1 : progressThreshold; + float progress = 0; + // Progress is always 0 when back is cancelled and not restarted. + if (!mCancelled) { + // If back is committed, progress is the distance between the last and first touch + // point, divided by the max drag distance. Otherwise, it's the distance between + // the last touch point and the starting threshold, divided by max drag distance. + // The starting threshold is initially the first touch location, and updated to + // the location everytime back is restarted after being cancelled. + float startX = mTriggerBack ? mInitTouchX : mStartThresholdX; + float deltaX = Math.max( + mSwipeEdge == BackEvent.EDGE_LEFT + ? mLatestTouchX - startX + : startX - mLatestTouchX, + 0); + progress = Math.min(Math.max(deltaX / progressThreshold, 0), 1); + } + return createProgressEvent(progress); + } + + BackEvent createProgressEvent(float progress) { + return new BackEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null); + } + + public void setProgressThreshold(float progressThreshold) { + mProgressThreshold = progressThreshold; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java index d6803e8052c6..d3a9a672ec76 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java @@ -52,7 +52,7 @@ public class BubbleBadgeIconFactory extends BaseIconFactory { userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon); } Bitmap userBadgedBitmap = createIconBitmap( - userBadgedAppIcon, 1, BITMAP_GENERATION_MODE_WITH_SHADOW); + userBadgedAppIcon, 1, MODE_WITH_SHADOW); return createIconBitmap(userBadgedBitmap); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 93413dbe7e5f..725b20525bf7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -28,10 +28,6 @@ import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTRO import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_BOTTOM; -import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_LEFT; -import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_NONE; -import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_RIGHT; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_GROUP_CANCELLED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_INVALID_INTENT; @@ -41,6 +37,7 @@ import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED; +import static com.android.wm.shell.floating.FloatingTasksController.SHOW_FLOATING_TASKS_AS_BUBBLES; import android.annotation.NonNull; import android.annotation.UserIdInt; @@ -59,10 +56,8 @@ import android.content.pm.ShortcutInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.graphics.PixelFormat; -import android.graphics.PointF; import android.graphics.Rect; import android.os.Binder; -import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; @@ -126,18 +121,6 @@ public class BubbleController implements ConfigurationChangeListener { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; - // TODO(b/173386799) keep in sync with Launcher3, not hooked up to anything - public static final String EXTRA_TASKBAR_CREATED = "taskbarCreated"; - public static final String EXTRA_BUBBLE_OVERFLOW_OPENED = "bubbleOverflowOpened"; - public static final String EXTRA_TASKBAR_VISIBLE = "taskbarVisible"; - public static final String EXTRA_TASKBAR_POSITION = "taskbarPosition"; - public static final String EXTRA_TASKBAR_ICON_SIZE = "taskbarIconSize"; - public static final String EXTRA_TASKBAR_BUBBLE_XY = "taskbarBubbleXY"; - public static final String EXTRA_TASKBAR_SIZE = "taskbarSize"; - public static final String LEFT_POSITION = "Left"; - public static final String RIGHT_POSITION = "Right"; - public static final String BOTTOM_POSITION = "Bottom"; - // Should match with PhoneWindowManager private static final String SYSTEM_DIALOG_REASON_KEY = "reason"; private static final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav"; @@ -470,52 +453,6 @@ public class BubbleController implements ConfigurationChangeListener { mBubbleData.setExpanded(true); } - /** Called when any taskbar state changes (e.g. visibility, position, sizes). */ - private void onTaskbarChanged(Bundle b) { - if (b == null) { - return; - } - boolean isVisible = b.getBoolean(EXTRA_TASKBAR_VISIBLE, false /* default */); - String position = b.getString(EXTRA_TASKBAR_POSITION, RIGHT_POSITION /* default */); - @BubblePositioner.TaskbarPosition int taskbarPosition = TASKBAR_POSITION_NONE; - switch (position) { - case LEFT_POSITION: - taskbarPosition = TASKBAR_POSITION_LEFT; - break; - case RIGHT_POSITION: - taskbarPosition = TASKBAR_POSITION_RIGHT; - break; - case BOTTOM_POSITION: - taskbarPosition = TASKBAR_POSITION_BOTTOM; - break; - } - int[] itemPosition = b.getIntArray(EXTRA_TASKBAR_BUBBLE_XY); - int iconSize = b.getInt(EXTRA_TASKBAR_ICON_SIZE); - int taskbarSize = b.getInt(EXTRA_TASKBAR_SIZE); - Log.w(TAG, "onTaskbarChanged:" - + " isVisible: " + isVisible - + " position: " + position - + " itemPosition: " + itemPosition[0] + "," + itemPosition[1] - + " iconSize: " + iconSize); - PointF point = new PointF(itemPosition[0], itemPosition[1]); - mBubblePositioner.setPinnedLocation(isVisible ? point : null); - mBubblePositioner.updateForTaskbar(iconSize, taskbarPosition, isVisible, taskbarSize); - if (mStackView != null) { - if (isVisible && b.getBoolean(EXTRA_TASKBAR_CREATED, false /* default */)) { - // If taskbar was created, add and remove the window so that bubbles display on top - removeFromWindowManagerMaybe(); - addToWindowManagerMaybe(); - } - mStackView.updateStackPosition(); - mBubbleIconFactory = new BubbleIconFactory(mContext); - mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(mContext); - mStackView.onDisplaySizeChanged(); - } - if (b.getBoolean(EXTRA_BUBBLE_OVERFLOW_OPENED, false)) { - openBubbleOverflow(); - } - } - /** * Called when the status bar has become visible or invisible (either permanently or * temporarily). @@ -654,6 +591,11 @@ public class BubbleController implements ConfigurationChangeListener { } mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation); } + if (SHOW_FLOATING_TASKS_AS_BUBBLES && mBubblePositioner.isLargeScreen()) { + mBubblePositioner.setUsePinnedLocation(true); + } else { + mBubblePositioner.setUsePinnedLocation(false); + } addToWindowManagerMaybe(); } @@ -1732,13 +1674,6 @@ public class BubbleController implements ConfigurationChangeListener { } @Override - public void onTaskbarChanged(Bundle b) { - mMainExecutor.execute(() -> { - BubbleController.this.onTaskbarChanged(b); - }); - } - - @Override public boolean handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback, Executor callbackExecutor) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java index 5dab8a071f76..4ded3ea951e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java @@ -79,6 +79,6 @@ public class BubbleIconFactory extends BaseIconFactory { true /* shrinkNonAdaptiveIcons */, null /* outscale */, outScale); - return createIconBitmap(icon, outScale[0], BITMAP_GENERATION_MODE_WITH_SHADOW); + return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index dbad5df9cf56..07c58527a815 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -713,6 +713,9 @@ public class BubblePositioner { * is being shown. */ public PointF getDefaultStartPosition() { + if (mPinLocation != null) { + return mPinLocation; + } // Start on the left if we're in LTR, right otherwise. final boolean startOnLeft = mContext.getResources().getConfiguration().getLayoutDirection() @@ -766,11 +769,18 @@ public class BubblePositioner { } /** - * In some situations bubbles will be pinned to a specific onscreen location. This sets the - * location to anchor the stack to. + * In some situations bubbles will be pinned to a specific onscreen location. This sets whether + * bubbles should be pinned or not. */ - public void setPinnedLocation(PointF point) { - mPinLocation = point; + public void setUsePinnedLocation(boolean usePinnedLocation) { + if (usePinnedLocation) { + mShowingInTaskbar = true; + mPinLocation = new PointF(mPositionRect.right - mBubbleSize, + mPositionRect.bottom - mBubbleSize); + } else { + mPinLocation = null; + mShowingInTaskbar = false; + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index be100bb1dd34..6efad097e3cc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -613,16 +613,11 @@ public class BubbleStackView extends FrameLayout mBubbleContainer.setActiveController(mStackAnimationController); hideFlyoutImmediate(); - if (mPositioner.showingInTaskbar()) { - // In taskbar, the stack isn't draggable so we shouldn't dispatch touch events. - mMagnetizedObject = null; - } else { - // Save the magnetized stack so we can dispatch touch events to it. - mMagnetizedObject = mStackAnimationController.getMagnetizedStack(); - mMagnetizedObject.clearAllTargets(); - mMagnetizedObject.addTarget(mMagneticTarget); - mMagnetizedObject.setMagnetListener(mStackMagnetListener); - } + // Save the magnetized stack so we can dispatch touch events to it. + mMagnetizedObject = mStackAnimationController.getMagnetizedStack(); + mMagnetizedObject.clearAllTargets(); + mMagnetizedObject.addTarget(mMagneticTarget); + mMagnetizedObject.setMagnetListener(mStackMagnetListener); mIsDraggingStack = true; @@ -641,10 +636,7 @@ public class BubbleStackView extends FrameLayout public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, float viewInitialY, float dx, float dy) { // If we're expanding or collapsing, ignore all touch events. - if (mIsExpansionAnimating - // Also ignore events if we shouldn't be draggable. - || (mPositioner.showingInTaskbar() && !mIsExpanded) - || mShowedUserEducationInTouchListenerActive) { + if (mIsExpansionAnimating || mShowedUserEducationInTouchListenerActive) { return; } @@ -661,7 +653,7 @@ public class BubbleStackView extends FrameLayout // bubble since it's stuck to the target. if (!passEventToMagnetizedObject(ev)) { updateBubbleShadows(true /* showForAllBubbles */); - if (mBubbleData.isExpanded() || mPositioner.showingInTaskbar()) { + if (mBubbleData.isExpanded()) { mExpandedAnimationController.dragBubbleOut( v, viewInitialX + dx, viewInitialY + dy); } else { @@ -678,9 +670,7 @@ public class BubbleStackView extends FrameLayout public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, float viewInitialY, float dx, float dy, float velX, float velY) { // If we're expanding or collapsing, ignore all touch events. - if (mIsExpansionAnimating - // Also ignore events if we shouldn't be draggable. - || (mPositioner.showingInTaskbar() && !mIsExpanded)) { + if (mIsExpansionAnimating) { return; } if (mShowedUserEducationInTouchListenerActive) { @@ -696,6 +686,8 @@ public class BubbleStackView extends FrameLayout // Re-show the expanded view if we hid it. showExpandedViewIfNeeded(); + } else if (mPositioner.showingInTaskbar()) { + mStackAnimationController.snapStackBack(); } else { // Fling the stack to the edge, and save whether or not it's going to end up on // the left side of the screen. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index b3104b518440..7f891ec6d215 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -23,7 +23,6 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.app.NotificationChannel; import android.content.pm.UserInfo; -import android.os.Bundle; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; @@ -114,9 +113,6 @@ public interface Bubbles { @Nullable Bubble getBubbleWithShortcutId(String shortcutId); - /** Called for any taskbar changes. */ - void onTaskbarChanged(Bundle b); - /** * We intercept notification entries (including group summaries) dismissed by the user when * there is an active bubble associated with it. We do this so that developers can still diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index 961722ba9bc0..0ee0ea60a1bc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -417,6 +417,17 @@ public class StackAnimationController extends } /** + * Snaps the stack back to the previous resting position. + */ + public void snapStackBack() { + if (mLayout == null) { + return; + } + PointF p = getStackPositionAlongNearestHorizontalEdge(); + springStackAfterFling(p.x, p.y); + } + + /** * Where the stack would be if it were snapped to the nearest horizontal edge (left or right). */ public PointF getStackPositionAlongNearestHorizontalEdge() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java new file mode 100644 index 000000000000..e029358cb3a2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import static android.content.Intent.EXTRA_DOCK_STATE; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import com.android.wm.shell.dagger.WMSingleton; + +import javax.inject.Inject; + +/** + * Provides information about the docked state of the device. + */ +@WMSingleton +public class DockStateReader { + + private static final IntentFilter DOCK_INTENT_FILTER = new IntentFilter( + Intent.ACTION_DOCK_EVENT); + + private final Context mContext; + + @Inject + public DockStateReader(Context context) { + mContext = context; + } + + /** + * @return True if the device is docked and false otherwise. + */ + public boolean isDocked() { + Intent dockStatus = mContext.registerReceiver(/* receiver */ null, DOCK_INTENT_FILTER); + if (dockStatus != null) { + int dockState = dockStatus.getIntExtra(EXTRA_DOCK_STATE, + Intent.EXTRA_DOCK_STATE_UNDOCKED); + return dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED; + } + return false; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java new file mode 100644 index 000000000000..aa5b0cb628e1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import android.os.IBinder; + +/** + * An interface for binders which can be registered to be sent to other processes. + */ +public interface ExternalInterfaceBinder { + /** + * Invalidates this binder (detaches it from the controller it would call). + */ + void invalidate(); + + /** + * Returns the IBinder to send. + */ + IBinder asBinder(); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 419e62daf586..c2ad1a98d167 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -118,6 +118,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private boolean mFreezeDividerWindow = false; private int mOrientation; private int mRotation; + private int mDensity; private final boolean mDimNonImeSide; @@ -290,9 +291,11 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange final int rotation = configuration.windowConfiguration.getRotation(); final Rect rootBounds = configuration.windowConfiguration.getBounds(); final int orientation = configuration.orientation; + final int density = configuration.densityDpi; if (mOrientation == orientation && mRotation == rotation + && mDensity == density && mRootBounds.equals(rootBounds)) { return false; } @@ -303,6 +306,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mTempRect.set(mRootBounds); mRootBounds.set(rootBounds); mRotation = rotation; + mDensity = density; mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null); initDividerPosition(mTempRect); updateInvisibleRect(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 235fd9c469ea..6627de58cce3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -37,6 +37,7 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState; @@ -109,6 +110,7 @@ public class CompatUIController implements OnDisplaysChangedListener, private final SyncTransactionQueue mSyncQueue; private final ShellExecutor mMainExecutor; private final Lazy<Transitions> mTransitionsLazy; + private final DockStateReader mDockStateReader; private CompatUICallback mCallback; @@ -127,7 +129,8 @@ public class CompatUIController implements OnDisplaysChangedListener, DisplayImeController imeController, SyncTransactionQueue syncQueue, ShellExecutor mainExecutor, - Lazy<Transitions> transitionsLazy) { + Lazy<Transitions> transitionsLazy, + DockStateReader dockStateReader) { mContext = context; mShellController = shellController; mDisplayController = displayController; @@ -138,6 +141,7 @@ public class CompatUIController implements OnDisplaysChangedListener, mTransitionsLazy = transitionsLazy; mCompatUIHintsState = new CompatUIHintsState(); shellInit.addInitCallback(this::onInit, this); + mDockStateReader = dockStateReader; } private void onInit() { @@ -315,7 +319,8 @@ public class CompatUIController implements OnDisplaysChangedListener, return new LetterboxEduWindowManager(context, taskInfo, mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), mTransitionsLazy.get(), - this::onLetterboxEduDismissed); + this::onLetterboxEduDismissed, + mDockStateReader); } private void onLetterboxEduDismissed() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java index 35f1038a6853..867d0ef732ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java @@ -34,6 +34,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIWindowManagerAbstract; import com.android.wm.shell.transition.Transitions; @@ -88,19 +89,21 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { */ private final int mDialogVerticalMargin; + private final DockStateReader mDockStateReader; + public LetterboxEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, Transitions transitions, - Runnable onDismissCallback) { + Runnable onDismissCallback, DockStateReader dockStateReader) { this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions, - onDismissCallback, new LetterboxEduAnimationController(context)); + onDismissCallback, new LetterboxEduAnimationController(context), dockStateReader); } @VisibleForTesting LetterboxEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback, - LetterboxEduAnimationController animationController) { + LetterboxEduAnimationController animationController, DockStateReader dockStateReader) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mTransitions = transitions; mOnDismissCallback = onDismissCallback; @@ -111,6 +114,7 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { Context.MODE_PRIVATE); mDialogVerticalMargin = (int) mContext.getResources().getDimension( R.dimen.letterbox_education_dialog_margin); + mDockStateReader = dockStateReader; } @Override @@ -130,13 +134,15 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { @Override protected boolean eligibleToShowLayout() { + // - The letterbox education should not be visible if the device is docked. // - If taskbar education is showing, the letterbox education shouldn't be shown for the // given task until the taskbar education is dismissed and the compat info changes (then // the controller will create a new instance of this class since this one isn't eligible). // - If the layout isn't null then it was previously showing, and we shouldn't check if the // user has seen the letterbox education before. - return mEligibleForLetterboxEducation && !isTaskbarEduShowing() && (mLayout != null - || !getHasSeenLetterboxEducation()); + return mEligibleForLetterboxEducation && !isTaskbarEduShowing() + && (mLayout != null || !getHasSeenLetterboxEducation()) + && !mDockStateReader.isDocked(); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index c25bbbf06dda..28a19597bd37 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -46,6 +46,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -69,7 +70,6 @@ import com.android.wm.shell.floating.FloatingTasksController; import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; -import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; @@ -192,33 +192,16 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static KidsModeTaskOrganizer provideKidsModeTaskOrganizer( - Context context, - ShellInit shellInit, - ShellCommandHandler shellCommandHandler, - SyncTransactionQueue syncTransactionQueue, - DisplayController displayController, - DisplayInsetsController displayInsetsController, - Optional<UnfoldAnimationController> unfoldAnimationController, - Optional<RecentTasksController> recentTasksOptional, - @ShellMainThread ShellExecutor mainExecutor, - @ShellMainThread Handler mainHandler - ) { - return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler, - syncTransactionQueue, displayController, displayInsetsController, - unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler); - } - - @WMSingleton - @Provides static CompatUIController provideCompatUIController(Context context, ShellInit shellInit, ShellController shellController, DisplayController displayController, DisplayInsetsController displayInsetsController, DisplayImeController imeController, SyncTransactionQueue syncQueue, - @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy) { + @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy, + DockStateReader dockStateReader) { return new CompatUIController(context, shellInit, shellController, displayController, - displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy); + displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy, + dockStateReader); } @WMSingleton @@ -278,13 +261,14 @@ public abstract class WMShellBaseModule { static Optional<BackAnimationController> provideBackAnimationController( Context context, ShellInit shellInit, + ShellController shellController, @ShellMainThread ShellExecutor shellExecutor, @ShellBackgroundThread Handler backgroundHandler ) { if (BackAnimationController.IS_ENABLED) { return Optional.of( - new BackAnimationController(shellInit, shellExecutor, backgroundHandler, - context)); + new BackAnimationController(shellInit, shellController, shellExecutor, + backgroundHandler, context)); } return Optional.empty(); } @@ -309,17 +293,17 @@ public abstract class WMShellBaseModule { // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} @BindsOptionalOf @DynamicOverride - abstract FullscreenTaskListener<?> optionalFullscreenTaskListener(); + abstract FullscreenTaskListener optionalFullscreenTaskListener(); @WMSingleton @Provides - static FullscreenTaskListener<?> provideFullscreenTaskListener( - @DynamicOverride Optional<FullscreenTaskListener<?>> fullscreenTaskListener, + static FullscreenTaskListener provideFullscreenTaskListener( + @DynamicOverride Optional<FullscreenTaskListener> fullscreenTaskListener, ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, Optional<RecentTasksController> recentTasksOptional, - Optional<WindowDecorViewModel<?>> windowDecorViewModelOptional) { + Optional<WindowDecorViewModel> windowDecorViewModelOptional) { if (fullscreenTaskListener.isPresent()) { return fullscreenTaskListener.get(); } else { @@ -333,7 +317,7 @@ public abstract class WMShellBaseModule { // @BindsOptionalOf - abstract WindowDecorViewModel<?> optionalWindowDecorViewModel(); + abstract WindowDecorViewModel optionalWindowDecorViewModel(); // // Unfold transition @@ -488,6 +472,7 @@ public abstract class WMShellBaseModule { static Optional<RecentTasksController> provideRecentTasksController( Context context, ShellInit shellInit, + ShellController shellController, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, @@ -495,9 +480,9 @@ public abstract class WMShellBaseModule { @ShellMainThread ShellExecutor mainExecutor ) { return Optional.ofNullable( - RecentTasksController.create(context, shellInit, shellCommandHandler, - taskStackListener, activityTaskManager, desktopModeTaskRepository, - mainExecutor)); + RecentTasksController.create(context, shellInit, shellController, + shellCommandHandler, taskStackListener, activityTaskManager, + desktopModeTaskRepository, mainExecutor)); } // @@ -514,14 +499,15 @@ public abstract class WMShellBaseModule { @Provides static Transitions provideTransitions(Context context, ShellInit shellInit, + ShellController shellController, ShellTaskOrganizer organizer, TransactionPool pool, DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor) { - return new Transitions(context, shellInit, organizer, pool, displayController, mainExecutor, - mainHandler, animExecutor); + return new Transitions(context, shellInit, shellController, organizer, pool, + displayController, mainExecutor, mainHandler, animExecutor); } @WMSingleton @@ -638,13 +624,15 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static StartingWindowController provideStartingWindowController(Context context, + static StartingWindowController provideStartingWindowController( + Context context, ShellInit shellInit, + ShellController shellController, ShellTaskOrganizer shellTaskOrganizer, @ShellSplashscreenThread ShellExecutor splashScreenExecutor, StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider, TransactionPool pool) { - return new StartingWindowController(context, shellInit, shellTaskOrganizer, + return new StartingWindowController(context, shellInit, shellController, shellTaskOrganizer, splashScreenExecutor, startingWindowTypeAlgorithm, iconProvider, pool); } @@ -781,12 +769,11 @@ public abstract class WMShellBaseModule { DisplayInsetsController displayInsetsController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, - KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional<BubbleController> bubblesOptional, Optional<SplitScreenController> splitScreenOptional, Optional<Pip> pipOptional, Optional<PipTouchHandler> pipTouchHandlerOptional, - FullscreenTaskListener<?> fullscreenTaskListener, + FullscreenTaskListener fullscreenTaskListener, Optional<UnfoldAnimationController> unfoldAnimationController, Optional<UnfoldTransitionHandler> unfoldTransitionHandler, Optional<FreeformComponents> freeformComponents, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 37a50b611039..f1670cd792cf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -55,7 +55,7 @@ import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.freeform.FreeformTaskListener; import com.android.wm.shell.freeform.FreeformTaskTransitionHandler; import com.android.wm.shell.freeform.FreeformTaskTransitionObserver; -import com.android.wm.shell.fullscreen.FullscreenTaskListener; +import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; @@ -182,7 +182,7 @@ public abstract class WMShellModule { @WMSingleton @Provides - static WindowDecorViewModel<?> provideWindowDecorViewModel( + static WindowDecorViewModel provideWindowDecorViewModel( Context context, @ShellMainThread Handler mainHandler, @ShellMainThread Choreographer mainChoreographer, @@ -191,13 +191,13 @@ public abstract class WMShellModule { SyncTransactionQueue syncQueue, @DynamicOverride DesktopModeController desktopModeController) { return new CaptionWindowDecorViewModel( - context, - mainHandler, - mainChoreographer, - taskOrganizer, - displayController, - syncQueue, - desktopModeController); + context, + mainHandler, + mainChoreographer, + taskOrganizer, + displayController, + syncQueue, + desktopModeController); } // @@ -208,7 +208,7 @@ public abstract class WMShellModule { @Provides @DynamicOverride static FreeformComponents provideFreeformComponents( - FreeformTaskListener<?> taskListener, + FreeformTaskListener taskListener, FreeformTaskTransitionHandler transitionHandler, FreeformTaskTransitionObserver transitionObserver) { return new FreeformComponents( @@ -217,18 +217,18 @@ public abstract class WMShellModule { @WMSingleton @Provides - static FreeformTaskListener<?> provideFreeformTaskListener( + static FreeformTaskListener provideFreeformTaskListener( Context context, ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, - WindowDecorViewModel<?> windowDecorViewModel) { + WindowDecorViewModel windowDecorViewModel) { // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic // override for this controller from the base module ShellInit init = FreeformComponents.isFreeformEnabled(context) ? shellInit : null; - return new FreeformTaskListener<>(init, shellTaskOrganizer, desktopModeTaskRepository, + return new FreeformTaskListener(init, shellTaskOrganizer, desktopModeTaskRepository, windowDecorViewModel); } @@ -237,7 +237,7 @@ public abstract class WMShellModule { static FreeformTaskTransitionHandler provideFreeformTaskTransitionHandler( ShellInit shellInit, Transitions transitions, - WindowDecorViewModel<?> windowDecorViewModel) { + WindowDecorViewModel windowDecorViewModel) { return new FreeformTaskTransitionHandler(shellInit, transitions, windowDecorViewModel); } @@ -247,10 +247,9 @@ public abstract class WMShellModule { Context context, ShellInit shellInit, Transitions transitions, - FullscreenTaskListener<?> fullscreenTaskListener, - FreeformTaskListener<?> freeformTaskListener) { + WindowDecorViewModel windowDecorViewModel) { return new FreeformTaskTransitionObserver( - context, shellInit, transitions, fullscreenTaskListener, freeformTaskListener); + context, shellInit, transitions, windowDecorViewModel); } // @@ -599,7 +598,9 @@ public abstract class WMShellModule { @WMSingleton @Provides @DynamicOverride - static DesktopModeController provideDesktopModeController(Context context, ShellInit shellInit, + static DesktopModeController provideDesktopModeController(Context context, + ShellInit shellInit, + ShellController shellController, ShellTaskOrganizer shellTaskOrganizer, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, Transitions transitions, @@ -607,7 +608,7 @@ public abstract class WMShellModule { @ShellMainThread Handler mainHandler, @ShellMainThread ShellExecutor mainExecutor ) { - return new DesktopModeController(context, shellInit, shellTaskOrganizer, + return new DesktopModeController(context, shellInit, shellController, shellTaskOrganizer, rootTaskDisplayAreaOrganizer, transitions, desktopModeTaskRepository, mainHandler, mainExecutor); } @@ -620,6 +621,28 @@ public abstract class WMShellModule { } // + // Kids mode + // + @WMSingleton + @Provides + static KidsModeTaskOrganizer provideKidsModeTaskOrganizer( + Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + SyncTransactionQueue syncTransactionQueue, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + Optional<UnfoldAnimationController> unfoldAnimationController, + Optional<RecentTasksController> recentTasksOptional, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler + ) { + return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler, + syncTransactionQueue, displayController, displayInsetsController, + unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler); + } + + // // Misc // @@ -630,6 +653,7 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DefaultMixedHandler defaultMixedHandler, + KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional<DesktopModeController> desktopModeController) { return new Object(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index ff3be38d09e1..cbd544cc4b86 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java @@ -18,14 +18,21 @@ package com.android.wm.shell.desktopmode; import com.android.wm.shell.common.annotations.ExternalThread; +import java.util.concurrent.Executor; + /** * Interface to interact with desktop mode feature in shell. */ @ExternalThread public interface DesktopMode { - /** Returns a binder that can be passed to an external process to manipulate DesktopMode. */ - default IDesktopMode createExternalInterface() { - return null; - } + /** + * Adds a listener to find out about changes in the visibility of freeform tasks. + * + * @param listener the listener to add. + * @param callbackExecutor the executor to call the listener on. + */ + void addListener(DesktopModeTaskRepository.VisibleTasksListener listener, + Executor callbackExecutor); + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index 99739c457aa6..34ff6d814c8d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -19,9 +19,12 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; @@ -29,44 +32,56 @@ import android.content.Context; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; +import android.os.IBinder; import android.os.UserHandle; import android.provider.Settings; import android.util.ArraySet; +import android.view.SurfaceControl; import android.window.DisplayAreaInfo; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; import java.util.Comparator; +import java.util.concurrent.Executor; /** * Handles windowing changes when desktop mode system setting changes */ -public class DesktopModeController implements RemoteCallable<DesktopModeController> { +public class DesktopModeController implements RemoteCallable<DesktopModeController>, + Transitions.TransitionHandler { private final Context mContext; + private final ShellController mShellController; private final ShellTaskOrganizer mShellTaskOrganizer; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final Transitions mTransitions; private final DesktopModeTaskRepository mDesktopModeTaskRepository; private final ShellExecutor mMainExecutor; - private final DesktopMode mDesktopModeImpl = new DesktopModeImpl(); + private final DesktopModeImpl mDesktopModeImpl = new DesktopModeImpl(); private final SettingsObserver mSettingsObserver; - public DesktopModeController(Context context, ShellInit shellInit, + public DesktopModeController(Context context, + ShellInit shellInit, + ShellController shellController, ShellTaskOrganizer shellTaskOrganizer, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, Transitions transitions, @@ -74,6 +89,7 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll @ShellMainThread Handler mainHandler, @ShellMainThread ShellExecutor mainExecutor) { mContext = context; + mShellController = shellController; mShellTaskOrganizer = shellTaskOrganizer; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mTransitions = transitions; @@ -85,10 +101,13 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll private void onInit() { ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopModeController"); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_DESKTOP_MODE, + this::createExternalInterface, this); mSettingsObserver.observe(); if (DesktopModeStatus.isActive(mContext)) { updateDesktopModeActive(true); } + mTransitions.addHandler(this); } @Override @@ -108,6 +127,24 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll return mDesktopModeImpl; } + /** + * Creates a new instance of the external interface to pass to another process. + */ + private ExternalInterfaceBinder createExternalInterface() { + return new IDesktopModeImpl(this); + } + + /** + * Adds a listener to find out about changes in the visibility of freeform tasks. + * + * @param listener the listener to add. + * @param callbackExecutor the executor to call the listener on. + */ + public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener, + Executor callbackExecutor) { + mDesktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor); + } + @VisibleForTesting void updateDesktopModeActive(boolean active) { ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active); @@ -157,7 +194,18 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll /** * Show apps on desktop */ - public void showDesktopApps() { + void showDesktopApps() { + WindowContainerTransaction wct = bringDesktopAppsToFront(); + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */); + } else { + mShellTaskOrganizer.applyTransaction(wct); + } + } + + @NonNull + private WindowContainerTransaction bringDesktopAppsToFront() { ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks(); ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size()); ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>(); @@ -173,7 +221,7 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll for (RunningTaskInfo task : taskInfos) { wct.reorder(task.token, true); } - mShellTaskOrganizer.applyTransaction(wct); + return wct; } /** @@ -195,6 +243,47 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll .configuration.windowConfiguration.getWindowingMode(); } + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + // This handler should never be the sole handler, so should not animate anything. + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + // Only do anything if we are in desktop mode and opening a task/app in freeform + if (!DesktopModeStatus.isActive(mContext)) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "skip shell transition request: desktop mode not active"); + return null; + } + if (request.getType() != TRANSIT_OPEN) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, + "skip shell transition request: only supports TRANSIT_OPEN"); + return null; + } + if (request.getTriggerTask() == null + || request.getTriggerTask().getWindowingMode() != WINDOWING_MODE_FREEFORM) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "skip shell transition request: not freeform task"); + return null; + } + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "handle shell transition request: %s", request); + + WindowContainerTransaction wct = mTransitions.dispatchRequest(transition, request, this); + if (wct == null) { + wct = new WindowContainerTransaction(); + } + wct.merge(bringDesktopAppsToFront(), true /* transfer */); + wct.reorder(request.getTriggerTask().token, true /* onTop */); + + return wct; + } + /** * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE} */ @@ -236,15 +325,12 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll @ExternalThread private final class DesktopModeImpl implements DesktopMode { - private IDesktopModeImpl mIDesktopMode; - @Override - public IDesktopMode createExternalInterface() { - if (mIDesktopMode != null) { - mIDesktopMode.invalidate(); - } - mIDesktopMode = new IDesktopModeImpl(DesktopModeController.this); - return mIDesktopMode; + public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener, + Executor callbackExecutor) { + mMainExecutor.execute(() -> { + DesktopModeController.this.addListener(listener, callbackExecutor); + }); } } @@ -252,7 +338,8 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll * The interface for calls from outside the host process. */ @BinderThread - private static class IDesktopModeImpl extends IDesktopMode.Stub { + private static class IDesktopModeImpl extends IDesktopMode.Stub + implements ExternalInterfaceBinder { private DesktopModeController mController; @@ -263,7 +350,8 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 988601c0e8a8..c91d54a62ae6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -16,7 +16,9 @@ package com.android.wm.shell.desktopmode +import android.util.ArrayMap import android.util.ArraySet +import java.util.concurrent.Executor /** * Keeps track of task data related to desktop mode. @@ -30,20 +32,39 @@ class DesktopModeTaskRepository { * Task gets removed from this list when it vanishes. Or when desktop mode is turned off. */ private val activeTasks = ArraySet<Int>() - private val listeners = ArraySet<Listener>() + private val visibleTasks = ArraySet<Int>() + private val activeTasksListeners = ArraySet<ActiveTasksListener>() + // Track visible tasks separately because a task may be part of the desktop but not visible. + private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>() /** - * Add a [Listener] to be notified of updates to the repository. + * Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository. */ - fun addListener(listener: Listener) { - listeners.add(listener) + fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) { + activeTasksListeners.add(activeTasksListener) } /** - * Remove a previously registered [Listener] + * Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. */ - fun removeListener(listener: Listener) { - listeners.remove(listener) + fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) { + visibleTasksListeners.put(visibleTasksListener, executor) + executor.execute( + Runnable { visibleTasksListener.onVisibilityChanged(visibleTasks.size > 0) }) + } + + /** + * Remove a previously registered [ActiveTasksListener] + */ + fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) { + activeTasksListeners.remove(activeTasksListener) + } + + /** + * Remove a previously registered [VisibleTasksListener] + */ + fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) { + visibleTasksListeners.remove(visibleTasksListener) } /** @@ -52,7 +73,7 @@ class DesktopModeTaskRepository { fun addActiveTask(taskId: Int) { val added = activeTasks.add(taskId) if (added) { - listeners.onEach { it.onActiveTasksChanged() } + activeTasksListeners.onEach { it.onActiveTasksChanged() } } } @@ -62,7 +83,7 @@ class DesktopModeTaskRepository { fun removeActiveTask(taskId: Int) { val removed = activeTasks.remove(taskId) if (removed) { - listeners.onEach { it.onActiveTasksChanged() } + activeTasksListeners.onEach { it.onActiveTasksChanged() } } } @@ -81,9 +102,43 @@ class DesktopModeTaskRepository { } /** - * Defines interface for classes that can listen to changes in repository state. + * Updates whether a freeform task with this id is visible or not and notifies listeners. + */ + fun updateVisibleFreeformTasks(taskId: Int, visible: Boolean) { + val prevCount: Int = visibleTasks.size + if (visible) { + visibleTasks.add(taskId) + } else { + visibleTasks.remove(taskId) + } + if (prevCount == 0 && visibleTasks.size == 1 || + prevCount > 0 && visibleTasks.size == 0) { + for ((listener, executor) in visibleTasksListeners) { + executor.execute( + Runnable { listener.onVisibilityChanged(visibleTasks.size > 0) }) + } + } + } + + /** + * Defines interface for classes that can listen to changes for active tasks in desktop mode. + */ + interface ActiveTasksListener { + /** + * Called when the active tasks change in desktop mode. + */ + @JvmDefault + fun onActiveTasksChanged() {} + } + + /** + * Defines interface for classes that can listen to changes for visible tasks in desktop mode. */ - interface Listener { - fun onActiveTasksChanged() + interface VisibleTasksListener { + /** + * Called when the desktop starts or stops showing freeform tasks. + */ + @JvmDefault + fun onVisibilityChanged(hasVisibleFreeformTasks: Boolean) {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md index 2aa933d641fa..fbf326eadcd5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md @@ -29,19 +29,37 @@ As mentioned in the [Dagger usage](dagger.md) docs, you need to determine whethe ### SysUI accessible components In addition to doing the above, you will also need to provide an interface for calling to SysUI from the Shell and vice versa. The current pattern is to have a parallel `Optional<Component name>` -interface that the `<Component name>Controller` implements and handles on the main Shell thread. +interface that the `<Component name>Controller` implements and handles on the main Shell thread +(see [SysUI/Shell threading](threading.md)). In addition, because components accessible to SysUI injection are explicitly listed, you'll have to add an appropriate method in `WMComponent` to get the interface and update the `Builder` in `SysUIComponent` to take the interface so it can be injected in SysUI code. The binding between the two is done in `SystemUIFactory#init()` which will need to be updated as well. +Specifically, to support calling into a controller from an external process (like Launcher): +- Create an implementation of the external interface within the controller +- Have all incoming calls post to the main shell thread (inject @ShellMainThread Executor into the + controller if needed) +- Note that callbacks into SysUI should take an associated executor to call back on + ### Launcher accessible components Because Launcher is not a part of SystemUI and is a separate process, exposing controllers to Launcher requires a new AIDL interface to be created and implemented by the controller. The implementation of the stub interface in the controller otherwise behaves similar to the interface to SysUI where it posts the work to the main Shell thread. +Specifically, to support calling into a controller from an external process (like Launcher): +- Create an implementation of the interface binder's `Stub` class within the controller, have it + extend `ExternalInterfaceBinder` and implement `invalidate()` to ensure it doesn't hold long + references to the outer controller +- Make the controller implement `RemoteCallable<T>`, and have all incoming calls use one of + the `ExecutorUtils.executeRemoteCallWithTaskPermission()` calls to verify the caller's identity + and ensure the call happens on the main shell thread and not the binder thread +- Inject `ShellController` and add the instance of the implementation as external interface +- In Launcher, update `TouchInteractionService` to pass the interface to `SystemUIProxy`, and then + call the SystemUIProxy method as needed in that code + ### Component initialization To initialize the component: - On the Shell side, you potentially need to do two things to initialize the component: @@ -64,8 +82,9 @@ adb logcat *:S WindowManagerShell ### General Do's & Dont's Do: -- Do add unit tests for all new components -- Do keep controllers simple and break them down as needed +- Add unit tests for all new components +- Keep controllers simple and break them down as needed +- Any SysUI callbacks should also take an associated executor to run the callback on Don't: - **Don't** do initialization in the constructor, only do initialization in the init callbacks. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java index 935666026bf4..f86d467360f9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java @@ -33,9 +33,4 @@ public interface FloatingTasks { * - If there is a floating task for this intent, and it's not stashed, this stashes it. */ void showOrSetStashed(Intent intent); - - /** Returns a binder that can be passed to an external process to manipulate FloatingTasks. */ - default IFloatingTasks createExternalInterface() { - return null; - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java index 67552991869b..b3c09d32055b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java @@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_FLOATING_TASKS; import android.annotation.Nullable; import android.content.Context; @@ -40,6 +41,7 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -136,11 +138,13 @@ public class FloatingTasksController implements RemoteCallable<FloatingTasksCont if (isFloatingTasksEnabled()) { shellInit.addInitCallback(this::onInit, this); } - mShellCommandHandler.addDumpCallback(this::dump, this); } protected void onInit() { mShellController.addConfigurationChangeListener(this); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_FLOATING_TASKS, + this::createExternalInterface, this); + mShellCommandHandler.addDumpCallback(this::dump, this); } /** Only used for testing. */ @@ -168,6 +172,10 @@ public class FloatingTasksController implements RemoteCallable<FloatingTasksCont return FLOATING_TASKS_ENABLED || mFloatingTasksEnabledForTests; } + private ExternalInterfaceBinder createExternalInterface() { + return new IFloatingTasksImpl(this); + } + @Override public void onThemeChanged() { if (mIsFloatingLayerAdded) { @@ -412,28 +420,18 @@ public class FloatingTasksController implements RemoteCallable<FloatingTasksCont */ @ExternalThread private class FloatingTaskImpl implements FloatingTasks { - private IFloatingTasksImpl mIFloatingTasks; - @Override public void showOrSetStashed(Intent intent) { mMainExecutor.execute(() -> FloatingTasksController.this.showOrSetStashed(intent)); } - - @Override - public IFloatingTasks createExternalInterface() { - if (mIFloatingTasks != null) { - mIFloatingTasks.invalidate(); - } - mIFloatingTasks = new IFloatingTasksImpl(FloatingTasksController.this); - return mIFloatingTasks; - } } /** * The interface for calls from outside the host process. */ @BinderThread - private static class IFloatingTasksImpl extends IFloatingTasks.Stub { + private static class IFloatingTasksImpl extends IFloatingTasks.Stub + implements ExternalInterfaceBinder { private FloatingTasksController mController; IFloatingTasksImpl(FloatingTasksController controller) { @@ -443,7 +441,8 @@ public class FloatingTasksController implements RemoteCallable<FloatingTasksCont /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index e2d5a499d1e1..eaa7158abbe5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -19,12 +19,8 @@ package com.android.wm.shell.freeform; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM; import android.app.ActivityManager.RunningTaskInfo; -import android.util.Log; import android.util.SparseArray; import android.view.SurfaceControl; -import android.window.TransitionInfo; - -import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; @@ -41,31 +37,26 @@ import java.util.Optional; /** * {@link ShellTaskOrganizer.TaskListener} for {@link * ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}. - * - * @param <T> the type of window decoration instance */ -public class FreeformTaskListener<T extends AutoCloseable> - implements ShellTaskOrganizer.TaskListener { +public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { private static final String TAG = "FreeformTaskListener"; private final ShellTaskOrganizer mShellTaskOrganizer; private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository; - private final WindowDecorViewModel<T> mWindowDecorationViewModel; + private final WindowDecorViewModel mWindowDecorationViewModel; - private final SparseArray<State<T>> mTasks = new SparseArray<>(); - private final SparseArray<T> mWindowDecorOfVanishedTasks = new SparseArray<>(); + private final SparseArray<State> mTasks = new SparseArray<>(); - private static class State<T extends AutoCloseable> { + private static class State { RunningTaskInfo mTaskInfo; SurfaceControl mLeash; - T mWindowDecoration; } public FreeformTaskListener( ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, - WindowDecorViewModel<T> windowDecorationViewModel) { + WindowDecorViewModel windowDecorationViewModel) { mShellTaskOrganizer = shellTaskOrganizer; mWindowDecorationViewModel = windowDecorationViewModel; mDesktopModeTaskRepository = desktopModeTaskRepository; @@ -80,13 +71,18 @@ public class FreeformTaskListener<T extends AutoCloseable> @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mTasks.get(taskInfo.taskId) != null) { + throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId); + } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Appeared: #%d", taskInfo.taskId); - final State<T> state = createOrUpdateTaskState(taskInfo, leash); + final State state = new State(); + state.mTaskInfo = taskInfo; + state.mLeash = leash; + mTasks.put(taskInfo.taskId, state); if (!Transitions.ENABLE_SHELL_TRANSITIONS) { SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - state.mWindowDecoration = - mWindowDecorationViewModel.createWindowDecoration(taskInfo, leash, t, t); + mWindowDecorationViewModel.createWindowDecoration(taskInfo, leash, t, t); t.apply(); } @@ -94,31 +90,13 @@ public class FreeformTaskListener<T extends AutoCloseable> ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "Adding active freeform task: #%d", taskInfo.taskId); mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId)); + mDesktopModeTaskRepository.ifPresent( + it -> it.updateVisibleFreeformTasks(taskInfo.taskId, true)); } } - private State<T> createOrUpdateTaskState(RunningTaskInfo taskInfo, SurfaceControl leash) { - State<T> state = mTasks.get(taskInfo.taskId); - if (state != null) { - updateTaskInfo(taskInfo); - return state; - } - - state = new State<>(); - state.mTaskInfo = taskInfo; - state.mLeash = leash; - mTasks.put(taskInfo.taskId, state); - - return state; - } - @Override public void onTaskVanished(RunningTaskInfo taskInfo) { - final State<T> state = mTasks.get(taskInfo.taskId); - if (state == null) { - // This is possible if the transition happens before this method. - return; - } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d", taskInfo.taskId); mTasks.remove(taskInfo.taskId); @@ -127,28 +105,22 @@ public class FreeformTaskListener<T extends AutoCloseable> ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "Removing active freeform task: #%d", taskInfo.taskId); mDesktopModeTaskRepository.ifPresent(it -> it.removeActiveTask(taskInfo.taskId)); + mDesktopModeTaskRepository.ifPresent( + it -> it.updateVisibleFreeformTasks(taskInfo.taskId, false)); } - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - // Save window decorations of closing tasks so that we can hand them over to the - // transition system if this method happens before the transition. In case where the - // transition didn't happen, it'd be cleared when the next transition finished. - if (state.mWindowDecoration != null) { - mWindowDecorOfVanishedTasks.put(taskInfo.taskId, state.mWindowDecoration); - } - return; + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + mWindowDecorationViewModel.destroyWindowDecoration(taskInfo); } - releaseWindowDecor(state.mWindowDecoration); } @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { - final State<T> state = updateTaskInfo(taskInfo); + final State state = mTasks.get(taskInfo.taskId); + state.mTaskInfo = taskInfo; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d", taskInfo.taskId); - if (state.mWindowDecoration != null) { - mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo, state.mWindowDecoration); - } + mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo); if (DesktopModeStatus.IS_SUPPORTED) { if (taskInfo.isVisible) { @@ -156,18 +128,11 @@ public class FreeformTaskListener<T extends AutoCloseable> "Adding active freeform task: #%d", taskInfo.taskId); mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId)); } + mDesktopModeTaskRepository.ifPresent( + it -> it.updateVisibleFreeformTasks(taskInfo.taskId, taskInfo.isVisible)); } } - private State<T> updateTaskInfo(RunningTaskInfo taskInfo) { - final State<T> state = mTasks.get(taskInfo.taskId); - if (state == null) { - throw new RuntimeException("Task info changed before appearing: #" + taskInfo.taskId); - } - state.mTaskInfo = taskInfo; - return state; - } - @Override public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { b.setParent(findTaskSurface(taskId)); @@ -186,103 +151,6 @@ public class FreeformTaskListener<T extends AutoCloseable> return mTasks.get(taskId).mLeash; } - /** - * Creates a window decoration for a transition. - * - * @param change the change of this task transition that needs to have the task layer as the - * leash - * @return {@code true} if it creates the window decoration; {@code false} otherwise - */ - boolean createWindowDecoration( - TransitionInfo.Change change, - SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT) { - final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); - if (state.mWindowDecoration != null) { - return false; - } - state.mWindowDecoration = mWindowDecorationViewModel.createWindowDecoration( - state.mTaskInfo, state.mLeash, startT, finishT); - return true; - } - - /** - * Gives out the ownership of the task's window decoration. The given task is leaving (of has - * left) this task listener. This is the transition system asking for the ownership. - * - * @param taskInfo the maximizing task - * @return the window decor of the maximizing task if any - */ - T giveWindowDecoration( - RunningTaskInfo taskInfo, - SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT) { - T windowDecor; - final State<T> state = mTasks.get(taskInfo.taskId); - if (state != null) { - windowDecor = state.mWindowDecoration; - state.mWindowDecoration = null; - } else { - windowDecor = - mWindowDecorOfVanishedTasks.removeReturnOld(taskInfo.taskId); - } - if (windowDecor == null) { - return null; - } - mWindowDecorationViewModel.setupWindowDecorationForTransition( - taskInfo, startT, finishT, windowDecor); - return windowDecor; - } - - /** - * Adopt the incoming window decoration and lets the window decoration prepare for a transition. - * - * @param change the change of this task transition that needs to have the task layer as the - * leash - * @param startT the start transaction of this transition - * @param finishT the finish transaction of this transition - * @param windowDecor the window decoration to adopt - * @return {@code true} if it adopts the window decoration; {@code false} otherwise - */ - boolean adoptWindowDecoration( - TransitionInfo.Change change, - SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT, - @Nullable AutoCloseable windowDecor) { - final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); - state.mWindowDecoration = mWindowDecorationViewModel.adoptWindowDecoration(windowDecor); - if (state.mWindowDecoration != null) { - mWindowDecorationViewModel.setupWindowDecorationForTransition( - state.mTaskInfo, startT, finishT, state.mWindowDecoration); - return true; - } else { - state.mWindowDecoration = mWindowDecorationViewModel.createWindowDecoration( - state.mTaskInfo, state.mLeash, startT, finishT); - return false; - } - } - - void onTaskTransitionFinished() { - if (mWindowDecorOfVanishedTasks.size() == 0) { - return; - } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Clearing window decors of vanished tasks. There could be visual defects " - + "if any of them is used later in transitions."); - for (int i = 0; i < mWindowDecorOfVanishedTasks.size(); ++i) { - releaseWindowDecor(mWindowDecorOfVanishedTasks.valueAt(i)); - } - mWindowDecorOfVanishedTasks.clear(); - } - - private void releaseWindowDecor(T windowDecor) { - try { - windowDecor.close(); - } catch (Exception e) { - Log.e(TAG, "Failed to release window decoration.", e); - } - } - @Override public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java index fd4c85fad77f..04fc79acadbd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java @@ -46,14 +46,14 @@ public class FreeformTaskTransitionHandler implements Transitions.TransitionHandler, FreeformTaskTransitionStarter { private final Transitions mTransitions; - private final WindowDecorViewModel<?> mWindowDecorViewModel; + private final WindowDecorViewModel mWindowDecorViewModel; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); public FreeformTaskTransitionHandler( ShellInit shellInit, Transitions transitions, - WindowDecorViewModel<?> windowDecorViewModel) { + WindowDecorViewModel windowDecorViewModel) { mTransitions = transitions; mWindowDecorViewModel = windowDecorViewModel; if (Transitions.ENABLE_SHELL_TRANSITIONS) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index 17d60671e964..f4888fbb2bb9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -16,13 +16,9 @@ package com.android.wm.shell.freeform; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; - import android.app.ActivityManager; import android.content.Context; import android.os.IBinder; -import android.util.Log; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -31,9 +27,9 @@ import android.window.WindowContainerToken; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; import java.util.ArrayList; import java.util.Collections; @@ -47,23 +43,19 @@ import java.util.Map; * be a part of transitions. */ public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver { - private static final String TAG = "FreeformTO"; - private final Transitions mTransitions; - private final FreeformTaskListener<?> mFreeformTaskListener; - private final FullscreenTaskListener<?> mFullscreenTaskListener; + private final WindowDecorViewModel mWindowDecorViewModel; - private final Map<IBinder, List<AutoCloseable>> mTransitionToWindowDecors = new HashMap<>(); + private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo = + new HashMap<>(); public FreeformTaskTransitionObserver( Context context, ShellInit shellInit, Transitions transitions, - FullscreenTaskListener<?> fullscreenTaskListener, - FreeformTaskListener<?> freeformTaskListener) { + WindowDecorViewModel windowDecorViewModel) { mTransitions = transitions; - mFreeformTaskListener = freeformTaskListener; - mFullscreenTaskListener = fullscreenTaskListener; + mWindowDecorViewModel = windowDecorViewModel; if (Transitions.ENABLE_SHELL_TRANSITIONS && FreeformComponents.isFreeformEnabled(context)) { shellInit.addInitCallback(this::onInit, this); } @@ -80,7 +72,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT) { - final ArrayList<AutoCloseable> windowDecors = new ArrayList<>(); + final ArrayList<ActivityManager.RunningTaskInfo> taskInfoList = new ArrayList<>(); final ArrayList<WindowContainerToken> taskParents = new ArrayList<>(); for (TransitionInfo.Change change : info.getChanges()) { if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) { @@ -110,92 +102,40 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs onOpenTransitionReady(change, startT, finishT); break; case WindowManager.TRANSIT_CLOSE: { - onCloseTransitionReady(change, windowDecors, startT, finishT); + taskInfoList.add(change.getTaskInfo()); + onCloseTransitionReady(change, startT, finishT); break; } case WindowManager.TRANSIT_CHANGE: - onChangeTransitionReady(info.getType(), change, startT, finishT); + onChangeTransitionReady(change, startT, finishT); break; } } - if (!windowDecors.isEmpty()) { - mTransitionToWindowDecors.put(transition, windowDecors); - } + mTransitionToTaskInfo.put(transition, taskInfoList); } private void onOpenTransitionReady( TransitionInfo.Change change, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - switch (change.getTaskInfo().getWindowingMode()){ - case WINDOWING_MODE_FREEFORM: - mFreeformTaskListener.createWindowDecoration(change, startT, finishT); - break; - case WINDOWING_MODE_FULLSCREEN: - mFullscreenTaskListener.createWindowDecoration(change, startT, finishT); - break; - } + mWindowDecorViewModel.createWindowDecoration( + change.getTaskInfo(), change.getLeash(), startT, finishT); } private void onCloseTransitionReady( TransitionInfo.Change change, - ArrayList<AutoCloseable> windowDecors, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - final AutoCloseable windowDecor; - switch (change.getTaskInfo().getWindowingMode()) { - case WINDOWING_MODE_FREEFORM: - windowDecor = mFreeformTaskListener.giveWindowDecoration(change.getTaskInfo(), - startT, finishT); - break; - case WINDOWING_MODE_FULLSCREEN: - windowDecor = mFullscreenTaskListener.giveWindowDecoration(change.getTaskInfo(), - startT, finishT); - break; - default: - windowDecor = null; - } - if (windowDecor != null) { - windowDecors.add(windowDecor); - } + mWindowDecorViewModel.setupWindowDecorationForTransition( + change.getTaskInfo(), startT, finishT); } private void onChangeTransitionReady( - int type, TransitionInfo.Change change, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - AutoCloseable windowDecor = null; - - boolean adopted = false; - final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { - windowDecor = mFreeformTaskListener.giveWindowDecoration( - change.getTaskInfo(), startT, finishT); - if (windowDecor != null) { - adopted = mFullscreenTaskListener.adoptWindowDecoration( - change, startT, finishT, windowDecor); - } else { - // will return false if it already has the window decor. - adopted = mFullscreenTaskListener.createWindowDecoration(change, startT, finishT); - } - } - - if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { - windowDecor = mFullscreenTaskListener.giveWindowDecoration( - change.getTaskInfo(), startT, finishT); - if (windowDecor != null) { - adopted = mFreeformTaskListener.adoptWindowDecoration( - change, startT, finishT, windowDecor); - } else { - // will return false if it already has the window decor. - adopted = mFreeformTaskListener.createWindowDecoration(change, startT, finishT); - } - } - - if (!adopted) { - releaseWindowDecor(windowDecor); - } + mWindowDecorViewModel.setupWindowDecorationForTransition( + change.getTaskInfo(), startT, finishT); } @Override @@ -203,43 +143,32 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @Override public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { - final List<AutoCloseable> windowDecorsOfMerged = mTransitionToWindowDecors.get(merged); - if (windowDecorsOfMerged == null) { + final List<ActivityManager.RunningTaskInfo> infoOfMerged = + mTransitionToTaskInfo.get(merged); + if (infoOfMerged == null) { // We are adding window decorations of the merged transition to them of the playing // transition so if there is none of them there is nothing to do. return; } - mTransitionToWindowDecors.remove(merged); + mTransitionToTaskInfo.remove(merged); - final List<AutoCloseable> windowDecorsOfPlaying = mTransitionToWindowDecors.get(playing); - if (windowDecorsOfPlaying != null) { - windowDecorsOfPlaying.addAll(windowDecorsOfMerged); + final List<ActivityManager.RunningTaskInfo> infoOfPlaying = + mTransitionToTaskInfo.get(playing); + if (infoOfPlaying != null) { + infoOfPlaying.addAll(infoOfMerged); } else { - mTransitionToWindowDecors.put(playing, windowDecorsOfMerged); + mTransitionToTaskInfo.put(playing, infoOfMerged); } } @Override public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) { - final List<AutoCloseable> windowDecors = mTransitionToWindowDecors.getOrDefault( - transition, Collections.emptyList()); - mTransitionToWindowDecors.remove(transition); + final List<ActivityManager.RunningTaskInfo> taskInfo = + mTransitionToTaskInfo.getOrDefault(transition, Collections.emptyList()); + mTransitionToTaskInfo.remove(transition); - for (AutoCloseable windowDecor : windowDecors) { - releaseWindowDecor(windowDecor); - } - mFullscreenTaskListener.onTaskTransitionFinished(); - mFreeformTaskListener.onTaskTransitionFinished(); - } - - private static void releaseWindowDecor(AutoCloseable windowDecor) { - if (windowDecor == null) { - return; - } - try { - windowDecor.close(); - } catch (Exception e) { - Log.e(TAG, "Failed to release window decoration.", e); + for (int i = 0; i < taskInfo.size(); ++i) { + mWindowDecorViewModel.destroyWindowDecoration(taskInfo.get(i)); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java index 76e296bb8c61..75a4091c7d78 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java @@ -22,13 +22,10 @@ import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.graphics.Point; -import android.util.Log; import android.util.SparseArray; import android.view.SurfaceControl; -import android.window.TransitionInfo; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; @@ -46,23 +43,20 @@ import java.util.Optional; * Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}. * @param <T> the type of window decoration instance */ -public class FullscreenTaskListener<T extends AutoCloseable> - implements ShellTaskOrganizer.TaskListener { +public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { private static final String TAG = "FullscreenTaskListener"; private final ShellTaskOrganizer mShellTaskOrganizer; - private final SparseArray<State<T>> mTasks = new SparseArray<>(); - private final SparseArray<T> mWindowDecorOfVanishedTasks = new SparseArray<>(); + private final SparseArray<State> mTasks = new SparseArray<>(); - private static class State<T extends AutoCloseable> { + private static class State { RunningTaskInfo mTaskInfo; SurfaceControl mLeash; - T mWindowDecoration; } private final SyncTransactionQueue mSyncQueue; private final Optional<RecentTasksController> mRecentTasksOptional; - private final Optional<WindowDecorViewModel<T>> mWindowDecorViewModelOptional; + private final Optional<WindowDecorViewModel> mWindowDecorViewModelOptional; /** * This constructor is used by downstream products. */ @@ -75,7 +69,7 @@ public class FullscreenTaskListener<T extends AutoCloseable> ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, Optional<RecentTasksController> recentTasksOptional, - Optional<WindowDecorViewModel<T>> windowDecorViewModelOptional) { + Optional<WindowDecorViewModel> windowDecorViewModelOptional) { mShellTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; mRecentTasksOptional = recentTasksOptional; @@ -98,21 +92,21 @@ public class FullscreenTaskListener<T extends AutoCloseable> ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d", taskInfo.taskId); final Point positionInParent = taskInfo.positionInParent; - final State<T> state = new State(); + final State state = new State(); state.mLeash = leash; state.mTaskInfo = taskInfo; mTasks.put(taskInfo.taskId, state); if (Transitions.ENABLE_SHELL_TRANSITIONS) return; updateRecentsForVisibleFullscreenTask(taskInfo); + boolean createdWindowDecor = false; if (mWindowDecorViewModelOptional.isPresent()) { SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - state.mWindowDecoration = - mWindowDecorViewModelOptional.get().createWindowDecoration(taskInfo, - leash, t, t); + createdWindowDecor = mWindowDecorViewModelOptional.get() + .createWindowDecoration(taskInfo, leash, t, t); t.apply(); } - if (state.mWindowDecoration == null) { + if (!createdWindowDecor) { mSyncQueue.runInSync(t -> { // Reset several properties back to fullscreen (PiP, for example, leaves all these // properties in a bad state). @@ -127,12 +121,11 @@ public class FullscreenTaskListener<T extends AutoCloseable> @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { - final State<T> state = mTasks.get(taskInfo.taskId); + final State state = mTasks.get(taskInfo.taskId); final Point oldPositionInParent = state.mTaskInfo.positionInParent; state.mTaskInfo = taskInfo; - if (state.mWindowDecoration != null) { - mWindowDecorViewModelOptional.get().onTaskInfoChanged( - state.mTaskInfo, state.mWindowDecoration); + if (mWindowDecorViewModelOptional.isPresent()) { + mWindowDecorViewModelOptional.get().onTaskInfoChanged(state.mTaskInfo); } if (Transitions.ENABLE_SHELL_TRANSITIONS) return; updateRecentsForVisibleFullscreenTask(taskInfo); @@ -147,160 +140,13 @@ public class FullscreenTaskListener<T extends AutoCloseable> @Override public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { - final State<T> state = mTasks.get(taskInfo.taskId); - if (state == null) { - // This is possible if the transition happens before this method. - return; - } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d", taskInfo.taskId); mTasks.remove(taskInfo.taskId); - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - // Save window decorations of closing tasks so that we can hand them over to the - // transition system if this method happens before the transition. In case where the - // transition didn't happen, it'd be cleared when the next transition finished. - if (state.mWindowDecoration != null) { - mWindowDecorOfVanishedTasks.put(taskInfo.taskId, state.mWindowDecoration); - } - return; - } - releaseWindowDecor(state.mWindowDecoration); - } - - /** - * Creates a window decoration for a transition. - * - * @param change the change of this task transition that needs to have the task layer as the - * leash - * @return {@code true} if a decoration was actually created. - */ - public boolean createWindowDecoration(TransitionInfo.Change change, - SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); - if (!mWindowDecorViewModelOptional.isPresent()) return false; - if (state.mWindowDecoration != null) { - // Already has a decoration. - return false; - } - T newWindowDecor = mWindowDecorViewModelOptional.get().createWindowDecoration( - state.mTaskInfo, state.mLeash, startT, finishT); - if (newWindowDecor != null) { - state.mWindowDecoration = newWindowDecor; - return true; - } - return false; - } - - /** - * Adopt the incoming window decoration and lets the window decoration prepare for a transition. - * - * @param change the change of this task transition that needs to have the task layer as the - * leash - * @param startT the start transaction of this transition - * @param finishT the finish transaction of this transition - * @param windowDecor the window decoration to adopt - * @return {@code true} if it adopts the window decoration; {@code false} otherwise - */ - public boolean adoptWindowDecoration( - TransitionInfo.Change change, - SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT, - @Nullable AutoCloseable windowDecor) { - if (!mWindowDecorViewModelOptional.isPresent()) { - return false; - } - final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash()); - state.mWindowDecoration = mWindowDecorViewModelOptional.get().adoptWindowDecoration( - windowDecor); - if (state.mWindowDecoration != null) { - mWindowDecorViewModelOptional.get().setupWindowDecorationForTransition( - state.mTaskInfo, startT, finishT, state.mWindowDecoration); - return true; - } else { - T newWindowDecor = mWindowDecorViewModelOptional.get().createWindowDecoration( - state.mTaskInfo, state.mLeash, startT, finishT); - if (newWindowDecor != null) { - state.mWindowDecoration = newWindowDecor; - } - return false; - } - } - - /** - * Clear window decors of vanished tasks. - */ - public void onTaskTransitionFinished() { - if (mWindowDecorOfVanishedTasks.size() == 0) { - return; - } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Clearing window decors of vanished tasks. There could be visual defects " - + "if any of them is used later in transitions."); - for (int i = 0; i < mWindowDecorOfVanishedTasks.size(); ++i) { - releaseWindowDecor(mWindowDecorOfVanishedTasks.valueAt(i)); - } - mWindowDecorOfVanishedTasks.clear(); - } - - /** - * Gives out the ownership of the task's window decoration. The given task is leaving (of has - * left) this task listener. This is the transition system asking for the ownership. - * - * @param taskInfo the maximizing task - * @return the window decor of the maximizing task if any - */ - public T giveWindowDecoration( - ActivityManager.RunningTaskInfo taskInfo, - SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT) { - T windowDecor; - final State<T> state = mTasks.get(taskInfo.taskId); - if (state != null) { - windowDecor = state.mWindowDecoration; - state.mWindowDecoration = null; - } else { - windowDecor = - mWindowDecorOfVanishedTasks.removeReturnOld(taskInfo.taskId); - } - if (mWindowDecorViewModelOptional.isPresent() && windowDecor != null) { - mWindowDecorViewModelOptional.get().setupWindowDecorationForTransition( - taskInfo, startT, finishT, windowDecor); - } - - return windowDecor; - } - - private State<T> createOrUpdateTaskState(ActivityManager.RunningTaskInfo taskInfo, - SurfaceControl leash) { - State<T> state = mTasks.get(taskInfo.taskId); - if (state != null) { - updateTaskInfo(taskInfo); - return state; - } - - state = new State<T>(); - state.mTaskInfo = taskInfo; - state.mLeash = leash; - mTasks.put(taskInfo.taskId, state); - - return state; - } - - private State<T> updateTaskInfo(ActivityManager.RunningTaskInfo taskInfo) { - final State<T> state = mTasks.get(taskInfo.taskId); - state.mTaskInfo = taskInfo; - return state; - } - - private void releaseWindowDecor(T windowDecor) { - if (windowDecor == null) { - return; - } - try { - windowDecor.close(); - } catch (Exception e) { - Log.e(TAG, "Failed to release window decoration.", e); + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + if (mWindowDecorViewModelOptional.isPresent()) { + mWindowDecorViewModelOptional.get().destroyWindowDecoration(taskInfo); } } @@ -342,6 +188,4 @@ public class FullscreenTaskListener<T extends AutoCloseable> public String toString() { return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN); } - - } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java index 7129165a78dc..2ee334873780 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java @@ -30,13 +30,6 @@ public interface OneHanded { OneHandedController.SUPPORT_ONE_HANDED_MODE, false); /** - * Returns a binder that can be passed to an external process to manipulate OneHanded. - */ - default IOneHanded createExternalInterface() { - return null; - } - - /** * Enters one handed mode. */ void startOneHanded(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index e0c4fe8c4fba..679d4ca2ac48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -24,6 +24,7 @@ import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE; import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING; import static com.android.wm.shell.onehanded.OneHandedState.STATE_EXITING; import static com.android.wm.shell.onehanded.OneHandedState.STATE_NONE; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED; import android.annotation.BinderThread; import android.content.ComponentName; @@ -49,6 +50,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerCallback; @@ -296,12 +298,18 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, mShellController.addConfigurationChangeListener(this); mShellController.addKeyguardChangeListener(this); mShellController.addUserChangeListener(this); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_ONE_HANDED, + this::createExternalInterface, this); } public OneHanded asOneHanded() { return mImpl; } + private ExternalInterfaceBinder createExternalInterface() { + return new IOneHandedImpl(this); + } + @Override public Context getContext() { return mContext; @@ -709,17 +717,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, */ @ExternalThread private class OneHandedImpl implements OneHanded { - private IOneHandedImpl mIOneHanded; - - @Override - public IOneHanded createExternalInterface() { - if (mIOneHanded != null) { - mIOneHanded.invalidate(); - } - mIOneHanded = new IOneHandedImpl(OneHandedController.this); - return mIOneHanded; - } - @Override public void startOneHanded() { mMainExecutor.execute(() -> { @@ -767,7 +764,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, * The interface for calls from outside the host process. */ @BinderThread - private static class IOneHandedImpl extends IOneHanded.Stub { + private static class IOneHandedImpl extends IOneHanded.Stub implements ExternalInterfaceBinder { private OneHandedController mController; IOneHandedImpl(OneHandedController controller) { @@ -777,7 +774,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl index 4def15db2f52..2624ee536b58 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl @@ -59,10 +59,15 @@ interface IPip { /** * Sets listener to get pinned stack animation callbacks. */ - oneway void setPinnedStackAnimationListener(IPipAnimationListener listener) = 3; + oneway void setPipAnimationListener(IPipAnimationListener listener) = 3; /** * Sets the shelf height and visibility. */ oneway void setShelfHeight(boolean visible, int shelfHeight) = 4; + + /** + * Sets the next pip animation type to be the alpha animation. + */ + oneway void setPipAnimationTypeToAlpha() = 5; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index c06881ae6ad7..f34d2a827e69 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -27,14 +27,6 @@ import java.util.function.Consumer; */ @ExternalThread public interface Pip { - - /** - * Returns a binder that can be passed to an external process to manipulate PIP. - */ - default IPip createExternalInterface() { - return null; - } - /** * Expand PIP, it's possible that specific request to activate the window via Alt-tab. */ @@ -51,15 +43,6 @@ public interface Pip { } /** - * Sets both shelf visibility and its height. - * - * @param visible visibility of shelf. - * @param height to specify the height for shelf. - */ - default void setShelfHeight(boolean visible, int height) { - } - - /** * Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed. * * @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()} @@ -68,14 +51,6 @@ public interface Pip { default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {} /** - * Set the pinned stack with {@link PipAnimationController.AnimationType} - * - * @param animationType The pre-defined {@link PipAnimationController.AnimationType} - */ - default void setPinnedStackAnimationType(int animationType) { - } - - /** * Called when showing Pip menu. */ default void showPictureInPictureMenu() {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 33761d23379d..2b36b4c0307d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -452,14 +452,17 @@ public class PipTransition extends PipTransitionController { @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull TaskInfo taskInfo, @Nullable TransitionInfo.Change pipTaskChange) { TransitionInfo.Change pipChange = pipTaskChange; - if (pipChange == null) { + if (mCurrentPipTaskToken == null) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG); + } else if (pipChange == null) { // The pipTaskChange is null, this can happen if we are reparenting the PIP activity // back to its original Task. In that case, we should animate the activity leash - // instead, which should be the only non-task, independent, TRANSIT_CHANGE window. + // instead, which should be the change whose last parent is the recorded PiP Task. for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() == null && change.getMode() == TRANSIT_CHANGE - && TransitionInfo.isIndependent(change, info)) { + if (mCurrentPipTaskToken.equals(change.getLastParent())) { + // Find the activity that is exiting PiP. pipChange = change; break; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index af47666efa5a..616d447247de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -23,6 +23,7 @@ import static android.view.WindowManager.INPUT_CONSUMER_PIP; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; @@ -32,6 +33,7 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -67,6 +69,7 @@ import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; @@ -158,6 +161,10 @@ public class PipController implements PipTransitionController.PipTransitionCallb // early bail out if the keep clear areas feature is disabled return; } + if (mPipBoundsState.isStashed()) { + // don't move when stashed + return; + } // if there is another animation ongoing, wait for it to finish and try again if (mPipAnimationController.isAnimating()) { mMainExecutor.removeCallbacks( @@ -631,6 +638,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb mShellController.addConfigurationChangeListener(this); mShellController.addKeyguardChangeListener(this); mShellController.addUserChangeListener(this); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_PIP, + this::createExternalInterface, this); + } + + private ExternalInterfaceBinder createExternalInterface() { + return new IPipImpl(this); } @Override @@ -732,6 +745,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb // Directly move PiP to its final destination bounds without animation. mPipTaskOrganizer.scheduleFinishResizePip(postChangeBounds); } + + // if the pip window size is beyond allowed bounds user resize to normal bounds + if (mPipBoundsState.getBounds().width() < mPipBoundsState.getMinSize().x + || mPipBoundsState.getBounds().width() > mPipBoundsState.getMaxSize().x + || mPipBoundsState.getBounds().height() < mPipBoundsState.getMinSize().y + || mPipBoundsState.getBounds().height() > mPipBoundsState.getMaxSize().y) { + mTouchHandler.userResizeTo(mPipBoundsState.getNormalBounds(), snapFraction); + } + } else { updateDisplayLayout.run(); } @@ -1039,17 +1061,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb * The interface for calls from outside the Shell, within the host process. */ private class PipImpl implements Pip { - private IPipImpl mIPip; - - @Override - public IPip createExternalInterface() { - if (mIPip != null) { - mIPip.invalidate(); - } - mIPip = new IPipImpl(PipController.this); - return mIPip; - } - @Override public void expandPip() { mMainExecutor.execute(() -> { @@ -1065,13 +1076,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void setShelfHeight(boolean visible, int height) { - mMainExecutor.execute(() -> { - PipController.this.setShelfHeight(visible, height); - }); - } - - @Override public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { mMainExecutor.execute(() -> { PipController.this.setOnIsInPipStateChangedListener(callback); @@ -1079,13 +1083,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void setPinnedStackAnimationType(int animationType) { - mMainExecutor.execute(() -> { - PipController.this.setPinnedStackAnimationType(animationType); - }); - } - - @Override public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) { mMainExecutor.execute(() -> { mPipBoundsState.addPipExclusionBoundsChangeCallback(listener); @@ -1111,7 +1108,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb * The interface for calls from outside the host process. */ @BinderThread - private static class IPipImpl extends IPip.Stub { + private static class IPipImpl extends IPip.Stub implements ExternalInterfaceBinder { private PipController mController; private final SingleInstanceRemoteListener<PipController, IPipAnimationListener> mListener; @@ -1142,7 +1139,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; } @@ -1178,8 +1176,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void setPinnedStackAnimationListener(IPipAnimationListener listener) { - executeRemoteCallWithTaskPermission(mController, "setPinnedStackAnimationListener", + public void setPipAnimationListener(IPipAnimationListener listener) { + executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener", (controller) -> { if (listener != null) { mListener.register(listener); @@ -1188,5 +1186,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); } + + @Override + public void setPipAnimationTypeToAlpha() { + executeRemoteCallWithTaskPermission(mController, "setPipAnimationTypeToAlpha", + (controller) -> { + controller.setPinnedStackAnimationType(ANIM_TYPE_ALPHA); + }); + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index afb64c9eec41..43d3f36f1fe5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -60,7 +60,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, FloatingContentCoordinator.FloatingContent { public static final boolean ENABLE_FLING_TO_DISMISS_PIP = - SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", true); + SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", false); private static final String TAG = "PipMotionHelper"; private static final boolean DEBUG = false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java index 89d85e4b292d..41ff0b35a035 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -96,6 +96,7 @@ public class PipResizeGestureHandler { private final Rect mDisplayBounds = new Rect(); private final Function<Rect, Rect> mMovementBoundsSupplier; private final Runnable mUpdateMovementBoundsRunnable; + private final Consumer<Rect> mUpdateResizeBoundsCallback; private int mDelta; private float mTouchSlop; @@ -137,6 +138,13 @@ public class PipResizeGestureHandler { mPhonePipMenuController = menuActivityController; mPipUiEventLogger = pipUiEventLogger; mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); + + mUpdateResizeBoundsCallback = (rect) -> { + mUserResizeBounds.set(rect); + mMotionHelper.synchronizePinnedStackBounds(); + mUpdateMovementBoundsRunnable.run(); + resetState(); + }; } public void init() { @@ -508,15 +516,50 @@ public class PipResizeGestureHandler { } } + private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) { + final int leftEdge = bounds.left; + + + final int fromLeft = Math.abs(leftEdge - movementBounds.left); + final int fromRight = Math.abs(movementBounds.right - leftEdge); + + // The PIP will be snapped to either the right or left edge, so calculate which one + // is closest to the current position. + final int newLeft = fromLeft < fromRight + ? movementBounds.left : movementBounds.right; + + bounds.offsetTo(newLeft, mLastResizeBounds.top); + } + + /** + * Resizes the pip window and updates user-resized bounds. + * + * @param bounds target bounds to resize to + * @param snapFraction snap fraction to apply after resizing + */ + void userResizeTo(Rect bounds, float snapFraction) { + Rect finalBounds = new Rect(bounds); + + // get the current movement bounds + final Rect movementBounds = mPipBoundsAlgorithm.getMovementBounds(finalBounds); + + // snap the target bounds to the either left or right edge, by choosing the closer one + snapToMovementBoundsEdge(finalBounds, movementBounds); + + // apply the requested snap fraction onto the target bounds + mPipBoundsAlgorithm.applySnapFraction(finalBounds, snapFraction); + + // resize from current bounds to target bounds without animation + mPipTaskOrganizer.scheduleUserResizePip(mPipBoundsState.getBounds(), finalBounds, null); + // set the flag that pip has been resized + mPipBoundsState.setHasUserResizedPip(true); + + // finish the resize operation and update the state of the bounds + mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback); + } + private void finishResize() { if (!mLastResizeBounds.isEmpty()) { - final Consumer<Rect> callback = (rect) -> { - mUserResizeBounds.set(mLastResizeBounds); - mMotionHelper.synchronizePinnedStackBounds(); - mUpdateMovementBoundsRunnable.run(); - resetState(); - }; - // Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped // position correctly. Drag-resize does not need to move, so just finalize resize. if (mOngoingPinchToResize) { @@ -526,24 +569,23 @@ public class PipResizeGestureHandler { || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) { resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y); } - final int leftEdge = mLastResizeBounds.left; - final Rect movementBounds = - mPipBoundsAlgorithm.getMovementBounds(mLastResizeBounds); - final int fromLeft = Math.abs(leftEdge - movementBounds.left); - final int fromRight = Math.abs(movementBounds.right - leftEdge); - // The PIP will be snapped to either the right or left edge, so calculate which one - // is closest to the current position. - final int newLeft = fromLeft < fromRight - ? movementBounds.left : movementBounds.right; - mLastResizeBounds.offsetTo(newLeft, mLastResizeBounds.top); + + // get the current movement bounds + final Rect movementBounds = mPipBoundsAlgorithm + .getMovementBounds(mLastResizeBounds); + + // snap mLastResizeBounds to the correct edge based on movement bounds + snapToMovementBoundsEdge(mLastResizeBounds, movementBounds); + final float snapFraction = mPipBoundsAlgorithm.getSnapFraction( mLastResizeBounds, movementBounds); mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, - PINCH_RESIZE_SNAP_DURATION, mAngle, callback); + PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback); } else { mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, - PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, callback); + PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE, + mUpdateResizeBoundsCallback); } final float magnetRadiusPercent = (float) mLastResizeBounds.width() / mMinSize.x / 2.f; mPipDismissTargetHandler diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index 1f3f31e025a0..975d4bba276e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -825,6 +825,16 @@ public class PipTouchHandler { } /** + * Resizes the pip window and updates user resized bounds + * + * @param bounds target bounds to resize to + * @param snapFraction snap fraction to apply after resizing + */ + void userResizeTo(Rect bounds, float snapFraction) { + mPipResizeGestureHandler.userResizeTo(bounds, snapFraction); + } + + /** * Gesture controlling normal movement of the PIP. */ private class DefaultPipTouchGesture extends PipTouchGesture { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java index 552ebde05274..93ffb3dc8115 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java @@ -17,22 +17,14 @@ package com.android.wm.shell.protolog; import android.annotation.Nullable; -import android.content.Context; -import android.util.Log; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.BaseProtoLogImpl; import com.android.internal.protolog.ProtoLogViewerConfigReader; import com.android.internal.protolog.common.IProtoLogGroup; -import com.android.wm.shell.R; import java.io.File; -import java.io.IOException; -import java.io.InputStream; import java.io.PrintWriter; -import org.json.JSONException; - /** * A service for the ProtoLog logging system. @@ -40,8 +32,9 @@ import org.json.JSONException; public class ShellProtoLogImpl extends BaseProtoLogImpl { private static final String TAG = "ProtoLogImpl"; private static final int BUFFER_CAPACITY = 1024 * 1024; - // TODO: Get the right path for the proto log file when we initialize the shell components - private static final String LOG_FILENAME = new File("wm_shell_log.pb").getAbsolutePath(); + // TODO: find a proper location to save the protolog message file + private static final String LOG_FILENAME = "/data/misc/wmtrace/shell_log.winscope"; + private static final String VIEWER_CONFIG_FILENAME = "/system_ext/etc/wmshell.protolog.json.gz"; private static ShellProtoLogImpl sServiceInstance = null; @@ -111,18 +104,8 @@ public class ShellProtoLogImpl extends BaseProtoLogImpl { } public int startTextLogging(String[] groups, PrintWriter pw) { - try (InputStream is = - getClass().getClassLoader().getResourceAsStream("wm_shell_protolog.json")){ - mViewerConfig.loadViewerConfig(is); - return setLogging(true /* setTextLogging */, true, pw, groups); - } catch (IOException e) { - Log.i(TAG, "Unable to load log definitions: IOException while reading " - + "wm_shell_protolog. " + e); - } catch (JSONException e) { - Log.i(TAG, "Unable to load log definitions: JSON parsing exception while reading " - + "wm_shell_protolog. " + e); - } - return -1; + mViewerConfig.loadViewerConfig(pw, VIEWER_CONFIG_FILENAME); + return setLogging(true /* setTextLogging */, true, pw, groups); } public int stopTextLogging(String[] groups, PrintWriter pw) { @@ -130,7 +113,8 @@ public class ShellProtoLogImpl extends BaseProtoLogImpl { } private ShellProtoLogImpl() { - super(new File(LOG_FILENAME), null, BUFFER_CAPACITY, new ProtoLogViewerConfigReader()); + super(new File(LOG_FILENAME), VIEWER_CONFIG_FILENAME, BUFFER_CAPACITY, + new ProtoLogViewerConfigReader()); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl index b71cc32a0347..1a6c1d65db03 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl @@ -16,7 +16,7 @@ package com.android.wm.shell.recents; -import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; import com.android.wm.shell.recents.IRecentTasksListener; import com.android.wm.shell.util.GroupedRecentTaskInfo; @@ -44,5 +44,5 @@ interface IRecentTasks { /** * Gets the set of running tasks. */ - ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) = 4; + RunningTaskInfo[] getRunningTasks(int maxNum) = 4; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl index 59f72335678e..e8f58fe2bfad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl @@ -16,7 +16,7 @@ package com.android.wm.shell.recents; -import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; /** * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks. @@ -31,10 +31,10 @@ oneway interface IRecentTasksListener { /** * Called when a running task appears. */ - void onRunningTaskAppeared(in ActivityManager.RunningTaskInfo taskInfo); + void onRunningTaskAppeared(in RunningTaskInfo taskInfo); /** * Called when a running task vanishes. */ - void onRunningTaskVanished(in ActivityManager.RunningTaskInfo taskInfo); -}
\ No newline at end of file + void onRunningTaskVanished(in RunningTaskInfo taskInfo); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java index 2a625524b48b..069066e4bd49 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java @@ -29,13 +29,6 @@ import java.util.function.Consumer; @ExternalThread public interface RecentTasks { /** - * Returns a binder that can be passed to an external process to fetch recent tasks. - */ - default IRecentTasks createExternalInterface() { - return null; - } - - /** * Gets the set of recent tasks. */ default void getRecentTasks(int maxNum, int flags, int userId, Executor callbackExecutor, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 02b5a35f653b..f9172ba183de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -20,6 +20,7 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.pm.PackageManager.FEATURE_PC; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -37,6 +38,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; @@ -48,6 +50,7 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.util.GroupedRecentTaskInfo; import com.android.wm.shell.util.SplitBounds; @@ -65,15 +68,16 @@ import java.util.function.Consumer; * Manages the recent task list from the system, caching it as necessary. */ public class RecentTasksController implements TaskStackListenerCallback, - RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.Listener { + RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener { private static final String TAG = RecentTasksController.class.getSimpleName(); private final Context mContext; + private final ShellController mShellController; private final ShellCommandHandler mShellCommandHandler; private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository; private final ShellExecutor mMainExecutor; private final TaskStackListenerImpl mTaskStackListener; - private final RecentTasks mImpl = new RecentTasksImpl(); + private final RecentTasksImpl mImpl = new RecentTasksImpl(); private final ActivityTaskManager mActivityTaskManager; private IRecentTasksListener mListener; private final boolean mIsDesktopMode; @@ -97,6 +101,7 @@ public class RecentTasksController implements TaskStackListenerCallback, public static RecentTasksController create( Context context, ShellInit shellInit, + ShellController shellController, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, @@ -106,18 +111,20 @@ public class RecentTasksController implements TaskStackListenerCallback, if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) { return null; } - return new RecentTasksController(context, shellInit, shellCommandHandler, taskStackListener, - activityTaskManager, desktopModeTaskRepository, mainExecutor); + return new RecentTasksController(context, shellInit, shellController, shellCommandHandler, + taskStackListener, activityTaskManager, desktopModeTaskRepository, mainExecutor); } RecentTasksController(Context context, ShellInit shellInit, + ShellController shellController, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, ActivityTaskManager activityTaskManager, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, ShellExecutor mainExecutor) { mContext = context; + mShellController = shellController; mShellCommandHandler = shellCommandHandler; mActivityTaskManager = activityTaskManager; mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); @@ -131,10 +138,16 @@ public class RecentTasksController implements TaskStackListenerCallback, return mImpl; } + private ExternalInterfaceBinder createExternalInterface() { + return new IRecentTasksImpl(this); + } + private void onInit() { + mShellController.addExternalInterface(KEY_EXTRA_SHELL_RECENT_TASKS, + this::createExternalInterface, this); mShellCommandHandler.addDumpCallback(this::dump, this); mTaskStackListener.addListener(this); - mDesktopModeTaskRepository.ifPresent(it -> it.addListener(this)); + mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this)); } /** @@ -366,17 +379,6 @@ public class RecentTasksController implements TaskStackListenerCallback, */ @ExternalThread private class RecentTasksImpl implements RecentTasks { - private IRecentTasksImpl mIRecentTasks; - - @Override - public IRecentTasks createExternalInterface() { - if (mIRecentTasks != null) { - mIRecentTasks.invalidate(); - } - mIRecentTasks = new IRecentTasksImpl(RecentTasksController.this); - return mIRecentTasks; - } - @Override public void getRecentTasks(int maxNum, int flags, int userId, Executor executor, Consumer<List<GroupedRecentTaskInfo>> callback) { @@ -393,7 +395,8 @@ public class RecentTasksController implements TaskStackListenerCallback, * The interface for calls from outside the host process. */ @BinderThread - private static class IRecentTasksImpl extends IRecentTasks.Stub { + private static class IRecentTasksImpl extends IRecentTasks.Stub + implements ExternalInterfaceBinder { private RecentTasksController mController; private final SingleInstanceRemoteListener<RecentTasksController, IRecentTasksListener> mListener; @@ -424,7 +427,8 @@ public class RecentTasksController implements TaskStackListenerCallback, /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl index ecdafa9a63f4..eb08d0ecbd06 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl @@ -79,26 +79,47 @@ interface ISplitScreen { /** * Starts tasks simultaneously in one transition. */ - oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId, - in Bundle sideOptions, int sidePosition, float splitRatio, - in RemoteTransition remoteTransition, in InstanceId instanceId) = 10; + oneway void startTasks(int taskId1, in Bundle options1, int taskId2, in Bundle options2, + int splitPosition, float splitRatio, in RemoteTransition remoteTransition, + in InstanceId instanceId) = 10; + + /** + * Starts a pair of intent and task in one transition. + */ + oneway void startIntentAndTask(in PendingIntent pendingIntent, in Intent fillInIntent, + in Bundle options1, int taskId, in Bundle options2, int sidePosition, float splitRatio, + in RemoteTransition remoteTransition, in InstanceId instanceId) = 16; + + /** + * Starts a pair of shortcut and task in one transition. + */ + oneway void startShortcutAndTask(in ShortcutInfo shortcutInfo, in Bundle options1, int taskId, + in Bundle options2, int splitPosition, float splitRatio, + in RemoteTransition remoteTransition, in InstanceId instanceId) = 17; /** * Version of startTasks using legacy transition system. */ - oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions, - int sideTaskId, in Bundle sideOptions, int sidePosition, - float splitRatio, in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 11; + oneway void startTasksWithLegacyTransition(int taskId1, in Bundle options1, int taskId2, + in Bundle options2, int splitPosition, float splitRatio, + in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 11; /** * Starts a pair of intent and task using legacy transition system. */ oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent, - in Intent fillInIntent, int taskId, in Bundle mainOptions,in Bundle sideOptions, - int sidePosition, float splitRatio, in RemoteAnimationAdapter adapter, + in Intent fillInIntent, in Bundle options1, int taskId, in Bundle options2, + int splitPosition, float splitRatio, in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 12; /** + * Starts a pair of shortcut and task using legacy transition system. + */ + oneway void startShortcutAndTaskWithLegacyTransition(in ShortcutInfo shortcutInfo, + in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio, + in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 15; + + /** * Blocking call that notifies and gets additional split-screen targets when entering * recents (for example: the dividerBar). * @param appTargets apps that will be re-parented to display area @@ -111,11 +132,5 @@ interface ISplitScreen { * does not expect split to currently be running. */ RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14; - - /** - * Starts a pair of shortcut and task using legacy transition system. - */ - oneway void startShortcutAndTaskWithLegacyTransition(in ShortcutInfo shortcutInfo, int taskId, - in Bundle mainOptions, in Bundle sideOptions, int sidePosition, float splitRatio, - in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 15; } +// Last id = 17 diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index e73b799b7a3d..d86aadc996e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -70,13 +70,6 @@ public interface SplitScreen { /** Unregisters listener that gets split screen callback. */ void unregisterSplitScreenListener(@NonNull SplitScreenListener listener); - /** - * Returns a binder that can be passed to an external process to manipulate SplitScreen. - */ - default ISplitScreen createExternalInterface() { - return null; - } - /** Called when device waking up finished. */ void onFinishedWakingUp(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 07a6895e2720..c6a2b8312ebd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -29,6 +29,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import android.app.ActivityManager; @@ -71,6 +72,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; @@ -214,6 +216,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return mImpl; } + private ExternalInterfaceBinder createExternalInterface() { + return new ISplitScreenImpl(this); + } + /** * This will be called after ShellTaskOrganizer has initialized/registered because of the * dependency order. @@ -224,6 +230,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mShellCommandHandler.addCommandCallback("splitscreen", mSplitScreenShellCommandHandler, this); mShellController.addKeyguardChangeListener(this); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_SPLIT_SCREEN, + this::createExternalInterface, this); if (mStageCoordinator == null) { // TODO: Multi-display mStageCoordinator = createStageCoordinator(); @@ -658,7 +666,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, */ @ExternalThread private class SplitScreenImpl implements SplitScreen { - private ISplitScreenImpl mISplitScreen; private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>(); private final SplitScreen.SplitScreenListener mListener = new SplitScreenListener() { @Override @@ -704,15 +711,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, }; @Override - public ISplitScreen createExternalInterface() { - if (mISplitScreen != null) { - mISplitScreen.invalidate(); - } - mISplitScreen = new ISplitScreenImpl(SplitScreenController.this); - return mISplitScreen; - } - - @Override public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) { if (mExecutors.containsKey(listener)) return; @@ -752,7 +750,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, * The interface for calls from outside the host process. */ @BinderThread - private static class ISplitScreenImpl extends ISplitScreen.Stub { + private static class ISplitScreenImpl extends ISplitScreen.Stub + implements ExternalInterfaceBinder { private SplitScreenController mController; private final SingleInstanceRemoteListener<SplitScreenController, ISplitScreenListener> mListener; @@ -779,7 +778,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; } @@ -828,47 +828,68 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, - int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + public void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1, + int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startTasks", (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition( - mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition, + taskId1, options1, taskId2, options2, splitPosition, splitRatio, adapter, instanceId)); } @Override public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, - Intent fillInIntent, int taskId, Bundle mainOptions, Bundle sideOptions, - int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, + Intent fillInIntent, Bundle options1, int taskId, Bundle options2, + int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntentAndTaskWithLegacyTransition", (controller) -> controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition( - pendingIntent, fillInIntent, taskId, mainOptions, sideOptions, - sidePosition, splitRatio, adapter, instanceId)); + pendingIntent, fillInIntent, options1, taskId, options2, + splitPosition, splitRatio, adapter, instanceId)); } @Override public void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, - int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions, - @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startShortcutAndTaskWithLegacyTransition", (controller) -> controller.mStageCoordinator.startShortcutAndTaskWithLegacyTransition( - shortcutInfo, taskId, mainOptions, sideOptions, sidePosition, + shortcutInfo, options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId)); } @Override - public void startTasks(int mainTaskId, @Nullable Bundle mainOptions, - int sideTaskId, @Nullable Bundle sideOptions, - @SplitPosition int sidePosition, float splitRatio, + public void startTasks(int taskId1, @Nullable Bundle options1, int taskId2, + @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startTasks", - (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions, - sideTaskId, sideOptions, sidePosition, splitRatio, remoteTransition, + (controller) -> controller.mStageCoordinator.startTasks(taskId1, options1, + taskId2, options2, splitPosition, splitRatio, remoteTransition, + instanceId)); + } + + @Override + public void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + executeRemoteCallWithTaskPermission(mController, "startIntentAndTask", + (controller) -> controller.mStageCoordinator.startIntentAndTask(pendingIntent, + fillInIntent, options1, taskId, options2, splitPosition, splitRatio, + remoteTransition, instanceId)); + } + + @Override + public void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1, + int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, @Nullable RemoteTransition remoteTransition, + InstanceId instanceId) { + executeRemoteCallWithTaskPermission(mController, "startShortcutAndTask", + (controller) -> controller.mStageCoordinator.startShortcutAndTask(shortcutInfo, + options1, taskId, options2, splitPosition, splitRatio, remoteTransition, instanceId)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index c17f8226c925..e2ac01f7b003 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -207,6 +207,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; private DefaultMixedHandler mMixedHandler; + private final Toast mSplitUnsupportedToast; private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks = new SplitWindowManager.ParentContainerCallbacks() { @@ -300,6 +301,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayLayout = new DisplayLayout(displayController.getDisplayLayout(displayId)); transitions.addHandler(this); mTaskOrganizer.addFocusListener(this); + mSplitUnsupportedToast = Toast.makeText(mContext, + R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT); } @VisibleForTesting @@ -329,6 +332,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayController.addDisplayWindowListener(this); mDisplayLayout = new DisplayLayout(); transitions.addHandler(this); + mSplitUnsupportedToast = Toast.makeText(mContext, + R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT); } public void setMixedHandler(DefaultMixedHandler mixedHandler) { @@ -470,6 +475,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainExecutor.execute(() -> exitSplitScreen(mMainStage.getChildCount() == 0 ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); + mSplitUnsupportedToast.show(); } else { // Switch the split position if launching as MULTIPLE_TASK failed. if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) { @@ -516,14 +522,55 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } /** Starts 2 tasks in one transition. */ - void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, - @Nullable Bundle sideOptions, @SplitPosition int sidePosition, float splitRatio, + void startTasks(int taskId1, @Nullable Bundle options1, int taskId2, + @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - mainOptions = mainOptions != null ? mainOptions : new Bundle(); - sideOptions = sideOptions != null ? sideOptions : new Bundle(); - setSideStagePosition(sidePosition, wct); + setSideStagePosition(splitPosition, wct); + options1 = options1 != null ? options1 : new Bundle(); + addActivityOptions(options1, mSideStage); + wct.startTask(taskId1, options1); + + startWithTask(wct, taskId2, options2, splitRatio, remoteTransition, instanceId); + } + + /** Start an intent and a task to a split pair in one transition. */ + void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + setSideStagePosition(splitPosition, wct); + options1 = options1 != null ? options1 : new Bundle(); + addActivityOptions(options1, mSideStage); + wct.sendPendingIntent(pendingIntent, fillInIntent, options1); + + startWithTask(wct, taskId, options2, splitRatio, remoteTransition, instanceId); + } + /** Starts a shortcut and a task to a split pair in one transition. */ + void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1, + int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + setSideStagePosition(splitPosition, wct); + options1 = options1 != null ? options1 : new Bundle(); + addActivityOptions(options1, mSideStage); + wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1); + + startWithTask(wct, taskId, options2, splitRatio, remoteTransition, instanceId); + } + + /** + * Starts with the second task to a split pair in one transition. + * + * @param wct transaction to start the first task + * @param instanceId if {@code null}, will not log. Otherwise it will be used in + * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} + */ + private void startWithTask(WindowContainerTransaction wct, int mainTaskId, + @Nullable Bundle mainOptions, float splitRatio, + @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { if (mMainStage.isActive()) { mMainStage.evictAllChildren(wct); mSideStage.evictAllChildren(wct); @@ -538,60 +585,61 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.setForceTranslucent(mRootTaskInfo.token, false); // Make sure the launch options will put tasks in the corresponding split roots + mainOptions = mainOptions != null ? mainOptions : new Bundle(); addActivityOptions(mainOptions, mMainStage); - addActivityOptions(sideOptions, mSideStage); // Add task launch requests wct.startTask(mainTaskId, mainOptions); - wct.startTask(sideTaskId, sideOptions); mSplitTransitions.startEnterTransition( TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null); setEnterInstanceId(instanceId); } - /** Starts 2 tasks in one legacy transition. */ - void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, - int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + /** Starts a pair of tasks using legacy transition. */ + void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1, + int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (sideOptions == null) sideOptions = new Bundle(); - addActivityOptions(sideOptions, mSideStage); - wct.startTask(sideTaskId, sideOptions); + if (options1 == null) options1 = new Bundle(); + addActivityOptions(options1, mSideStage); + wct.startTask(taskId1, options1); - startWithLegacyTransition(wct, mainTaskId, mainOptions, sidePosition, splitRatio, adapter, + startWithLegacyTransition(wct, taskId2, options2, splitPosition, splitRatio, adapter, instanceId); } - /** Start an intent and a task ordered by {@code intentFirst}. */ + /** Starts a pair of intent and task using legacy transition. */ void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent, - int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions, - @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (sideOptions == null) sideOptions = new Bundle(); - addActivityOptions(sideOptions, mSideStage); - wct.sendPendingIntent(pendingIntent, fillInIntent, sideOptions); + if (options1 == null) options1 = new Bundle(); + addActivityOptions(options1, mSideStage); + wct.sendPendingIntent(pendingIntent, fillInIntent, options1); - startWithLegacyTransition(wct, taskId, mainOptions, sidePosition, splitRatio, adapter, + startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter, instanceId); } + /** Starts a pair of shortcut and task using legacy transition. */ void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, - int taskId, @Nullable Bundle mainOptions, @Nullable Bundle sideOptions, - @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (sideOptions == null) sideOptions = new Bundle(); - addActivityOptions(sideOptions, mSideStage); - wct.startShortcut(mContext.getPackageName(), shortcutInfo, sideOptions); + if (options1 == null) options1 = new Bundle(); + addActivityOptions(options1, mSideStage); + wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1); - startWithLegacyTransition(wct, taskId, mainOptions, sidePosition, splitRatio, adapter, + startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter, instanceId); } /** + * @param wct transaction to start the first task * @param instanceId if {@code null}, will not log. Otherwise it will be used in * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} */ @@ -694,6 +742,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainExecutor.execute(() -> exitSplitScreen(mMainStage.getChildCount() == 0 ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); + mSplitUnsupportedToast.show(); } else { mSyncQueue.queue(evictWct); } @@ -1727,6 +1776,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @StageType private int getStageType(StageTaskListener stage) { + if (stage == null) return STAGE_TYPE_UNDEFINED; return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; } @@ -1981,8 +2031,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - // TODO: fallback logic. Probably start a new transition to exit split before applying - // anything here. Ideally consolidate with transition-merging. + // TODO(b/250853925): fallback logic. Probably start a new transition to exit split before + // applying anything here. Ideally consolidate with transition-merging. if (info.getType() == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) { if (mainChild == null && sideChild == null) { throw new IllegalStateException("Launched a task in split, but didn't receive any" @@ -2244,13 +2294,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onNoLongerSupportMultiWindow() { if (mMainStage.isActive()) { - final Toast splitUnsupportedToast = Toast.makeText(mContext, - R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT); final boolean isMainStage = mMainStageListener == this; if (!ENABLE_SHELL_TRANSITIONS) { StageCoordinator.this.exitSplitScreen(isMainStage ? mMainStage : mSideStage, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); - splitUnsupportedToast.show(); + mSplitUnsupportedToast.show(); return; } @@ -2259,7 +2307,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareExitSplitScreen(stageType, wct); mSplitTransitions.startDismissTransition(wct,StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); - splitUnsupportedToast.show(); + mSplitUnsupportedToast.show(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java index 014f02bcf8b7..8bba44049c88 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java @@ -15,38 +15,20 @@ */ package com.android.wm.shell.startingsurface; -import static android.view.Choreographer.CALLBACK_COMMIT; import static android.view.View.GONE; import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLASHSCREEN_EXIT_ANIM; import android.animation.Animator; -import android.animation.ValueAnimator; import android.content.Context; -import android.graphics.BlendMode; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.RadialGradient; import android.graphics.Rect; -import android.graphics.Shader; -import android.util.MathUtils; import android.util.Slog; -import android.view.Choreographer; import android.view.SurfaceControl; -import android.view.SyncRtSurfaceTransactionApplier; import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.animation.Interpolator; -import android.view.animation.PathInterpolator; import android.window.SplashScreenView; import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.TransactionPool; /** @@ -55,14 +37,8 @@ import com.android.wm.shell.common.TransactionPool; */ public class SplashScreenExitAnimation implements Animator.AnimatorListener { private static final boolean DEBUG_EXIT_ANIMATION = false; - private static final boolean DEBUG_EXIT_ANIMATION_BLEND = false; private static final String TAG = StartingWindowController.TAG; - private static final Interpolator ICON_INTERPOLATOR = new PathInterpolator(0.15f, 0f, 1f, 1f); - private static final Interpolator MASK_RADIUS_INTERPOLATOR = - new PathInterpolator(0f, 0f, 0.4f, 1f); - private static final Interpolator SHIFT_UP_INTERPOLATOR = new PathInterpolator(0f, 0f, 0f, 1f); - private final SurfaceControl mFirstWindowSurface; private final Rect mFirstWindowFrame = new Rect(); private final SplashScreenView mSplashScreenView; @@ -75,9 +51,6 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { private final float mBrandingStartAlpha; private final TransactionPool mTransactionPool; - private ValueAnimator mMainAnimator; - private ShiftUpAnimation mShiftUpAnimation; - private RadialVanishAnimation mRadialVanishAnimation; private Runnable mFinishCallback; SplashScreenExitAnimation(Context context, SplashScreenView view, SurfaceControl leash, @@ -121,187 +94,10 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { } void startAnimations() { - mMainAnimator = createAnimator(); - mMainAnimator.start(); - } - - // fade out icon, reveal app, shift up main window - private ValueAnimator createAnimator() { - // reveal app - final float transparentRatio = 0.8f; - final int globalHeight = mSplashScreenView.getHeight(); - final int verticalCircleCenter = 0; - final int finalVerticalLength = globalHeight - verticalCircleCenter; - final int halfWidth = mSplashScreenView.getWidth() / 2; - final int endRadius = (int) (0.5 + (1f / transparentRatio * (int) - Math.sqrt(finalVerticalLength * finalVerticalLength + halfWidth * halfWidth))); - final int[] colors = {Color.WHITE, Color.WHITE, Color.TRANSPARENT}; - final float[] stops = {0f, transparentRatio, 1f}; - - mRadialVanishAnimation = new RadialVanishAnimation(mSplashScreenView); - mRadialVanishAnimation.setCircleCenter(halfWidth, verticalCircleCenter); - mRadialVanishAnimation.setRadius(0 /* initRadius */, endRadius); - mRadialVanishAnimation.setRadialPaintParam(colors, stops); - - if (mFirstWindowSurface != null && mFirstWindowSurface.isValid()) { - // shift up main window - View occludeHoleView = new View(mSplashScreenView.getContext()); - if (DEBUG_EXIT_ANIMATION_BLEND) { - occludeHoleView.setBackgroundColor(Color.BLUE); - } else { - occludeHoleView.setBackgroundColor(mSplashScreenView.getInitBackgroundColor()); - } - final ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( - WindowManager.LayoutParams.MATCH_PARENT, mMainWindowShiftLength); - mSplashScreenView.addView(occludeHoleView, params); - - mShiftUpAnimation = new ShiftUpAnimation(0, -mMainWindowShiftLength, occludeHoleView); - } - - ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); - animator.setDuration(mAnimationDuration); - animator.setInterpolator(Interpolators.LINEAR); - animator.addListener(this); - animator.addUpdateListener(a -> onAnimationProgress((float) a.getAnimatedValue())); - return animator; - } - - private static class RadialVanishAnimation extends View { - private final SplashScreenView mView; - private int mInitRadius; - private int mFinishRadius; - - private final Point mCircleCenter = new Point(); - private final Matrix mVanishMatrix = new Matrix(); - private final Paint mVanishPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - - RadialVanishAnimation(SplashScreenView target) { - super(target.getContext()); - mView = target; - mView.addView(this); - mVanishPaint.setAlpha(0); - } - - void onAnimationProgress(float linearProgress) { - if (mVanishPaint.getShader() == null) { - return; - } - - final float radiusProgress = MASK_RADIUS_INTERPOLATOR.getInterpolation(linearProgress); - final float alphaProgress = Interpolators.ALPHA_OUT.getInterpolation(linearProgress); - final float scale = mInitRadius + (mFinishRadius - mInitRadius) * radiusProgress; - - mVanishMatrix.setScale(scale, scale); - mVanishMatrix.postTranslate(mCircleCenter.x, mCircleCenter.y); - mVanishPaint.getShader().setLocalMatrix(mVanishMatrix); - mVanishPaint.setAlpha(Math.round(0xFF * alphaProgress)); - - postInvalidate(); - } - - void setRadius(int initRadius, int finishRadius) { - if (DEBUG_EXIT_ANIMATION) { - Slog.v(TAG, "RadialVanishAnimation setRadius init: " + initRadius - + " final " + finishRadius); - } - mInitRadius = initRadius; - mFinishRadius = finishRadius; - } - - void setCircleCenter(int x, int y) { - if (DEBUG_EXIT_ANIMATION) { - Slog.v(TAG, "RadialVanishAnimation setCircleCenter x: " + x + " y " + y); - } - mCircleCenter.set(x, y); - } - - void setRadialPaintParam(int[] colors, float[] stops) { - // setup gradient shader - final RadialGradient rShader = - new RadialGradient(0, 0, 1, colors, stops, Shader.TileMode.CLAMP); - mVanishPaint.setShader(rShader); - if (!DEBUG_EXIT_ANIMATION_BLEND) { - // We blend the reveal gradient with the splash screen using DST_OUT so that the - // splash screen is fully visible when radius = 0 (or gradient opacity is 0) and - // fully invisible when radius = finishRadius AND gradient opacity is 1. - mVanishPaint.setBlendMode(BlendMode.DST_OUT); - } - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - canvas.drawRect(0, 0, mView.getWidth(), mView.getHeight(), mVanishPaint); - } - } - - private final class ShiftUpAnimation { - private final float mFromYDelta; - private final float mToYDelta; - private final View mOccludeHoleView; - private final SyncRtSurfaceTransactionApplier mApplier; - private final Matrix mTmpTransform = new Matrix(); - - ShiftUpAnimation(float fromYDelta, float toYDelta, View occludeHoleView) { - mFromYDelta = fromYDelta; - mToYDelta = toYDelta; - mOccludeHoleView = occludeHoleView; - mApplier = new SyncRtSurfaceTransactionApplier(occludeHoleView); - } - - void onAnimationProgress(float linearProgress) { - if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid() - || !mSplashScreenView.isAttachedToWindow()) { - return; - } - - final float progress = SHIFT_UP_INTERPOLATOR.getInterpolation(linearProgress); - final float dy = mFromYDelta + (mToYDelta - mFromYDelta) * progress; - - mOccludeHoleView.setTranslationY(dy); - mTmpTransform.setTranslate(0 /* dx */, dy); - - // set the vsyncId to ensure the transaction doesn't get applied too early. - final SurfaceControl.Transaction tx = mTransactionPool.acquire(); - tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); - mTmpTransform.postTranslate(mFirstWindowFrame.left, - mFirstWindowFrame.top + mMainWindowShiftLength); - - SyncRtSurfaceTransactionApplier.SurfaceParams - params = new SyncRtSurfaceTransactionApplier.SurfaceParams - .Builder(mFirstWindowSurface) - .withMatrix(mTmpTransform) - .withMergeTransaction(tx) - .build(); - mApplier.scheduleApply(params); - - mTransactionPool.release(tx); - } - - void finish() { - if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid()) { - return; - } - final SurfaceControl.Transaction tx = mTransactionPool.acquire(); - if (mSplashScreenView.isAttachedToWindow()) { - tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); - - SyncRtSurfaceTransactionApplier.SurfaceParams - params = new SyncRtSurfaceTransactionApplier.SurfaceParams - .Builder(mFirstWindowSurface) - .withWindowCrop(null) - .withMergeTransaction(tx) - .build(); - mApplier.scheduleApply(params); - } else { - tx.setWindowCrop(mFirstWindowSurface, null); - tx.apply(); - } - mTransactionPool.release(tx); - - Choreographer.getSfInstance().postCallback(CALLBACK_COMMIT, - mFirstWindowSurface::release, null); - } + SplashScreenExitAnimationUtils.startAnimations(mSplashScreenView, mFirstWindowSurface, + mMainWindowShiftLength, mTransactionPool, mFirstWindowFrame, mAnimationDuration, + mIconFadeOutDuration, mIconStartAlpha, mBrandingStartAlpha, mAppRevealDelay, + mAppRevealDuration, this); } private void reset() { @@ -316,9 +112,6 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { mFinishCallback = null; } } - if (mShiftUpAnimation != null) { - mShiftUpAnimation.finish(); - } } @Override @@ -342,40 +135,4 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { public void onAnimationRepeat(Animator animation) { // ignore } - - private void onFadeOutProgress(float linearProgress) { - final float iconProgress = ICON_INTERPOLATOR.getInterpolation( - getProgress(linearProgress, 0 /* delay */, mIconFadeOutDuration)); - final View iconView = mSplashScreenView.getIconView(); - final View brandingView = mSplashScreenView.getBrandingView(); - if (iconView != null) { - iconView.setAlpha(mIconStartAlpha * (1 - iconProgress)); - } - if (brandingView != null) { - brandingView.setAlpha(mBrandingStartAlpha * (1 - iconProgress)); - } - } - - private void onAnimationProgress(float linearProgress) { - onFadeOutProgress(linearProgress); - - final float revealLinearProgress = getProgress(linearProgress, mAppRevealDelay, - mAppRevealDuration); - - if (mRadialVanishAnimation != null) { - mRadialVanishAnimation.onAnimationProgress(revealLinearProgress); - } - - if (mShiftUpAnimation != null) { - mShiftUpAnimation.onAnimationProgress(revealLinearProgress); - } - } - - private float getProgress(float linearProgress, long delay, long duration) { - return MathUtils.constrain( - (linearProgress * (mAnimationDuration) - delay) / duration, - 0.0f, - 1.0f - ); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java new file mode 100644 index 000000000000..3098e55ec78b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.startingsurface; + +import static android.view.Choreographer.CALLBACK_COMMIT; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.BlendMode; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.RadialGradient; +import android.graphics.Rect; +import android.graphics.Shader; +import android.util.MathUtils; +import android.util.Slog; +import android.view.Choreographer; +import android.view.SurfaceControl; +import android.view.SyncRtSurfaceTransactionApplier; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; +import android.window.SplashScreenView; + +import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.common.TransactionPool; + +/** + * Utilities for creating the splash screen window animations. + * @hide + */ +public class SplashScreenExitAnimationUtils { + private static final boolean DEBUG_EXIT_ANIMATION = false; + private static final boolean DEBUG_EXIT_ANIMATION_BLEND = false; + private static final String TAG = "SplashScreenExitAnimationUtils"; + + private static final Interpolator ICON_INTERPOLATOR = new PathInterpolator(0.15f, 0f, 1f, 1f); + private static final Interpolator MASK_RADIUS_INTERPOLATOR = + new PathInterpolator(0f, 0f, 0.4f, 1f); + private static final Interpolator SHIFT_UP_INTERPOLATOR = new PathInterpolator(0f, 0f, 0f, 1f); + + /** + * Creates and starts the animator to fade out the icon, reveal the app, and shift up main + * window. + * @hide + */ + public static void startAnimations(ViewGroup splashScreenView, + SurfaceControl firstWindowSurface, int mainWindowShiftLength, + TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration, + int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha, + int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener) { + ValueAnimator animator = + createAnimator(splashScreenView, firstWindowSurface, mainWindowShiftLength, + transactionPool, firstWindowFrame, animationDuration, iconFadeOutDuration, + iconStartAlpha, brandingStartAlpha, appRevealDelay, appRevealDuration, + animatorListener); + animator.start(); + } + + /** + * Creates the animator to fade out the icon, reveal the app, and shift up main window. + * @hide + */ + private static ValueAnimator createAnimator(ViewGroup splashScreenView, + SurfaceControl firstWindowSurface, int mMainWindowShiftLength, + TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration, + int iconFadeOutDuration, float iconStartAlpha, float brandingStartAlpha, + int appRevealDelay, int appRevealDuration, Animator.AnimatorListener animatorListener) { + // reveal app + final float transparentRatio = 0.8f; + final int globalHeight = splashScreenView.getHeight(); + final int verticalCircleCenter = 0; + final int finalVerticalLength = globalHeight - verticalCircleCenter; + final int halfWidth = splashScreenView.getWidth() / 2; + final int endRadius = (int) (0.5 + (1f / transparentRatio * (int) + Math.sqrt(finalVerticalLength * finalVerticalLength + halfWidth * halfWidth))); + final int[] colors = {Color.WHITE, Color.WHITE, Color.TRANSPARENT}; + final float[] stops = {0f, transparentRatio, 1f}; + + RadialVanishAnimation radialVanishAnimation = new RadialVanishAnimation(splashScreenView); + radialVanishAnimation.setCircleCenter(halfWidth, verticalCircleCenter); + radialVanishAnimation.setRadius(0 /* initRadius */, endRadius); + radialVanishAnimation.setRadialPaintParam(colors, stops); + + View occludeHoleView = null; + ShiftUpAnimation shiftUpAnimation = null; + if (firstWindowSurface != null && firstWindowSurface.isValid()) { + // shift up main window + occludeHoleView = new View(splashScreenView.getContext()); + if (DEBUG_EXIT_ANIMATION_BLEND) { + occludeHoleView.setBackgroundColor(Color.BLUE); + } else if (splashScreenView instanceof SplashScreenView) { + occludeHoleView.setBackgroundColor( + ((SplashScreenView) splashScreenView).getInitBackgroundColor()); + } else { + occludeHoleView.setBackgroundColor( + isDarkTheme(splashScreenView.getContext()) ? Color.BLACK : Color.WHITE); + } + final ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, mMainWindowShiftLength); + splashScreenView.addView(occludeHoleView, params); + + shiftUpAnimation = new ShiftUpAnimation(0, -mMainWindowShiftLength, occludeHoleView, + firstWindowSurface, splashScreenView, transactionPool, firstWindowFrame, + mMainWindowShiftLength); + } + + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + animator.setDuration(animationDuration); + animator.setInterpolator(Interpolators.LINEAR); + if (animatorListener != null) { + animator.addListener(animatorListener); + } + View finalOccludeHoleView = occludeHoleView; + ShiftUpAnimation finalShiftUpAnimation = shiftUpAnimation; + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (finalShiftUpAnimation != null) { + finalShiftUpAnimation.finish(); + } + splashScreenView.removeView(radialVanishAnimation); + splashScreenView.removeView(finalOccludeHoleView); + } + }); + animator.addUpdateListener(animation -> { + float linearProgress = (float) animation.getAnimatedValue(); + + // Fade out progress + final float iconProgress = + ICON_INTERPOLATOR.getInterpolation(getProgress( + linearProgress, 0 /* delay */, iconFadeOutDuration, animationDuration)); + View iconView = null; + View brandingView = null; + if (splashScreenView instanceof SplashScreenView) { + iconView = ((SplashScreenView) splashScreenView).getIconView(); + brandingView = ((SplashScreenView) splashScreenView).getBrandingView(); + } + if (iconView != null) { + iconView.setAlpha(iconStartAlpha * (1 - iconProgress)); + } + if (brandingView != null) { + brandingView.setAlpha(brandingStartAlpha * (1 - iconProgress)); + } + + final float revealLinearProgress = getProgress(linearProgress, appRevealDelay, + appRevealDuration, animationDuration); + + radialVanishAnimation.onAnimationProgress(revealLinearProgress); + + if (finalShiftUpAnimation != null) { + finalShiftUpAnimation.onAnimationProgress(revealLinearProgress); + } + }); + return animator; + } + + private static float getProgress(float linearProgress, long delay, long duration, + int animationDuration) { + return MathUtils.constrain( + (linearProgress * (animationDuration) - delay) / duration, + 0.0f, + 1.0f + ); + } + + private static boolean isDarkTheme(Context context) { + Configuration configuration = context.getResources().getConfiguration(); + int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK; + return nightMode == Configuration.UI_MODE_NIGHT_YES; + } + + /** + * View which creates a circular reveal of the underlying view. + * @hide + */ + @SuppressLint("ViewConstructor") + public static class RadialVanishAnimation extends View { + private final ViewGroup mView; + private int mInitRadius; + private int mFinishRadius; + + private final Point mCircleCenter = new Point(); + private final Matrix mVanishMatrix = new Matrix(); + private final Paint mVanishPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + public RadialVanishAnimation(ViewGroup target) { + super(target.getContext()); + mView = target; + mView.addView(this); + if (getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { + ((ViewGroup.MarginLayoutParams) getLayoutParams()).setMargins(0, 0, 0, 0); + } + mVanishPaint.setAlpha(0); + } + + void onAnimationProgress(float linearProgress) { + if (mVanishPaint.getShader() == null) { + return; + } + + final float radiusProgress = MASK_RADIUS_INTERPOLATOR.getInterpolation(linearProgress); + final float alphaProgress = Interpolators.ALPHA_OUT.getInterpolation(linearProgress); + final float scale = mInitRadius + (mFinishRadius - mInitRadius) * radiusProgress; + + mVanishMatrix.setScale(scale, scale); + mVanishMatrix.postTranslate(mCircleCenter.x, mCircleCenter.y); + mVanishPaint.getShader().setLocalMatrix(mVanishMatrix); + mVanishPaint.setAlpha(Math.round(0xFF * alphaProgress)); + + postInvalidate(); + } + + void setRadius(int initRadius, int finishRadius) { + if (DEBUG_EXIT_ANIMATION) { + Slog.v(TAG, "RadialVanishAnimation setRadius init: " + initRadius + + " final " + finishRadius); + } + mInitRadius = initRadius; + mFinishRadius = finishRadius; + } + + void setCircleCenter(int x, int y) { + if (DEBUG_EXIT_ANIMATION) { + Slog.v(TAG, "RadialVanishAnimation setCircleCenter x: " + x + " y " + y); + } + mCircleCenter.set(x, y); + } + + void setRadialPaintParam(int[] colors, float[] stops) { + // setup gradient shader + final RadialGradient rShader = + new RadialGradient(0, 0, 1, colors, stops, Shader.TileMode.CLAMP); + mVanishPaint.setShader(rShader); + if (!DEBUG_EXIT_ANIMATION_BLEND) { + // We blend the reveal gradient with the splash screen using DST_OUT so that the + // splash screen is fully visible when radius = 0 (or gradient opacity is 0) and + // fully invisible when radius = finishRadius AND gradient opacity is 1. + mVanishPaint.setBlendMode(BlendMode.DST_OUT); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.drawRect(0, 0, mView.getWidth(), mView.getHeight(), mVanishPaint); + } + } + + /** + * Shifts up the main window. + * @hide + */ + public static final class ShiftUpAnimation { + private final float mFromYDelta; + private final float mToYDelta; + private final View mOccludeHoleView; + private final SyncRtSurfaceTransactionApplier mApplier; + private final Matrix mTmpTransform = new Matrix(); + private final SurfaceControl mFirstWindowSurface; + private final ViewGroup mSplashScreenView; + private final TransactionPool mTransactionPool; + private final Rect mFirstWindowFrame; + private final int mMainWindowShiftLength; + + public ShiftUpAnimation(float fromYDelta, float toYDelta, View occludeHoleView, + SurfaceControl firstWindowSurface, ViewGroup splashScreenView, + TransactionPool transactionPool, Rect firstWindowFrame, + int mainWindowShiftLength) { + mFromYDelta = fromYDelta; + mToYDelta = toYDelta; + mOccludeHoleView = occludeHoleView; + mApplier = new SyncRtSurfaceTransactionApplier(occludeHoleView); + mFirstWindowSurface = firstWindowSurface; + mSplashScreenView = splashScreenView; + mTransactionPool = transactionPool; + mFirstWindowFrame = firstWindowFrame; + mMainWindowShiftLength = mainWindowShiftLength; + } + + void onAnimationProgress(float linearProgress) { + if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid() + || !mSplashScreenView.isAttachedToWindow()) { + return; + } + + final float progress = SHIFT_UP_INTERPOLATOR.getInterpolation(linearProgress); + final float dy = mFromYDelta + (mToYDelta - mFromYDelta) * progress; + + mOccludeHoleView.setTranslationY(dy); + mTmpTransform.setTranslate(0 /* dx */, dy); + + // set the vsyncId to ensure the transaction doesn't get applied too early. + final SurfaceControl.Transaction tx = mTransactionPool.acquire(); + tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); + mTmpTransform.postTranslate(mFirstWindowFrame.left, + mFirstWindowFrame.top + mMainWindowShiftLength); + + SyncRtSurfaceTransactionApplier.SurfaceParams + params = new SyncRtSurfaceTransactionApplier.SurfaceParams + .Builder(mFirstWindowSurface) + .withMatrix(mTmpTransform) + .withMergeTransaction(tx) + .build(); + mApplier.scheduleApply(params); + + mTransactionPool.release(tx); + } + + void finish() { + if (mFirstWindowSurface == null || !mFirstWindowSurface.isValid()) { + return; + } + final SurfaceControl.Transaction tx = mTransactionPool.acquire(); + if (mSplashScreenView.isAttachedToWindow()) { + tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId()); + + SyncRtSurfaceTransactionApplier.SurfaceParams + params = new SyncRtSurfaceTransactionApplier.SurfaceParams + .Builder(mFirstWindowSurface) + .withWindowCrop(null) + .withMergeTransaction(tx) + .build(); + mApplier.scheduleApply(params); + } else { + tx.setWindowCrop(mFirstWindowSurface, null); + tx.apply(); + } + mTransactionPool.release(tx); + + Choreographer.getSfInstance().postCallback(CALLBACK_COMMIT, + mFirstWindowSurface::release, null); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index 8cee4f1dc8fb..6ce981e25f5e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -432,7 +432,8 @@ public class SplashscreenContentDrawer { final ShapeIconFactory factory = new ShapeIconFactory( SplashscreenContentDrawer.this.mContext, scaledIconDpi, mFinalIconSize); - final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(iconDrawable); + final Bitmap bitmap = factory.createScaledBitmap(iconDrawable, + BaseIconFactory.MODE_DEFAULT); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); createIconDrawable(new BitmapDrawable(bitmap), true, mHighResIconProvider.mLoadInDetail); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java index 76105a39189b..538bbec2aa2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java @@ -22,14 +22,6 @@ import android.graphics.Color; * Interface to engage starting window feature. */ public interface StartingSurface { - - /** - * Returns a binder that can be passed to an external process to manipulate starting windows. - */ - default IStartingWindow createExternalInterface() { - return null; - } - /** * Returns the background color for a starting window if existing. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java index 379af21ac956..0c23f109feaf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java @@ -23,6 +23,7 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; @@ -43,10 +44,12 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.util.function.TriConsumer; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; /** @@ -76,6 +79,7 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo private TriConsumer<Integer, Integer, Integer> mTaskLaunchingCallback; private final StartingSurfaceImpl mImpl = new StartingSurfaceImpl(); private final Context mContext; + private final ShellController mShellController; private final ShellTaskOrganizer mShellTaskOrganizer; private final ShellExecutor mSplashScreenExecutor; /** @@ -86,12 +90,14 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo public StartingWindowController(Context context, ShellInit shellInit, + ShellController shellController, ShellTaskOrganizer shellTaskOrganizer, ShellExecutor splashScreenExecutor, StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider, TransactionPool pool) { mContext = context; + mShellController = shellController; mShellTaskOrganizer = shellTaskOrganizer; mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor, iconProvider, pool); @@ -107,8 +113,14 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo return mImpl; } + private ExternalInterfaceBinder createExternalInterface() { + return new IStartingWindowImpl(this); + } + private void onInit() { mShellTaskOrganizer.initStartingWindow(this); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_STARTING_WINDOW, + this::createExternalInterface, this); } @Override @@ -222,17 +234,6 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo * The interface for calls from outside the Shell, within the host process. */ private class StartingSurfaceImpl implements StartingSurface { - private IStartingWindowImpl mIStartingWindow; - - @Override - public IStartingWindowImpl createExternalInterface() { - if (mIStartingWindow != null) { - mIStartingWindow.invalidate(); - } - mIStartingWindow = new IStartingWindowImpl(StartingWindowController.this); - return mIStartingWindow; - } - @Override public int getBackgroundColor(TaskInfo taskInfo) { synchronized (mTaskBackgroundColors) { @@ -256,7 +257,8 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo * The interface for calls from outside the host process. */ @BinderThread - private static class IStartingWindowImpl extends IStartingWindow.Stub { + private static class IStartingWindowImpl extends IStartingWindow.Stub + implements ExternalInterfaceBinder { private StartingWindowController mController; private SingleInstanceRemoteListener<StartingWindowController, IStartingWindowListener> mListener; @@ -276,7 +278,8 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mController = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java index 57993948886b..fdf073f0bf26 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java @@ -23,23 +23,28 @@ import static android.content.pm.ActivityInfo.CONFIG_LOCALE; import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_INIT; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SYSUI_EVENTS; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; +import android.os.Bundle; +import android.util.ArrayMap; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ExternalThread; import java.io.PrintWriter; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Supplier; /** * Handles event callbacks from SysUI that can be used within the Shell. @@ -59,6 +64,11 @@ public class ShellController { private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners = new CopyOnWriteArrayList<>(); + private ArrayMap<String, Supplier<ExternalInterfaceBinder>> mExternalInterfaceSuppliers = + new ArrayMap<>(); + // References to the existing interfaces, to be invalidated when they are recreated + private ArrayMap<String, ExternalInterfaceBinder> mExternalInterfaces = new ArrayMap<>(); + private Configuration mLastConfiguration; @@ -67,6 +77,11 @@ public class ShellController { mShellInit = shellInit; mShellCommandHandler = shellCommandHandler; mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mShellCommandHandler.addDumpCallback(this::dump, this); } /** @@ -124,6 +139,47 @@ public class ShellController { mUserChangeListeners.remove(listener); } + /** + * Adds an interface that can be called from a remote process. This method takes a supplier + * because each binder reference is valid for a single process, and in multi-user mode, SysUI + * will request new binder instances for each instance of Launcher that it provides binders + * to. + * + * @param extra the key for the interface, {@see ShellSharedConstants} + * @param binderSupplier the supplier of the binder to pass to the external process + * @param callerInstance the instance of the caller, purely for logging + */ + public void addExternalInterface(String extra, Supplier<ExternalInterfaceBinder> binderSupplier, + Object callerInstance) { + ProtoLog.v(WM_SHELL_INIT, "Adding external interface from %s with key %s", + callerInstance.getClass().getSimpleName(), extra); + if (mExternalInterfaceSuppliers.containsKey(extra)) { + throw new IllegalArgumentException("Supplier with same key already exists: " + + extra); + } + mExternalInterfaceSuppliers.put(extra, binderSupplier); + } + + /** + * Updates the given bundle with the set of external interfaces, invalidating the old set of + * binders. + */ + private void createExternalInterfaces(Bundle output) { + // Invalidate the old binders + for (int i = 0; i < mExternalInterfaces.size(); i++) { + mExternalInterfaces.valueAt(i).invalidate(); + } + mExternalInterfaces.clear(); + + // Create new binders for each key + for (int i = 0; i < mExternalInterfaceSuppliers.size(); i++) { + final String key = mExternalInterfaceSuppliers.keyAt(i); + final ExternalInterfaceBinder b = mExternalInterfaceSuppliers.valueAt(i).get(); + mExternalInterfaces.put(key, b); + output.putBinder(key, b.asBinder()); + } + } + @VisibleForTesting void onConfigurationChanged(Configuration newConfig) { // The initial config is send on startup and doesn't trigger listener callbacks @@ -204,6 +260,14 @@ public class ShellController { pw.println(innerPrefix + "mLastConfiguration=" + mLastConfiguration); pw.println(innerPrefix + "mKeyguardChangeListeners=" + mKeyguardChangeListeners.size()); pw.println(innerPrefix + "mUserChangeListeners=" + mUserChangeListeners.size()); + + if (!mExternalInterfaces.isEmpty()) { + pw.println(innerPrefix + "mExternalInterfaces={"); + for (String key : mExternalInterfaces.keySet()) { + pw.println(innerPrefix + "\t" + key + ": " + mExternalInterfaces.get(key)); + } + pw.println(innerPrefix + "}"); + } } /** @@ -211,7 +275,6 @@ public class ShellController { */ @ExternalThread private class ShellInterfaceImpl implements ShellInterface { - @Override public void onInit() { try { @@ -222,28 +285,6 @@ public class ShellController { } @Override - public void dump(PrintWriter pw) { - try { - mMainExecutor.executeBlocking(() -> mShellCommandHandler.dump(pw)); - } catch (InterruptedException e) { - throw new RuntimeException("Failed to dump the Shell in 2s", e); - } - } - - @Override - public boolean handleCommand(String[] args, PrintWriter pw) { - try { - boolean[] result = new boolean[1]; - mMainExecutor.executeBlocking(() -> { - result[0] = mShellCommandHandler.handleCommand(args, pw); - }); - return result[0]; - } catch (InterruptedException e) { - throw new RuntimeException("Failed to handle Shell command in 2s", e); - } - } - - @Override public void onConfigurationChanged(Configuration newConfiguration) { mMainExecutor.execute(() -> ShellController.this.onConfigurationChanged(newConfiguration)); @@ -274,5 +315,38 @@ public class ShellController { mMainExecutor.execute(() -> ShellController.this.onUserProfilesChanged(profiles)); } + + @Override + public boolean handleCommand(String[] args, PrintWriter pw) { + try { + boolean[] result = new boolean[1]; + mMainExecutor.executeBlocking(() -> { + result[0] = mShellCommandHandler.handleCommand(args, pw); + }); + return result[0]; + } catch (InterruptedException e) { + throw new RuntimeException("Failed to handle Shell command in 2s", e); + } + } + + @Override + public void createExternalInterfaces(Bundle bundle) { + try { + mMainExecutor.executeBlocking(() -> { + ShellController.this.createExternalInterfaces(bundle); + }); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to get Shell command in 2s", e); + } + } + + @Override + public void dump(PrintWriter pw) { + try { + mMainExecutor.executeBlocking(() -> mShellCommandHandler.dump(pw)); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to dump the Shell in 2s", e); + } + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java index 2108c824ac6f..bc5dd11ef54e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java @@ -19,6 +19,7 @@ package com.android.wm.shell.sysui; import android.content.Context; import android.content.pm.UserInfo; import android.content.res.Configuration; +import android.os.Bundle; import androidx.annotation.NonNull; @@ -37,18 +38,6 @@ public interface ShellInterface { default void onInit() {} /** - * Dumps the shell state. - */ - default void dump(PrintWriter pw) {} - - /** - * Handles a shell command. - */ - default boolean handleCommand(final String[] args, PrintWriter pw) { - return false; - } - - /** * Notifies the Shell that the configuration has changed. */ default void onConfigurationChanged(Configuration newConfiguration) {} @@ -74,4 +63,21 @@ public interface ShellInterface { * Notifies the Shell when a profile belonging to the user changes. */ default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {} + + /** + * Handles a shell command. + */ + default boolean handleCommand(final String[] args, PrintWriter pw) { + return false; + } + + /** + * Updates the given {@param bundle} with the set of exposed interfaces. + */ + default void createExternalInterfaces(Bundle bundle) {} + + /** + * Dumps the shell state. + */ + default void dump(PrintWriter pw) {} } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java new file mode 100644 index 000000000000..bdda6a8e926b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.sysui; + +/** + * General shell-related constants that are shared with users of the library. + */ +public class ShellSharedConstants { + // See IPip.aidl + public static final String KEY_EXTRA_SHELL_PIP = "extra_shell_pip"; + // See ISplitScreen.aidl + public static final String KEY_EXTRA_SHELL_SPLIT_SCREEN = "extra_shell_split_screen"; + // See IOneHanded.aidl + public static final String KEY_EXTRA_SHELL_ONE_HANDED = "extra_shell_one_handed"; + // See IShellTransitions.aidl + public static final String KEY_EXTRA_SHELL_SHELL_TRANSITIONS = + "extra_shell_shell_transitions"; + // See IStartingWindow.aidl + public static final String KEY_EXTRA_SHELL_STARTING_WINDOW = + "extra_shell_starting_window"; + // See IRecentTasks.aidl + public static final String KEY_EXTRA_SHELL_RECENT_TASKS = "extra_shell_recent_tasks"; + // See IBackAnimation.aidl + public static final String KEY_EXTRA_SHELL_BACK_ANIMATION = "extra_shell_back_animation"; + // See IFloatingTasks.aidl + public static final String KEY_EXTRA_SHELL_FLOATING_TASKS = "extra_shell_floating_tasks"; + // See IDesktopMode.aidl + public static final String KEY_EXTRA_SHELL_DESKTOP_MODE = "extra_shell_desktop_mode"; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index dbb2948de5db..af79386caf9c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -59,6 +59,7 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition; +import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; import static com.android.wm.shell.transition.TransitionAnimationHelper.sDisableCustomTaskAnimationProperty; @@ -76,10 +77,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.graphics.Canvas; import android.graphics.Insets; -import android.graphics.Paint; -import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -89,7 +87,6 @@ import android.os.IBinder; import android.os.UserHandle; import android.util.ArrayMap; import android.view.Choreographer; -import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.WindowManager; @@ -525,123 +522,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } } - private void edgeExtendWindow(TransitionInfo.Change change, - Animation a, SurfaceControl.Transaction startTransaction, - SurfaceControl.Transaction finishTransaction) { - // Do not create edge extension surface for transfer starting window change. - // The app surface could be empty thus nothing can draw on the hardware renderer, which will - // block this thread when calling Surface#unlockCanvasAndPost. - if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { - return; - } - final Transformation transformationAtStart = new Transformation(); - a.getTransformationAt(0, transformationAtStart); - final Transformation transformationAtEnd = new Transformation(); - a.getTransformationAt(1, transformationAtEnd); - - // We want to create an extension surface that is the maximal size and the animation will - // take care of cropping any part that overflows. - final Insets maxExtensionInsets = Insets.min( - transformationAtStart.getInsets(), transformationAtEnd.getInsets()); - - final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(), - change.getEndAbsBounds().height()); - final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(), - change.getEndAbsBounds().width()); - if (maxExtensionInsets.left < 0) { - final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight); - final Rect extensionRect = new Rect(0, 0, - -maxExtensionInsets.left, targetSurfaceHeight); - final int xPos = maxExtensionInsets.left; - final int yPos = 0; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Left Edge Extension", startTransaction, finishTransaction); - } - - if (maxExtensionInsets.top < 0) { - final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1); - final Rect extensionRect = new Rect(0, 0, - targetSurfaceWidth, -maxExtensionInsets.top); - final int xPos = 0; - final int yPos = maxExtensionInsets.top; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Top Edge Extension", startTransaction, finishTransaction); - } - - if (maxExtensionInsets.right < 0) { - final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0, - targetSurfaceWidth, targetSurfaceHeight); - final Rect extensionRect = new Rect(0, 0, - -maxExtensionInsets.right, targetSurfaceHeight); - final int xPos = targetSurfaceWidth; - final int yPos = 0; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Right Edge Extension", startTransaction, finishTransaction); - } - - if (maxExtensionInsets.bottom < 0) { - final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1, - targetSurfaceWidth, targetSurfaceHeight); - final Rect extensionRect = new Rect(0, 0, - targetSurfaceWidth, -maxExtensionInsets.bottom); - final int xPos = maxExtensionInsets.left; - final int yPos = targetSurfaceHeight; - createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, - "Bottom Edge Extension", startTransaction, finishTransaction); - } - } - - private SurfaceControl createExtensionSurface(SurfaceControl surfaceToExtend, Rect edgeBounds, - Rect extensionRect, int xPos, int yPos, String layerName, - SurfaceControl.Transaction startTransaction, - SurfaceControl.Transaction finishTransaction) { - final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder() - .setName(layerName) - .setParent(surfaceToExtend) - .setHidden(true) - .setCallsite("DefaultTransitionHandler#startAnimation") - .setOpaque(true) - .setBufferSize(extensionRect.width(), extensionRect.height()) - .build(); - - SurfaceControl.LayerCaptureArgs captureArgs = - new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend) - .setSourceCrop(edgeBounds) - .setFrameScale(1) - .setPixelFormat(PixelFormat.RGBA_8888) - .setChildrenOnly(true) - .setAllowProtected(true) - .build(); - final SurfaceControl.ScreenshotHardwareBuffer edgeBuffer = - SurfaceControl.captureLayers(captureArgs); - - if (edgeBuffer == null) { - ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Failed to capture edge of window."); - return null; - } - - android.graphics.BitmapShader shader = - new android.graphics.BitmapShader(edgeBuffer.asBitmap(), - android.graphics.Shader.TileMode.CLAMP, - android.graphics.Shader.TileMode.CLAMP); - final Paint paint = new Paint(); - paint.setShader(shader); - - final Surface surface = new Surface(edgeExtensionLayer); - Canvas c = surface.lockHardwareCanvas(); - c.drawRect(extensionRect, paint); - surface.unlockCanvasAndPost(c); - surface.release(); - - startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE); - startTransaction.setPosition(edgeExtensionLayer, xPos, yPos); - startTransaction.setVisibility(edgeExtensionLayer, true); - finishTransaction.remove(edgeExtensionLayer); - - return edgeExtensionLayer; - } - @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @@ -739,12 +619,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // Animation length is already expected to be scaled. va.overrideDurationScale(1.0f); va.setDuration(anim.computeDurationHint()); - va.addUpdateListener(animation -> { + final ValueAnimator.AnimatorUpdateListener updateListener = animation -> { final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix, position, cornerRadius, clipRect); - }); + }; + va.addUpdateListener(updateListener); final Runnable finisher = () -> { applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix, @@ -757,20 +638,30 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { }); }; va.addListener(new AnimatorListenerAdapter() { + // It is possible for the end/cancel to be called more than once, which may cause + // issues if the animating surface has already been released. Track the finished + // state here to skip duplicate callbacks. See b/252872225. private boolean mFinished = false; @Override public void onAnimationEnd(Animator animation) { - if (mFinished) return; - mFinished = true; - finisher.run(); + onFinish(); } @Override public void onAnimationCancel(Animator animation) { + onFinish(); + } + + private void onFinish() { if (mFinished) return; mFinished = true; finisher.run(); + // The update listener can continue to be called after the animation has ended if + // end() is called manually again before the finisher removes the animation. + // Remove it manually here to prevent animating a released surface. + // See b/252872225. + va.removeUpdateListener(updateListener); } }); animations.add(va); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java index b34049d4ec42..da39017a0313 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java @@ -27,14 +27,6 @@ import com.android.wm.shell.common.annotations.ExternalThread; */ @ExternalThread public interface ShellTransitions { - - /** - * Returns a binder that can be passed to an external process to manipulate remote transitions. - */ - default IShellTransitions createExternalInterface() { - return null; - } - /** * Registers a remote transition. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index efee6f40b53e..b75c55274cff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -24,6 +24,7 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.transitTypeToString; +import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; @@ -34,10 +35,19 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.BitmapShader; +import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Insets; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.Shader; import android.os.SystemProperties; +import android.view.Surface; import android.view.SurfaceControl; import android.view.animation.Animation; +import android.view.animation.Transformation; import android.window.TransitionInfo; import com.android.internal.R; @@ -217,4 +227,126 @@ public class TransitionAnimationHelper { .show(animationBackgroundSurface); finishTransaction.remove(animationBackgroundSurface); } + + /** + * Adds edge extension surface to the given {@code change} for edge extension animation. + */ + public static void edgeExtendWindow(@NonNull TransitionInfo.Change change, + @NonNull Animation a, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + // Do not create edge extension surface for transfer starting window change. + // The app surface could be empty thus nothing can draw on the hardware renderer, which will + // block this thread when calling Surface#unlockCanvasAndPost. + if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { + return; + } + final Transformation transformationAtStart = new Transformation(); + a.getTransformationAt(0, transformationAtStart); + final Transformation transformationAtEnd = new Transformation(); + a.getTransformationAt(1, transformationAtEnd); + + // We want to create an extension surface that is the maximal size and the animation will + // take care of cropping any part that overflows. + final Insets maxExtensionInsets = Insets.min( + transformationAtStart.getInsets(), transformationAtEnd.getInsets()); + + final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(), + change.getEndAbsBounds().height()); + final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(), + change.getEndAbsBounds().width()); + if (maxExtensionInsets.left < 0) { + final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight); + final Rect extensionRect = new Rect(0, 0, + -maxExtensionInsets.left, targetSurfaceHeight); + final int xPos = maxExtensionInsets.left; + final int yPos = 0; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Left Edge Extension", startTransaction, finishTransaction); + } + + if (maxExtensionInsets.top < 0) { + final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1); + final Rect extensionRect = new Rect(0, 0, + targetSurfaceWidth, -maxExtensionInsets.top); + final int xPos = 0; + final int yPos = maxExtensionInsets.top; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Top Edge Extension", startTransaction, finishTransaction); + } + + if (maxExtensionInsets.right < 0) { + final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0, + targetSurfaceWidth, targetSurfaceHeight); + final Rect extensionRect = new Rect(0, 0, + -maxExtensionInsets.right, targetSurfaceHeight); + final int xPos = targetSurfaceWidth; + final int yPos = 0; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Right Edge Extension", startTransaction, finishTransaction); + } + + if (maxExtensionInsets.bottom < 0) { + final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1, + targetSurfaceWidth, targetSurfaceHeight); + final Rect extensionRect = new Rect(0, 0, + targetSurfaceWidth, -maxExtensionInsets.bottom); + final int xPos = maxExtensionInsets.left; + final int yPos = targetSurfaceHeight; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Bottom Edge Extension", startTransaction, finishTransaction); + } + } + + /** + * Takes a screenshot of {@code surfaceToExtend}'s edge and extends it for edge extension + * animation. + */ + private static SurfaceControl createExtensionSurface(@NonNull SurfaceControl surfaceToExtend, + @NonNull Rect edgeBounds, @NonNull Rect extensionRect, int xPos, int yPos, + @NonNull String layerName, @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder() + .setName(layerName) + .setParent(surfaceToExtend) + .setHidden(true) + .setCallsite("TransitionAnimationHelper#createExtensionSurface") + .setOpaque(true) + .setBufferSize(extensionRect.width(), extensionRect.height()) + .build(); + + final SurfaceControl.LayerCaptureArgs captureArgs = + new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend) + .setSourceCrop(edgeBounds) + .setFrameScale(1) + .setPixelFormat(PixelFormat.RGBA_8888) + .setChildrenOnly(true) + .setAllowProtected(true) + .build(); + final SurfaceControl.ScreenshotHardwareBuffer edgeBuffer = + SurfaceControl.captureLayers(captureArgs); + + if (edgeBuffer == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Failed to capture edge of window."); + return null; + } + + final BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(), + Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + final Paint paint = new Paint(); + paint.setShader(shader); + + final Surface surface = new Surface(edgeExtensionLayer); + final Canvas c = surface.lockHardwareCanvas(); + c.drawRect(extensionRect, paint); + surface.unlockCanvasAndPost(c); + surface.release(); + + startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE); + startTransaction.setPosition(edgeExtensionLayer, xPos, yPos); + startTransaction.setVisibility(edgeExtensionLayer, true); + finishTransaction.remove(edgeExtensionLayer); + + return edgeExtensionLayer; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 29d25bc39223..db1f19aa87b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -25,10 +25,12 @@ import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.fixScale; import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD; +import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS; import android.annotation.NonNull; import android.annotation.Nullable; @@ -61,11 +63,13 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; @@ -115,6 +119,7 @@ public class Transitions implements RemoteCallable<Transitions> { private final DefaultTransitionHandler mDefaultTransitionHandler; private final RemoteTransitionHandler mRemoteTransitionHandler; private final DisplayController mDisplayController; + private final ShellController mShellController; private final ShellTransitionImpl mImpl = new ShellTransitionImpl(); /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ @@ -142,6 +147,7 @@ public class Transitions implements RemoteCallable<Transitions> { public Transitions(@NonNull Context context, @NonNull ShellInit shellInit, + @NonNull ShellController shellController, @NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, @NonNull DisplayController displayController, @@ -156,10 +162,14 @@ public class Transitions implements RemoteCallable<Transitions> { mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit, displayController, pool, mainExecutor, mainHandler, animExecutor); mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor); + mShellController = shellController; shellInit.addInitCallback(this::onInit, this); } private void onInit() { + mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS, + this::createExternalInterface, this); + // The very last handler (0 in the list) should be the default one. mHandlers.add(mDefaultTransitionHandler); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Default"); @@ -193,6 +203,10 @@ public class Transitions implements RemoteCallable<Transitions> { return mImpl; } + private ExternalInterfaceBinder createExternalInterface() { + return new IShellTransitionsImpl(this); + } + @Override public Context getContext() { return mContext; @@ -308,6 +322,11 @@ public class Transitions implements RemoteCallable<Transitions> { boolean isOpening = isOpeningType(info.getType()); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); + if ((change.getFlags() & TransitionInfo.FLAG_IS_SYSTEM_WINDOW) != 0) { + // Currently system windows are controlled by WindowState, so don't change their + // surfaces. Otherwise their window tokens could be hidden unexpectedly. + continue; + } final SurfaceControl leash = change.getLeash(); final int mode = info.getChanges().get(i).getMode(); @@ -441,32 +460,35 @@ public class Transitions implements RemoteCallable<Transitions> { return; } - // apply transfer starting window directly if there is no other task change. Since this - // is an activity->activity situation, we can detect it by selecting transitions with only - // 2 changes where neither are tasks and one is a starting-window recipient. final int changeSize = info.getChanges().size(); - if (changeSize == 2) { - boolean nonTaskChange = true; - boolean transferStartingWindow = false; - for (int i = changeSize - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() != null) { - nonTaskChange = false; - break; - } - if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { - transferStartingWindow = true; - } - } - if (nonTaskChange && transferStartingWindow) { - t.apply(); - finishT.apply(); - // Treat this as an abort since we are bypassing any merge logic and effectively - // finishing immediately. - onAbort(transitionToken); - return; + boolean taskChange = false; + boolean transferStartingWindow = false; + boolean allOccluded = changeSize > 0; + for (int i = changeSize - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + taskChange |= change.getTaskInfo() != null; + transferStartingWindow |= change.hasFlags(FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT); + if (!change.hasFlags(FLAG_IS_OCCLUDED)) { + allOccluded = false; } } + // There does not need animation when: + // A. Transfer starting window. Apply transfer starting window directly if there is no other + // task change. Since this is an activity->activity situation, we can detect it by selecting + // transitions with only 2 changes where neither are tasks and one is a starting-window + // recipient. + if (!taskChange && transferStartingWindow && changeSize == 2 + // B. It's visibility change if the TRANSIT_TO_BACK/TO_FRONT happened when all + // changes are underneath another change. + || ((info.getType() == TRANSIT_TO_BACK || info.getType() == TRANSIT_TO_FRONT) + && allOccluded)) { + t.apply(); + finishT.apply(); + // Treat this as an abort since we are bypassing any merge logic and effectively + // finishing immediately. + onAbort(transitionToken); + return; + } final ActiveTransition active = mActiveTransitions.get(activeIdx); active.mInfo = info; @@ -542,6 +564,22 @@ public class Transitions implements RemoteCallable<Transitions> { "This shouldn't happen, maybe the default handler is broken."); } + /** + * Gives every handler (in order) a chance to handle request until one consumes the transition. + * @return the WindowContainerTransaction given by the handler which consumed the transition. + */ + public WindowContainerTransaction dispatchRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request, @Nullable TransitionHandler skip) { + for (int i = mHandlers.size() - 1; i >= 0; --i) { + if (mHandlers.get(i) == skip) continue; + WindowContainerTransaction wct = mHandlers.get(i).handleRequest(transition, request); + if (wct != null) { + return wct; + } + } + return null; + } + /** Special version of finish just for dealing with no-op/invalid transitions. */ private void onAbort(IBinder transition) { onFinish(transition, null /* wct */, null /* wctCB */, true /* abort */); @@ -716,8 +754,8 @@ public class Transitions implements RemoteCallable<Transitions> { null /* newDisplayAreaInfo */); } } - active.mToken = mOrganizer.startTransition( - request.getType(), transitionToken, wct); + mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct); + active.mToken = transitionToken; mActiveTransitions.add(active); } @@ -726,7 +764,7 @@ public class Transitions implements RemoteCallable<Transitions> { @NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) { final ActiveTransition active = new ActiveTransition(); active.mHandler = handler; - active.mToken = mOrganizer.startTransition(type, null /* token */, wct); + active.mToken = mOrganizer.startNewTransition(type, wct); mActiveTransitions.add(active); return active.mToken; } @@ -897,17 +935,6 @@ public class Transitions implements RemoteCallable<Transitions> { */ @ExternalThread private class ShellTransitionImpl implements ShellTransitions { - private IShellTransitionsImpl mIShellTransitions; - - @Override - public IShellTransitions createExternalInterface() { - if (mIShellTransitions != null) { - mIShellTransitions.invalidate(); - } - mIShellTransitions = new IShellTransitionsImpl(Transitions.this); - return mIShellTransitions; - } - @Override public void registerRemote(@NonNull TransitionFilter filter, @NonNull RemoteTransition remoteTransition) { @@ -928,7 +955,8 @@ public class Transitions implements RemoteCallable<Transitions> { * The interface for calls from outside the host process. */ @BinderThread - private static class IShellTransitionsImpl extends IShellTransitions.Stub { + private static class IShellTransitionsImpl extends IShellTransitions.Stub + implements ExternalInterfaceBinder { private Transitions mTransitions; IShellTransitionsImpl(Transitions transitions) { @@ -938,7 +966,8 @@ public class Transitions implements RemoteCallable<Transitions> { /** * Invalidates this instance, preventing future calls from updating the controller. */ - void invalidate() { + @Override + public void invalidate() { mTransitions = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java index e90389764af3..f209521b1da4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java @@ -33,6 +33,8 @@ public class SplitBounds implements Parcelable { // This class is orientation-agnostic, so we compute both for later use public final float topTaskPercent; public final float leftTaskPercent; + public final float dividerWidthPercent; + public final float dividerHeightPercent; /** * If {@code true}, that means at the time of creation of this object, the * split-screened apps were vertically stacked. This is useful in scenarios like @@ -62,8 +64,12 @@ public class SplitBounds implements Parcelable { appsStackedVertically = false; } - leftTaskPercent = this.leftTopBounds.width() / (float) rightBottomBounds.right; - topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom; + float totalWidth = rightBottomBounds.right - leftTopBounds.left; + float totalHeight = rightBottomBounds.bottom - leftTopBounds.top; + leftTaskPercent = leftTopBounds.width() / totalWidth; + topTaskPercent = leftTopBounds.height() / totalHeight; + dividerWidthPercent = visualDividerBounds.width() / totalWidth; + dividerHeightPercent = visualDividerBounds.height() / totalHeight; } public SplitBounds(Parcel parcel) { @@ -75,6 +81,8 @@ public class SplitBounds implements Parcelable { appsStackedVertically = parcel.readBoolean(); leftTopTaskId = parcel.readInt(); rightBottomTaskId = parcel.readInt(); + dividerWidthPercent = parcel.readInt(); + dividerHeightPercent = parcel.readInt(); } @Override @@ -87,6 +95,8 @@ public class SplitBounds implements Parcelable { parcel.writeBoolean(appsStackedVertically); parcel.writeInt(leftTopTaskId); parcel.writeInt(rightBottomTaskId); + parcel.writeFloat(dividerWidthPercent); + parcel.writeFloat(dividerHeightPercent); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 9e49b51e1504..dca516a327b0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -19,14 +19,20 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; import android.content.Context; +import android.hardware.input.InputManager; import android.os.Handler; +import android.os.SystemClock; +import android.util.Log; +import android.util.SparseArray; import android.view.Choreographer; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; @@ -46,7 +52,9 @@ import com.android.wm.shell.transition.Transitions; * View model for the window decoration with a caption and shadows. Works with * {@link CaptionWindowDecoration}. */ -public class CaptionWindowDecorViewModel implements WindowDecorViewModel<CaptionWindowDecoration> { + +public class CaptionWindowDecorViewModel implements WindowDecorViewModel { + private static final String TAG = "CaptionViewModel"; private final ActivityTaskManager mActivityTaskManager; private final ShellTaskOrganizer mTaskOrganizer; private final Context mContext; @@ -57,6 +65,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption private FreeformTaskTransitionStarter mTransitionStarter; private DesktopModeController mDesktopModeController; + private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); + public CaptionWindowDecorViewModel( Context context, Handler mainHandler, @@ -81,12 +91,12 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption } @Override - public CaptionWindowDecoration createWindowDecoration( + public boolean createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - if (!shouldShowWindowDecor(taskInfo)) return null; + if (!shouldShowWindowDecor(taskInfo)) return false; final CaptionWindowDecoration windowDecoration = new CaptionWindowDecoration( mContext, mDisplayController, @@ -96,47 +106,45 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption mMainHandler, mMainChoreographer, mSyncQueue); + mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); + TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration); CaptionTouchEventListener touchEventListener = new CaptionTouchEventListener(taskInfo, taskPositioner); windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); windowDecoration.setDragResizeCallback(taskPositioner); - setupWindowDecorationForTransition(taskInfo, startT, finishT, windowDecoration); - setupCaptionColor(taskInfo, windowDecoration); - return windowDecoration; + setupWindowDecorationForTransition(taskInfo, startT, finishT); + return true; } @Override - public CaptionWindowDecoration adoptWindowDecoration(AutoCloseable windowDecor) { - if (!(windowDecor instanceof CaptionWindowDecoration)) return null; - final CaptionWindowDecoration captionWindowDecor = (CaptionWindowDecoration) windowDecor; - if (!shouldShowWindowDecor(captionWindowDecor.mTaskInfo)) { - return null; - } - return captionWindowDecor; - } + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + if (decoration == null) return; - @Override - public void onTaskInfoChanged(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { decoration.relayout(taskInfo); - - setupCaptionColor(taskInfo, decoration); - } - - private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { - int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); - decoration.setCaptionColor(statusBarColor); } @Override public void setupWindowDecorationForTransition( RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT, - CaptionWindowDecoration decoration) { + SurfaceControl.Transaction finishT) { + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + if (decoration == null) return; + decoration.relayout(taskInfo, startT, finishT); } + @Override + public void destroyWindowDecoration(RunningTaskInfo taskInfo) { + final CaptionWindowDecoration decoration = + mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId); + if (decoration == null) return; + + decoration.close(); + } + private class CaptionTouchEventListener implements View.OnClickListener, View.OnTouchListener { @@ -145,6 +153,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption private final DragResizeCallback mDragResizeCallback; private int mDragPointerId = -1; + private boolean mDragActive = false; private CaptionTouchEventListener( RunningTaskInfo taskInfo, @@ -165,42 +174,38 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption } else { mSyncQueue.queue(wct); } - } else if (id == R.id.maximize_window) { - WindowContainerTransaction wct = new WindowContainerTransaction(); - RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); - int targetWindowingMode = taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN - ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_FREEFORM; - int displayWindowingMode = - taskInfo.configuration.windowConfiguration.getDisplayWindowingMode(); - wct.setWindowingMode(mTaskToken, - targetWindowingMode == displayWindowingMode - ? WINDOWING_MODE_UNDEFINED : targetWindowingMode); - if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) { - wct.setBounds(mTaskToken, null); - } - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mTransitionStarter.startWindowingModeTransition(targetWindowingMode, wct); - } else { - mSyncQueue.queue(wct); - } - } else if (id == R.id.minimize_window) { - WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.reorder(mTaskToken, false); - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mTransitionStarter.startMinimizedModeTransition(wct); - } else { - mSyncQueue.queue(wct); - } + } else if (id == R.id.back_button) { + injectBackKey(); + } + } + private void injectBackKey() { + sendBackEvent(KeyEvent.ACTION_DOWN); + sendBackEvent(KeyEvent.ACTION_UP); + } + + private void sendBackEvent(int action) { + final long when = SystemClock.uptimeMillis(); + final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK, + 0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, + 0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, + InputDevice.SOURCE_KEYBOARD); + + ev.setDisplayId(mContext.getDisplay().getDisplayId()); + if (!InputManager.getInstance() + .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) { + Log.e(TAG, "Inject input event fail"); } } @Override public boolean onTouch(View v, MotionEvent e) { - if (v.getId() != R.id.caption) { + int id = v.getId(); + if (id != R.id.caption_handle && id != R.id.caption) { return false; } - handleEventForMove(e); - + if (id == R.id.caption_handle || mDragActive) { + handleEventForMove(e); + } if (e.getAction() != MotionEvent.ACTION_DOWN) { return false; } @@ -223,6 +228,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption } switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: + mDragActive = true; mDragPointerId = e.getPointerId(0); mDragResizeCallback.onDragResizeStart( 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); @@ -235,6 +241,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { + mDragActive = false; int dragPointerIdx = e.findPointerIndex(mDragPointerId); int statusBarHeight = mDisplayController.getDisplayLayout(taskInfo.displayId) .stableInsets().top; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 733f6b7d5dbf..9d61c14e1435 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -21,13 +21,12 @@ import android.app.WindowConfiguration; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.VectorDrawable; import android.os.Handler; import android.view.Choreographer; import android.view.SurfaceControl; import android.view.View; +import android.view.ViewConfiguration; import android.window.WindowContainerTransaction; import com.android.wm.shell.R; @@ -38,28 +37,11 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; /** * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with - * {@link CaptionWindowDecorViewModel}. The caption bar contains maximize and close buttons. - * - * {@link CaptionWindowDecorViewModel} can change the color of the caption bar based on the foremost - * app's request through {@link #setCaptionColor(int)}, in which it changes the foreground color of - * caption buttons according to the luminance of the background. + * {@link CaptionWindowDecorViewModel}. The caption bar contains a handle, back button, and close button. * * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't. */ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { - // The thickness of shadows of a window that has focus in DIP. - private static final int DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP = 20; - // The thickness of shadows of a window that doesn't have focus in DIP. - private static final int DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP = 5; - - // Height of button (32dp) + 2 * margin (5dp each) - private static final int DECOR_CAPTION_HEIGHT_IN_DIP = 42; - private static final int RESIZE_HANDLE_IN_DIP = 30; - - private static final Rect EMPTY_OUTSET = new Rect(); - private static final Rect RESIZE_HANDLE_OUTSET = new Rect( - RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP); - private final Handler mHandler; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; @@ -70,9 +52,12 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL private DragResizeInputListener mDragResizeListener; + private RelayoutParams mRelayoutParams = new RelayoutParams(); private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult = new WindowDecoration.RelayoutResult<>(); + private boolean mDesktopActive; + CaptionWindowDecoration( Context context, DisplayController displayController, @@ -87,6 +72,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mHandler = handler; mChoreographer = choreographer; mSyncQueue = syncQueue; + mDesktopActive = DesktopModeStatus.isActive(mContext); } void setCaptionListeners( @@ -112,19 +98,32 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - final int shadowRadiusDp = taskInfo.isFocused - ? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP; - final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode() - == WindowConfiguration.WINDOWING_MODE_FREEFORM; - final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable; - final Rect outset = isDragResizeable ? RESIZE_HANDLE_OUTSET : EMPTY_OUTSET; + final int shadowRadiusID = taskInfo.isFocused + ? R.dimen.freeform_decor_shadow_focused_thickness + : R.dimen.freeform_decor_shadow_unfocused_thickness; + final boolean isFreeform = + taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; + final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - relayout(taskInfo, R.layout.caption_window_decoration, oldRootView, - DECOR_CAPTION_HEIGHT_IN_DIP, outset, shadowRadiusDp, startT, finishT, wct, mResult); - taskInfo = null; // Clear it just in case we use it accidentally + + int outsetLeftId = R.dimen.freeform_resize_handle; + int outsetTopId = R.dimen.freeform_resize_handle; + int outsetRightId = R.dimen.freeform_resize_handle; + int outsetBottomId = R.dimen.freeform_resize_handle; + + mRelayoutParams.reset(); + mRelayoutParams.mRunningTaskInfo = taskInfo; + mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration; + mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; + mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width; + mRelayoutParams.mShadowRadiusId = shadowRadiusID; + if (isDragResizeable) { + mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId); + } + relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); mTaskOrganizer.applyTransaction(wct); @@ -137,6 +136,17 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL setupRootView(); } + // If this task is not focused, do not show caption. + setCaptionVisibility(taskInfo.isFocused); + + // Only handle should show if Desktop Mode is inactive. + boolean desktopCurrentStatus = DesktopModeStatus.isActive(mContext); + if (mDesktopActive != desktopCurrentStatus && taskInfo.isFocused) { + mDesktopActive = desktopCurrentStatus; + setButtonVisibility(); + } + taskInfo = null; // Clear it just in case we use it accidentally + if (!isDragResizeable) { closeDragResizeListener(); return; @@ -145,16 +155,21 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) { closeDragResizeListener(); mDragResizeListener = new DragResizeInputListener( - mContext, - mHandler, - mChoreographer, - mDisplay.getDisplayId(), - mDecorationContainerSurface, - mDragResizeCallback); + mContext, + mHandler, + mChoreographer, + mDisplay.getDisplayId(), + mDecorationContainerSurface, + mDragResizeCallback); } + int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop(); + int resize_handle = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_handle); + int resize_corner = mResult.mRootView.getResources() + .getDimensionPixelSize(R.dimen.freeform_resize_corner); mDragResizeListener.setGeometry( - mResult.mWidth, mResult.mHeight, (int) (mResult.mDensity * RESIZE_HANDLE_IN_DIP)); + mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop); } /** @@ -163,42 +178,46 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL private void setupRootView() { View caption = mResult.mRootView.findViewById(R.id.caption); caption.setOnTouchListener(mOnCaptionTouchListener); - View maximize = caption.findViewById(R.id.maximize_window); - if (DesktopModeStatus.IS_SUPPORTED) { - // Hide maximize button when desktop mode is available - maximize.setVisibility(View.GONE); - } else { - maximize.setVisibility(View.VISIBLE); - maximize.setOnClickListener(mOnCaptionButtonClickListener); - } View close = caption.findViewById(R.id.close_window); close.setOnClickListener(mOnCaptionButtonClickListener); - View minimize = caption.findViewById(R.id.minimize_window); - minimize.setOnClickListener(mOnCaptionButtonClickListener); + View back = caption.findViewById(R.id.back_button); + back.setOnClickListener(mOnCaptionButtonClickListener); + View handle = caption.findViewById(R.id.caption_handle); + handle.setOnTouchListener(mOnCaptionTouchListener); + setButtonVisibility(); } - void setCaptionColor(int captionColor) { - if (mResult.mRootView == null) { - return; - } - + /** + * Sets caption visibility based on task focus. + * + * @param visible whether or not the caption should be visible + */ + private void setCaptionVisibility(boolean visible) { + int v = visible ? View.VISIBLE : View.GONE; View caption = mResult.mRootView.findViewById(R.id.caption); - GradientDrawable captionDrawable = (GradientDrawable) caption.getBackground(); - captionDrawable.setColor(captionColor); + caption.setVisibility(v); + } + /** + * Sets the visibility of buttons and color of caption based on desktop mode status + * + */ + public void setButtonVisibility() { + int v = mDesktopActive ? View.VISIBLE : View.GONE; + View caption = mResult.mRootView.findViewById(R.id.caption); + View back = caption.findViewById(R.id.back_button); + View close = caption.findViewById(R.id.close_window); + back.setVisibility(v); + close.setVisibility(v); int buttonTintColorRes = - Color.valueOf(captionColor).luminance() < 0.5 - ? R.color.decor_button_light_color - : R.color.decor_button_dark_color; + mDesktopActive ? R.color.decor_button_dark_color + : R.color.decor_button_light_color; ColorStateList buttonTintColor = caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */); - View maximize = caption.findViewById(R.id.maximize_window); - VectorDrawable maximizeBackground = (VectorDrawable) maximize.getBackground(); - maximizeBackground.setTintList(buttonTintColor); - - View close = caption.findViewById(R.id.close_window); - VectorDrawable closeBackground = (VectorDrawable) close.getBackground(); - closeBackground.setTintList(buttonTintColor); + View handle = caption.findViewById(R.id.caption_handle); + VectorDrawable handleBackground = (VectorDrawable) handle.getBackground(); + handleBackground.setTintList(buttonTintColor); + caption.getBackground().setTint(v == View.VISIBLE ? Color.WHITE : Color.TRANSPARENT); } private void closeDragResizeListener() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index 3d014959a952..b9f16b63de48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -16,11 +16,13 @@ package com.android.wm.shell.windowdecor; +import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import android.content.Context; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; @@ -42,8 +44,11 @@ import com.android.internal.view.BaseIWindow; /** * An input event listener registered to InputDispatcher to receive input events on task edges and - * convert them to drag resize requests. + * and corners. Converts them to drag resize requests. + * Task edges are for resizing with a mouse. + * Task corners are for resizing with touch input. */ +// TODO(b/251270585): investigate how to pass taps in corners to the tasks class DragResizeInputListener implements AutoCloseable { private static final String TAG = "DragResizeInputListener"; @@ -63,8 +68,15 @@ class DragResizeInputListener implements AutoCloseable { private int mWidth; private int mHeight; private int mResizeHandleThickness; + private int mCornerSize; + + private Rect mLeftTopCornerBounds; + private Rect mRightTopCornerBounds; + private Rect mLeftBottomCornerBounds; + private Rect mRightBottomCornerBounds; private int mDragPointerId = -1; + private int mTouchSlop; DragResizeInputListener( Context context, @@ -118,16 +130,23 @@ class DragResizeInputListener implements AutoCloseable { * @param height The height of the drag resize handler in pixels, including resize handle * thickness. That is task height + 2 * resize handle thickness. * @param resizeHandleThickness The thickness of the resize handle in pixels. + * @param cornerSize The size of the resize handle centered in each corner. + * @param touchSlop The distance in pixels user has to drag with touch for it to register as + * a resize action. */ - void setGeometry(int width, int height, int resizeHandleThickness) { + void setGeometry(int width, int height, int resizeHandleThickness, int cornerSize, + int touchSlop) { if (mWidth == width && mHeight == height - && mResizeHandleThickness == resizeHandleThickness) { + && mResizeHandleThickness == resizeHandleThickness + && mCornerSize == cornerSize) { return; } mWidth = width; mHeight = height; mResizeHandleThickness = resizeHandleThickness; + mCornerSize = cornerSize; + mTouchSlop = touchSlop; Region touchRegion = new Region(); final Rect topInputBounds = new Rect(0, 0, mWidth, mResizeHandleThickness); @@ -146,6 +165,40 @@ class DragResizeInputListener implements AutoCloseable { mWidth, mHeight); touchRegion.union(bottomInputBounds); + // Set up touch areas in each corner. + int cornerRadius = mCornerSize / 2; + mLeftTopCornerBounds = new Rect( + mResizeHandleThickness - cornerRadius, + mResizeHandleThickness - cornerRadius, + mResizeHandleThickness + cornerRadius, + mResizeHandleThickness + cornerRadius + ); + touchRegion.union(mLeftTopCornerBounds); + + mRightTopCornerBounds = new Rect( + mWidth - mResizeHandleThickness - cornerRadius, + mResizeHandleThickness - cornerRadius, + mWidth - mResizeHandleThickness + cornerRadius, + mResizeHandleThickness + cornerRadius + ); + touchRegion.union(mRightTopCornerBounds); + + mLeftBottomCornerBounds = new Rect( + mResizeHandleThickness - cornerRadius, + mHeight - mResizeHandleThickness - cornerRadius, + mResizeHandleThickness + cornerRadius, + mHeight - mResizeHandleThickness + cornerRadius + ); + touchRegion.union(mLeftBottomCornerBounds); + + mRightBottomCornerBounds = new Rect( + mWidth - mResizeHandleThickness - cornerRadius, + mHeight - mResizeHandleThickness - cornerRadius, + mWidth - mResizeHandleThickness + cornerRadius, + mHeight - mResizeHandleThickness + cornerRadius + ); + touchRegion.union(mRightBottomCornerBounds); + try { mWindowSession.updateInputChannel( mInputChannel.getToken(), @@ -173,6 +226,9 @@ class DragResizeInputListener implements AutoCloseable { private final Choreographer mChoreographer; private final Runnable mConsumeBatchEventRunnable; private boolean mConsumeBatchEventScheduled; + private boolean mShouldHandleEvents; + private boolean mDragging; + private final PointF mActionDownPoint = new PointF(); private TaskResizeInputEventReceiver( InputChannel inputChannel, Handler handler, Choreographer choreographer) { @@ -216,41 +272,101 @@ class DragResizeInputListener implements AutoCloseable { } MotionEvent e = (MotionEvent) inputEvent; + boolean result = false; + // Check if this is a touch event vs mouse event. + // Touch events are tracked in four corners. Other events are tracked in resize edges. + boolean isTouch = (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; + switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: { - mDragPointerId = e.getPointerId(0); - mCallback.onDragResizeStart( - calculateCtrlType(e.getX(0), e.getY(0)), e.getRawX(0), e.getRawY(0)); + float x = e.getX(0); + float y = e.getY(0); + if (isTouch) { + mShouldHandleEvents = isInCornerBounds(x, y); + } else { + mShouldHandleEvents = isInResizeHandleBounds(x, y); + } + if (mShouldHandleEvents) { + mDragPointerId = e.getPointerId(0); + float rawX = e.getRawX(0); + float rawY = e.getRawY(0); + mActionDownPoint.set(rawX, rawY); + int ctrlType = calculateCtrlType(isTouch, x, y); + mCallback.onDragResizeStart(ctrlType, rawX, rawY); + result = true; + } break; } case MotionEvent.ACTION_MOVE: { + if (!mShouldHandleEvents) { + break; + } int dragPointerIndex = e.findPointerIndex(mDragPointerId); - mCallback.onDragResizeMove( - e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); + float rawX = e.getRawX(dragPointerIndex); + float rawY = e.getRawY(dragPointerIndex); + if (isTouch) { + // Check for touch slop for touch events + float dx = rawX - mActionDownPoint.x; + float dy = rawY - mActionDownPoint.y; + if (!mDragging && Math.hypot(dx, dy) > mTouchSlop) { + mDragging = true; + } + } else { + // For all other types allow immediate dragging. + mDragging = true; + } + if (mDragging) { + mCallback.onDragResizeMove(rawX, rawY); + result = true; + } break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { - int dragPointerIndex = e.findPointerIndex(mDragPointerId); - mCallback.onDragResizeEnd( - e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); + if (mDragging) { + int dragPointerIndex = e.findPointerIndex(mDragPointerId); + mCallback.onDragResizeEnd( + e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); + } + mDragging = false; + mShouldHandleEvents = false; + mActionDownPoint.set(0, 0); mDragPointerId = -1; + result = true; break; } case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_MOVE: { updateCursorType(e.getXCursorPosition(), e.getYCursorPosition()); + result = true; break; } case MotionEvent.ACTION_HOVER_EXIT: mInputManager.setPointerIconType(PointerIcon.TYPE_DEFAULT); + result = true; break; } - return true; + return result; + } + + private boolean isInCornerBounds(float xf, float yf) { + return calculateCornersCtrlType(xf, yf) != 0; + } + + private boolean isInResizeHandleBounds(float x, float y) { + return calculateResizeHandlesCtrlType(x, y) != 0; + } + + @TaskPositioner.CtrlType + private int calculateCtrlType(boolean isTouch, float x, float y) { + if (isTouch) { + return calculateCornersCtrlType(x, y); + } + return calculateResizeHandlesCtrlType(x, y); } @TaskPositioner.CtrlType - private int calculateCtrlType(float x, float y) { + private int calculateResizeHandlesCtrlType(float x, float y) { int ctrlType = 0; if (x < mResizeHandleThickness) { ctrlType |= TaskPositioner.CTRL_TYPE_LEFT; @@ -267,8 +383,27 @@ class DragResizeInputListener implements AutoCloseable { return ctrlType; } + @TaskPositioner.CtrlType + private int calculateCornersCtrlType(float x, float y) { + int xi = (int) x; + int yi = (int) y; + if (mLeftTopCornerBounds.contains(xi, yi)) { + return TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_TOP; + } + if (mLeftBottomCornerBounds.contains(xi, yi)) { + return TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_BOTTOM; + } + if (mRightTopCornerBounds.contains(xi, yi)) { + return TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_TOP; + } + if (mRightBottomCornerBounds.contains(xi, yi)) { + return TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_BOTTOM; + } + return 0; + } + private void updateCursorType(float x, float y) { - @TaskPositioner.CtrlType int ctrlType = calculateCtrlType(x, y); + @TaskPositioner.CtrlType int ctrlType = calculateResizeHandlesCtrlType(x, y); int cursorType = PointerIcon.TYPE_DEFAULT; switch (ctrlType) { @@ -292,4 +427,4 @@ class DragResizeInputListener implements AutoCloseable { mInputManager.setPointerIconType(cursorType); } } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java index 280569b05d87..27c10114ac0e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java @@ -25,9 +25,10 @@ import com.android.wm.shell.ShellTaskOrganizer; class TaskPositioner implements DragResizeCallback { - @IntDef({CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM}) + @IntDef({CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM}) @interface CtrlType {} + static final int CTRL_TYPE_UNDEFINED = 0; static final int CTRL_TYPE_LEFT = 1; static final int CTRL_TYPE_RIGHT = 2; static final int CTRL_TYPE_TOP = 4; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java index d9697d288ab6..d7f71c8235f1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -19,8 +19,6 @@ package com.android.wm.shell.windowdecor; import android.app.ActivityManager; import android.view.SurfaceControl; -import androidx.annotation.Nullable; - import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; /** @@ -28,10 +26,8 @@ import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; * customize {@link WindowDecoration}. Its implementations are responsible to interpret user's * interactions with UI widgets in window decorations and send corresponding requests to system * servers. - * - * @param <T> The actual decoration type */ -public interface WindowDecorViewModel<T extends AutoCloseable> { +public interface WindowDecorViewModel { /** * Sets the transition starter that starts freeform task transitions. @@ -50,29 +46,19 @@ public interface WindowDecorViewModel<T extends AutoCloseable> { * @param finishT the finish transaction to restore states after the transition * @return the window decoration object */ - @Nullable T createWindowDecoration( + boolean createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT); /** - * Adopts the window decoration if possible. - * May be {@code null} if a window decor is not needed or the given one is incompatible. - * - * @param windowDecor the potential window decoration to adopt - * @return the window decoration if it can be adopted, or {@code null} otherwise. - */ - @Nullable T adoptWindowDecoration(@Nullable AutoCloseable windowDecor); - - /** * Notifies a task info update on the given task, with the window decoration created previously * for this task by {@link #createWindowDecoration}. * * @param taskInfo the new task info of the task - * @param windowDecoration the window decoration created for the task */ - void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo, T windowDecoration); + void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo); /** * Notifies a transition is about to start about the given task to give the window decoration a @@ -80,11 +66,16 @@ public interface WindowDecorViewModel<T extends AutoCloseable> { * * @param startT the start transaction to be applied before the transition * @param finishT the finish transaction to restore states after the transition - * @param windowDecoration the window decoration created for the task */ void setupWindowDecorationForTransition( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT, - T windowDecoration); + SurfaceControl.Transaction finishT); + + /** + * Destroys the window decoration of the give task. + * + * @param taskInfo the info of the task + */ + void destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 3e3a864f48c7..b314163802ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -19,11 +19,11 @@ package com.android.wm.shell.windowdecor; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; -import android.util.DisplayMetrics; import android.view.Display; import android.view.InsetsState; import android.view.LayoutInflater; @@ -91,7 +91,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> SurfaceControl mTaskBackgroundSurface; SurfaceControl mCaptionContainerSurface; - private CaptionWindowManager mCaptionWindowManager; + private WindowlessWindowManager mCaptionWindowManager; private SurfaceControlViewHost mViewHost; private final Rect mCaptionInsetsRect = new Rect(); @@ -142,15 +142,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> */ abstract void relayout(RunningTaskInfo taskInfo); - void relayout(RunningTaskInfo taskInfo, int layoutResId, T rootView, float captionHeightDp, - Rect outsetsDp, float shadowRadiusDp, SurfaceControl.Transaction startT, - SurfaceControl.Transaction finishT, WindowContainerTransaction wct, + void relayout(RelayoutParams params, SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView, RelayoutResult<T> outResult) { outResult.reset(); final Configuration oldTaskConfig = mTaskInfo.getConfiguration(); - if (taskInfo != null) { - mTaskInfo = taskInfo; + if (params.mRunningTaskInfo != null) { + mTaskInfo = params.mRunningTaskInfo; } if (!mTaskInfo.isVisible) { @@ -159,7 +158,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return; } - if (rootView == null && layoutResId == 0) { + if (rootView == null && params.mLayoutResId == 0) { throw new IllegalArgumentException("layoutResId and rootView can't both be invalid."); } @@ -176,15 +175,15 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return; } mDecorWindowContext = mContext.createConfigurationContext(taskConfig); - if (layoutResId != 0) { - outResult.mRootView = - (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null); + if (params.mLayoutResId != 0) { + outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) + .inflate(params.mLayoutResId, null); } } if (outResult.mRootView == null) { - outResult.mRootView = - (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null); + outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) + .inflate(params.mLayoutResId , null); } // DecorationContainerSurface @@ -200,18 +199,19 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); - outResult.mDensity = taskConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; - final int decorContainerOffsetX = -(int) (outsetsDp.left * outResult.mDensity); - final int decorContainerOffsetY = -(int) (outsetsDp.top * outResult.mDensity); + final Resources resources = mDecorWindowContext.getResources(); + final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId); + final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId); outResult.mWidth = taskBounds.width() - + (int) (outsetsDp.right * outResult.mDensity) + + loadDimensionPixelSize(resources, params.mOutsetRightId) - decorContainerOffsetX; outResult.mHeight = taskBounds.height() - + (int) (outsetsDp.bottom * outResult.mDensity) + + loadDimensionPixelSize(resources, params.mOutsetBottomId) - decorContainerOffsetY; startT.setPosition( mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY) - .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight) + .setWindowCrop(mDecorationContainerSurface, + outResult.mWidth, outResult.mHeight) // TODO(b/244455401): Change the z-order when it's better organized .setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1) .show(mDecorationContainerSurface); @@ -226,12 +226,13 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .build(); } - float shadowRadius = outResult.mDensity * shadowRadiusDp; + float shadowRadius = loadDimension(resources, params.mShadowRadiusId); int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor(); mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f; mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f; mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f; - startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height()) + startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), + taskBounds.height()) .setShadowRadius(mTaskBackgroundSurface, shadowRadius) .setColor(mTaskBackgroundSurface, mTmpColor) // TODO(b/244455401): Change the z-order when it's better organized @@ -248,23 +249,31 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .build(); } - final int captionHeight = (int) Math.ceil(captionHeightDp * outResult.mDensity); + final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); + final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId); + + //Prevent caption from going offscreen if task is too high up + final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2; + startT.setPosition( - mCaptionContainerSurface, -decorContainerOffsetX, -decorContainerOffsetY) + mCaptionContainerSurface, -decorContainerOffsetX + + taskBounds.width() / 2 - captionWidth / 2, + -decorContainerOffsetY - captionYPos) .setWindowCrop(mCaptionContainerSurface, taskBounds.width(), captionHeight) .show(mCaptionContainerSurface); if (mCaptionWindowManager == null) { // Put caption under a container surface because ViewRootImpl sets the destination frame // of windowless window layers and BLASTBufferQueue#update() doesn't support offset. - mCaptionWindowManager = new CaptionWindowManager( - mTaskInfo.getConfiguration(), mCaptionContainerSurface); + mCaptionWindowManager = new WindowlessWindowManager( + mTaskInfo.getConfiguration(), mCaptionContainerSurface, + null /* hostInputToken */); } // Caption view mCaptionWindowManager.setConfiguration(taskConfig); final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(taskBounds.width(), captionHeight, + new WindowManager.LayoutParams(captionWidth, captionHeight, WindowManager.LayoutParams.TYPE_APPLICATION, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); lp.setTitle("Caption of Task=" + mTaskInfo.taskId); @@ -282,8 +291,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> // Caption insets mCaptionInsetsRect.set(taskBounds); - mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight; - wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES); + mCaptionInsetsRect.bottom = + mCaptionInsetsRect.top + captionHeight - captionYPos; + wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, + CAPTION_INSETS_TYPES); } else { startT.hide(mCaptionContainerSurface); } @@ -358,31 +369,64 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> releaseViews(); } + private static int loadDimensionPixelSize(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; + } + return resources.getDimensionPixelSize(resourceId); + } + + private static float loadDimension(Resources resources, int resourceId) { + if (resourceId == Resources.ID_NULL) { + return 0; + } + return resources.getDimension(resourceId); + } + + static class RelayoutParams{ + RunningTaskInfo mRunningTaskInfo; + int mLayoutResId; + int mCaptionHeightId; + int mCaptionWidthId; + int mShadowRadiusId; + + int mOutsetTopId; + int mOutsetBottomId; + int mOutsetLeftId; + int mOutsetRightId; + + void setOutsets(int leftId, int topId, int rightId, int bottomId) { + mOutsetLeftId = leftId; + mOutsetTopId = topId; + mOutsetRightId = rightId; + mOutsetBottomId = bottomId; + } + + void reset() { + mLayoutResId = Resources.ID_NULL; + mCaptionHeightId = Resources.ID_NULL; + mCaptionWidthId = Resources.ID_NULL; + mShadowRadiusId = Resources.ID_NULL; + + mOutsetTopId = Resources.ID_NULL; + mOutsetBottomId = Resources.ID_NULL; + mOutsetLeftId = Resources.ID_NULL; + mOutsetRightId = Resources.ID_NULL; + } + } + static class RelayoutResult<T extends View & TaskFocusStateConsumer> { int mWidth; int mHeight; - float mDensity; T mRootView; void reset() { mWidth = 0; mHeight = 0; - mDensity = 0; mRootView = null; } } - private static class CaptionWindowManager extends WindowlessWindowManager { - CaptionWindowManager(Configuration config, SurfaceControl rootSurface) { - super(config, rootSurface, null /* hostInputToken */); - } - - @Override - public void setConfiguration(Configuration configuration) { - super.setConfiguration(configuration); - } - } - interface SurfaceControlViewHostFactory { default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { return new SurfaceControlViewHost(c, d, wmm); diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml new file mode 100644 index 000000000000..8949a75d1a15 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <!-- Resources used in WindowDecorationTests --> + <dimen name="test_freeform_decor_caption_height">32dp</dimen> + <dimen name="test_freeform_decor_caption_width">216dp</dimen> + <dimen name="test_window_decor_left_outset">10dp</dimen> + <dimen name="test_window_decor_top_outset">20dp</dimen> + <dimen name="test_window_decor_right_outset">30dp</dimen> + <dimen name="test_window_decor_bottom_outset">40dp</dimen> + <dimen name="test_window_decor_shadow_radius">5dp</dimen> + <dimen name="test_window_decor_resize_handle">10dp</dimen> +</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java index 98b59126227c..79070b1469be 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java @@ -40,6 +40,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import java.util.ArrayList; + /** * Tests for {@link ActivityEmbeddingAnimationRunner}. * @@ -62,13 +64,13 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim final TransitionInfo.Change embeddingChange = createChange(); embeddingChange.setFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY); info.addChange(embeddingChange); - doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any()); + doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any()); mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction); final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class); verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction), - finishCallback.capture()); + finishCallback.capture(), any()); verify(mStartTransaction).apply(); verify(mAnimator).start(); verifyNoMoreInteractions(mFinishTransaction); @@ -88,7 +90,8 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim info.addChange(embeddingChange); final Animator animator = mAnimRunner.createAnimator( info, mStartTransaction, mFinishTransaction, - () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */)); + () -> mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */), + new ArrayList()); // The animation should be empty when it is behind starting window. assertEquals(0, animator.getDuration()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java index 3792e8361284..54a12ab999c5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java @@ -56,13 +56,12 @@ abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase { @Mock SurfaceControl.Transaction mFinishTransaction; @Mock - Transitions.TransitionFinishCallback mFinishCallback; - @Mock Animator mAnimator; ActivityEmbeddingController mController; ActivityEmbeddingAnimationRunner mAnimRunner; ActivityEmbeddingAnimationSpec mAnimSpec; + Transitions.TransitionFinishCallback mFinishCallback; @CallSuper @Before @@ -75,9 +74,11 @@ abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase { assertNotNull(mAnimRunner); mAnimSpec = mAnimRunner.mAnimationSpec; assertNotNull(mAnimSpec); + mFinishCallback = (wct, wctCB) -> {}; spyOn(mController); spyOn(mAnimRunner); spyOn(mAnimSpec); + spyOn(mFinishCallback); } /** Creates a mock {@link TransitionInfo.Change}. */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java index baecf6fe6673..4d98b6ba4f7a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java @@ -55,7 +55,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation @Before public void setup() { super.setUp(); - doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any()); + doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any(), any()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 90a377309edd..2e328b0736dd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -63,7 +63,9 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.util.test.FakeSettingsProvider; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Ignore; @@ -102,6 +104,9 @@ public class BackAnimationControllerTest extends ShellTestCase { @Mock private IBackNaviAnimationController mIBackNaviAnimationController; + @Mock + private ShellController mShellController; + private BackAnimationController mController; private int mEventTime = 0; @@ -118,10 +123,11 @@ public class BackAnimationControllerTest extends ShellTestCase { ANIMATION_ENABLED); mTestableLooper = TestableLooper.get(this); mShellInit = spy(new ShellInit(mShellExecutor)); - mController = new BackAnimationController(mShellInit, + mController = new BackAnimationController(mShellInit, mShellController, mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction, mActivityTaskManager, mContext, mContentResolver); + mController.setEnableUAnimation(true); mShellInit.init(); mEventTime = 0; mShellExecutor.flushAll(); @@ -175,6 +181,12 @@ public class BackAnimationControllerTest extends ShellTestCase { } @Test + public void instantiateController_addExternalInterface() { + verify(mShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION), any(), any()); + } + + @Test @Ignore("b/207481538") public void crossActivity_screenshotAttachedAndVisible() { SurfaceControl screenshotSurface = new SurfaceControl(); @@ -234,10 +246,10 @@ public class BackAnimationControllerTest extends ShellTestCase { // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); - verify(mIOnBackInvokedCallback).onBackStarted(); ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class); - verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture()); + verify(mIOnBackInvokedCallback).onBackStarted(backEventCaptor.capture()); assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget()); + verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackEvent.class)); // Check that back invocation is dispatched. mController.setTriggerBack(true); // Fake trigger back @@ -250,7 +262,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // Toggle the setting off Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0"); ShellInit shellInit = new ShellInit(mShellExecutor); - mController = new BackAnimationController(shellInit, + mController = new BackAnimationController(shellInit, mShellController, mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction, mActivityTaskManager, mContext, mContentResolver); @@ -265,11 +277,11 @@ public class BackAnimationControllerTest extends ShellTestCase { triggerBackGesture(); - verify(appCallback, never()).onBackStarted(); + verify(appCallback, never()).onBackStarted(any(BackEvent.class)); verify(appCallback, never()).onBackProgressed(backEventCaptor.capture()); verify(appCallback, times(1)).onBackInvoked(); - verify(mIOnBackInvokedCallback, never()).onBackStarted(); + verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackEvent.class)); verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture()); verify(mIOnBackInvokedCallback, never()).onBackInvoked(); } @@ -302,7 +314,7 @@ public class BackAnimationControllerTest extends ShellTestCase { doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); - verify(mIOnBackInvokedCallback).onBackStarted(); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); } @Test @@ -321,7 +333,7 @@ public class BackAnimationControllerTest extends ShellTestCase { doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); - verify(mIOnBackInvokedCallback).onBackStarted(); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); } @@ -337,7 +349,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100); simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME, animationTarget); - verify(mIOnBackInvokedCallback).onBackStarted(); + verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class)); // Check that back invocation is dispatched. mController.setTriggerBack(true); // Fake trigger back diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java new file mode 100644 index 000000000000..3aefc3f03a8a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.back; + +import static org.junit.Assert.assertEquals; + +import android.window.BackEvent; + +import org.junit.Before; +import org.junit.Test; + +public class TouchTrackerTest { + private static final float FAKE_THRESHOLD = 400; + private static final float INITIAL_X_LEFT_EDGE = 5; + private static final float INITIAL_X_RIGHT_EDGE = FAKE_THRESHOLD - INITIAL_X_LEFT_EDGE; + private TouchTracker mTouchTracker; + + @Before + public void setUp() throws Exception { + mTouchTracker = new TouchTracker(); + mTouchTracker.setProgressThreshold(FAKE_THRESHOLD); + } + + @Test + public void generatesProgress_onStart() { + mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT); + BackEvent event = mTouchTracker.createStartEvent(null); + assertEquals(event.getProgress(), 0f, 0f); + } + + @Test + public void generatesProgress_leftEdge() { + mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT); + float touchX = 10; + + // Pre-commit + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + + // Post-commit + touchX += 100; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + + // Cancel + touchX -= 10; + mTouchTracker.setTriggerBack(false); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Cancel more + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restart + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restarted, but pre-commit + float restartX = touchX; + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - restartX) / FAKE_THRESHOLD, 0f); + + // Restarted, post-commit + touchX += 10; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f); + } + + @Test + public void generatesProgress_rightEdge() { + mTouchTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0, BackEvent.EDGE_RIGHT); + float touchX = INITIAL_X_RIGHT_EDGE - 10; // Fake right edge + + // Pre-commit + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + + // Post-commit + touchX -= 100; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + + // Cancel + touchX += 10; + mTouchTracker.setTriggerBack(false); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Cancel more + touchX += 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restart + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), 0, 0f); + + // Restarted, but pre-commit + float restartX = touchX; + touchX -= 10; + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (restartX - touchX) / FAKE_THRESHOLD, 0f); + + // Restarted, post-commit + touchX -= 10; + mTouchTracker.setTriggerBack(true); + mTouchTracker.update(touchX, 0); + assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f); + } + + private float getProgress() { + return mTouchTracker.createProgressEvent().getProgress(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 695550dd8fa5..f6d6c03bc2ee 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -16,6 +16,7 @@ package com.android.wm.shell.common.split; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static com.google.common.truth.Truth.assertThat; @@ -91,6 +92,14 @@ public class SplitLayoutTests extends ShellTestCase { // Verify updateConfiguration returns true if the root bounds changed. config.windowConfiguration.setBounds(new Rect(0, 0, 2160, 1080)); assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); + + // Verify updateConfiguration returns true if the orientation changed. + config.orientation = ORIENTATION_LANDSCAPE; + assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); + + // Verify updateConfiguration returns true if the density changed. + config.densityDpi = 123; + assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index 6292130ddec9..2fc0914acbd4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -51,6 +51,7 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager; @@ -93,6 +94,7 @@ public class CompatUIControllerTest extends ShellTestCase { private @Mock Lazy<Transitions> mMockTransitionsLazy; private @Mock CompatUIWindowManager mMockCompatLayout; private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout; + private @Mock DockStateReader mDockStateReader; @Captor ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; @@ -113,7 +115,7 @@ public class CompatUIControllerTest extends ShellTestCase { mShellInit = spy(new ShellInit(mMockExecutor)); mController = new CompatUIController(mContext, mShellInit, mMockShellController, mMockDisplayController, mMockDisplayInsetsController, mMockImeController, - mMockSyncQueue, mMockExecutor, mMockTransitionsLazy) { + mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader) { @Override CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java index f3a8cf45b7f8..16517c0a0010 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java @@ -54,6 +54,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.transition.Transitions; @@ -103,6 +104,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { @Mock private SurfaceControlViewHost mViewHost; @Mock private Transitions mTransitions; @Mock private Runnable mOnDismissCallback; + @Mock private DockStateReader mDockStateReader; private SharedPreferences mSharedPreferences; @Nullable @@ -153,6 +155,16 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + public void testCreateLayout_eligibleAndDocked_doesNotCreateLayout() { + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ + true, /* isDocked */ true); + + assertFalse(windowManager.createLayout(/* canShow= */ true)); + + assertNull(windowManager.mLayout); + } + + @Test public void testCreateLayout_taskBarEducationIsShowing_doesNotCreateLayout() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true, USER_ID_1, /* isTaskbarEduShowing= */ true); @@ -382,17 +394,27 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */ false); } + private LetterboxEduWindowManager createWindowManager(boolean eligible, boolean isDocked) { + return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */ + false, isDocked); + } + private LetterboxEduWindowManager createWindowManager(boolean eligible, int userId, boolean isTaskbarEduShowing) { + return createWindowManager(eligible, userId, isTaskbarEduShowing, /* isDocked */false); + } + + private LetterboxEduWindowManager createWindowManager(boolean eligible, + int userId, boolean isTaskbarEduShowing, boolean isDocked) { + doReturn(isDocked).when(mDockStateReader).isDocked(); LetterboxEduWindowManager windowManager = new LetterboxEduWindowManager(mContext, createTaskInfo(eligible, userId), mSyncTransactionQueue, mTaskListener, createDisplayLayout(), mTransitions, mOnDismissCallback, - mAnimationController); + mAnimationController, mDockStateReader); spyOn(windowManager); doReturn(mViewHost).when(windowManager).createSurfaceViewHost(); doReturn(isTaskbarEduShowing).when(windowManager).isTaskbarEduShowing(); - return windowManager; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java index dd23d97d9199..79b520c734c8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -20,6 +20,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; @@ -35,10 +37,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.testing.AndroidTestingRunner; import android.window.DisplayAreaInfo; +import android.window.TransitionRequestInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import android.window.WindowContainerTransaction.Change; @@ -52,6 +56,7 @@ import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -68,6 +73,8 @@ import org.mockito.Mockito; public class DesktopModeControllerTest extends ShellTestCase { @Mock + private ShellController mShellController; + @Mock private ShellTaskOrganizer mShellTaskOrganizer; @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; @@ -94,8 +101,8 @@ public class DesktopModeControllerTest extends ShellTestCase { mDesktopModeTaskRepository = new DesktopModeTaskRepository(); - mController = new DesktopModeController(mContext, mShellInit, mShellTaskOrganizer, - mRootTaskDisplayAreaOrganizer, mMockTransitions, + mController = new DesktopModeController(mContext, mShellInit, mShellController, + mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mMockTransitions, mDesktopModeTaskRepository, mMockHandler, mExecutor); when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(anyInt())).thenReturn( @@ -240,6 +247,44 @@ public class DesktopModeControllerTest extends ShellTestCase { assertThat(op2.getContainer()).isEqualTo(token2.binder()); } + @Test + public void testHandleTransitionRequest_desktopModeNotActive_returnsNull() { + when(DesktopModeStatus.isActive(any())).thenReturn(false); + WindowContainerTransaction wct = mController.handleRequest( + new Binder(), + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + assertThat(wct).isNull(); + } + + @Test + public void testHandleTransitionRequest_notTransitOpen_returnsNull() { + WindowContainerTransaction wct = mController.handleRequest( + new Binder(), + new TransitionRequestInfo(TRANSIT_TO_FRONT, null /* trigger */, null /* remote */)); + assertThat(wct).isNull(); + } + + @Test + public void testHandleTransitionRequest_notFreeform_returnsNull() { + ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo(); + trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + WindowContainerTransaction wct = mController.handleRequest( + new Binder(), + new TransitionRequestInfo(TRANSIT_TO_FRONT, trigger, null /* remote */)); + assertThat(wct).isNull(); + } + + @Test + public void testHandleTransitionRequest_returnsWct() { + ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo(); + trigger.token = new MockToken().mToken; + trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + WindowContainerTransaction wct = mController.handleRequest( + mock(IBinder.class), + new TransitionRequestInfo(TRANSIT_OPEN, trigger, null /* remote */)); + assertThat(wct).isNotNull(); + } + private static class MockToken { private final WindowContainerToken mToken; private final IBinder mBinder; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 9b28d11f6a9d..aaa5c8a35acb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.desktopmode import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -38,7 +39,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun addActiveTask_listenerNotifiedAndTaskIsActive() { val listener = TestListener() - repo.addListener(listener) + repo.addActiveTaskListener(listener) repo.addActiveTask(1) assertThat(listener.activeTaskChangedCalls).isEqualTo(1) @@ -48,7 +49,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun addActiveTask_sameTaskDoesNotNotify() { val listener = TestListener() - repo.addListener(listener) + repo.addActiveTaskListener(listener) repo.addActiveTask(1) repo.addActiveTask(1) @@ -58,7 +59,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun addActiveTask_multipleTasksAddedNotifiesForEach() { val listener = TestListener() - repo.addListener(listener) + repo.addActiveTaskListener(listener) repo.addActiveTask(1) repo.addActiveTask(2) @@ -68,7 +69,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun removeActiveTask_listenerNotifiedAndTaskNotActive() { val listener = TestListener() - repo.addListener(listener) + repo.addActiveTaskListener(listener) repo.addActiveTask(1) repo.removeActiveTask(1) @@ -80,7 +81,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun removeActiveTask_removeNotExistingTaskDoesNotNotify() { val listener = TestListener() - repo.addListener(listener) + repo.addActiveTaskListener(listener) repo.removeActiveTask(99) assertThat(listener.activeTaskChangedCalls).isEqualTo(0) } @@ -90,10 +91,69 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { assertThat(repo.isActiveTask(99)).isFalse() } - class TestListener : DesktopModeTaskRepository.Listener { + @Test + fun addListener_notifiesVisibleFreeformTask() { + repo.updateVisibleFreeformTasks(1, true) + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isTrue() + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(1) + } + + @Test + fun updateVisibleFreeformTasks_addVisibleTasksNotifiesListener() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + repo.updateVisibleFreeformTasks(1, true) + repo.updateVisibleFreeformTasks(2, true) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isTrue() + // Equal to 2 because adding the listener notifies the current state + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2) + } + + @Test + fun updateVisibleFreeformTasks_removeVisibleTasksNotifiesListener() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + repo.updateVisibleFreeformTasks(1, true) + repo.updateVisibleFreeformTasks(2, true) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isTrue() + repo.updateVisibleFreeformTasks(1, false) + executor.flushAll() + + // Equal to 2 because adding the listener notifies the current state + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2) + + repo.updateVisibleFreeformTasks(2, false) + executor.flushAll() + + assertThat(listener.hasVisibleFreeformTasks).isFalse() + assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(3) + } + + class TestListener : DesktopModeTaskRepository.ActiveTasksListener { var activeTaskChangedCalls = 0 override fun onActiveTasksChanged() { activeTaskChangedCalls++ } } + + class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener { + var hasVisibleFreeformTasks = false + var visibleFreeformTaskChangedCalls = 0 + + override fun onVisibilityChanged(hasVisibleTasks: Boolean) { + hasVisibleFreeformTasks = hasVisibleTasks + visibleFreeformTaskChangedCalls++ + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java index a88c83779f25..d378a177650a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java @@ -52,6 +52,7 @@ import com.android.wm.shell.floating.views.FloatingTaskLayer; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.After; import org.junit.Before; @@ -168,6 +169,18 @@ public class FloatingTasksControllerTest extends ShellTestCase { } } + @Test + public void onInit_addExternalInterface() { + if (FLOATING_TASKS_ACTUALLY_ENABLED) { + createController(); + setUpTabletConfig(); + mController.onInit(); + + verify(mShellController, times(1)).addExternalInterface( + ShellSharedConstants.KEY_EXTRA_SHELL_FLOATING_TASKS, any(), any()); + } + } + // // Tests for floating layer, which is only available for tablets. // diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java index 0fd5cb081ea9..7068a84c3056 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java @@ -17,17 +17,10 @@ package com.android.wm.shell.freeform; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; -import static com.android.wm.shell.transition.Transitions.TRANSIT_MAXIMIZE; -import static com.android.wm.shell.transition.Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE; - -import static org.mockito.Mockito.any; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.same; @@ -44,9 +37,9 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; -import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.WindowDecorViewModel; import org.junit.Before; import org.junit.Test; @@ -65,9 +58,7 @@ public class FreeformTaskTransitionObserverTest { @Mock private Transitions mTransitions; @Mock - private FullscreenTaskListener<?> mFullscreenTaskListener; - @Mock - private FreeformTaskListener<?> mFreeformTaskListener; + private WindowDecorViewModel mWindowDecorViewModel; private FreeformTaskTransitionObserver mTransitionObserver; @@ -82,7 +73,7 @@ public class FreeformTaskTransitionObserverTest { doReturn(pm).when(context).getPackageManager(); mTransitionObserver = new FreeformTaskTransitionObserver( - context, mShellInit, mTransitions, mFullscreenTaskListener, mFreeformTaskListener); + context, mShellInit, mTransitions, mWindowDecorViewModel); if (Transitions.ENABLE_SHELL_TRANSITIONS) { final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass( Runnable.class); @@ -112,11 +103,12 @@ public class FreeformTaskTransitionObserverTest { mTransitionObserver.onTransitionReady(transition, info, startT, finishT); mTransitionObserver.onTransitionStarting(transition); - verify(mFreeformTaskListener).createWindowDecoration(change, startT, finishT); + verify(mWindowDecorViewModel).createWindowDecoration( + change.getTaskInfo(), change.getLeash(), startT, finishT); } @Test - public void testObtainsWindowDecorOnCloseTransition_freeform() { + public void testPreparesWindowDecorOnCloseTransition_freeform() { final TransitionInfo.Change change = createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0); @@ -128,7 +120,8 @@ public class FreeformTaskTransitionObserverTest { mTransitionObserver.onTransitionReady(transition, info, startT, finishT); mTransitionObserver.onTransitionStarting(transition); - verify(mFreeformTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT); + verify(mWindowDecorViewModel).setupWindowDecorationForTransition( + change.getTaskInfo(), startT, finishT); } @Test @@ -138,17 +131,13 @@ public class FreeformTaskTransitionObserverTest { final TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0); info.addChange(change); - final AutoCloseable windowDecor = mock(AutoCloseable.class); - doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration( - eq(change.getTaskInfo()), any(), any()); - final IBinder transition = mock(IBinder.class); final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); mTransitionObserver.onTransitionReady(transition, info, startT, finishT); mTransitionObserver.onTransitionStarting(transition); - verify(windowDecor, never()).close(); + verify(mWindowDecorViewModel, never()).destroyWindowDecoration(change.getTaskInfo()); } @Test @@ -159,8 +148,6 @@ public class FreeformTaskTransitionObserverTest { info.addChange(change); final AutoCloseable windowDecor = mock(AutoCloseable.class); - doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration( - eq(change.getTaskInfo()), any(), any()); final IBinder transition = mock(IBinder.class); final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); @@ -169,7 +156,7 @@ public class FreeformTaskTransitionObserverTest { mTransitionObserver.onTransitionStarting(transition); mTransitionObserver.onTransitionFinished(transition, false); - verify(windowDecor).close(); + verify(mWindowDecorViewModel).destroyWindowDecoration(change.getTaskInfo()); } @Test @@ -192,10 +179,6 @@ public class FreeformTaskTransitionObserverTest { final TransitionInfo info2 = new TransitionInfo(TRANSIT_CLOSE, 0); info2.addChange(change2); - final AutoCloseable windowDecor2 = mock(AutoCloseable.class); - doReturn(windowDecor2).when(mFreeformTaskListener).giveWindowDecoration( - eq(change2.getTaskInfo()), any(), any()); - final IBinder transition2 = mock(IBinder.class); final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); final SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); @@ -204,7 +187,7 @@ public class FreeformTaskTransitionObserverTest { mTransitionObserver.onTransitionFinished(transition1, false); - verify(windowDecor2).close(); + verify(mWindowDecorViewModel).destroyWindowDecoration(change2.getTaskInfo()); } @Test @@ -215,10 +198,6 @@ public class FreeformTaskTransitionObserverTest { final TransitionInfo info1 = new TransitionInfo(TRANSIT_CLOSE, 0); info1.addChange(change1); - final AutoCloseable windowDecor1 = mock(AutoCloseable.class); - doReturn(windowDecor1).when(mFreeformTaskListener).giveWindowDecoration( - eq(change1.getTaskInfo()), any(), any()); - final IBinder transition1 = mock(IBinder.class); final SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); final SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); @@ -231,10 +210,6 @@ public class FreeformTaskTransitionObserverTest { final TransitionInfo info2 = new TransitionInfo(TRANSIT_CLOSE, 0); info2.addChange(change2); - final AutoCloseable windowDecor2 = mock(AutoCloseable.class); - doReturn(windowDecor2).when(mFreeformTaskListener).giveWindowDecoration( - eq(change2.getTaskInfo()), any(), any()); - final IBinder transition2 = mock(IBinder.class); final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); final SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); @@ -243,48 +218,8 @@ public class FreeformTaskTransitionObserverTest { mTransitionObserver.onTransitionFinished(transition1, false); - verify(windowDecor1).close(); - verify(windowDecor2).close(); - } - - @Test - public void testTransfersWindowDecorOnMaximize() { - final TransitionInfo.Change change = - createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FULLSCREEN); - final TransitionInfo info = new TransitionInfo(TRANSIT_MAXIMIZE, 0); - info.addChange(change); - - final AutoCloseable windowDecor = mock(AutoCloseable.class); - doReturn(windowDecor).when(mFreeformTaskListener).giveWindowDecoration( - eq(change.getTaskInfo()), any(), any()); - - final IBinder transition = mock(IBinder.class); - final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); - final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); - mTransitionObserver.onTransitionReady(transition, info, startT, finishT); - mTransitionObserver.onTransitionStarting(transition); - - verify(mFreeformTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT); - verify(mFullscreenTaskListener).adoptWindowDecoration( - eq(change), same(startT), same(finishT), any()); - } - - @Test - public void testTransfersWindowDecorOnRestoreFromMaximize() { - final TransitionInfo.Change change = - createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info = new TransitionInfo(TRANSIT_RESTORE_FROM_MAXIMIZE, 0); - info.addChange(change); - - final IBinder transition = mock(IBinder.class); - final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); - final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); - mTransitionObserver.onTransitionReady(transition, info, startT, finishT); - mTransitionObserver.onTransitionStarting(transition); - - verify(mFullscreenTaskListener).giveWindowDecoration(change.getTaskInfo(), startT, finishT); - verify(mFreeformTaskListener).adoptWindowDecoration( - eq(change), same(startT), same(finishT), any()); + verify(mWindowDecorViewModel).destroyWindowDecoration(change1.getTaskInfo()); + verify(mWindowDecorViewModel).destroyWindowDecoration(change2.getTaskInfo()); } private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java index cf8297eec061..8ad3d2a72617 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java @@ -51,6 +51,7 @@ import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Test; @@ -176,6 +177,12 @@ public class OneHandedControllerTest extends OneHandedTestCase { } @Test + public void testControllerRegisteresExternalInterface() { + verify(mMockShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED), any(), any()); + } + + @Test public void testDefaultShouldNotInOneHanded() { // Assert default transition state is STATE_NONE assertThat(mSpiedTransitionState.getState()).isEqualTo(STATE_NONE); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 1e08f1e55797..d06fb55a5769 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -20,6 +20,7 @@ import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -61,6 +62,7 @@ import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Test; @@ -152,6 +154,12 @@ public class PipControllerTest extends ShellTestCase { } @Test + public void instantiatePipController_registerExternalInterface() { + verify(mShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_PIP), any(), any()); + } + + @Test public void instantiatePipController_registerUserChangeListener() { verify(mShellController, times(1)).addUserChangeListener(any()); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java index dba037db72eb..3bd2ae76ebfd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java @@ -16,6 +16,7 @@ package com.android.wm.shell.pip.phone; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -55,6 +56,7 @@ import org.mockito.MockitoAnnotations; @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) public class PipResizeGestureHandlerTest extends ShellTestCase { + private static final float DEFAULT_SNAP_FRACTION = 2.0f; private static final int STEP_SIZE = 40; private final MotionEvent.PointerProperties[] mPp = new MotionEvent.PointerProperties[2]; @@ -196,6 +198,51 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { < mPipBoundsState.getBounds().width()); } + @Test + public void testUserResizeTo() { + // resizing the bounds to normal bounds at first + mPipResizeGestureHandler.userResizeTo(mPipBoundsState.getNormalBounds(), + DEFAULT_SNAP_FRACTION); + + assertPipBoundsUserResizedTo(mPipBoundsState.getNormalBounds()); + + verify(mPipTaskOrganizer, times(1)) + .scheduleUserResizePip(any(), any(), any()); + + verify(mPipTaskOrganizer, times(1)) + .scheduleFinishResizePip(any(), any()); + + // bounds with max size + final Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x, + mPipBoundsState.getMaxSize().y); + + // resizing the bounds to maximum bounds the second time + mPipResizeGestureHandler.userResizeTo(maxBounds, DEFAULT_SNAP_FRACTION); + + assertPipBoundsUserResizedTo(maxBounds); + + // another call to scheduleUserResizePip() and scheduleFinishResizePip() makes + // the total number of invocations 2 for each method + verify(mPipTaskOrganizer, times(2)) + .scheduleUserResizePip(any(), any(), any()); + + verify(mPipTaskOrganizer, times(2)) + .scheduleFinishResizePip(any(), any()); + } + + private void assertPipBoundsUserResizedTo(Rect bounds) { + // check user-resized bounds + assertEquals(mPipResizeGestureHandler.getUserResizeBounds().width(), bounds.width()); + assertEquals(mPipResizeGestureHandler.getUserResizeBounds().height(), bounds.height()); + + // check if the bounds are the same + assertEquals(mPipBoundsState.getBounds().width(), bounds.width()); + assertEquals(mPipBoundsState.getBounds().height(), bounds.height()); + + // a flag should be set to indicate pip has been resized by the user + assertTrue(mPipBoundsState.hasUserResizedPip()); + } + private MotionEvent obtainMotionEvent(int action, int topLeft, int bottomRight) { final MotionEvent.PointerCoords[] pc = new MotionEvent.PointerCoords[2]; for (int i = 0; i < 2; i++) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index b8aaaa76e3c7..f6ac3ee0a8e4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -28,6 +28,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -57,7 +58,9 @@ import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import com.android.wm.shell.util.GroupedRecentTaskInfo; import com.android.wm.shell.util.SplitBounds; @@ -84,6 +87,8 @@ public class RecentTasksControllerTest extends ShellTestCase { @Mock private TaskStackListenerImpl mTaskStackListener; @Mock + private ShellController mShellController; + @Mock private ShellCommandHandler mShellCommandHandler; @Mock private DesktopModeTaskRepository mDesktopModeTaskRepository; @@ -101,7 +106,7 @@ public class RecentTasksControllerTest extends ShellTestCase { when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); mShellInit = spy(new ShellInit(mMainExecutor)); mRecentTasksController = spy(new RecentTasksController(mContext, mShellInit, - mShellCommandHandler, mTaskStackListener, mActivityTaskManager, + mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager, Optional.of(mDesktopModeTaskRepository), mMainExecutor)); mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler, null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController), @@ -121,6 +126,12 @@ public class RecentTasksControllerTest extends ShellTestCase { } @Test + public void instantiateController_addExternalInterface() { + verify(mShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS), any(), any()); + } + + @Test public void testAddRemoveSplitNotifyChange() { ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index 5a68361c595c..55883ab2ef70 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -58,6 +58,7 @@ import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -133,6 +134,15 @@ public class SplitScreenControllerTests extends ShellTestCase { } @Test + public void instantiateController_addExternalInterface() { + doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor(); + when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout()); + mSplitScreenController.onInit(); + verify(mShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN), any(), any()); + } + + @Test public void testShouldAddMultipleTaskFlag_notInSplitScreen() { doReturn(false).when(mSplitScreenController).isSplitScreenVisible(); doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java index 35515e3bb6e8..90165d1cd1b2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java @@ -21,6 +21,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -36,7 +37,9 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Test; @@ -56,25 +59,34 @@ public class StartingWindowControllerTests extends ShellTestCase { private @Mock Context mContext; private @Mock DisplayManager mDisplayManager; - private @Mock ShellInit mShellInit; + private @Mock ShellController mShellController; private @Mock ShellTaskOrganizer mTaskOrganizer; private @Mock ShellExecutor mMainExecutor; private @Mock StartingWindowTypeAlgorithm mTypeAlgorithm; private @Mock IconProvider mIconProvider; private @Mock TransactionPool mTransactionPool; private StartingWindowController mController; + private ShellInit mShellInit; @Before public void setUp() { MockitoAnnotations.initMocks(this); doReturn(mock(Display.class)).when(mDisplayManager).getDisplay(anyInt()); doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class)); - mController = new StartingWindowController(mContext, mShellInit, mTaskOrganizer, - mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool); + mShellInit = spy(new ShellInit(mMainExecutor)); + mController = new StartingWindowController(mContext, mShellInit, mShellController, + mTaskOrganizer, mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool); + mShellInit.init(); } @Test - public void instantiate_addInitCallback() { + public void instantiateController_addInitCallback() { verify(mShellInit, times(1)).addInitCallback(any(), any()); } + + @Test + public void instantiateController_addExternalInterface() { + verify(mShellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW), any(), any()); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java index d6ddba9e927d..fbc50c68eff9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java @@ -16,12 +16,16 @@ package com.android.wm.shell.sysui; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import android.content.Context; import android.content.pm.UserInfo; import android.content.res.Configuration; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -30,6 +34,7 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.ShellExecutor; import org.junit.After; @@ -49,6 +54,7 @@ import java.util.Locale; public class ShellControllerTest extends ShellTestCase { private static final int TEST_USER_ID = 100; + private static final String EXTRA_TEST_BINDER = "test_binder"; @Mock private ShellInit mShellInit; @@ -81,6 +87,47 @@ public class ShellControllerTest extends ShellTestCase { } @Test + public void testAddExternalInterface_ensureCallback() { + Binder callback = new Binder(); + ExternalInterfaceBinder wrapper = new ExternalInterfaceBinder() { + @Override + public void invalidate() { + // Do nothing + } + + @Override + public IBinder asBinder() { + return callback; + } + }; + mController.addExternalInterface(EXTRA_TEST_BINDER, () -> wrapper, this); + + Bundle b = new Bundle(); + mController.asShell().createExternalInterfaces(b); + assertTrue(b.getIBinder(EXTRA_TEST_BINDER) == callback); + } + + @Test + public void testAddExternalInterface_disallowDuplicateKeys() { + Binder callback = new Binder(); + ExternalInterfaceBinder wrapper = new ExternalInterfaceBinder() { + @Override + public void invalidate() { + // Do nothing + } + + @Override + public IBinder asBinder() { + return callback; + } + }; + mController.addExternalInterface(EXTRA_TEST_BINDER, () -> wrapper, this); + assertThrows(IllegalArgumentException.class, () -> { + mController.addExternalInterface(EXTRA_TEST_BINDER, () -> wrapper, this); + }); + } + + @Test public void testAddUserChangeListener_ensureCallback() { mController.addUserChangeListener(mUserChangeListener); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index c6492bee040e..c764741d4cd6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -45,7 +45,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.clearInvocations; @@ -67,10 +66,12 @@ import android.view.SurfaceControl; import android.view.WindowManager; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.IWindowContainerToken; import android.window.RemoteTransition; import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import android.window.WindowOrganizer; @@ -86,7 +87,9 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Test; @@ -117,18 +120,31 @@ public class ShellTransitionTests extends ShellTestCase { @Before public void setUp() { doAnswer(invocation -> invocation.getArguments()[1]) - .when(mOrganizer).startTransition(anyInt(), any(), any()); + .when(mOrganizer).startTransition(any(), any()); } @Test public void instantiate_addInitCallback() { ShellInit shellInit = mock(ShellInit.class); - final Transitions t = new Transitions(mContext, shellInit, mOrganizer, mTransactionPool, - createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor); + final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), + mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, + mMainHandler, mAnimExecutor); verify(shellInit, times(1)).addInitCallback(any(), eq(t)); } @Test + public void instantiateController_addExternalInterface() { + ShellInit shellInit = new ShellInit(mMainExecutor); + ShellController shellController = mock(ShellController.class); + final Transitions t = new Transitions(mContext, shellInit, shellController, + mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, + mMainHandler, mAnimExecutor); + shellInit.init(); + verify(shellController, times(1)).addExternalInterface( + eq(ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS), any(), any()); + } + + @Test public void testBasicTransitionFlow() { Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); @@ -136,7 +152,7 @@ public class ShellTransitionTests extends ShellTestCase { IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); - verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); + verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), @@ -188,7 +204,7 @@ public class ShellTransitionTests extends ShellTestCase { // Make a request that will be rejected by the testhandler. transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); - verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), isNull()); + verify(mOrganizer, times(1)).startTransition(eq(transitToken), isNull()); transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class)); assertEquals(1, mDefaultHandler.activeCount()); @@ -199,10 +215,12 @@ public class ShellTransitionTests extends ShellTestCase { // Make a request that will be handled by testhandler but not animated by it. RunningTaskInfo mwTaskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); + // Make the wct non-empty. + handlerWCT.setFocusable(new WindowContainerToken(mock(IWindowContainerToken.class)), true); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, mwTaskInfo, null /* remote */)); verify(mOrganizer, times(1)).startTransition( - eq(TRANSIT_OPEN), eq(transitToken), eq(handlerWCT)); + eq(transitToken), eq(handlerWCT)); transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class)); assertEquals(1, mDefaultHandler.activeCount()); @@ -217,8 +235,8 @@ public class ShellTransitionTests extends ShellTestCase { transitions.addHandler(topHandler); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_CHANGE, mwTaskInfo, null /* remote */)); - verify(mOrganizer, times(1)).startTransition( - eq(TRANSIT_CHANGE), eq(transitToken), eq(handlerWCT)); + verify(mOrganizer, times(2)).startTransition( + eq(transitToken), eq(handlerWCT)); TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE) .addChange(TRANSIT_CHANGE).build(); transitions.onTransitionReady(transitToken, change, mock(SurfaceControl.Transaction.class), @@ -256,7 +274,7 @@ public class ShellTransitionTests extends ShellTestCase { transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, new RemoteTransition(testRemote))); - verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); + verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), @@ -406,7 +424,7 @@ public class ShellTransitionTests extends ShellTestCase { IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); - verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); + verify(mOrganizer, times(1)).startTransition(eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class), @@ -1060,8 +1078,9 @@ public class ShellTransitionTests extends ShellTestCase { private Transitions createTestTransitions() { ShellInit shellInit = new ShellInit(mMainExecutor); - final Transitions t = new Transitions(mContext, shellInit, mOrganizer, mTransactionPool, - createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor); + final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), + mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, + mMainHandler, mAnimExecutor); shellInit.init(); return t; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index ab6ac949d4a3..4d37e5dbc4dc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -50,11 +50,13 @@ import android.view.WindowManager.LayoutParams; import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.tests.R; import org.junit.Before; import org.junit.Test; @@ -76,12 +78,9 @@ import java.util.function.Supplier; @SmallTest @RunWith(AndroidTestingRunner.class) public class WindowDecorationTests extends ShellTestCase { - private static final int CAPTION_HEIGHT_DP = 32; - private static final int SHADOW_RADIUS_DP = 5; private static final Rect TASK_BOUNDS = new Rect(100, 300, 400, 400); private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60); - private final Rect mOutsetsDp = new Rect(); private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult = new WindowDecoration.RelayoutResult<>(); @@ -103,6 +102,7 @@ public class WindowDecorationTests extends ShellTestCase { private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>(); private SurfaceControl.Transaction mMockSurfaceControlStartT; private SurfaceControl.Transaction mMockSurfaceControlFinishT; + private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams(); @Before public void setUp() { @@ -146,7 +146,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mOutsetsDp.set(10, 20, 30, 40); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -196,8 +200,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mOutsetsDp.set(10, 20, 30, 40); - + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -220,21 +227,22 @@ public class WindowDecorationTests extends ShellTestCase { verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); verify(captionContainerSurfaceBuilder).setContainerLayer(); - verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40); + verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -46, 8); verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64); verify(mMockSurfaceControlStartT).show(captionContainerSurface); verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any()); + verify(mMockSurfaceControlViewHost) .setView(same(mMockView), argThat(lp -> lp.height == 64 - && lp.width == 300 + && lp.width == 432 && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0)); if (ViewRootImpl.CAPTION_ON_SHELL) { verify(mMockView).setTaskFocusState(true); verify(mMockWindowContainerTransaction) .addRectInsetsProvider(taskInfo.token, - new Rect(100, 300, 400, 364), + new Rect(100, 300, 400, 332), new int[] { InsetsState.ITYPE_CAPTION_BAR }); } @@ -247,7 +255,6 @@ public class WindowDecorationTests extends ShellTestCase { assertEquals(380, mRelayoutResult.mWidth); assertEquals(220, mRelayoutResult.mHeight); - assertEquals(2, mRelayoutResult.mDensity, 0.f); } @Test @@ -286,7 +293,11 @@ public class WindowDecorationTests extends ShellTestCase { // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is // 64px. taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; - mOutsetsDp.set(10, 20, 30, 40); + mRelayoutParams.setOutsets( + R.dimen.test_window_decor_left_outset, + R.dimen.test_window_decor_top_outset, + R.dimen.test_window_decor_right_outset, + R.dimen.test_window_decor_bottom_outset); final SurfaceControl taskSurface = mock(SurfaceControl.class); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface); @@ -357,7 +368,8 @@ public class WindowDecorationTests extends ShellTestCase { private TestWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) { - return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, + return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(), + mMockDisplayController, mMockShellTaskOrganizer, taskInfo, testSurface, new MockObjectSupplier<>(mMockSurfaceControlBuilders, () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))), @@ -409,9 +421,13 @@ public class WindowDecorationTests extends ShellTestCase { @Override void relayout(ActivityManager.RunningTaskInfo taskInfo) { - relayout(null /* taskInfo */, 0 /* layoutResId */, mMockView, CAPTION_HEIGHT_DP, - mOutsetsDp, SHADOW_RADIUS_DP, mMockSurfaceControlStartT, - mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mRelayoutResult); + mRelayoutParams.mLayoutResId = 0; + mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height; + mRelayoutParams.mCaptionWidthId = R.dimen.test_freeform_decor_caption_width; + mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius; + + relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, + mMockWindowContainerTransaction, mMockView, mRelayoutResult); } } } diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java index 2fe7b1668d08..262f5f198c22 100644 --- a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java +++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java @@ -100,16 +100,12 @@ public final class BluetoothMidiDevice { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + Log.d(TAG, "onConnectionStateChange() status: " + status + ", newState: " + newState); String intentAction; if (newState == BluetoothProfile.STATE_CONNECTED) { Log.d(TAG, "Connected to GATT server."); Log.d(TAG, "Attempting to start service discovery:" + mBluetoothGatt.discoverServices()); - if (!mBluetoothGatt.requestMtu(MAX_PACKET_SIZE)) { - Log.e(TAG, "request mtu failed"); - mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); - mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); - } } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { Log.i(TAG, "Disconnected from GATT server."); close(); @@ -118,6 +114,7 @@ public final class BluetoothMidiDevice { @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { + Log.d(TAG, "onServicesDiscovered() status: " + status); if (status == BluetoothGatt.GATT_SUCCESS) { BluetoothGattService service = gatt.getService(MIDI_SERVICE); if (service != null) { @@ -137,9 +134,14 @@ public final class BluetoothMidiDevice { // Specification says to read the characteristic first and then // switch to receiving notifications mBluetoothGatt.readCharacteristic(characteristic); - } - openBluetoothDevice(mBluetoothDevice); + // Request higher MTU size + if (!gatt.requestMtu(MAX_PACKET_SIZE)) { + Log.e(TAG, "request mtu failed"); + mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); + mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); + } + } } } else { Log.e(TAG, "onServicesDiscovered received: " + status); @@ -235,13 +237,13 @@ public final class BluetoothMidiDevice { System.arraycopy(buffer, 0, mCachedBuffer, 0, count); if (DEBUG) { - logByteArray("Sent ", mCharacteristic.getValue(), 0, - mCharacteristic.getValue().length); + logByteArray("Sent ", mCachedBuffer, 0, mCachedBuffer.length); } - if (mBluetoothGatt.writeCharacteristic(mCharacteristic, mCachedBuffer, - mCharacteristic.getWriteType()) != BluetoothGatt.GATT_SUCCESS) { - Log.w(TAG, "could not write characteristic to Bluetooth GATT"); + int result = mBluetoothGatt.writeCharacteristic(mCharacteristic, mCachedBuffer, + mCharacteristic.getWriteType()); + if (result != BluetoothGatt.GATT_SUCCESS) { + Log.w(TAG, "could not write characteristic to Bluetooth GATT. result: " + result); return false; } @@ -254,6 +256,10 @@ public final class BluetoothMidiDevice { mBluetoothDevice = device; mService = service; + // Set a small default packet size in case there is an issue with configuring MTUs. + mPacketEncoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); + mPacketDecoder.setMaxPacketSize(DEFAULT_PACKET_SIZE); + mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, mGattCallback); mContext = context; diff --git a/packages/CompanionDeviceManager/res/layout/list_item_device.xml b/packages/CompanionDeviceManager/res/layout/list_item_device.xml index 0a5afe4363ac..d4439f9e7e64 100644 --- a/packages/CompanionDeviceManager/res/layout/list_item_device.xml +++ b/packages/CompanionDeviceManager/res/layout/list_item_device.xml @@ -29,7 +29,9 @@ android:layout_width="24dp" android:layout_height="24dp" android:layout_marginStart="24dp" - android:tint="@android:color/system_accent1_600"/> + android:tint="@android:color/system_accent1_600" + android:importantForAccessibility="no" + android:contentDescription="@null"/> <TextView android:id="@android:id/text1" @@ -37,7 +39,6 @@ android:layout_height="wrap_content" android:paddingStart="24dp" android:paddingEnd="24dp" - android:singleLine="true" android:textAppearance="?android:attr/textAppearanceListItemSmall"/> -</LinearLayout>
\ No newline at end of file +</LinearLayout> diff --git a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml index 54916a24b7df..a3d71b953be0 100644 --- a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml +++ b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml @@ -30,7 +30,8 @@ android:layout_height="24dp" android:layout_marginTop="8dp" android:layout_marginEnd="12dp" - android:contentDescription="Permission Icon"/> + android:importantForAccessibility="no" + android:contentDescription="@null"/> <LinearLayout android:layout_width="match_parent" diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java index fc5ff085139c..4e7e36797b7e 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java @@ -103,7 +103,7 @@ public class CompanionDeviceDiscoveryService extends Service { private final Runnable mTimeoutRunnable = this::timeout; - private boolean mStopAfterFirstMatch;; + private boolean mStopAfterFirstMatch; /** * A state enum for devices' discovery. @@ -350,7 +350,7 @@ public class CompanionDeviceDiscoveryService extends Service { } return; } - if (DEBUG) Log.i(TAG, "onDeviceFound() " + device.toShortString() + " - New device."); + Log.i(TAG, "onDeviceFound() " + device.toShortString() + " - New device."); // First: make change. mDevicesFound.add(device); @@ -363,9 +363,9 @@ public class CompanionDeviceDiscoveryService extends Service { }); } - private void onDeviceLost(@Nullable DeviceFilterPair<?> device) { + private void onDeviceLost(@NonNull DeviceFilterPair<?> device) { runOnMainThread(() -> { - if (DEBUG) Log.i(TAG, "onDeviceLost(), device=" + device.toShortString()); + Log.i(TAG, "onDeviceLost(), device=" + device.toShortString()); // First: make change. mDevicesFound.remove(device); diff --git a/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java b/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java index eff9e74e0e70..ee65ef4e92b6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java +++ b/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java @@ -22,8 +22,11 @@ package com.android.settingslib; public class AccessibilityContentDescriptions { private AccessibilityContentDescriptions() {} + + public static final int PHONE_SIGNAL_STRENGTH_NONE = R.string.accessibility_no_phone; + public static final int[] PHONE_SIGNAL_STRENGTH = { - R.string.accessibility_no_phone, + PHONE_SIGNAL_STRENGTH_NONE, R.string.accessibility_phone_one_bar, R.string.accessibility_phone_two_bars, R.string.accessibility_phone_three_bars, diff --git a/packages/SettingsLib/src/com/android/settingslib/MobileNetworkTypeIcon.kt b/packages/SettingsLib/src/com/android/settingslib/MobileNetworkTypeIcon.kt new file mode 100644 index 000000000000..3daf8c20d241 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/MobileNetworkTypeIcon.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes + +/** + * A specification for the icon displaying the mobile network type -- 4G, 5G, LTE, etc. (aka "RAT + * icon" or "data type icon"). This is *not* the signal strength triangle. + * + * This is intended to eventually replace [SignalIcon.MobileIconGroup]. But for now, + * [MobileNetworkTypeIcons] just reads from the existing set of [SignalIcon.MobileIconGroup] + * instances to not duplicate data. + * + * TODO(b/238425913): Remove [SignalIcon.MobileIconGroup] and replace it with this class so that we + * don't need to fill in the superfluous fields from its parent [SignalIcon.IconGroup] class. Then + * this class can become either a sealed class or an enum with parameters. + */ +data class MobileNetworkTypeIcon( + /** A human-readable name for this network type, used for logging. */ + val name: String, + + /** The resource ID of the icon drawable to use. */ + @DrawableRes val iconResId: Int, + + /** The resource ID of the content description to use. */ + @StringRes val contentDescriptionResId: Int, +) diff --git a/packages/SettingsLib/src/com/android/settingslib/MobileNetworkTypeIcons.kt b/packages/SettingsLib/src/com/android/settingslib/MobileNetworkTypeIcons.kt new file mode 100644 index 000000000000..2c5ee8902766 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/MobileNetworkTypeIcons.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib + +import com.android.settingslib.mobile.TelephonyIcons.ICON_NAME_TO_ICON + +/** + * A utility class to fetch instances of [MobileNetworkTypeIcon] given a + * [SignalIcon.MobileIconGroup]. + * + * Use [getNetworkTypeIcon] to fetch the instances. + */ +class MobileNetworkTypeIcons { + companion object { + /** + * A map from a [SignalIcon.MobileIconGroup.name] to an instance of [MobileNetworkTypeIcon], + * which is the preferred class going forward. + */ + private val MOBILE_NETWORK_TYPE_ICONS: Map<String, MobileNetworkTypeIcon> + + init { + // Build up the mapping from the old implementation to the new one. + val tempMap: MutableMap<String, MobileNetworkTypeIcon> = mutableMapOf() + + ICON_NAME_TO_ICON.forEach { (_, mobileIconGroup) -> + tempMap[mobileIconGroup.name] = mobileIconGroup.toNetworkTypeIcon() + } + + MOBILE_NETWORK_TYPE_ICONS = tempMap + } + + /** + * A converter function between the old mobile network type icon implementation and the new + * one. Given an instance of the old class [mobileIconGroup], outputs an instance of the + * new class [MobileNetworkTypeIcon]. + */ + @JvmStatic + fun getNetworkTypeIcon( + mobileIconGroup: SignalIcon.MobileIconGroup + ): MobileNetworkTypeIcon { + return MOBILE_NETWORK_TYPE_ICONS[mobileIconGroup.name] + ?: mobileIconGroup.toNetworkTypeIcon() + } + + private fun SignalIcon.MobileIconGroup.toNetworkTypeIcon(): MobileNetworkTypeIcon { + return MobileNetworkTypeIcon( + name = this.name, + iconResId = this.dataType, + contentDescriptionResId = this.dataContentDescription + ) + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/SignalIcon.java b/packages/SettingsLib/src/com/android/settingslib/SignalIcon.java index 280e40726c03..6aaab3c56480 100644 --- a/packages/SettingsLib/src/com/android/settingslib/SignalIcon.java +++ b/packages/SettingsLib/src/com/android/settingslib/SignalIcon.java @@ -15,6 +15,9 @@ */ package com.android.settingslib; +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; + /** * Icons for SysUI and Settings. */ @@ -66,34 +69,31 @@ public class SignalIcon { } /** - * Holds icons for a given MobileState. + * Holds RAT icons for a given MobileState. */ public static class MobileIconGroup extends IconGroup { - public final int dataContentDescription; // mContentDescriptionDataType - public final int dataType; + @StringRes public final int dataContentDescription; + @DrawableRes public final int dataType; public MobileIconGroup( String name, - int[][] sbIcons, - int[][] qsIcons, - int[] contentDesc, - int sbNullState, - int qsNullState, - int sbDiscState, - int qsDiscState, - int discContentDesc, int dataContentDesc, int dataType ) { super(name, - sbIcons, - qsIcons, - contentDesc, - sbNullState, - qsNullState, - sbDiscState, - qsDiscState, - discContentDesc); + // The rest of the values are the same for every type of MobileIconGroup, so + // just provide them here. + // TODO(b/238425913): Eventually replace with {@link MobileNetworkTypeIcon} so + // that we don't have to fill in these superfluous fields. + /* sbIcons= */ null, + /* qsIcons= */ null, + /* contentDesc= */ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, + /* sbNullState= */ 0, + /* qsNullState= */ 0, + /* sbDiscState= */ 0, + /* qsDiscState= */ 0, + /* discContentDesc= */ + AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH_NONE); this.dataContentDescription = dataContentDesc; this.dataType = dataType; } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 5662ce6bd808..6bc1160a8d0a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -356,7 +356,7 @@ public class CachedBluetoothDeviceManager { * @return {@code true}, if the device should pair automatically; Otherwise, return * {@code false}. */ - public synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) { + private synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) { boolean isOngoingSetMemberPair = mOngoingSetMemberPair != null; int bondState = device.getBondState(); if (isOngoingSetMemberPair || bondState != BluetoothDevice.BOND_NONE @@ -365,10 +365,44 @@ public class CachedBluetoothDeviceManager { + " , device.getBondState: " + bondState); return false; } + return true; + } - Log.d(TAG, "Bond " + device.getName() + " by CSIP"); + /** + * Called when we found a set member of a group. The function will check the {@code groupId} if + * it exists and the bond state of the device is BOND_NONE, and if there isn't any ongoing pair + * , and then pair the device automatically. + * + * @param device The found device + * @param groupId The group id of the found device + */ + public synchronized void pairDeviceByCsip(BluetoothDevice device, int groupId) { + if (!shouldPairByCsip(device, groupId)) { + return; + } + Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " by CSIP"); mOngoingSetMemberPair = device; - return true; + syncConfigFromMainDevice(device, groupId); + device.createBond(BluetoothDevice.TRANSPORT_LE); + } + + private void syncConfigFromMainDevice(BluetoothDevice device, int groupId) { + if (!isOngoingPairByCsip(device)) { + return; + } + CachedBluetoothDevice memberDevice = findDevice(device); + CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(memberDevice); + if (mainDevice == null) { + mainDevice = mCsipDeviceManager.getCachedDevice(groupId); + } + + if (mainDevice == null || mainDevice.equals(memberDevice)) { + Log.d(TAG, "no mainDevice"); + return; + } + + // The memberDevice set PhonebookAccessPermission + device.setPhonebookAccessPermission(mainDevice.getDevice().getPhonebookAccessPermission()); } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index d5de3f0525a0..20a6cd8e09ce 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -101,7 +101,14 @@ public class CsipDeviceManager { return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID; } - private CachedBluetoothDevice getCachedDevice(int groupId) { + /** + * To find the device with {@code groupId}. + * + * @param groupId The group id + * @return if we could find a device with this {@code groupId} return this device. Otherwise, + * return null. + */ + public CachedBluetoothDevice getCachedDevice(int groupId) { log("getCachedDevice: groupId: " + groupId); for (int i = mCachedDevices.size() - 1; i >= 0; i--) { CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java b/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java index 23e0923d7280..094567c400a3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/TelephonyIcons.java @@ -16,7 +16,6 @@ package com.android.settingslib.mobile; -import com.android.settingslib.AccessibilityContentDescriptions; import com.android.settingslib.R; import com.android.settingslib.SignalIcon.MobileIconGroup; @@ -49,297 +48,129 @@ public class TelephonyIcons { public static final MobileIconGroup CARRIER_NETWORK_CHANGE = new MobileIconGroup( "CARRIER_NETWORK_CHANGE", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.carrier_network_change_mode, - 0 + /* dataType= */ 0 ); public static final MobileIconGroup THREE_G = new MobileIconGroup( "3G", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_3g, TelephonyIcons.ICON_3G ); public static final MobileIconGroup WFC = new MobileIconGroup( "WFC", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], - 0, - 0); + /* dataContentDescription= */ 0, + /* dataType= */ 0); public static final MobileIconGroup UNKNOWN = new MobileIconGroup( "Unknown", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], - 0, - 0); + /* dataContentDescription= */ 0, + /* dataType= */ 0); public static final MobileIconGroup E = new MobileIconGroup( "E", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_edge, TelephonyIcons.ICON_E ); public static final MobileIconGroup ONE_X = new MobileIconGroup( "1X", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_cdma, TelephonyIcons.ICON_1X ); public static final MobileIconGroup G = new MobileIconGroup( "G", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_gprs, TelephonyIcons.ICON_G ); public static final MobileIconGroup H = new MobileIconGroup( "H", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_3_5g, TelephonyIcons.ICON_H ); public static final MobileIconGroup H_PLUS = new MobileIconGroup( "H+", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_3_5g_plus, TelephonyIcons.ICON_H_PLUS ); public static final MobileIconGroup FOUR_G = new MobileIconGroup( "4G", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_4g, TelephonyIcons.ICON_4G ); public static final MobileIconGroup FOUR_G_PLUS = new MobileIconGroup( "4G+", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_4g_plus, TelephonyIcons.ICON_4G_PLUS ); public static final MobileIconGroup LTE = new MobileIconGroup( "LTE", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_lte, TelephonyIcons.ICON_LTE ); public static final MobileIconGroup LTE_PLUS = new MobileIconGroup( "LTE+", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_lte_plus, TelephonyIcons.ICON_LTE_PLUS ); public static final MobileIconGroup FOUR_G_LTE = new MobileIconGroup( "4G LTE", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_4g_lte, TelephonyIcons.ICON_4G_LTE ); public static final MobileIconGroup FOUR_G_LTE_PLUS = new MobileIconGroup( "4G LTE+", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_4g_lte_plus, TelephonyIcons.ICON_4G_LTE_PLUS ); public static final MobileIconGroup LTE_CA_5G_E = new MobileIconGroup( "5Ge", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_5ge_html, TelephonyIcons.ICON_5G_E ); public static final MobileIconGroup NR_5G = new MobileIconGroup( "5G", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_5g, TelephonyIcons.ICON_5G ); public static final MobileIconGroup NR_5G_PLUS = new MobileIconGroup( "5G_PLUS", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_5g_plus, TelephonyIcons.ICON_5G_PLUS ); public static final MobileIconGroup DATA_DISABLED = new MobileIconGroup( "DataDisabled", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.cell_data_off_content_description, 0 ); public static final MobileIconGroup NOT_DEFAULT_DATA = new MobileIconGroup( "NotDefaultData", - null, - null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - 0, - 0, - 0, - 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.not_default_data_content_description, - 0 + /* dataType= */ 0 ); public static final MobileIconGroup CARRIER_MERGED_WIFI = new MobileIconGroup( "CWF", - /* sbIcons= */ null, - /* qsIcons= */ null, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH, - /* sbNullState= */ 0, - /* qsNullState= */ 0, - /* sbDiscState= */ 0, - /* qsDiscState= */ 0, - AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0], R.string.data_connection_carrier_wifi, TelephonyIcons.ICON_CWF ); - // When adding a new MobileIconGround, check if the dataContentDescription has to be filtered + // When adding a new MobileIconGroup, check if the dataContentDescription has to be filtered // in QSCarrier#hasValidTypeContentDescription /** Mapping icon name(lower case) to the icon object. */ @@ -368,14 +199,6 @@ public class TelephonyIcons { ICON_NAME_TO_ICON.put("notdefaultdata", NOT_DEFAULT_DATA); } - public static final int[] WIFI_CALL_STRENGTH_ICONS = { - R.drawable.ic_wifi_call_strength_0, - R.drawable.ic_wifi_call_strength_1, - R.drawable.ic_wifi_call_strength_2, - R.drawable.ic_wifi_call_strength_3, - R.drawable.ic_wifi_call_strength_4 - }; - public static final int[] MOBILE_CALL_STRENGTH_ICONS = { R.drawable.ic_mobile_call_strength_0, R.drawable.ic_mobile_call_strength_1, @@ -384,4 +207,3 @@ public class TelephonyIcons { R.drawable.ic_mobile_call_strength_4 }; } - diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java new file mode 100644 index 000000000000..f969a63dc663 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib; + +import static com.google.common.truth.Truth.assertThat; + +import com.android.settingslib.mobile.TelephonyIcons; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class MobileNetworkTypeIconsTest { + + @Test + public void getNetworkTypeIcon_hPlus_returnsHPlus() { + MobileNetworkTypeIcon icon = + MobileNetworkTypeIcons.getNetworkTypeIcon(TelephonyIcons.H_PLUS); + + assertThat(icon.getName()).isEqualTo(TelephonyIcons.H_PLUS.name); + assertThat(icon.getIconResId()).isEqualTo(TelephonyIcons.ICON_H_PLUS); + } + + @Test + public void getNetworkTypeIcon_fourG_returnsFourG() { + MobileNetworkTypeIcon icon = + MobileNetworkTypeIcons.getNetworkTypeIcon(TelephonyIcons.FOUR_G); + + assertThat(icon.getName()).isEqualTo(TelephonyIcons.FOUR_G.name); + assertThat(icon.getIconResId()).isEqualTo(TelephonyIcons.ICON_4G); + } + + @Test + public void getNetworkTypeIcon_unknown_returnsUnknown() { + SignalIcon.MobileIconGroup unknownGroup = new SignalIcon.MobileIconGroup( + "testUnknownNameHere", /* dataContentDesc= */ 45, /* dataType= */ 6); + + MobileNetworkTypeIcon icon = MobileNetworkTypeIcons.getNetworkTypeIcon(unknownGroup); + + assertThat(icon.getName()).isEqualTo("testUnknownNameHere"); + assertThat(icon.getIconResId()).isEqualTo(6); + assertThat(icon.getContentDescriptionResId()).isEqualTo(45); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java index 62552f914459..61802a87361c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java @@ -582,4 +582,24 @@ public class CachedBluetoothDeviceManagerTest { assertThat(mCachedDeviceManager.isSubDevice(mDevice2)).isTrue(); assertThat(mCachedDeviceManager.isSubDevice(mDevice3)).isFalse(); } + + @Test + public void pairDeviceByCsip_device2AndCapGroup1_device2StartsPairing() { + doReturn(CAP_GROUP1).when(mCsipSetCoordinatorProfile).getGroupUuidMapByDevice(mDevice1); + when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + when(mDevice1.getPhonebookAccessPermission()).thenReturn(BluetoothDevice.ACCESS_ALLOWED); + CachedBluetoothDevice cachedDevice1 = mCachedDeviceManager.addDevice(mDevice1); + assertThat(cachedDevice1).isNotNull(); + when(mDevice2.getBondState()).thenReturn(BluetoothDevice.BOND_NONE); + CachedBluetoothDevice cachedDevice2 = mCachedDeviceManager.addDevice(mDevice2); + assertThat(cachedDevice2).isNotNull(); + + int groupId = CAP_GROUP1.keySet().stream().findFirst().orElse( + BluetoothCsipSetCoordinator.GROUP_ID_INVALID); + assertThat(groupId).isNotEqualTo(BluetoothCsipSetCoordinator.GROUP_ID_INVALID); + mCachedDeviceManager.pairDeviceByCsip(mDevice2, groupId); + + verify(mDevice2).setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); + verify(mDevice2).createBond(BluetoothDevice.TRANSPORT_LE); + } } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index a6bfc408be7e..76cee7bacd85 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -44,8 +44,6 @@ public class SystemSettings { Settings.System.DIM_SCREEN, Settings.System.SCREEN_OFF_TIMEOUT, Settings.System.SCREEN_BRIGHTNESS_MODE, - Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, - Settings.System.SCREEN_BRIGHTNESS_FOR_VR, Settings.System.ADAPTIVE_SLEEP, // moved to secure Settings.System.APPLY_RAMPING_RINGER, Settings.System.VIBRATE_INPUT_DEVICES, diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java index 808ea9ede9dc..6d375ac215a4 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -549,7 +549,7 @@ public class SettingsHelper { try { IActivityManager am = ActivityManager.getService(); - Configuration config = am.getConfiguration(); + final Configuration config = new Configuration(); config.setLocales(merged); // indicate this isn't some passing default - the user wants this remembered config.userSetLocale = true; diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index fd7554f11873..528af2ec2528 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -376,9 +376,11 @@ final class SettingsState { Setting newSetting = new Setting(name, oldSetting.getValue(), null, oldSetting.getPackageName(), oldSetting.getTag(), false, oldSetting.getId()); - mSettings.put(name, newSetting); - updateMemoryUsagePerPackageLocked(newSetting.getPackageName(), oldValue, + int newSize = getNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), oldValue, newSetting.getValue(), oldDefaultValue, newSetting.getDefaultValue()); + checkNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize); + mSettings.put(name, newSetting); + updateMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize); scheduleWriteIfNeededLocked(); } } @@ -410,6 +412,12 @@ final class SettingsState { Setting oldState = mSettings.get(name); String oldValue = (oldState != null) ? oldState.value : null; String oldDefaultValue = (oldState != null) ? oldState.defaultValue : null; + String newDefaultValue = makeDefault ? value : oldDefaultValue; + + int newSize = getNewMemoryUsagePerPackageLocked(packageName, oldValue, value, + oldDefaultValue, newDefaultValue); + checkNewMemoryUsagePerPackageLocked(packageName, newSize); + Setting newState; if (oldState != null) { @@ -430,8 +438,7 @@ final class SettingsState { addHistoricalOperationLocked(HISTORICAL_OPERATION_UPDATE, newState); - updateMemoryUsagePerPackageLocked(packageName, oldValue, value, - oldDefaultValue, newState.getDefaultValue()); + updateMemoryUsagePerPackageLocked(packageName, newSize); scheduleWriteIfNeededLocked(); @@ -552,13 +559,14 @@ final class SettingsState { } Setting oldState = mSettings.remove(name); + int newSize = getNewMemoryUsagePerPackageLocked(oldState.packageName, oldState.value, + null, oldState.defaultValue, null); FrameworkStatsLog.write(FrameworkStatsLog.SETTING_CHANGED, name, /* value= */ "", /* newValue= */ "", oldState.value, /* tag */ "", false, getUserIdFromKey(mKey), FrameworkStatsLog.SETTING_CHANGED__REASON__DELETED); - updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value, - null, oldState.defaultValue, null); + updateMemoryUsagePerPackageLocked(oldState.packageName, newSize); addHistoricalOperationLocked(HISTORICAL_OPERATION_DELETE, oldState); @@ -579,16 +587,18 @@ final class SettingsState { Setting oldSetting = new Setting(setting); String oldValue = setting.getValue(); String oldDefaultValue = setting.getDefaultValue(); + String newValue = oldDefaultValue; + String newDefaultValue = oldDefaultValue; + + int newSize = getNewMemoryUsagePerPackageLocked(setting.packageName, oldValue, + newValue, oldDefaultValue, newDefaultValue); + checkNewMemoryUsagePerPackageLocked(setting.packageName, newSize); if (!setting.reset()) { return false; } - String newValue = setting.getValue(); - String newDefaultValue = setting.getDefaultValue(); - - updateMemoryUsagePerPackageLocked(setting.packageName, oldValue, - newValue, oldDefaultValue, newDefaultValue); + updateMemoryUsagePerPackageLocked(setting.packageName, newSize); addHistoricalOperationLocked(HISTORICAL_OPERATION_RESET, oldSetting); @@ -696,38 +706,49 @@ final class SettingsState { } @GuardedBy("mLock") - private void updateMemoryUsagePerPackageLocked(String packageName, String oldValue, - String newValue, String oldDefaultValue, String newDefaultValue) { - if (mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED) { - return; - } + private boolean isExemptFromMemoryUsageCap(String packageName) { + return mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED + || SYSTEM_PACKAGE_NAME.equals(packageName); + } - if (SYSTEM_PACKAGE_NAME.equals(packageName)) { + @GuardedBy("mLock") + private void checkNewMemoryUsagePerPackageLocked(String packageName, int newSize) + throws IllegalStateException { + if (isExemptFromMemoryUsageCap(packageName)) { return; } + if (newSize > mMaxBytesPerAppPackage) { + throw new IllegalStateException("You are adding too many system settings. " + + "You should stop using system settings for app specific data" + + " package: " + packageName); + } + } + @GuardedBy("mLock") + private int getNewMemoryUsagePerPackageLocked(String packageName, String oldValue, + String newValue, String oldDefaultValue, String newDefaultValue) { + if (isExemptFromMemoryUsageCap(packageName)) { + return 0; + } + final Integer currentSize = mPackageToMemoryUsage.get(packageName); final int oldValueSize = (oldValue != null) ? oldValue.length() : 0; final int newValueSize = (newValue != null) ? newValue.length() : 0; final int oldDefaultValueSize = (oldDefaultValue != null) ? oldDefaultValue.length() : 0; final int newDefaultValueSize = (newDefaultValue != null) ? newDefaultValue.length() : 0; final int deltaSize = newValueSize + newDefaultValueSize - oldValueSize - oldDefaultValueSize; + return Math.max((currentSize != null) ? currentSize + deltaSize : deltaSize, 0); + } - Integer currentSize = mPackageToMemoryUsage.get(packageName); - final int newSize = Math.max((currentSize != null) - ? currentSize + deltaSize : deltaSize, 0); - - if (newSize > mMaxBytesPerAppPackage) { - throw new IllegalStateException("You are adding too many system settings. " - + "You should stop using system settings for app specific data" - + " package: " + packageName); + @GuardedBy("mLock") + private void updateMemoryUsagePerPackageLocked(String packageName, int newSize) { + if (isExemptFromMemoryUsageCap(packageName)) { + return; } - if (DEBUG) { Slog.i(LOG_TAG, "Settings for package: " + packageName + " size: " + newSize + " bytes."); } - mPackageToMemoryUsage.put(packageName, newSize); } diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 8e82b8b5219a..8b9d1180d2ea 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -102,7 +102,9 @@ public class SettingsBackupTest { Settings.System.MIN_REFRESH_RATE, // depends on hardware capabilities Settings.System.PEAK_REFRESH_RATE, // depends on hardware capabilities Settings.System.SCREEN_BRIGHTNESS_FLOAT, + Settings.System.SCREEN_BRIGHTNESS_FOR_VR, Settings.System.SCREEN_BRIGHTNESS_FOR_VR_FLOAT, + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, Settings.System.MULTI_AUDIO_FOCUS_ENABLED, // form-factor/OEM specific Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED ); diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java index 69eb7133f46f..66b809aeae30 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java @@ -20,6 +20,8 @@ import android.test.AndroidTestCase; import android.util.TypedXmlSerializer; import android.util.Xml; +import com.google.common.base.Strings; + import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -276,4 +278,40 @@ public class SettingsStateTest extends AndroidTestCase { settingsState.setVersionLocked(SettingsState.SETTINGS_VERSION_NEW_ENCODING); return settingsState; } + + public void testInsertSetting_memoryUsage() { + SettingsState settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + // No exception should be thrown when there is no cap + settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001), + null, false, "p1"); + settingsState.deleteSettingLocked(SETTING_NAME); + + settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper()); + // System package doesn't have memory usage limit + settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001), + null, false, SYSTEM_PACKAGE); + settingsState.deleteSettingLocked(SETTING_NAME); + + // Should not throw if usage is under the cap + settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 19999), + null, false, "p1"); + settingsState.deleteSettingLocked(SETTING_NAME); + try { + settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001), + null, false, "p1"); + fail("Should throw because it exceeded per package memory usage"); + } catch (IllegalStateException ex) { + assertTrue(ex.getMessage().contains("p1")); + } + try { + settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001), + null, false, "p1"); + fail("Should throw because it exceeded per package memory usage"); + } catch (IllegalStateException ex) { + assertTrue(ex.getMessage().contains("p1")); + } + assertTrue(settingsState.getSettingLocked(SETTING_NAME).isNull()); + } } diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 2107ba083d48..c28792e0521f 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -297,5 +297,6 @@ android_app { dxflags: ["--multi-dex"], required: [ "privapp_whitelist_com.android.systemui", + "wmshell.protolog.json.gz", ], } diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 2737ecf5ffa6..b5145f926abd 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -402,6 +402,9 @@ android:permission="com.android.systemui.permission.SELF" android:exported="false" /> + <service android:name=".screenshot.ScreenshotCrossProfileService" + android:permission="com.android.systemui.permission.SELF" + android:exported="false" /> <service android:name=".screenrecord.RecordingService" /> diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index ccafa2d7e308..402d73c443a3 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -6,14 +6,19 @@ dsandler@android.com aaliomer@google.com aaronjli@google.com +acul@google.com adamcohen@google.com +aioana@google.com alexflo@google.com +andonian@google.com +aroederer@google.com arteiro@google.com asc@google.com awickham@google.com ayepin@google.com bbade@google.com beverlyt@google.com +bhinegardner@google.com bhnm@google.com brycelee@google.com brzezinski@google.com @@ -25,6 +30,7 @@ dupin@google.com ethibodeau@google.com evanlaird@google.com florenceyang@google.com +gallmann@google.com gwasserman@google.com hwwang@google.com hyunyoungs@google.com @@ -57,7 +63,9 @@ mrcasey@google.com mrenouf@google.com nickchameyev@google.com nicomazz@google.com +nijamkin@google.com ogunwale@google.com +omarmt@google.com patmanning@google.com peanutbutter@google.com peskal@google.com @@ -65,6 +73,7 @@ pinyaoting@google.com pixel@google.com pomini@google.com rahulbanerjee@google.com +rasheedlewis@google.com roosa@google.com saff@google.com santie@google.com diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml index f7150ab548dd..2d82307aca76 100644 --- a/packages/SystemUI/animation/res/values/ids.xml +++ b/packages/SystemUI/animation/res/values/ids.xml @@ -16,7 +16,6 @@ --> <resources> <!-- DialogLaunchAnimator --> - <item type="id" name="tag_launch_animation_running"/> <item type="id" name="tag_dialog_background"/> <!-- ViewBoundsAnimator --> diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index 9656b8a99d41..fdfad2bc2fa1 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -25,16 +25,15 @@ import android.graphics.Rect import android.os.Looper import android.util.Log import android.util.MathUtils -import android.view.GhostView import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewRootImpl import android.view.WindowInsets import android.view.WindowManager import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS import android.widget.FrameLayout import com.android.internal.jank.InteractionJankMonitor -import com.android.internal.jank.InteractionJankMonitor.Configuration import com.android.internal.jank.InteractionJankMonitor.CujType import kotlin.math.roundToInt @@ -46,6 +45,7 @@ private const val TAG = "DialogLaunchAnimator" * * This animator also allows to easily animate a dialog into an activity. * + * @see show * @see showFromView * @see showFromDialog * @see createActivityLaunchController @@ -67,8 +67,108 @@ constructor( ActivityLaunchAnimator.INTERPOLATORS.copy( positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator ) + } + + /** + * A controller that takes care of applying the dialog launch and exit animations to the source + * that triggered the animation. + */ + interface Controller { + /** The [ViewRootImpl] of this controller. */ + val viewRoot: ViewRootImpl + + /** + * The identity object of the source animated by this controller. This animator will ensure + * that 2 animations with the same source identity are not going to run at the same time, to + * avoid flickers when a dialog is shown from the same source more or less at the same time + * (for instance if the user clicks an expandable button twice). + */ + val sourceIdentity: Any + + /** The CUJ associated to this controller. */ + val cuj: DialogCuj? + + /** + * Move the drawing of the source in the overlay of [viewGroup]. + * + * Once this method is called, and until [stopDrawingInOverlay] is called, the source + * controlled by this Controller should be drawn in the overlay of [viewGroup] so that it is + * drawn above all other elements in the same [viewRoot]. + */ + fun startDrawingInOverlayOf(viewGroup: ViewGroup) + + /** + * Move the drawing of the source back in its original location. + * + * @see startDrawingInOverlayOf + */ + fun stopDrawingInOverlay() + + /** + * Create the [LaunchAnimator.Controller] that will be called to animate the source + * controlled by this [Controller] during the dialog launch animation. + * + * At the end of this animation, the source should *not* be visible anymore (until the + * dialog is closed and is animated back into the source). + */ + fun createLaunchController(): LaunchAnimator.Controller + + /** + * Create the [LaunchAnimator.Controller] that will be called to animate the source + * controlled by this [Controller] during the dialog exit animation. + * + * At the end of this animation, the source should be visible again. + */ + fun createExitController(): LaunchAnimator.Controller + + /** + * Whether we should animate the dialog back into the source when it is dismissed. If this + * methods returns `false`, then the dialog will simply fade out and + * [onExitAnimationCancelled] will be called. + * + * Note that even when this returns `true`, the exit animation might still be cancelled (in + * which case [onExitAnimationCancelled] will also be called). + */ + fun shouldAnimateExit(): Boolean - private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.tag_launch_animation_running + /** + * Called if we decided to *not* animate the dialog into the source for some reason. This + * means that [createExitController] will *not* be called and this implementation should + * make sure that the source is back in its original state, before it was animated into the + * dialog. In particular, the source should be visible again. + */ + fun onExitAnimationCancelled() + + /** + * Return the [InteractionJankMonitor.Configuration.Builder] to be used for animations + * controlled by this controller. + */ + // TODO(b/252723237): Make this non-nullable + fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? + + companion object { + /** + * Create a [Controller] that can animate [source] to and from a dialog. + * + * Important: The view must be attached to a [ViewGroup] when calling this function and + * during the animation. For safety, this method will return null when it is not. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be + * properly animated. + */ + fun fromView(source: View, cuj: DialogCuj? = null): Controller? { + if (source.parent !is ViewGroup) { + Log.e( + TAG, + "Skipping animation as view $source is not attached to a ViewGroup", + Exception(), + ) + return null + } + + return ViewDialogLaunchAnimatorController(source, cuj) + } + } } /** @@ -96,7 +196,33 @@ constructor( dialog: Dialog, view: View, cuj: DialogCuj? = null, - animateBackgroundBoundsChange: Boolean = false, + animateBackgroundBoundsChange: Boolean = false + ) { + val controller = Controller.fromView(view, cuj) + if (controller == null) { + dialog.show() + } else { + show(dialog, controller, animateBackgroundBoundsChange) + } + } + + /** + * Show [dialog] by expanding it from a source controlled by [controller]. + * + * If [animateBackgroundBoundsChange] is true, then the background of the dialog will be + * animated when the dialog bounds change. + * + * Note: The background of [view] should be a (rounded) rectangle so that it can be properly + * animated. + * + * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be + * made fullscreen and 2 views will be inserted between the dialog DecorView and its children. + */ + @JvmOverloads + fun show( + dialog: Dialog, + controller: Controller, + animateBackgroundBoundsChange: Boolean = false ) { if (Looper.myLooper() != Looper.getMainLooper()) { throw IllegalStateException( @@ -109,9 +235,13 @@ constructor( // intent is to launch a dialog from another dialog. val animatedParent = openedDialogs.firstOrNull { - it.dialog.window.decorView.viewRootImpl == view.viewRootImpl + it.dialog.window.decorView.viewRootImpl == controller.viewRoot + } + val animateFrom = + animatedParent?.dialogContentWithBackground?.let { + Controller.fromView(it, controller.cuj) } - val animateFrom = animatedParent?.dialogContentWithBackground ?: view + ?: controller if (animatedParent == null && animateFrom !is LaunchableView) { // Make sure the View we launch from implements LaunchableView to avoid visibility @@ -126,15 +256,17 @@ constructor( ) } - // Make sure we don't run the launch animation from the same view twice at the same time. - if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) { - Log.e(TAG, "Not running dialog launch animation as there is already one running") + // Make sure we don't run the launch animation from the same source twice at the same time. + if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) { + Log.e( + TAG, + "Not running dialog launch animation from source as it is already expanded into a" + + " dialog" + ) dialog.show() return } - animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true) - val animatedDialog = AnimatedDialog( launchAnimator, @@ -146,7 +278,6 @@ constructor( animateBackgroundBoundsChange, animatedParent, isForTesting, - cuj ) openedDialogs.add(animatedDialog) @@ -154,8 +285,8 @@ constructor( } /** - * Launch [dialog] from [another dialog][animateFrom] that was shown using [showFromView]. This - * will allow for dismissing the whole stack. + * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will + * allow for dismissing the whole stack. * * @see dismissStack */ @@ -181,32 +312,55 @@ constructor( /** * Create an [ActivityLaunchAnimator.Controller] that can be used to launch an activity from the - * dialog that contains [View]. Note that the dialog must have been show using [showFromView] - * and be currently showing, otherwise this will return null. + * dialog that contains [View]. Note that the dialog must have been shown using this animator, + * otherwise this method will return null. * * The returned controller will take care of dismissing the dialog at the right time after the * activity started, when the dialog to app animation is done (or when it is cancelled). If this * method returns null, then the dialog won't be dismissed. * - * Note: The background of [view] should be a (rounded) rectangle so that it can be properly - * animated. - * * @param view any view inside the dialog to animate. */ @JvmOverloads fun createActivityLaunchController( view: View, - cujType: Int? = null + cujType: Int? = null, ): ActivityLaunchAnimator.Controller? { val animatedDialog = openedDialogs.firstOrNull { it.dialog.window.decorView.viewRootImpl == view.viewRootImpl } ?: return null + return createActivityLaunchController(animatedDialog, cujType) + } + /** + * Create an [ActivityLaunchAnimator.Controller] that can be used to launch an activity from + * [dialog]. Note that the dialog must have been shown using this animator, otherwise this + * method will return null. + * + * The returned controller will take care of dismissing the dialog at the right time after the + * activity started, when the dialog to app animation is done (or when it is cancelled). If this + * method returns null, then the dialog won't be dismissed. + * + * @param dialog the dialog to animate. + */ + @JvmOverloads + fun createActivityLaunchController( + dialog: Dialog, + cujType: Int? = null, + ): ActivityLaunchAnimator.Controller? { + val animatedDialog = openedDialogs.firstOrNull { it.dialog == dialog } ?: return null + return createActivityLaunchController(animatedDialog, cujType) + } + + private fun createActivityLaunchController( + animatedDialog: AnimatedDialog, + cujType: Int? = null + ): ActivityLaunchAnimator.Controller? { // At this point, we know that the intent of the caller is to dismiss the dialog to show - // an app, so we disable the exit animation into the touch surface because we will never - // want to run it anyways. + // an app, so we disable the exit animation into the source because we will never want to + // run it anyways. animatedDialog.exitAnimationDisabled = true val dialog = animatedDialog.dialog @@ -252,7 +406,7 @@ constructor( // If this dialog was shown from a cascade of other dialogs, make sure those ones // are dismissed too. - animatedDialog.touchSurface = animatedDialog.prepareForStackDismiss() + animatedDialog.prepareForStackDismiss() // Remove the dim. dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) @@ -283,12 +437,11 @@ constructor( } /** - * Ensure that all dialogs currently shown won't animate into their touch surface when - * dismissed. + * Ensure that all dialogs currently shown won't animate into their source when dismissed. * * This is a temporary API meant to be called right before we both dismiss a dialog and start an - * activity, which currently does not look good if we animate the dialog into the touch surface - * at the same time as the activity starts. + * activity, which currently does not look good if we animate the dialog into their source at + * the same time as the activity starts. * * TODO(b/193634619): Remove this function and animate dialog into opening activity instead. */ @@ -297,13 +450,11 @@ constructor( } /** - * Dismiss [dialog]. If it was launched from another dialog using [showFromView], also dismiss - * the stack of dialogs, animating back to the original touchSurface. + * Dismiss [dialog]. If it was launched from another dialog using this animator, also dismiss + * the stack of dialogs and simply fade out [dialog]. */ fun dismissStack(dialog: Dialog) { - openedDialogs - .firstOrNull { it.dialog == dialog } - ?.let { it.touchSurface = it.prepareForStackDismiss() } + openedDialogs.firstOrNull { it.dialog == dialog }?.prepareForStackDismiss() dialog.dismiss() } @@ -337,8 +488,11 @@ private class AnimatedDialog( private val callback: DialogLaunchAnimator.Callback, private val interactionJankMonitor: InteractionJankMonitor, - /** The view that triggered the dialog after being tapped. */ - var touchSurface: View, + /** + * The controller of the source that triggered the dialog and that will animate into/from the + * dialog. + */ + val controller: DialogLaunchAnimator.Controller, /** * A callback that will be called with this [AnimatedDialog] after the dialog was dismissed and @@ -359,9 +513,6 @@ private class AnimatedDialog( * Whether synchronization should be disabled, which can be useful if we are running in a test. */ private val forceDisableSynchronization: Boolean, - - /** Interaction to which the dialog animation is associated. */ - private val cuj: DialogCuj? = null ) { /** * The DecorView of this dialog window. @@ -383,17 +534,18 @@ private class AnimatedDialog( private var originalDialogBackgroundColor = Color.BLACK /** - * Whether we are currently launching/showing the dialog by animating it from [touchSurface]. + * Whether we are currently launching/showing the dialog by animating it from its source + * controlled by [controller]. */ private var isLaunching = true - /** Whether we are currently dismissing/hiding the dialog by animating into [touchSurface]. */ + /** Whether we are currently dismissing/hiding the dialog by animating into its source. */ private var isDismissing = false private var dismissRequested = false var exitAnimationDisabled = false - private var isTouchSurfaceGhostDrawn = false + private var isSourceDrawnInDialog = false private var isOriginalDialogViewLaidOut = false /** A layout listener to animate the dialog height change. */ @@ -410,13 +562,20 @@ private class AnimatedDialog( */ private var decorViewLayoutListener: View.OnLayoutChangeListener? = null + private var hasInstrumentedJank = false + fun start() { + val cuj = controller.cuj if (cuj != null) { - val config = Configuration.Builder.withView(cuj.cujType, touchSurface) - if (cuj.tag != null) { - config.setTag(cuj.tag) + val config = controller.jankConfigurationBuilder() + if (config != null) { + if (cuj.tag != null) { + config.setTag(cuj.tag) + } + + interactionJankMonitor.begin(config) + hasInstrumentedJank = true } - interactionJankMonitor.begin(config) } // Create the dialog so that its onCreate() method is called, which usually sets the dialog @@ -618,47 +777,45 @@ private class AnimatedDialog( // Show the dialog. dialog.show() - addTouchSurfaceGhost() + moveSourceDrawingToDialog() } - private fun addTouchSurfaceGhost() { + private fun moveSourceDrawingToDialog() { if (decorView.viewRootImpl == null) { - // Make sure that we have access to the dialog view root to synchronize the creation of - // the ghost. - decorView.post(::addTouchSurfaceGhost) + // Make sure that we have access to the dialog view root to move the drawing to the + // dialog overlay. + decorView.post(::moveSourceDrawingToDialog) return } - // Create a ghost of the touch surface (which will make the touch surface invisible) and add - // it to the host dialog. We trigger a one off synchronization to make sure that this is - // done in sync between the two different windows. + // Move the drawing of the source in the overlay of this dialog, then animate. We trigger a + // one-off synchronization to make sure that this is done in sync between the two different + // windows. synchronizeNextDraw( then = { - isTouchSurfaceGhostDrawn = true + isSourceDrawnInDialog = true maybeStartLaunchAnimation() } ) - GhostView.addGhost(touchSurface, decorView) - - // The ghost of the touch surface was just created, so the touch surface is currently - // invisible. We need to make sure that it stays invisible as long as the dialog is shown or - // animating. - (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true) + controller.startDrawingInOverlayOf(decorView) } /** - * Synchronize the next draw of the touch surface and dialog view roots so that they are - * performed at the same time, in the same transaction. This is necessary to make sure that the - * ghost of the touch surface is drawn at the same time as the touch surface is made invisible - * (or inversely, removed from the UI when the touch surface is made visible). + * Synchronize the next draw of the source and dialog view roots so that they are performed at + * the same time, in the same transaction. This is necessary to make sure that the source is + * drawn in the overlay at the same time as it is removed from its original position (or + * inversely, removed from the overlay when the source is moved back to its original position). */ private fun synchronizeNextDraw(then: () -> Unit) { if (forceDisableSynchronization) { + // Don't synchronize when inside an automated test. then() return } - ViewRootSync.synchronizeNextDraw(touchSurface, decorView, then) + ViewRootSync.synchronizeNextDraw(controller.viewRoot.view, decorView, then) + decorView.invalidate() + controller.viewRoot.view.invalidate() } private fun findFirstViewGroupWithBackground(view: View): ViewGroup? { @@ -681,7 +838,7 @@ private class AnimatedDialog( } private fun maybeStartLaunchAnimation() { - if (!isTouchSurfaceGhostDrawn || !isOriginalDialogViewLaidOut) { + if (!isSourceDrawnInDialog || !isOriginalDialogViewLaidOut) { return } @@ -690,19 +847,7 @@ private class AnimatedDialog( startAnimation( isLaunching = true, - onLaunchAnimationStart = { - // Remove the temporary ghost. Another ghost (that ghosts only the touch surface - // content, and not its background) will be added right after this and will be - // animated. - GhostView.removeGhost(touchSurface) - }, onLaunchAnimationEnd = { - touchSurface.setTag(R.id.tag_launch_animation_running, null) - - // We hide the touch surface when the dialog is showing. We will make this view - // visible again when dismissing the dialog. - touchSurface.visibility = View.INVISIBLE - isLaunching = false // dismiss was called during the animation, dismiss again now to actually dismiss. @@ -718,7 +863,10 @@ private class AnimatedDialog( backgroundLayoutListener ) } - cuj?.run { interactionJankMonitor.end(cujType) } + + if (hasInstrumentedJank) { + interactionJankMonitor.end(controller.cuj!!.cujType) + } } ) } @@ -753,8 +901,8 @@ private class AnimatedDialog( } /** - * Hide the dialog into the touch surface and call [onAnimationFinished] when the animation is - * done (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually + * Hide the dialog into the source and call [onAnimationFinished] when the animation is done + * (passing animationRan=true) or if it's skipped (passing animationRan=false) to actually * dismiss the dialog. */ private fun hideDialogIntoView(onAnimationFinished: (Boolean) -> Unit) { @@ -763,17 +911,9 @@ private class AnimatedDialog( decorView.removeOnLayoutChangeListener(decorViewLayoutListener) } - if (!shouldAnimateDialogIntoView()) { - Log.i(TAG, "Skipping animation of dialog into the touch surface") - - // Make sure we allow the touch surface to change its visibility again. - (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false) - - // If the view is invisible it's probably because of us, so we make it visible again. - if (touchSurface.visibility == View.INVISIBLE) { - touchSurface.visibility = View.VISIBLE - } - + if (!shouldAnimateDialogIntoSource()) { + Log.i(TAG, "Skipping animation of dialog into the source") + controller.onExitAnimationCancelled() onAnimationFinished(false /* instantDismiss */) onDialogDismissed(this@AnimatedDialog) return @@ -786,10 +926,6 @@ private class AnimatedDialog( dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) }, onLaunchAnimationEnd = { - // Make sure we allow the touch surface to change its visibility again. - (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(false) - - touchSurface.visibility = View.VISIBLE val dialogContentWithBackground = this.dialogContentWithBackground!! dialogContentWithBackground.visibility = View.INVISIBLE @@ -799,14 +935,11 @@ private class AnimatedDialog( ) } - // Make sure that the removal of the ghost and making the touch surface visible is - // done at the same time. - synchronizeNextDraw( - then = { - onAnimationFinished(true /* instantDismiss */) - onDialogDismissed(this@AnimatedDialog) - } - ) + controller.stopDrawingInOverlay() + synchronizeNextDraw { + onAnimationFinished(true /* instantDismiss */) + onDialogDismissed(this@AnimatedDialog) + } } ) } @@ -816,27 +949,34 @@ private class AnimatedDialog( onLaunchAnimationStart: () -> Unit = {}, onLaunchAnimationEnd: () -> Unit = {} ) { - // Create 2 ghost controllers to animate both the dialog and the touch surface in the - // dialog. - val startView = if (isLaunching) touchSurface else dialogContentWithBackground!! - val endView = if (isLaunching) dialogContentWithBackground!! else touchSurface - val startViewController = GhostedViewLaunchAnimatorController(startView) - val endViewController = GhostedViewLaunchAnimatorController(endView) - startViewController.launchContainer = decorView - endViewController.launchContainer = decorView - - val endState = endViewController.createAnimatorState() + // Create 2 controllers to animate both the dialog and the source. + val startController = + if (isLaunching) { + controller.createLaunchController() + } else { + GhostedViewLaunchAnimatorController(dialogContentWithBackground!!) + } + val endController = + if (isLaunching) { + GhostedViewLaunchAnimatorController(dialogContentWithBackground!!) + } else { + controller.createExitController() + } + startController.launchContainer = decorView + endController.launchContainer = decorView + + val endState = endController.createAnimatorState() val controller = object : LaunchAnimator.Controller { override var launchContainer: ViewGroup - get() = startViewController.launchContainer + get() = startController.launchContainer set(value) { - startViewController.launchContainer = value - endViewController.launchContainer = value + startController.launchContainer = value + endController.launchContainer = value } override fun createAnimatorState(): LaunchAnimator.State { - return startViewController.createAnimatorState() + return startController.createAnimatorState() } override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { @@ -845,15 +985,29 @@ private class AnimatedDialog( // onLaunchAnimationStart on the controller (which will create its own ghost). onLaunchAnimationStart() - startViewController.onLaunchAnimationStart(isExpandingFullyAbove) - endViewController.onLaunchAnimationStart(isExpandingFullyAbove) + startController.onLaunchAnimationStart(isExpandingFullyAbove) + endController.onLaunchAnimationStart(isExpandingFullyAbove) } override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { - startViewController.onLaunchAnimationEnd(isExpandingFullyAbove) - endViewController.onLaunchAnimationEnd(isExpandingFullyAbove) - - onLaunchAnimationEnd() + // onLaunchAnimationEnd is called by an Animator at the end of the animation, + // on a Choreographer animation tick. The following calls will move the animated + // content from the dialog overlay back to its original position, and this + // change must be reflected in the next frame given that we then sync the next + // frame of both the content and dialog ViewRoots. However, in case that content + // is rendered by Compose, whose compositions are also scheduled on a + // Choreographer frame, any state change made *right now* won't be reflected in + // the next frame given that a Choreographer frame can't schedule another and + // have it happen in the same frame. So we post the forwarded calls to + // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring + // that the move of the content back to its original window will be reflected in + // the next frame right after [onLaunchAnimationEnd] is called. + dialog.context.mainExecutor.execute { + startController.onLaunchAnimationEnd(isExpandingFullyAbove) + endController.onLaunchAnimationEnd(isExpandingFullyAbove) + + onLaunchAnimationEnd() + } } override fun onLaunchAnimationProgress( @@ -861,11 +1015,11 @@ private class AnimatedDialog( progress: Float, linearProgress: Float ) { - startViewController.onLaunchAnimationProgress(state, progress, linearProgress) + startController.onLaunchAnimationProgress(state, progress, linearProgress) // The end view is visible only iff the starting view is not visible. state.visible = !state.visible - endViewController.onLaunchAnimationProgress(state, progress, linearProgress) + endController.onLaunchAnimationProgress(state, progress, linearProgress) // If the dialog content is complex, its dimension might change during the // launch animation. The animation end position might also change during the @@ -873,14 +1027,16 @@ private class AnimatedDialog( // Therefore we update the end state to the new position/size. Usually the // dialog dimension or position will change in the early frames, so changing the // end state shouldn't really be noticeable. - endViewController.fillGhostedViewState(endState) + if (endController is GhostedViewLaunchAnimatorController) { + endController.fillGhostedViewState(endState) + } } } launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor) } - private fun shouldAnimateDialogIntoView(): Boolean { + private fun shouldAnimateDialogIntoSource(): Boolean { // Don't animate if the dialog was previously hidden using hide() or if we disabled the exit // animation. if (exitAnimationDisabled || !dialog.isShowing) { @@ -888,24 +1044,12 @@ private class AnimatedDialog( } // If we are dreaming, the dialog was probably closed because of that so we don't animate - // into the touchSurface. + // into the source. if (callback.isDreaming()) { return false } - // The touch surface should be invisible by now, if it's not then something else changed its - // visibility and we probably don't want to run the animation. - if (touchSurface.visibility != View.INVISIBLE) { - return false - } - - // If the touch surface is not attached or one of its ancestors is not visible, then we - // don't run the animation either. - if (!touchSurface.isAttachedToWindow) { - return false - } - - return (touchSurface.parent as? View)?.isShown ?: true + return controller.shouldAnimateExit() } /** A layout listener to animate the change of bounds of the dialog background. */ @@ -988,17 +1132,13 @@ private class AnimatedDialog( } } - fun prepareForStackDismiss(): View { + fun prepareForStackDismiss() { if (parentAnimatedDialog == null) { - return touchSurface + return } parentAnimatedDialog.exitAnimationDisabled = true parentAnimatedDialog.dialog.hide() - val view = parentAnimatedDialog.prepareForStackDismiss() + parentAnimatedDialog.prepareForStackDismiss() parentAnimatedDialog.dialog.dismiss() - // Make the touch surface invisible, so we end up animating to it when we actually - // dismiss the stack - view.visibility = View.INVISIBLE - return view } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt index 8ce372dbb278..40a5e9794d37 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt @@ -30,7 +30,12 @@ interface Expandable { */ fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller? - // TODO(b/230830644): Introduce DialogLaunchAnimator and a function to expose it here. + /** + * Create a [DialogLaunchAnimator.Controller] that can be used to expand this [Expandable] into + * a Dialog, or return `null` if this [Expandable] should not be animated (e.g. if it is + * currently not attached or visible). + */ + fun dialogLaunchController(cuj: DialogCuj? = null): DialogLaunchAnimator.Controller? companion object { /** @@ -39,6 +44,7 @@ interface Expandable { * Note: The background of [view] should be a (rounded) rectangle so that it can be properly * animated. */ + @JvmStatic fun fromView(view: View): Expandable { return object : Expandable { override fun activityLaunchController( @@ -46,6 +52,12 @@ interface Expandable { ): ActivityLaunchAnimator.Controller? { return ActivityLaunchAnimator.Controller.fromView(view, cujType) } + + override fun dialogLaunchController( + cuj: DialogCuj? + ): DialogLaunchAnimator.Controller? { + return DialogLaunchAnimator.Controller.fromView(view, cuj) + } } } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt index eb000ad312d7..0028d13ffd5e 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt @@ -199,6 +199,10 @@ open class GhostedViewLaunchAnimatorController @JvmOverloads constructor( // the content before fading out the background. ghostView = GhostView.addGhost(ghostedView, launchContainer) + // The ghost was just created, so ghostedView is currently invisible. We need to make sure + // that it stays invisible as long as we are animating. + (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(true) + val matrix = ghostView?.animationMatrix ?: Matrix.IDENTITY_MATRIX matrix.getValues(initialGhostViewMatrixValues) @@ -293,6 +297,7 @@ open class GhostedViewLaunchAnimatorController @JvmOverloads constructor( backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha GhostView.removeGhost(ghostedView) + (ghostedView as? LaunchableView)?.setShouldBlockVisibilityChanges(false) launchContainerOverlay.remove(backgroundView) // Make sure that the view is considered VISIBLE by accessibility by first making it diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index f79b328190dd..5f1bb83715c2 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -89,6 +89,11 @@ class TextAnimator( var y: Float = 0f /** + * The current line of text being drawn, in a multi-line TextView. + */ + var lineNo: Int = 0 + + /** * Mutable text size of the glyph in pixels. */ var textSize: Float = 0f diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt index d427a57f3b87..0448c818f765 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt @@ -244,7 +244,7 @@ class TextInterpolator( canvas.translate(origin, layout.getLineBaseline(lineNo).toFloat()) run.fontRuns.forEach { fontRun -> - drawFontRun(canvas, run, fontRun, tmpPaint) + drawFontRun(canvas, run, fontRun, lineNo, tmpPaint) } } finally { canvas.restore() @@ -349,7 +349,7 @@ class TextInterpolator( var glyphFilter: GlyphCallback? = null // Draws single font run. - private fun drawFontRun(c: Canvas, line: Run, run: FontRun, paint: Paint) { + private fun drawFontRun(c: Canvas, line: Run, run: FontRun, lineNo: Int, paint: Paint) { var arrayIndex = 0 val font = fontInterpolator.lerp(run.baseFont, run.targetFont, progress) @@ -368,11 +368,13 @@ class TextInterpolator( tmpGlyph.font = font tmpGlyph.runStart = run.start tmpGlyph.runLength = run.end - run.start + tmpGlyph.lineNo = lineNo tmpPaintForGlyph.set(paint) var prevStart = run.start for (i in run.start until run.end) { + tmpGlyph.glyphIndex = i tmpGlyph.glyphId = line.glyphIds[i] tmpGlyph.x = MathUtils.lerp(line.baseX[i], line.targetX[i], progress) tmpGlyph.y = MathUtils.lerp(line.baseY[i], line.targetY[i], progress) diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt new file mode 100644 index 000000000000..ecee598afe4e --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation + +import android.view.GhostView +import android.view.View +import android.view.ViewGroup +import android.view.ViewRootImpl +import com.android.internal.jank.InteractionJankMonitor + +/** A [DialogLaunchAnimator.Controller] that can animate a [View] from/to a dialog. */ +class ViewDialogLaunchAnimatorController +internal constructor( + private val source: View, + override val cuj: DialogCuj?, +) : DialogLaunchAnimator.Controller { + override val viewRoot: ViewRootImpl + get() = source.viewRootImpl + + override val sourceIdentity: Any = source + + override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { + // Create a temporary ghost of the source (which will make it invisible) and add it + // to the host dialog. + GhostView.addGhost(source, viewGroup) + + // The ghost of the source was just created, so the source is currently invisible. + // We need to make sure that it stays invisible as long as the dialog is shown or + // animating. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true) + } + + override fun stopDrawingInOverlay() { + // Note: here we should remove the ghost from the overlay, but in practice this is + // already done by the launch controllers created below. + + // Make sure we allow the source to change its visibility again. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) + source.visibility = View.VISIBLE + } + + override fun createLaunchController(): LaunchAnimator.Controller { + val delegate = GhostedViewLaunchAnimatorController(source) + return object : LaunchAnimator.Controller by delegate { + override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another + // ghost (that ghosts only the source content, and not its background) will + // be added right after this by the delegate and will be animated. + GhostView.removeGhost(source) + delegate.onLaunchAnimationStart(isExpandingFullyAbove) + } + + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + + // We hide the source when the dialog is showing. We will make this view + // visible again when dismissing the dialog. This does nothing if the source + // implements [LaunchableView], as it's already INVISIBLE in that case. + source.visibility = View.INVISIBLE + } + } + } + + override fun createExitController(): LaunchAnimator.Controller { + return GhostedViewLaunchAnimatorController(source) + } + + override fun shouldAnimateExit(): Boolean { + // The source should be invisible by now, if it's not then something else changed + // its visibility and we probably don't want to run the animation. + if (source.visibility != View.INVISIBLE) { + return false + } + + return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true) + } + + override fun onExitAnimationCancelled() { + // Make sure we allow the source to change its visibility again. + (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false) + + // If the view is invisible it's probably because of us, so we make it visible + // again. + if (source.visibility == View.INVISIBLE) { + source.visibility = View.VISIBLE + } + } + + override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? { + val type = cuj?.cujType ?: return null + return InteractionJankMonitor.Configuration.Builder.withView(type, source) + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt index dc2c63561d79..58ffef25cb42 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt @@ -360,14 +360,21 @@ class ViewHierarchyAnimator { * [interpolator] and [duration]. * * The end state of the animation is controlled by [destination]. This value can be any of - * the four corners, any of the four edges, or the center of the view. + * the four corners, any of the four edges, or the center of the view. If any margins are + * added on the side(s) of the [destination], the translation of those margins can be + * included by specifying [includeMargins]. + * + * @param onAnimationEnd an optional runnable that will be run once the animation finishes + * successfully. Will not be run if the animation is cancelled. */ @JvmOverloads fun animateRemoval( rootView: View, destination: Hotspot = Hotspot.CENTER, interpolator: Interpolator = DEFAULT_REMOVAL_INTERPOLATOR, - duration: Long = DEFAULT_DURATION + duration: Long = DEFAULT_DURATION, + includeMargins: Boolean = false, + onAnimationEnd: Runnable? = null, ): Boolean { if ( !occupiesSpace( @@ -391,13 +398,28 @@ class ViewHierarchyAnimator { addListener(child, listener, recursive = false) } - // Remove the view so that a layout update is triggered for the siblings and they - // animate to their next position while the view's removal is also animating. - parent.removeView(rootView) - // By adding the view to the overlay, we can animate it while it isn't part of the view - // hierarchy. It is correctly positioned because we have its previous bounds, and we set - // them manually during the animation. - parent.overlay.add(rootView) + val viewHasSiblings = parent.childCount > 1 + if (viewHasSiblings) { + // Remove the view so that a layout update is triggered for the siblings and they + // animate to their next position while the view's removal is also animating. + parent.removeView(rootView) + // By adding the view to the overlay, we can animate it while it isn't part of the + // view hierarchy. It is correctly positioned because we have its previous bounds, + // and we set them manually during the animation. + parent.overlay.add(rootView) + } + // If this view has no siblings, the parent view may shrink to (0,0) size and mess + // up the animation if we immediately remove the view. So instead, we just leave the + // view in the real hierarchy until the animation finishes. + + val endRunnable = Runnable { + if (viewHasSiblings) { + parent.overlay.remove(rootView) + } else { + parent.removeView(rootView) + } + onAnimationEnd?.run() + } val startValues = mapOf( @@ -409,10 +431,12 @@ class ViewHierarchyAnimator { val endValues = processEndValuesForRemoval( destination, + rootView, rootView.left, rootView.top, rootView.right, - rootView.bottom + rootView.bottom, + includeMargins, ) val boundsToAnimate = mutableSetOf<Bound>() @@ -430,7 +454,8 @@ class ViewHierarchyAnimator { endValues, interpolator, duration, - ephemeral = true + ephemeral = true, + endRunnable, ) if (rootView is ViewGroup) { @@ -463,7 +488,6 @@ class ViewHierarchyAnimator { .alpha(0f) .setInterpolator(Interpolators.ALPHA_OUT) .setDuration(duration / 2) - .withEndAction { parent.overlay.remove(rootView) } .start() } } @@ -477,7 +501,6 @@ class ViewHierarchyAnimator { .setInterpolator(Interpolators.ALPHA_OUT) .setDuration(duration / 2) .setStartDelay(duration / 2) - .withEndAction { parent.overlay.remove(rootView) } .start() } @@ -700,70 +723,111 @@ class ViewHierarchyAnimator { * | | -> | | -> | | -> x---x -> x * | | x-------x x-----x * x---------x + * 4) destination=TOP, includeMargins=true (and view has large top margin) + * x---------x + * x---------x + * x---------x x---------x + * x---------x | | + * x---------x | | x---------x + * | | | | + * | | -> x---------x -> -> -> + * | | + * x---------x * ``` */ private fun processEndValuesForRemoval( destination: Hotspot, + rootView: View, left: Int, top: Int, right: Int, - bottom: Int + bottom: Int, + includeMargins: Boolean = false, ): Map<Bound, Int> { - val endLeft = - when (destination) { - Hotspot.CENTER -> (left + right) / 2 - Hotspot.BOTTOM, - Hotspot.BOTTOM_LEFT, - Hotspot.LEFT, - Hotspot.TOP_LEFT, - Hotspot.TOP -> left - Hotspot.TOP_RIGHT, - Hotspot.RIGHT, - Hotspot.BOTTOM_RIGHT -> right - } - val endTop = - when (destination) { - Hotspot.CENTER -> (top + bottom) / 2 - Hotspot.LEFT, - Hotspot.TOP_LEFT, - Hotspot.TOP, - Hotspot.TOP_RIGHT, - Hotspot.RIGHT -> top - Hotspot.BOTTOM_RIGHT, - Hotspot.BOTTOM, - Hotspot.BOTTOM_LEFT -> bottom - } - val endRight = - when (destination) { - Hotspot.CENTER -> (left + right) / 2 - Hotspot.TOP, - Hotspot.TOP_RIGHT, - Hotspot.RIGHT, - Hotspot.BOTTOM_RIGHT, - Hotspot.BOTTOM -> right - Hotspot.BOTTOM_LEFT, - Hotspot.LEFT, - Hotspot.TOP_LEFT -> left - } - val endBottom = - when (destination) { - Hotspot.CENTER -> (top + bottom) / 2 - Hotspot.RIGHT, - Hotspot.BOTTOM_RIGHT, - Hotspot.BOTTOM, - Hotspot.BOTTOM_LEFT, - Hotspot.LEFT -> bottom - Hotspot.TOP_LEFT, - Hotspot.TOP, - Hotspot.TOP_RIGHT -> top - } + val marginAdjustment = + if (includeMargins && + (rootView.layoutParams is ViewGroup.MarginLayoutParams)) { + val marginLp = rootView.layoutParams as ViewGroup.MarginLayoutParams + DimenHolder( + left = marginLp.leftMargin, + top = marginLp.topMargin, + right = marginLp.rightMargin, + bottom = marginLp.bottomMargin + ) + } else { + DimenHolder(0, 0, 0, 0) + } - return mapOf( - Bound.LEFT to endLeft, - Bound.TOP to endTop, - Bound.RIGHT to endRight, - Bound.BOTTOM to endBottom - ) + // These are the end values to use *if* this bound is part of the destination. + val endLeft = left - marginAdjustment.left + val endTop = top - marginAdjustment.top + val endRight = right + marginAdjustment.right + val endBottom = bottom + marginAdjustment.bottom + + // For the below calculations: We need to ensure that the destination bound and the + // bound *opposite* to the destination bound end at the same value, to ensure that the + // view has size 0 for that dimension. + // For example, + // - If destination=TOP, then endTop == endBottom. Left and right stay the same. + // - If destination=RIGHT, then endRight == endLeft. Top and bottom stay the same. + // - If destination=BOTTOM_LEFT, then endBottom == endTop AND endLeft == endRight. + + return when (destination) { + Hotspot.TOP -> mapOf( + Bound.TOP to endTop, + Bound.BOTTOM to endTop, + Bound.LEFT to left, + Bound.RIGHT to right, + ) + Hotspot.TOP_RIGHT -> mapOf( + Bound.TOP to endTop, + Bound.BOTTOM to endTop, + Bound.RIGHT to endRight, + Bound.LEFT to endRight, + ) + Hotspot.RIGHT -> mapOf( + Bound.RIGHT to endRight, + Bound.LEFT to endRight, + Bound.TOP to top, + Bound.BOTTOM to bottom, + ) + Hotspot.BOTTOM_RIGHT -> mapOf( + Bound.BOTTOM to endBottom, + Bound.TOP to endBottom, + Bound.RIGHT to endRight, + Bound.LEFT to endRight, + ) + Hotspot.BOTTOM -> mapOf( + Bound.BOTTOM to endBottom, + Bound.TOP to endBottom, + Bound.LEFT to left, + Bound.RIGHT to right, + ) + Hotspot.BOTTOM_LEFT -> mapOf( + Bound.BOTTOM to endBottom, + Bound.TOP to endBottom, + Bound.LEFT to endLeft, + Bound.RIGHT to endLeft, + ) + Hotspot.LEFT -> mapOf( + Bound.LEFT to endLeft, + Bound.RIGHT to endLeft, + Bound.TOP to top, + Bound.BOTTOM to bottom, + ) + Hotspot.TOP_LEFT -> mapOf( + Bound.TOP to endTop, + Bound.BOTTOM to endTop, + Bound.LEFT to endLeft, + Bound.RIGHT to endLeft, + ) + Hotspot.CENTER -> mapOf( + Bound.LEFT to (endLeft + endRight) / 2, + Bound.RIGHT to (endLeft + endRight) / 2, + Bound.TOP to (endTop + endBottom) / 2, + Bound.BOTTOM to (endTop + endBottom) / 2, + ) + } } /** @@ -1043,4 +1107,12 @@ class ViewHierarchyAnimator { abstract fun setValue(view: View, value: Int) abstract fun getValue(view: View): Int } + + /** Simple data class to hold a set of dimens for left, top, right, bottom. */ + private data class DimenHolder( + val left: Int, + val top: Int, + val right: Int, + val bottom: Int, + ) } diff --git a/packages/SystemUI/checks/Android.bp b/packages/SystemUI/checks/Android.bp index 9671adde4904..40580d29380b 100644 --- a/packages/SystemUI/checks/Android.bp +++ b/packages/SystemUI/checks/Android.bp @@ -47,6 +47,10 @@ java_test_host { "tests/**/*.kt", "tests/**/*.java", ], + data: [ + ":framework", + ":androidx.annotation_annotation", + ], static_libs: [ "SystemUILintChecker", "junit", diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt new file mode 100644 index 000000000000..1d808ba7ee16 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.SdkConstants.CLASS_CONTEXT +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiModifierListOwner +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.getParentOfType + +/** + * Warns if {@code Context.bindService}, {@code Context.bindServiceAsUser}, or {@code + * Context.unbindService} is not called on a {@code WorkerThread} + */ +@Suppress("UnstableApiUsage") +class BindServiceOnMainThreadDetector : Detector(), SourceCodeScanner { + + override fun getApplicableMethodNames(): List<String> { + return listOf("bindService", "bindServiceAsUser", "unbindService") + } + + private fun hasWorkerThreadAnnotation( + context: JavaContext, + annotated: PsiModifierListOwner? + ): Boolean { + return context.evaluator.getAnnotations(annotated, inHierarchy = true).any { uAnnotation -> + uAnnotation.qualifiedName == "androidx.annotation.WorkerThread" + } + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) { + if ( + !hasWorkerThreadAnnotation(context, node.getParentOfType(UMethod::class.java)) && + !hasWorkerThreadAnnotation(context, node.getParentOfType(UClass::class.java)) + ) { + context.report( + ISSUE, + method, + context.getLocation(node), + "This method should be annotated with `@WorkerThread` because " + + "it calls ${method.name}", + ) + } + } + } + + companion object { + @JvmField + val ISSUE: Issue = + Issue.create( + id = "BindServiceOnMainThread", + briefDescription = "Service bound or unbound on main thread", + explanation = + """ + Binding and unbinding services are synchronous calls to `ActivityManager`. \ + They usually take multiple milliseconds to complete. If called on the main \ + thread, it will likely cause missed frames. To fix it, use a `@Background \ + Executor` and annotate the calling method with `@WorkerThread`. + """, + category = Category.PERFORMANCE, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + BindServiceOnMainThreadDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt index 8d48f0957be4..112992913661 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt @@ -16,6 +16,7 @@ package com.android.internal.systemui.lint +import com.android.SdkConstants.CLASS_CONTEXT import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation @@ -48,14 +49,14 @@ class BroadcastSentViaContextDetector : Detector(), SourceCodeScanner { return } - val evaulator = context.evaluator - if (evaulator.isMemberInSubClassOf(method, "android.content.Context")) { + val evaluator = context.evaluator + if (evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) { context.report( ISSUE, method, context.getNameLocation(node), - "Please don't call sendBroadcast/sendBroadcastAsUser directly on " + - "Context, use com.android.systemui.broadcast.BroadcastSender instead." + "`Context.${method.name}()` should be replaced with " + + "`BroadcastSender.${method.name}()`" ) } } @@ -65,14 +66,14 @@ class BroadcastSentViaContextDetector : Detector(), SourceCodeScanner { val ISSUE: Issue = Issue.create( id = "BroadcastSentViaContext", - briefDescription = "Broadcast sent via Context instead of BroadcastSender.", - explanation = - "Broadcast was sent via " + - "Context.sendBroadcast/Context.sendBroadcastAsUser. Please use " + - "BroadcastSender.sendBroadcast/BroadcastSender.sendBroadcastAsUser " + - "which will schedule dispatch of broadcasts on background thread. " + - "Sending broadcasts on main thread causes jank due to synchronous " + - "Binder calls.", + briefDescription = "Broadcast sent via `Context` instead of `BroadcastSender`", + // lint trims indents and converts \ to line continuations + explanation = """ + Broadcasts sent via `Context.sendBroadcast()` or \ + `Context.sendBroadcastAsUser()` will block the main thread and may cause \ + missed frames. Instead, use `BroadcastSender.sendBroadcast()` or \ + `BroadcastSender.sendBroadcastAsUser()` which will schedule and dispatch \ + broadcasts on a background worker thread.""", category = Category.PERFORMANCE, priority = 8, severity = Severity.WARNING, diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt deleted file mode 100644 index a629eeeb0102..000000000000 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/GetMainLooperViaContextDetector.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.systemui.lint - -import com.android.tools.lint.detector.api.Category -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.Implementation -import com.android.tools.lint.detector.api.Issue -import com.android.tools.lint.detector.api.JavaContext -import com.android.tools.lint.detector.api.Scope -import com.android.tools.lint.detector.api.Severity -import com.android.tools.lint.detector.api.SourceCodeScanner -import com.intellij.psi.PsiMethod -import org.jetbrains.uast.UCallExpression - -@Suppress("UnstableApiUsage") -class GetMainLooperViaContextDetector : Detector(), SourceCodeScanner { - - override fun getApplicableMethodNames(): List<String> { - return listOf("getMainThreadHandler", "getMainLooper", "getMainExecutor") - } - - override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { - if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) { - context.report( - ISSUE, - method, - context.getNameLocation(node), - "Please inject a @Main Executor instead." - ) - } - } - - companion object { - @JvmField - val ISSUE: Issue = - Issue.create( - id = "GetMainLooperViaContextDetector", - briefDescription = "Please use idiomatic SystemUI executors, injecting " + - "them via Dagger.", - explanation = "Injecting the @Main Executor is preferred in order to make" + - "dependencies explicit and increase testability. It's much " + - "easier to pass a FakeExecutor on your test ctor than to " + - "deal with loopers in unit tests.", - category = Category.LINT, - priority = 8, - severity = Severity.WARNING, - implementation = Implementation(GetMainLooperViaContextDetector::class.java, - Scope.JAVA_FILE_SCOPE) - ) - } -} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt index 925fae0ebfb4..bab76ab4bce2 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceViaContextDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt @@ -16,6 +16,7 @@ package com.android.internal.systemui.lint +import com.android.SdkConstants.CLASS_CONTEXT import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation @@ -28,20 +29,19 @@ import com.intellij.psi.PsiMethod import org.jetbrains.uast.UCallExpression @Suppress("UnstableApiUsage") -class BindServiceViaContextDetector : Detector(), SourceCodeScanner { +class NonInjectedMainThreadDetector : Detector(), SourceCodeScanner { override fun getApplicableMethodNames(): List<String> { - return listOf("bindService", "bindServiceAsUser", "unbindService") + return listOf("getMainThreadHandler", "getMainLooper", "getMainExecutor") } override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { - if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) { + if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) { context.report( - ISSUE, - method, - context.getNameLocation(node), - "Binding or unbinding services are synchronous calls, please make " + - "sure you're on a @Background Executor." + ISSUE, + method, + context.getNameLocation(node), + "Replace with injected `@Main Executor`." ) } } @@ -50,18 +50,20 @@ class BindServiceViaContextDetector : Detector(), SourceCodeScanner { @JvmField val ISSUE: Issue = Issue.create( - id = "BindServiceViaContextDetector", - briefDescription = "Service bound/unbound via Context, please make sure " + - "you're on a background thread.", + id = "NonInjectedMainThread", + briefDescription = "Main thread usage without dependency injection", explanation = - "Binding or unbinding services are synchronous calls to ActivityManager, " + - "they usually take multiple milliseconds to complete and will make" + - "the caller drop frames. Make sure you're on a @Background Executor.", - category = Category.PERFORMANCE, + """ + Main thread should be injected using the `@Main Executor` instead \ + of using the accessors in `Context`. This is to make the \ + dependencies explicit and increase testability. It's much easier \ + to pass a `FakeExecutor` on test constructors than it is to deal \ + with loopers in unit tests.""", + category = Category.LINT, priority = 8, severity = Severity.WARNING, implementation = - Implementation(BindServiceViaContextDetector::class.java, Scope.JAVA_FILE_SCOPE) + Implementation(NonInjectedMainThreadDetector::class.java, Scope.JAVA_FILE_SCOPE) ) } } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt new file mode 100644 index 000000000000..b62290025437 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.SdkConstants.CLASS_CONTEXT +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +/** Detects usage of Context.getSystemService() and suggests to use an injected instance instead. */ +@Suppress("UnstableApiUsage") +class NonInjectedServiceDetector : Detector(), SourceCodeScanner { + + override fun getApplicableMethodNames(): List<String> { + return listOf("getSystemService", "get") + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + val evaluator = context.evaluator + if ( + !evaluator.isStatic(method) && + method.name == "getSystemService" && + method.containingClass?.qualifiedName == CLASS_CONTEXT + ) { + context.report( + ISSUE, + method, + context.getNameLocation(node), + "Use `@Inject` to get system-level service handles instead of " + + "`Context.getSystemService()`" + ) + } else if ( + evaluator.isStatic(method) && + method.name == "get" && + method.containingClass?.qualifiedName == "android.accounts.AccountManager" + ) { + context.report( + ISSUE, + method, + context.getNameLocation(node), + "Replace `AccountManager.get()` with an injected instance of `AccountManager`" + ) + } + } + + companion object { + @JvmField + val ISSUE: Issue = + Issue.create( + id = "NonInjectedService", + briefDescription = "System service not injected", + explanation = + """ + `Context.getSystemService()` should be avoided because it makes testing \ + difficult. Instead, use an injected service. For example, instead of calling \ + `Context.getSystemService(UserManager.class)` in a class, annotate the class' \ + constructor with `@Inject` and add `UserManager` to the parameters. + """, + category = Category.CORRECTNESS, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation(NonInjectedServiceDetector::class.java, Scope.JAVA_FILE_SCOPE) + ) + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt index b72d03db0617..4ba3afc7f7e2 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt @@ -16,6 +16,7 @@ package com.android.internal.systemui.lint +import com.android.SdkConstants.CLASS_CONTEXT import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation @@ -27,6 +28,7 @@ import com.android.tools.lint.detector.api.SourceCodeScanner import com.intellij.psi.PsiMethod import org.jetbrains.uast.UCallExpression +@Suppress("UnstableApiUsage") class RegisterReceiverViaContextDetector : Detector(), SourceCodeScanner { override fun getApplicableMethodNames(): List<String> { @@ -34,12 +36,12 @@ class RegisterReceiverViaContextDetector : Detector(), SourceCodeScanner { } override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { - if (context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) { + if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) { context.report( ISSUE, method, context.getNameLocation(node), - "BroadcastReceivers should be registered via BroadcastDispatcher." + "Register `BroadcastReceiver` using `BroadcastDispatcher` instead of `Context`" ) } } @@ -48,14 +50,16 @@ class RegisterReceiverViaContextDetector : Detector(), SourceCodeScanner { @JvmField val ISSUE: Issue = Issue.create( - id = "RegisterReceiverViaContextDetector", - briefDescription = "Broadcast registrations via Context are blocking " + - "calls. Please use BroadcastDispatcher.", - explanation = - "Context#registerReceiver is a blocking call to the system server, " + - "making it very likely that you'll drop a frame. Please use " + - "BroadcastDispatcher instead (or move this call to a " + - "@Background Executor.)", + id = "RegisterReceiverViaContext", + briefDescription = "Blocking broadcast registration", + // lint trims indents and converts \ to line continuations + explanation = """ + `Context.registerReceiver()` is a blocking call to the system server, \ + making it very likely that you'll drop a frame. Please use \ + `BroadcastDispatcher` instead, which registers the receiver on a \ + background thread. `BroadcastDispatcher` also improves our visibility \ + into ANRs.""", + moreInfo = "go/identifying-broadcast-threads", category = Category.PERFORMANCE, priority = 8, severity = Severity.WARNING, diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt index b00661575c14..7be21a512f89 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt @@ -49,8 +49,7 @@ class SlowUserQueryDetector : Detector(), SourceCodeScanner { ISSUE_SLOW_USER_ID_QUERY, method, context.getNameLocation(node), - "ActivityManager.getCurrentUser() is slow. " + - "Use UserTracker.getUserId() instead." + "Use `UserTracker.getUserId()` instead of `ActivityManager.getCurrentUser()`" ) } if ( @@ -62,7 +61,7 @@ class SlowUserQueryDetector : Detector(), SourceCodeScanner { ISSUE_SLOW_USER_INFO_QUERY, method, context.getNameLocation(node), - "UserManager.getUserInfo() is slow. " + "Use UserTracker.getUserInfo() instead." + "Use `UserTracker.getUserInfo()` instead of `UserManager.getUserInfo()`" ) } } @@ -72,11 +71,13 @@ class SlowUserQueryDetector : Detector(), SourceCodeScanner { val ISSUE_SLOW_USER_ID_QUERY: Issue = Issue.create( id = "SlowUserIdQuery", - briefDescription = "User ID queried using ActivityManager instead of UserTracker.", + briefDescription = "User ID queried using ActivityManager", explanation = - "ActivityManager.getCurrentUser() makes a binder call and is slow. " + - "Instead, inject a UserTracker and call UserTracker.getUserId(). For " + - "more info, see: http://go/multi-user-in-systemui-slides", + """ + `ActivityManager.getCurrentUser()` uses a blocking binder call and is slow. \ + Instead, inject a `UserTracker` and call `UserTracker.getUserId()`. + """, + moreInfo = "http://go/multi-user-in-systemui-slides", category = Category.PERFORMANCE, priority = 8, severity = Severity.WARNING, @@ -88,11 +89,13 @@ class SlowUserQueryDetector : Detector(), SourceCodeScanner { val ISSUE_SLOW_USER_INFO_QUERY: Issue = Issue.create( id = "SlowUserInfoQuery", - briefDescription = "User info queried using UserManager instead of UserTracker.", + briefDescription = "User info queried using UserManager", explanation = - "UserManager.getUserInfo() makes a binder call and is slow. " + - "Instead, inject a UserTracker and call UserTracker.getUserInfo(). For " + - "more info, see: http://go/multi-user-in-systemui-slides", + """ + `UserManager.getUserInfo()` uses a blocking binder call and is slow. \ + Instead, inject a `UserTracker` and call `UserTracker.getUserInfo()`. + """, + moreInfo = "http://go/multi-user-in-systemui-slides", category = Category.PERFORMANCE, priority = 8, severity = Severity.WARNING, diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt index a584894fed71..4b9aa13c0240 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt @@ -32,7 +32,8 @@ import org.jetbrains.uast.UReferenceExpression class SoftwareBitmapDetector : Detector(), SourceCodeScanner { override fun getApplicableReferenceNames(): List<String> { - return mutableListOf("ALPHA_8", "RGB_565", "ARGB_8888", "RGBA_F16", "RGBA_1010102") + return mutableListOf( + "ALPHA_8", "RGB_565", "ARGB_4444", "ARGB_8888", "RGBA_F16", "RGBA_1010102") } override fun visitReference( @@ -40,14 +41,13 @@ class SoftwareBitmapDetector : Detector(), SourceCodeScanner { reference: UReferenceExpression, referenced: PsiElement ) { - val evaluator = context.evaluator if (evaluator.isMemberInClass(referenced as? PsiField, "android.graphics.Bitmap.Config")) { context.report( ISSUE, referenced, - context.getNameLocation(referenced), - "Usage of Config.HARDWARE is highly encouraged." + context.getNameLocation(reference), + "Replace software bitmap with `Config.HARDWARE`" ) } } @@ -56,12 +56,12 @@ class SoftwareBitmapDetector : Detector(), SourceCodeScanner { @JvmField val ISSUE: Issue = Issue.create( - id = "SoftwareBitmapDetector", - briefDescription = "Software bitmap detected. Please use Config.HARDWARE instead.", - explanation = - "Software bitmaps occupy twice as much memory, when compared to Config.HARDWARE. " + - "In case you need to manipulate the pixels, please consider to either use" + - "a shader (encouraged), or a short lived software bitmap.", + id = "SoftwareBitmap", + briefDescription = "Software bitmap", + explanation = """ + Software bitmaps occupy twice as much memory as `Config.HARDWARE` bitmaps \ + do. However, hardware bitmaps are read-only. If you need to manipulate the \ + pixels, use a shader (preferably) or a short lived software bitmap.""", category = Category.PERFORMANCE, priority = 8, severity = Severity.WARNING, diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt new file mode 100644 index 000000000000..1db072548a76 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +private const val CLASS_SETTINGS = "android.provider.Settings" + +/** + * Detects usage of static methods in android.provider.Settings and suggests to use an injected + * settings provider instance instead. + */ +@Suppress("UnstableApiUsage") +class StaticSettingsProviderDetector : Detector(), SourceCodeScanner { + override fun getApplicableMethodNames(): List<String> { + return listOf( + "getFloat", + "getInt", + "getLong", + "getString", + "getUriFor", + "putFloat", + "putInt", + "putLong", + "putString" + ) + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + val evaluator = context.evaluator + val className = method.containingClass?.qualifiedName + if ( + className != "$CLASS_SETTINGS.Global" && + className != "$CLASS_SETTINGS.Secure" && + className != "$CLASS_SETTINGS.System" + ) { + return + } + if (!evaluator.isStatic(method)) { + return + } + + val subclassName = className.substring(CLASS_SETTINGS.length + 1) + + context.report( + ISSUE, + method, + context.getNameLocation(node), + "`@Inject` a ${subclassName}Settings instead" + ) + } + + companion object { + @JvmField + val ISSUE: Issue = + Issue.create( + id = "StaticSettingsProvider", + briefDescription = "Static settings provider usage", + explanation = + """ + Static settings provider methods, such as `Settings.Global.putInt()`, should \ + not be used because they make testing difficult. Instead, use an injected \ + settings provider. For example, instead of calling `Settings.Secure.getInt()`, \ + annotate the class constructor with `@Inject` and add `SecureSettings` to the \ + parameters. + """, + category = Category.CORRECTNESS, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + StaticSettingsProviderDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt index 4879883e7c2e..3f334c1cdb9c 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt @@ -28,13 +28,15 @@ class SystemUIIssueRegistry : IssueRegistry() { override val issues: List<Issue> get() = listOf( - BindServiceViaContextDetector.ISSUE, + BindServiceOnMainThreadDetector.ISSUE, BroadcastSentViaContextDetector.ISSUE, SlowUserQueryDetector.ISSUE_SLOW_USER_ID_QUERY, SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY, - GetMainLooperViaContextDetector.ISSUE, + NonInjectedMainThreadDetector.ISSUE, RegisterReceiverViaContextDetector.ISSUE, SoftwareBitmapDetector.ISSUE, + NonInjectedServiceDetector.ISSUE, + StaticSettingsProviderDetector.ISSUE ) override val api: Int diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt new file mode 100644 index 000000000000..141dd0535986 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.annotations.NonNull +import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java +import com.android.tools.lint.checks.infrastructure.TestFiles.LibraryReferenceTestFile +import java.io.File +import org.intellij.lang.annotations.Language + +@Suppress("UnstableApiUsage") +@NonNull +private fun indentedJava(@NonNull @Language("JAVA") source: String) = java(source).indented() + +/* + * This file contains stubs of framework APIs and System UI classes for testing purposes only. The + * stubs are not used in the lint detectors themselves. + */ +internal val androidStubs = + arrayOf( + LibraryReferenceTestFile(File("framework.jar").canonicalFile), + LibraryReferenceTestFile(File("androidx.annotation_annotation.jar").canonicalFile), + indentedJava( + """ +package com.android.systemui.settings; +import android.content.pm.UserInfo; + +public interface UserTracker { + int getUserId(); + UserInfo getUserInfo(); +} +""" + ), + ) diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt new file mode 100644 index 000000000000..c35ac61a6543 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +@Suppress("UnstableApiUsage") +class BindServiceOnMainThreadDetectorTest : SystemUILintDetectorTest() { + + override fun getDetector(): Detector = BindServiceOnMainThreadDetector() + + override fun getIssues(): List<Issue> = listOf(BindServiceOnMainThreadDetector.ISSUE) + + @Test + fun testBindService() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + + public class TestClass { + public void bind(Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW); + context.bindService(intent, null, 0); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BindServiceOnMainThreadDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: This method should be annotated with @WorkerThread because it calls bindService [BindServiceOnMainThread] + context.bindService(intent, null, 0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testBindServiceAsUser() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.os.UserHandle; + + public class TestClass { + public void bind(Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW); + context.bindServiceAsUser(intent, null, 0, UserHandle.ALL); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BindServiceOnMainThreadDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:8: Warning: This method should be annotated with @WorkerThread because it calls bindServiceAsUser [BindServiceOnMainThread] + context.bindServiceAsUser(intent, null, 0, UserHandle.ALL); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testUnbindService() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.content.ServiceConnection; + + public class TestClass { + public void unbind(Context context, ServiceConnection connection) { + context.unbindService(connection); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BindServiceOnMainThreadDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: This method should be annotated with @WorkerThread because it calls unbindService [BindServiceOnMainThread] + context.unbindService(connection); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testWorkerMethod() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.content.ServiceConnection; + import androidx.annotation.WorkerThread; + + public class TestClass { + @WorkerThread + public void unbind(Context context, ServiceConnection connection) { + context.unbindService(connection); + } + } + + public class ChildTestClass extends TestClass { + @Override + public void unbind(Context context, ServiceConnection connection) { + context.unbindService(connection); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BindServiceOnMainThreadDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testWorkerClass() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.content.ServiceConnection; + import androidx.annotation.WorkerThread; + + @WorkerThread + public class TestClass { + public void unbind(Context context, ServiceConnection connection) { + context.unbindService(connection); + } + } + + public class ChildTestClass extends TestClass { + @Override + public void unbind(Context context, ServiceConnection connection) { + context.unbindService(connection); + } + + public void bind(Context context, ServiceConnection connection) { + context.bind(connection); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BindServiceOnMainThreadDetector.ISSUE) + .run() + .expectClean() + } + + private val stubs = androidStubs +} diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt new file mode 100644 index 000000000000..376acb56fac9 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +@Suppress("UnstableApiUsage") +class BroadcastSentViaContextDetectorTest : SystemUILintDetectorTest() { + + override fun getDetector(): Detector = BroadcastSentViaContextDetector() + + override fun getIssues(): List<Issue> = listOf(BroadcastSentViaContextDetector.ISSUE) + + @Test + fun testSendBroadcast() { + println(stubs.size) + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + + public class TestClass { + public void send(Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW); + context.sendBroadcast(intent); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BroadcastSentViaContextDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Context.sendBroadcast() should be replaced with BroadcastSender.sendBroadcast() [BroadcastSentViaContext] + context.sendBroadcast(intent); + ~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testSendBroadcastAsUser() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.os.UserHandle; + + public class TestClass { + public void send(Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW); + context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BroadcastSentViaContextDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:8: Warning: Context.sendBroadcastAsUser() should be replaced with BroadcastSender.sendBroadcastAsUser() [BroadcastSentViaContext] + context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); + ~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testSendBroadcastInActivity() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.app.Activity; + import android.os.UserHandle; + + public class TestClass { + public void send(Activity activity) { + Intent intent = new Intent(Intent.ACTION_VIEW); + activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); + } + + } + """ + ) + .indented(), + *stubs + ) + .issues(BroadcastSentViaContextDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:8: Warning: Context.sendBroadcastAsUser() should be replaced with BroadcastSender.sendBroadcastAsUser() [BroadcastSentViaContext] + activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); + ~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testSendBroadcastInBroadcastSender() { + lint() + .files( + TestFiles.java( + """ + package com.android.systemui.broadcast; + import android.app.Activity; + import android.os.UserHandle; + + public class BroadcastSender { + public void send(Activity activity) { + Intent intent = new Intent(Intent.ACTION_VIEW); + activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); + } + + } + """ + ) + .indented(), + *stubs + ) + .issues(BroadcastSentViaContextDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testNoopIfNoCall() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + + public class TestClass { + public void sendBroadcast() { + Intent intent = new Intent(Intent.ACTION_VIEW); + context.startActivity(intent); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(BroadcastSentViaContextDetector.ISSUE) + .run() + .expectClean() + } + + private val stubs = androidStubs +} diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt new file mode 100644 index 000000000000..301c338f9b42 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +@Suppress("UnstableApiUsage") +class NonInjectedMainThreadDetectorTest : SystemUILintDetectorTest() { + + override fun getDetector(): Detector = NonInjectedMainThreadDetector() + + override fun getIssues(): List<Issue> = listOf(NonInjectedMainThreadDetector.ISSUE) + + @Test + fun testGetMainThreadHandler() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.os.Handler; + + public class TestClass { + public void test(Context context) { + Handler mainThreadHandler = context.getMainThreadHandler(); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(NonInjectedMainThreadDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread] + Handler mainThreadHandler = context.getMainThreadHandler(); + ~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testGetMainLooper() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.os.Looper; + + public class TestClass { + public void test(Context context) { + Looper mainLooper = context.getMainLooper(); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(NonInjectedMainThreadDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread] + Looper mainLooper = context.getMainLooper(); + ~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testGetMainExecutor() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import java.util.concurrent.Executor; + + public class TestClass { + public void test(Context context) { + Executor mainExecutor = context.getMainExecutor(); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(NonInjectedMainThreadDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Replace with injected @Main Executor. [NonInjectedMainThread] + Executor mainExecutor = context.getMainExecutor(); + ~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + private val stubs = androidStubs +} diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt new file mode 100644 index 000000000000..0a74bfcfee57 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +@Suppress("UnstableApiUsage") +class NonInjectedServiceDetectorTest : SystemUILintDetectorTest() { + + override fun getDetector(): Detector = NonInjectedServiceDetector() + override fun getIssues(): List<Issue> = listOf(NonInjectedServiceDetector.ISSUE) + + @Test + fun testGetServiceWithString() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + + public class TestClass { + public void getSystemServiceWithoutDagger(Context context) { + context.getSystemService("user"); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(NonInjectedServiceDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Use @Inject to get system-level service handles instead of Context.getSystemService() [NonInjectedService] + context.getSystemService("user"); + ~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testGetServiceWithClass() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.os.UserManager; + + public class TestClass { + public void getSystemServiceWithoutDagger(Context context) { + context.getSystemService(UserManager.class); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(NonInjectedServiceDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Use @Inject to get system-level service handles instead of Context.getSystemService() [NonInjectedService] + context.getSystemService(UserManager.class); + ~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testGetAccountManager() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + import android.accounts.AccountManager; + + public class TestClass { + public void getSystemServiceWithoutDagger(Context context) { + AccountManager.get(context); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(NonInjectedServiceDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Replace AccountManager.get() with an injected instance of AccountManager [NonInjectedService] + AccountManager.get(context); + ~~~ + 0 errors, 1 warnings + """ + ) + } + + private val stubs = androidStubs +} diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt index 76c0519d917d..9ed7aa029b1d 100644 --- a/packages/SystemUI/checks/tests/com/android/systemui/lint/RegisterReceiverViaContextDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt @@ -16,27 +16,22 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.checks.infrastructure.TestFile import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test -class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { +@Suppress("UnstableApiUsage") +class RegisterReceiverViaContextDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = RegisterReceiverViaContextDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) - override fun getIssues(): List<Issue> = listOf( - RegisterReceiverViaContextDetector.ISSUE) - - private val explanation = "BroadcastReceivers should be registered via BroadcastDispatcher." + override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE) @Test fun testRegisterReceiver() { - lint().files( + lint() + .files( TestFiles.java( """ package test.pkg; @@ -44,24 +39,33 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { import android.content.Context; import android.content.IntentFilter; - public class TestClass1 { + public class TestClass { public void bind(Context context, BroadcastReceiver receiver, IntentFilter filter) { context.registerReceiver(receiver, filter, 0); } } """ - ).indented(), - *stubs) - .issues(RegisterReceiverViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) + ) + .indented(), + *stubs + ) + .issues(RegisterReceiverViaContextDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:9: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext] + context.registerReceiver(receiver, filter, 0); + ~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) } @Test fun testRegisterReceiverAsUser() { - lint().files( + lint() + .files( TestFiles.java( """ package test.pkg; @@ -71,7 +75,7 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { import android.os.Handler; import android.os.UserHandle; - public class TestClass1 { + public class TestClass { public void bind(Context context, BroadcastReceiver receiver, IntentFilter filter, Handler handler) { context.registerReceiverAsUser(receiver, UserHandle.ALL, filter, @@ -79,17 +83,26 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs) - .issues(RegisterReceiverViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) + ) + .indented(), + *stubs + ) + .issues(RegisterReceiverViaContextDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:11: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext] + context.registerReceiverAsUser(receiver, UserHandle.ALL, filter, + ~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) } @Test fun testRegisterReceiverForAllUsers() { - lint().files( + lint() + .files( TestFiles.java( """ package test.pkg; @@ -99,7 +112,7 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { import android.os.Handler; import android.os.UserHandle; - public class TestClass1 { + public class TestClass { public void bind(Context context, BroadcastReceiver receiver, IntentFilter filter, Handler handler) { context.registerReceiverForAllUsers(receiver, filter, "permission", @@ -107,65 +120,21 @@ class RegisterReceiverViaContextDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs) - .issues(RegisterReceiverViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) + ) + .indented(), + *stubs + ) + .issues(RegisterReceiverViaContextDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:11: Warning: Register BroadcastReceiver using BroadcastDispatcher instead of Context [RegisterReceiverViaContext] + context.registerReceiverForAllUsers(receiver, filter, "permission", + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) } - private val contextStub: TestFile = java( - """ - package android.content; - import android.os.Handler; - import android.os.UserHandle; - - public class Context { - public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter, - int flags) {}; - public void registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user, - IntentFilter filter, String broadcastPermission, Handler scheduler) {}; - public void registerReceiverForAllUsers(BroadcastReceiver receiver, IntentFilter filter, - String broadcastPermission, Handler scheduler) {}; - } - """ - ) - - private val broadcastReceiverStub: TestFile = java( - """ - package android.content; - - public class BroadcastReceiver {} - """ - ) - - private val intentFilterStub: TestFile = java( - """ - package android.content; - - public class IntentFilter {} - """ - ) - - private val handlerStub: TestFile = java( - """ - package android.os; - - public class Handler {} - """ - ) - - private val userHandleStub: TestFile = java( - """ - package android.os; - - public enum UserHandle { - ALL - } - """ - ) - - private val stubs = arrayOf(contextStub, broadcastReceiverStub, intentFilterStub, handlerStub, - userHandleStub) + private val stubs = androidStubs } diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt index 2738f0409fd0..54cac7b35598 100644 --- a/packages/SystemUI/checks/tests/com/android/systemui/lint/SlowUserQueryDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt @@ -1,17 +1,30 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.checks.infrastructure.TestFile import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test -class SlowUserQueryDetectorTest : LintDetectorTest() { +@Suppress("UnstableApiUsage") +class SlowUserQueryDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = SlowUserQueryDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf( @@ -28,7 +41,7 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { package test.pkg; import android.app.ActivityManager; - public class TestClass1 { + public class TestClass { public void slewlyGetCurrentUser() { ActivityManager.getCurrentUser(); } @@ -43,10 +56,13 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY ) .run() - .expectWarningCount(1) - .expectContains( - "ActivityManager.getCurrentUser() is slow. " + - "Use UserTracker.getUserId() instead." + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Use UserTracker.getUserId() instead of ActivityManager.getCurrentUser() [SlowUserIdQuery] + ActivityManager.getCurrentUser(); + ~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ ) } @@ -59,7 +75,7 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { package test.pkg; import android.os.UserManager; - public class TestClass2 { + public class TestClass { public void slewlyGetUserInfo(UserManager userManager) { userManager.getUserInfo(); } @@ -74,9 +90,13 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY ) .run() - .expectWarningCount(1) - .expectContains( - "UserManager.getUserInfo() is slow. " + "Use UserTracker.getUserInfo() instead." + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Use UserTracker.getUserInfo() instead of UserManager.getUserInfo() [SlowUserInfoQuery] + userManager.getUserInfo(); + ~~~~~~~~~~~ + 0 errors, 1 warnings + """ ) } @@ -89,7 +109,7 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { package test.pkg; import com.android.systemui.settings.UserTracker; - public class TestClass3 { + public class TestClass { public void quicklyGetUserId(UserTracker userTracker) { userTracker.getUserId(); } @@ -116,7 +136,7 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { package test.pkg; import com.android.systemui.settings.UserTracker; - public class TestClass4 { + public class TestClass { public void quicklyGetUserId(UserTracker userTracker) { userTracker.getUserInfo(); } @@ -134,61 +154,5 @@ class SlowUserQueryDetectorTest : LintDetectorTest() { .expectClean() } - private val activityManagerStub: TestFile = - java( - """ - package android.app; - - public class ActivityManager { - public static int getCurrentUser() {}; - } - """ - ) - - private val userManagerStub: TestFile = - java( - """ - package android.os; - import android.content.pm.UserInfo; - import android.annotation.UserIdInt; - - public class UserManager { - public UserInfo getUserInfo(@UserIdInt int userId) {}; - } - """ - ) - - private val userIdIntStub: TestFile = - java( - """ - package android.annotation; - - public @interface UserIdInt {} - """ - ) - - private val userInfoStub: TestFile = - java( - """ - package android.content.pm; - - public class UserInfo {} - """ - ) - - private val userTrackerStub: TestFile = - java( - """ - package com.android.systemui.settings; - import android.content.pm.UserInfo; - - public interface UserTracker { - public int getUserId(); - public UserInfo getUserInfo(); - } - """ - ) - - private val stubs = - arrayOf(activityManagerStub, userManagerStub, userIdIntStub, userInfoStub, userTrackerStub) + private val stubs = androidStubs } diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt index 890f2b8eb924..c632636eb9c8 100644 --- a/packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt @@ -16,82 +16,73 @@ package com.android.internal.systemui.lint -import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.checks.infrastructure.TestFile import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Issue import org.junit.Test @Suppress("UnstableApiUsage") -class SoftwareBitmapDetectorTest : LintDetectorTest() { +class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() { override fun getDetector(): Detector = SoftwareBitmapDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE) - private val explanation = "Usage of Config.HARDWARE is highly encouraged." - @Test fun testSoftwareBitmap() { - lint().files( + lint() + .files( TestFiles.java( """ import android.graphics.Bitmap; - public class TestClass1 { + public class TestClass { public void test() { Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565); Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); } } """ - ).indented(), - *stubs) - .issues(SoftwareBitmapDetector.ISSUE) - .run() - .expectWarningCount(2) - .expectContains(explanation) + ) + .indented(), + *stubs + ) + .issues(SoftwareBitmapDetector.ISSUE) + .run() + .expect( + """ + src/TestClass.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565); + ~~~~~~~ + src/TestClass.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap] + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888); + ~~~~~~~~~ + 0 errors, 2 warnings + """ + ) } @Test fun testHardwareBitmap() { - lint().files( + lint() + .files( TestFiles.java( - """ + """ import android.graphics.Bitmap; - public class TestClass1 { + public class TestClass { public void test() { Bitmap.createBitmap(300, 300, Bitmap.Config.HARDWARE); } } """ - ).indented(), - *stubs) - .issues(SoftwareBitmapDetector.ISSUE) - .run() - .expectWarningCount(0) + ), + *stubs + ) + .issues(SoftwareBitmapDetector.ISSUE) + .run() + .expectClean() } - private val bitmapStub: TestFile = java( - """ - package android.graphics; - - public class Bitmap { - public enum Config { - ARGB_8888, - RGB_565, - HARDWARE - } - public static Bitmap createBitmap(int width, int height, Config config) { - return null; - } - } - """ - ) - - private val stubs = arrayOf(bitmapStub) + private val stubs = androidStubs } diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt new file mode 100644 index 000000000000..b83ed7067bc3 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +@Suppress("UnstableApiUsage") +class StaticSettingsProviderDetectorTest : SystemUILintDetectorTest() { + + override fun getDetector(): Detector = StaticSettingsProviderDetector() + override fun getIssues(): List<Issue> = listOf(StaticSettingsProviderDetector.ISSUE) + + @Test + fun testGetServiceWithString() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + + import android.provider.Settings; + import android.provider.Settings.Global; + import android.provider.Settings.Secure; + + public class TestClass { + public void getSystemServiceWithoutDagger(Context context) { + final ContentResolver cr = mContext.getContentResolver(); + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND); + Global.getInt(cr, Settings.Global.UNLOCK_SOUND); + Global.getLong(cr, Settings.Global.UNLOCK_SOUND); + Global.getString(cr, Settings.Global.UNLOCK_SOUND); + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1); + Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1"); + Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1); + Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT); + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1"); + Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(StaticSettingsProviderDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:10: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~~~ + src/test/pkg/TestClass.java:11: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getInt(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~ + src/test/pkg/TestClass.java:12: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getLong(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~~ + src/test/pkg/TestClass.java:13: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getString(cr, Settings.Global.UNLOCK_SOUND); + ~~~~~~~~~ + src/test/pkg/TestClass.java:14: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:15: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1); + ~~~~~~ + src/test/pkg/TestClass.java:16: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:17: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:18: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:19: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1); + ~~~~~~ + src/test/pkg/TestClass.java:20: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:21: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider] + Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:23: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~~~ + src/test/pkg/TestClass.java:24: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~ + src/test/pkg/TestClass.java:25: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~~ + src/test/pkg/TestClass.java:26: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED); + ~~~~~~~~~ + src/test/pkg/TestClass.java:27: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:28: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + ~~~~~~ + src/test/pkg/TestClass.java:29: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:30: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:31: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:32: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1); + ~~~~~~ + src/test/pkg/TestClass.java:33: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:34: Warning: @Inject a SecureSettings instead [StaticSettingsProvider] + Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:36: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~~~ + src/test/pkg/TestClass.java:37: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~ + src/test/pkg/TestClass.java:38: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~~ + src/test/pkg/TestClass.java:39: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT); + ~~~~~~~~~ + src/test/pkg/TestClass.java:40: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:41: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + ~~~~~~ + src/test/pkg/TestClass.java:42: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:43: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1"); + ~~~~~~~~~ + src/test/pkg/TestClass.java:44: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f); + ~~~~~~~~ + src/test/pkg/TestClass.java:45: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1); + ~~~~~~ + src/test/pkg/TestClass.java:46: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L); + ~~~~~~~ + src/test/pkg/TestClass.java:47: Warning: @Inject a SystemSettings instead [StaticSettingsProvider] + Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1"); + ~~~~~~~~~ + 0 errors, 36 warnings + """ + ) + } + + private val stubs = androidStubs +} diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt new file mode 100644 index 000000000000..3f93f075fe8b --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt @@ -0,0 +1,48 @@ +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestLintTask +import java.io.File +import org.junit.ClassRule +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.model.Statement + +@Suppress("UnstableApiUsage") +@RunWith(JUnit4::class) +abstract class SystemUILintDetectorTest : LintDetectorTest() { + + companion object { + @ClassRule + @JvmField + val libraryChecker: LibraryExists = + LibraryExists("framework.jar", "androidx.annotation_annotation.jar") + } + + class LibraryExists(vararg val libraryNames: String) : TestRule { + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + for (libName in libraryNames) { + val libFile = File(libName) + if (!libFile.canonicalFile.exists()) { + throw Exception( + "Could not find $libName in the test's working directory. " + + "File ${libFile.absolutePath} does not exist." + ) + } + } + base.evaluate() + } + } + } + } + /** + * Customize the lint task to disable SDK usage completely. This ensures that running the tests + * in Android Studio has the same result as running the tests in atest + */ + override fun lint(): TestLintTask = + super.lint().allowMissingSdk(true).sdkHome(File("/dev/null")) +} diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/BindServiceViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/systemui/lint/BindServiceViaContextDetectorTest.kt deleted file mode 100644 index bf685f7c178e..000000000000 --- a/packages/SystemUI/checks/tests/com/android/systemui/lint/BindServiceViaContextDetectorTest.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.systemui.lint - -import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.checks.infrastructure.TestFile -import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.Issue -import org.junit.Test - -class BindServiceViaContextDetectorTest : LintDetectorTest() { - - override fun getDetector(): Detector = BindServiceViaContextDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) - - override fun getIssues(): List<Issue> = listOf( - BindServiceViaContextDetector.ISSUE) - - private val explanation = "Binding or unbinding services are synchronous calls" - - @Test - fun testBindService() { - lint().files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - - public class TestClass1 { - public void bind(Context context) { - Intent intent = new Intent(Intent.ACTION_VIEW); - context.bindService(intent, null, 0); - } - } - """ - ).indented(), - *stubs) - .issues(BindServiceViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) - } - - @Test - fun testBindServiceAsUser() { - lint().files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - import android.os.UserHandle; - - public class TestClass1 { - public void bind(Context context) { - Intent intent = new Intent(Intent.ACTION_VIEW); - context.bindServiceAsUser(intent, null, 0, UserHandle.ALL); - } - } - """ - ).indented(), - *stubs) - .issues(BindServiceViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) - } - - @Test - fun testUnbindService() { - lint().files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - import android.content.ServiceConnection; - - public class TestClass1 { - public void unbind(Context context, ServiceConnection connection) { - context.unbindService(connection); - } - } - """ - ).indented(), - *stubs) - .issues(BindServiceViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) - } - - private val contextStub: TestFile = java( - """ - package android.content; - import android.os.UserHandle; - - public class Context { - public void bindService(Intent intent) {}; - public void bindServiceAsUser(Intent intent, ServiceConnection connection, int flags, - UserHandle userHandle) {}; - public void unbindService(ServiceConnection connection) {}; - } - """ - ) - - private val serviceConnectionStub: TestFile = java( - """ - package android.content; - - public class ServiceConnection {} - """ - ) - - private val userHandleStub: TestFile = java( - """ - package android.os; - - public enum UserHandle { - ALL - } - """ - ) - - private val stubs = arrayOf(contextStub, serviceConnectionStub, userHandleStub) -} diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/systemui/lint/BroadcastSentViaContextDetectorTest.kt deleted file mode 100644 index da010212f211..000000000000 --- a/packages/SystemUI/checks/tests/com/android/systemui/lint/BroadcastSentViaContextDetectorTest.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.android.internal.systemui.lint - -import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestFile -import com.android.tools.lint.checks.infrastructure.TestLintTask -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.Issue -import org.junit.Test - -class BroadcastSentViaContextDetectorTest : LintDetectorTest() { - - override fun getDetector(): Detector = BroadcastSentViaContextDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) - - override fun getIssues(): List<Issue> = listOf( - BroadcastSentViaContextDetector.ISSUE) - - @Test - fun testSendBroadcast() { - lint().files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - - public class TestClass1 { - public void send(Context context) { - Intent intent = new Intent(Intent.ACTION_VIEW); - context.sendBroadcast(intent); - } - } - """ - ).indented(), - *stubs) - .issues(BroadcastSentViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains( - "Please don't call sendBroadcast/sendBroadcastAsUser directly on " + - "Context, use com.android.systemui.broadcast.BroadcastSender instead.") - } - - @Test - fun testSendBroadcastAsUser() { - lint().files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - import android.os.UserHandle; - - public class TestClass1 { - public void send(Context context) { - Intent intent = new Intent(Intent.ACTION_VIEW); - context.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); - } - } - """).indented(), - *stubs) - .issues(BroadcastSentViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains( - "Please don't call sendBroadcast/sendBroadcastAsUser directly on " + - "Context, use com.android.systemui.broadcast.BroadcastSender instead.") - } - - @Test - fun testSendBroadcastInActivity() { - lint().files( - TestFiles.java( - """ - package test.pkg; - import android.app.Activity; - import android.os.UserHandle; - - public class TestClass1 { - public void send(Activity activity) { - Intent intent = new Intent(Intent.ACTION_VIEW); - activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission"); - } - - } - """).indented(), - *stubs) - .issues(BroadcastSentViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains( - "Please don't call sendBroadcast/sendBroadcastAsUser directly on " + - "Context, use com.android.systemui.broadcast.BroadcastSender instead.") - } - - @Test - fun testNoopIfNoCall() { - lint().files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - - public class TestClass1 { - public void sendBroadcast() { - Intent intent = new Intent(Intent.ACTION_VIEW); - context.startActivity(intent); - } - } - """).indented(), - *stubs) - .issues(BroadcastSentViaContextDetector.ISSUE) - .run() - .expectClean() - } - - private val contextStub: TestFile = java( - """ - package android.content; - import android.os.UserHandle; - - public class Context { - public void sendBroadcast(Intent intent) {}; - public void sendBroadcast(Intent intent, String receiverPermission) {}; - public void sendBroadcastAsUser(Intent intent, UserHandle userHandle, - String permission) {}; - } - """ - ) - - private val activityStub: TestFile = java( - """ - package android.app; - import android.content.Context; - - public class Activity extends Context {} - """ - ) - - private val userHandleStub: TestFile = java( - """ - package android.os; - - public enum UserHandle { - ALL - } - """ - ) - - private val stubs = arrayOf(contextStub, activityStub, userHandleStub) -} diff --git a/packages/SystemUI/checks/tests/com/android/systemui/lint/GetMainLooperViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/systemui/lint/GetMainLooperViaContextDetectorTest.kt deleted file mode 100644 index ec761cd7660d..000000000000 --- a/packages/SystemUI/checks/tests/com/android/systemui/lint/GetMainLooperViaContextDetectorTest.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.systemui.lint - -import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.checks.infrastructure.TestFile -import com.android.tools.lint.checks.infrastructure.TestFiles -import com.android.tools.lint.checks.infrastructure.TestLintTask -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.Issue -import org.junit.Test - -class GetMainLooperViaContextDetectorTest : LintDetectorTest() { - - override fun getDetector(): Detector = GetMainLooperViaContextDetector() - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) - - override fun getIssues(): List<Issue> = listOf(GetMainLooperViaContextDetector.ISSUE) - - private val explanation = "Please inject a @Main Executor instead." - - @Test - fun testGetMainThreadHandler() { - lint().files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - import android.os.Handler; - - public class TestClass1 { - public void test(Context context) { - Handler mainThreadHandler = context.getMainThreadHandler(); - } - } - """ - ).indented(), - *stubs) - .issues(GetMainLooperViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) - } - - @Test - fun testGetMainLooper() { - lint().files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - import android.os.Looper; - - public class TestClass1 { - public void test(Context context) { - Looper mainLooper = context.getMainLooper(); - } - } - """ - ).indented(), - *stubs) - .issues(GetMainLooperViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) - } - - @Test - fun testGetMainExecutor() { - lint().files( - TestFiles.java( - """ - package test.pkg; - import android.content.Context; - import java.util.concurrent.Executor; - - public class TestClass1 { - public void test(Context context) { - Executor mainExecutor = context.getMainExecutor(); - } - } - """ - ).indented(), - *stubs) - .issues(GetMainLooperViaContextDetector.ISSUE) - .run() - .expectWarningCount(1) - .expectContains(explanation) - } - - private val contextStub: TestFile = java( - """ - package android.content; - import android.os.Handler;import android.os.Looper;import java.util.concurrent.Executor; - - public class Context { - public Looper getMainLooper() { return null; }; - public Executor getMainExecutor() { return null; }; - public Handler getMainThreadHandler() { return null; }; - } - """ - ) - - private val looperStub: TestFile = java( - """ - package android.os; - - public class Looper {} - """ - ) - - private val handlerStub: TestFile = java( - """ - package android.os; - - public class Handler {} - """ - ) - - private val stubs = arrayOf(contextStub, looperStub, handlerStub) -} diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp index 4cfe39225a9b..fbdb526d6b31 100644 --- a/packages/SystemUI/compose/core/Android.bp +++ b/packages/SystemUI/compose/core/Android.bp @@ -30,8 +30,11 @@ android_library { ], static_libs: [ + "SystemUIAnimationLib", + "androidx.compose.runtime_runtime", "androidx.compose.material3_material3", + "androidx.savedstate_savedstate", ], kotlincflags: ["-Xjvm-default=all"], diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt new file mode 100644 index 000000000000..edbd68400f83 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.compose.animation + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroupOverlay +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCompositionContext +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.Density +import androidx.lifecycle.ViewTreeLifecycleOwner +import androidx.lifecycle.ViewTreeViewModelStoreOwner +import androidx.savedstate.findViewTreeSavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.android.systemui.animation.LaunchAnimator +import kotlin.math.min + +/** + * Create an expandable shape that can launch into an Activity or a Dialog. + * + * Example: + * ``` + * Expandable( + * color = MaterialTheme.colorScheme.primary, + * shape = RoundedCornerShape(16.dp), + * ) { controller -> + * Row( + * Modifier + * // For activities: + * .clickable { activityStarter.startActivity(intent, controller.forActivity()) } + * + * // For dialogs: + * .clickable { dialogLaunchAnimator.show(dialog, controller.forDialog()) } + * ) { ... } + * } + * ``` + * + * @sample com.android.systemui.compose.gallery.ActivityLaunchScreen + * @sample com.android.systemui.compose.gallery.DialogLaunchScreen + */ +@Composable +fun Expandable( + color: Color, + shape: Shape, + modifier: Modifier = Modifier, + contentColor: Color = contentColorFor(color), + content: @Composable (ExpandableController) -> Unit, +) { + Expandable( + rememberExpandableController(color, shape, contentColor), + modifier, + content, + ) +} + +/** + * Create an expandable shape that can launch into an Activity or a Dialog. + * + * This overload can be used in cases where you need to create the [ExpandableController] before + * composing this [Expandable], for instance if something outside of this Expandable can trigger a + * launch animation + * + * Example: + * ``` + * // The controller that you can use to trigger the animations from anywhere. + * val controller = + * rememberExpandableController( + * color = MaterialTheme.colorScheme.primary, + * shape = RoundedCornerShape(16.dp), + * ) + * + * Expandable(controller) { + * ... + * } + * ``` + * + * @sample com.android.systemui.compose.gallery.ActivityLaunchScreen + * @sample com.android.systemui.compose.gallery.DialogLaunchScreen + */ +@Composable +fun Expandable( + controller: ExpandableController, + modifier: Modifier = Modifier, + content: @Composable (ExpandableController) -> Unit, +) { + val controller = controller as ExpandableControllerImpl + val color = controller.color + val contentColor = controller.contentColor + val shape = controller.shape + + // TODO(b/230830644): Use movableContentOf to preserve the content state instead once the + // Compose libraries have been updated and include aosp/2163631. + val wrappedContent = + @Composable { controller: ExpandableController -> + CompositionLocalProvider( + LocalContentColor provides contentColor, + ) { + content(controller) + } + } + + val thisExpandableSize by remember { + derivedStateOf { controller.boundsInComposeViewRoot.value.size } + } + + // Make sure we don't read animatorState directly here to avoid recomposition every time the + // state changes (i.e. every frame of the animation). + val isAnimating by remember { + derivedStateOf { + controller.animatorState.value != null && controller.overlay.value != null + } + } + + when { + isAnimating -> { + // Don't compose the movable content during the animation, as it should be composed only + // once at all times. We make this spacer exactly the same size as this Expandable when + // it is visible. + Spacer( + modifier + .clip(shape) + .requiredSize(with(controller.density) { thisExpandableSize.toDpSize() }) + ) + + // The content and its animated background in the overlay. We draw it only when we are + // animating. + AnimatedContentInOverlay( + color, + thisExpandableSize, + controller.animatorState, + controller.overlay.value + ?: error("AnimatedContentInOverlay shouldn't be composed with null overlay."), + controller, + wrappedContent, + controller.composeViewRoot, + { controller.currentComposeViewInOverlay.value = it }, + controller.density, + ) + } + controller.isDialogShowing.value -> { + Box( + modifier + .drawWithContent { /* Don't draw anything when the dialog is shown. */} + .onGloballyPositioned { + controller.boundsInComposeViewRoot.value = it.boundsInRoot() + } + ) { wrappedContent(controller) } + } + else -> { + Box( + modifier.clip(shape).background(color, shape).onGloballyPositioned { + controller.boundsInComposeViewRoot.value = it.boundsInRoot() + } + ) { wrappedContent(controller) } + } + } +} + +/** Draw [content] in [overlay] while respecting its screen position given by [animatorState]. */ +@Composable +private fun AnimatedContentInOverlay( + color: Color, + sizeInOriginalLayout: Size, + animatorState: State<LaunchAnimator.State?>, + overlay: ViewGroupOverlay, + controller: ExpandableController, + content: @Composable (ExpandableController) -> Unit, + composeViewRoot: View, + onOverlayComposeViewChanged: (View?) -> Unit, + density: Density, +) { + val compositionContext = rememberCompositionContext() + val context = LocalContext.current + + // Create the ComposeView and force its content composition so that the movableContent is + // composed exactly once when we start animating. + val composeViewInOverlay = + remember(context, density) { + val startWidth = sizeInOriginalLayout.width + val startHeight = sizeInOriginalLayout.height + val contentModifier = + Modifier + // Draw the content with the same size as it was at the start of the animation + // so that its content is laid out exactly the same way. + .requiredSize(with(density) { sizeInOriginalLayout.toDpSize() }) + .drawWithContent { + val animatorState = animatorState.value ?: return@drawWithContent + + // Scale the content with the background while keeping its aspect ratio. + val widthRatio = + if (startWidth != 0f) { + animatorState.width.toFloat() / startWidth + } else { + 1f + } + val heightRatio = + if (startHeight != 0f) { + animatorState.height.toFloat() / startHeight + } else { + 1f + } + val scale = min(widthRatio, heightRatio) + scale(scale) { this@drawWithContent.drawContent() } + } + + val composeView = + ComposeView(context).apply { + setContent { + Box( + Modifier.fillMaxSize().drawWithContent { + val animatorState = animatorState.value ?: return@drawWithContent + if (!animatorState.visible) { + return@drawWithContent + } + + val topRadius = animatorState.topCornerRadius + val bottomRadius = animatorState.bottomCornerRadius + if (topRadius == bottomRadius) { + // Shortcut to avoid Outline calculation and allocation. + val cornerRadius = CornerRadius(topRadius) + drawRoundRect(color, cornerRadius = cornerRadius) + } else { + val shape = + RoundedCornerShape( + topStart = topRadius, + topEnd = topRadius, + bottomStart = bottomRadius, + bottomEnd = bottomRadius, + ) + val outline = shape.createOutline(size, layoutDirection, this) + drawOutline(outline, color = color) + } + + drawContent() + }, + // We center the content in the expanding container. + contentAlignment = Alignment.Center, + ) { + Box(contentModifier) { content(controller) } + } + } + } + + // Set the owners. + val overlayViewGroup = + getOverlayViewGroup( + context, + overlay, + ) + ViewTreeLifecycleOwner.set( + overlayViewGroup, + ViewTreeLifecycleOwner.get(composeViewRoot), + ) + ViewTreeViewModelStoreOwner.set( + overlayViewGroup, + ViewTreeViewModelStoreOwner.get(composeViewRoot), + ) + overlayViewGroup.setViewTreeSavedStateRegistryOwner( + composeViewRoot.findViewTreeSavedStateRegistryOwner() + ) + + composeView.setParentCompositionContext(compositionContext) + + composeView + } + + DisposableEffect(overlay, composeViewInOverlay) { + // Add the ComposeView to the overlay. + overlay.add(composeViewInOverlay) + + val startState = + animatorState.value + ?: throw IllegalStateException( + "AnimatedContentInOverlay shouldn't be composed with null animatorState." + ) + measureAndLayoutComposeViewInOverlay(composeViewInOverlay, startState) + onOverlayComposeViewChanged(composeViewInOverlay) + + onDispose { + composeViewInOverlay.disposeComposition() + overlay.remove(composeViewInOverlay) + onOverlayComposeViewChanged(null) + } + } +} + +internal fun measureAndLayoutComposeViewInOverlay( + view: View, + state: LaunchAnimator.State, +) { + val exactWidth = state.width + val exactHeight = state.height + view.measure( + View.MeasureSpec.makeSafeMeasureSpec(exactWidth, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeSafeMeasureSpec(exactHeight, View.MeasureSpec.EXACTLY), + ) + + val parent = view.parent as ViewGroup + val parentLocation = parent.locationOnScreen + val offsetX = parentLocation[0] + val offsetY = parentLocation[1] + view.layout( + state.left - offsetX, + state.top - offsetY, + state.right - offsetX, + state.bottom - offsetY, + ) +} + +// TODO(b/230830644): Add hidden API to ViewGroupOverlay to access this ViewGroup directly? +private fun getOverlayViewGroup(context: Context, overlay: ViewGroupOverlay): ViewGroup { + val view = View(context) + overlay.add(view) + var current = view.parent + while (current.parent != null) { + current = current.parent + } + overlay.remove(view) + return current as ViewGroup +} diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt new file mode 100644 index 000000000000..50c3d7e1e76b --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.compose.animation + +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroupOverlay +import android.view.ViewRootImpl +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable +import com.android.systemui.animation.LaunchAnimator +import kotlin.math.roundToInt + +/** A controller that can control animated launches from an [Expandable]. */ +interface ExpandableController { + /** The [Expandable] controlled by this controller. */ + val expandable: Expandable +} + +/** + * Create an [ExpandableController] to control an [Expandable]. This is useful if you need to create + * the controller before the [Expandable], for instance to handle clicks outside of the Expandable + * that would still trigger a dialog/activity launch animation. + */ +@Composable +fun rememberExpandableController( + color: Color, + shape: Shape, + contentColor: Color = contentColorFor(color), +): ExpandableController { + val composeViewRoot = LocalView.current + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + + // The current animation state, if we are currently animating a dialog or activity. + val animatorState = remember { mutableStateOf<LaunchAnimator.State?>(null) } + + // Whether a dialog controlled by this ExpandableController is currently showing. + val isDialogShowing = remember { mutableStateOf(false) } + + // The overlay in which we should animate the launch. + val overlay = remember { mutableStateOf<ViewGroupOverlay?>(null) } + + // The current [ComposeView] being animated in the [overlay], if any. + val currentComposeViewInOverlay = remember { mutableStateOf<View?>(null) } + + // The bounds in [composeViewRoot] of the expandable controlled by this controller. + val boundsInComposeViewRoot = remember { mutableStateOf(Rect.Zero) } + + // Whether this composable is still composed. We only do the dialog exit animation if this is + // true. + val isComposed = remember { mutableStateOf(true) } + DisposableEffect(Unit) { onDispose { isComposed.value = false } } + + return remember(color, contentColor, shape, composeViewRoot, density, layoutDirection) { + ExpandableControllerImpl( + color, + contentColor, + shape, + composeViewRoot, + density, + animatorState, + isDialogShowing, + overlay, + currentComposeViewInOverlay, + boundsInComposeViewRoot, + layoutDirection, + isComposed, + ) + } +} + +internal class ExpandableControllerImpl( + internal val color: Color, + internal val contentColor: Color, + internal val shape: Shape, + internal val composeViewRoot: View, + internal val density: Density, + internal val animatorState: MutableState<LaunchAnimator.State?>, + internal val isDialogShowing: MutableState<Boolean>, + internal val overlay: MutableState<ViewGroupOverlay?>, + internal val currentComposeViewInOverlay: MutableState<View?>, + internal val boundsInComposeViewRoot: MutableState<Rect>, + private val layoutDirection: LayoutDirection, + private val isComposed: State<Boolean>, +) : ExpandableController { + override val expandable: Expandable = + object : Expandable { + override fun activityLaunchController( + cujType: Int?, + ): ActivityLaunchAnimator.Controller? { + if (!isComposed.value) { + return null + } + + return activityController(cujType) + } + + override fun dialogLaunchController(cuj: DialogCuj?): DialogLaunchAnimator.Controller? { + if (!isComposed.value) { + return null + } + + return dialogController(cuj) + } + } + + /** + * Create a [LaunchAnimator.Controller] that is going to be used to drive an activity or dialog + * animation. This controller will: + * 1. Compute the start/end animation state using [boundsInComposeViewRoot] and the location of + * composeViewRoot on the screen. + * 2. Update [animatorState] with the current animation state if we are animating, or null + * otherwise. + */ + private fun launchController(): LaunchAnimator.Controller { + return object : LaunchAnimator.Controller { + private val rootLocationOnScreen = intArrayOf(0, 0) + + override var launchContainer: ViewGroup = composeViewRoot.rootView as ViewGroup + + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + animatorState.value = null + } + + override fun onLaunchAnimationProgress( + state: LaunchAnimator.State, + progress: Float, + linearProgress: Float + ) { + // We copy state given that it's always the same object that is mutated by + // ActivityLaunchAnimator. + animatorState.value = + LaunchAnimator.State( + state.top, + state.bottom, + state.left, + state.right, + state.topCornerRadius, + state.bottomCornerRadius, + ) + .apply { visible = state.visible } + + // Force measure and layout the ComposeView in the overlay whenever the animation + // state changes. + currentComposeViewInOverlay.value?.let { + measureAndLayoutComposeViewInOverlay(it, state) + } + } + + override fun createAnimatorState(): LaunchAnimator.State { + val boundsInRoot = boundsInComposeViewRoot.value + val outline = + shape.createOutline( + Size(boundsInRoot.width, boundsInRoot.height), + layoutDirection, + density, + ) + + val (topCornerRadius, bottomCornerRadius) = + when (outline) { + is Outline.Rectangle -> 0f to 0f + is Outline.Rounded -> { + val roundRect = outline.roundRect + + // TODO(b/230830644): Add better support different corner radii. + val topCornerRadius = + maxOf( + roundRect.topLeftCornerRadius.x, + roundRect.topLeftCornerRadius.y, + roundRect.topRightCornerRadius.x, + roundRect.topRightCornerRadius.y, + ) + val bottomCornerRadius = + maxOf( + roundRect.bottomLeftCornerRadius.x, + roundRect.bottomLeftCornerRadius.y, + roundRect.bottomRightCornerRadius.x, + roundRect.bottomRightCornerRadius.y, + ) + + topCornerRadius to bottomCornerRadius + } + else -> + error( + "ExpandableState only supports (rounded) rectangles at the " + + "moment." + ) + } + + val rootLocation = rootLocationOnScreen() + return LaunchAnimator.State( + top = rootLocation.y.roundToInt(), + bottom = (rootLocation.y + boundsInRoot.height).roundToInt(), + left = rootLocation.x.roundToInt(), + right = (rootLocation.x + boundsInRoot.width).roundToInt(), + topCornerRadius = topCornerRadius, + bottomCornerRadius = bottomCornerRadius, + ) + } + + private fun rootLocationOnScreen(): Offset { + composeViewRoot.getLocationOnScreen(rootLocationOnScreen) + val boundsInRoot = boundsInComposeViewRoot.value + val x = rootLocationOnScreen[0] + boundsInRoot.left + val y = rootLocationOnScreen[1] + boundsInRoot.top + return Offset(x, y) + } + } + } + + /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */ + private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller { + val delegate = launchController() + return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate { + override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + delegate.onLaunchAnimationStart(isExpandingFullyAbove) + overlay.value = composeViewRoot.rootView.overlay as ViewGroupOverlay + } + + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + overlay.value = null + } + } + } + + private fun dialogController(cuj: DialogCuj?): DialogLaunchAnimator.Controller { + return object : DialogLaunchAnimator.Controller { + override val viewRoot: ViewRootImpl = composeViewRoot.viewRootImpl + override val sourceIdentity: Any = this@ExpandableControllerImpl + override val cuj: DialogCuj? = cuj + + override fun startDrawingInOverlayOf(viewGroup: ViewGroup) { + val newOverlay = viewGroup.overlay as ViewGroupOverlay + if (newOverlay != overlay.value) { + overlay.value = newOverlay + } + } + + override fun stopDrawingInOverlay() { + if (overlay.value != null) { + overlay.value = null + } + } + + override fun createLaunchController(): LaunchAnimator.Controller { + val delegate = launchController() + return object : LaunchAnimator.Controller by delegate { + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + + // Make sure we don't draw this expandable when the dialog is showing. + isDialogShowing.value = true + } + } + } + + override fun createExitController(): LaunchAnimator.Controller { + val delegate = launchController() + return object : LaunchAnimator.Controller by delegate { + override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { + delegate.onLaunchAnimationEnd(isExpandingFullyAbove) + isDialogShowing.value = false + } + } + } + + override fun shouldAnimateExit(): Boolean = isComposed.value + + override fun onExitAnimationCancelled() { + isDialogShowing.value = false + } + + override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? { + // TODO(b/252723237): Add support for jank monitoring when animating from a + // Composable. + return null + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt index 3175dcfa092b..4d94bab6c26c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt @@ -17,8 +17,6 @@ package com.android.systemui.user.ui.compose -import android.graphics.Bitmap -import android.graphics.Canvas import android.graphics.drawable.Drawable import androidx.appcompat.content.res.AppCompatResources import androidx.compose.foundation.Image @@ -50,10 +48,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.ColorPainter -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -62,6 +58,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap import com.android.systemui.common.ui.compose.load import com.android.systemui.compose.SysUiOutlinedButton import com.android.systemui.compose.SysUiTextButton @@ -356,10 +353,11 @@ private fun MenuItem( remember(viewModel.iconResourceId) { val drawable = checkNotNull(AppCompatResources.getDrawable(context, viewModel.iconResourceId)) + val size = with(density) { 20.dp.toPx() }.toInt() drawable .toBitmap( - size = with(density) { 20.dp.toPx() }.toInt(), - tintColor = Color.White, + width = size, + height = size, ) .asImageBitmap() } @@ -392,32 +390,3 @@ private fun MenuItem( ), ) } - -/** - * Converts the [Drawable] to a [Bitmap]. - * - * Note that this is a relatively memory-heavy operation as it allocates a whole bitmap and draws - * the `Drawable` onto it. Use sparingly and with care. - */ -private fun Drawable.toBitmap( - size: Int? = null, - tintColor: Color? = null, -): Bitmap { - val bitmap = - if (intrinsicWidth <= 0 || intrinsicHeight <= 0) { - Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) - } else { - Bitmap.createBitmap( - size ?: intrinsicWidth, - size ?: intrinsicHeight, - Bitmap.Config.ARGB_8888 - ) - } - val canvas = Canvas(bitmap) - setBounds(0, 0, canvas.width, canvas.height) - if (tintColor != null) { - setTint(tintColor.toArgb()) - } - draw(canvas) - return bitmap -} diff --git a/packages/SystemUI/docs/device-entry/doze.md b/packages/SystemUI/docs/device-entry/doze.md index 6b6dce5da169..10bd3679a13b 100644 --- a/packages/SystemUI/docs/device-entry/doze.md +++ b/packages/SystemUI/docs/device-entry/doze.md @@ -1,5 +1,7 @@ # Doze +`Dozing` is a low-powered state of the device. If Always-on Display (AOD), pulsing, or wake-gestures are enabled, then the device will enter the `dozing` state after a user intent to turn off the screen (ie: power button) or the screen times out. + Always-on Display (AOD) provides an alternative 'screen-off' experience. Instead, of completely turning the display off, it provides a distraction-free, glanceable experience for the phone in a low-powered mode. In this low-powered mode, the display will have a lower refresh rate and the UI should frequently shift its displayed contents in order to prevent burn-in. The recommended max on-pixel-ratio (OPR) is 5% to reduce battery consumption.  @@ -58,7 +60,7 @@ When Dozing is enabled, it can still be suppressed based on the device state. On Refer to the documentation in [DozeSuppressors][15] for more information. ## AOD burn-in and image retention -Because AOD will show an image on the screen for an elogated period of time, AOD designs must take into consideration burn-in (leaving a permanent mark on the screen). Temporary burn-in is called image-retention. +Because AOD will show an image on the screen for an elongated period of time, AOD designs must take into consideration burn-in (leaving a permanent mark on the screen). Temporary burn-in is called image-retention. To prevent burn-in, it is recommended to often shift UI on the screen. [DozeUi][17] schedules a call to dozeTimeTick every minute to request a shift in UI for all elements on AOD. The amount of shift can be determined by undergoing simulated AOD testing since this may vary depending on the display. diff --git a/packages/SystemUI/docs/device-entry/glossary.md b/packages/SystemUI/docs/device-entry/glossary.md index f3d12c21a3a5..7f19b1688de0 100644 --- a/packages/SystemUI/docs/device-entry/glossary.md +++ b/packages/SystemUI/docs/device-entry/glossary.md @@ -2,38 +2,38 @@ ## Keyguard -| Term | Description | -| :-----------: | ----------- | -| Keyguard, [keyguard.md][1] | Coordinates the first experience when turning on the display of a device, as long as the user has not specified a security method of NONE. Consists of the lock screen and bouncer.| -| Lock screen<br><br>| The first screen available when turning on the display of a device, as long as the user has not specified a security method of NONE. On the lock screen, users can access:<ul><li>Quick Settings - users can swipe down from the top of the screen to interact with quick settings tiles</li><li>[Keyguard Status Bar][9] - This special status bar shows SIM related information and system icons.</li><li>Clock - uses the font specified at [clock.xml][8]. If the clock font supports variable weights, users will experience delightful clock weight animations - in particular, on transitions between the lock screen and AOD.</li><li>Notifications - ability to view and interact with notifications depending on user lock screen notification settings: `Settings > Display > Lock screen > Privacy`</li><li>Message area - contains device information like biometric errors, charging information and device policy information. Also includes user configured information from `Settings > Display > Lock screen > Add text on lock screen`. </li><li>Bouncer - if the user has a primary authentication method, they can swipe up from the bottom of the screen to bring up the bouncer.</li></ul>The lock screen is one state of the notification shade. See [StatusBarState#KEYGUARD][10] and [StatusBarState#SHADE_LOCKED][10].| -| Bouncer, [bouncer.md][2]<br><br>| The component responsible for displaying the primary security method set by the user (password, PIN, pattern). The bouncer can also show SIM-related security methods, allowing the user to unlock the device or SIM.| -| Split shade | State of the shade (which keyguard is a part of) in which notifications are on the right side and Quick Settings on the left. For keyguard that means notifications being on the right side and clock with media being on the left.<br><br>Split shade is automatically activated - using resources - for big screens in landscape, see [sw600dp-land/config.xml][3] `config_use_split_notification_shade`.<br><br>In that state we can see the big clock more often - every time when media is not visible on the lock screen. When there is no media and no notifications - or we enter AOD - big clock is always positioned in the center of the screen.<br><br>The magic of positioning views happens by changing constraints of [NotificationsQuickSettingsContainer][4] and positioning elements vertically in [KeyguardClockPositionAlgorithm][5]| -| Ambient display (AOD), [doze.md][6]<br><br>| UI shown when the device is in a low-powered display state. This is controlled by the doze component. The same lock screen views (ie: clock, notification shade) are used on AOD. The AOSP image on the left shows the usage of a clock that does not support variable weights which is why the clock is thicker in that image than what users see on Pixel devices.| +| Term | Description | +|--------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Keyguard, [keyguard.md][1] | Coordinates the first experience when turning on the display of a device, as long as the user has not specified a security method of NONE. Consists of the lock screen and bouncer. | +| Lock screen<br><br> | The first screen available when turning on the display of a device, as long as the user has not specified a security method of NONE. On the lock screen, users can access:<ul><li>Quick Settings - users can swipe down from the top of the screen to interact with quick settings tiles</li><li>[Keyguard Status Bar][9] - This special status bar shows SIM related information and system icons.</li><li>Clock - uses the font specified at [clock.xml][8]. If the clock font supports variable weights, users will experience delightful clock weight animations - in particular, on transitions between the lock screen and AOD.</li><li>Notifications - ability to view and interact with notifications depending on user lock screen notification settings: `Settings > Display > Lock screen > Privacy`</li><li>Message area - contains device information like biometric errors, charging information and device policy information. Also includes user configured information from `Settings > Display > Lock screen > Add text on lock screen`. </li><li>Bouncer - if the user has a primary authentication method, they can swipe up from the bottom of the screen to bring up the bouncer.</li></ul>The lock screen is one state of the notification shade. See [StatusBarState#KEYGUARD][10] and [StatusBarState#SHADE_LOCKED][10]. | +| Bouncer, [bouncer.md][2]<br><br> | The component responsible for displaying the primary security method set by the user (password, PIN, pattern). The bouncer can also show SIM-related security methods, allowing the user to unlock the device or SIM. | +| Split shade | State of the shade (which keyguard is a part of) in which notifications are on the right side and Quick Settings on the left. For keyguard that means notifications being on the right side and clock with media being on the left.<br><br>Split shade is automatically activated - using resources - for big screens in landscape, see [sw600dp-land/config.xml][3] `config_use_split_notification_shade`.<br><br>In that state we can see the big clock more often - every time when media is not visible on the lock screen. When there is no media and no notifications - or we enter AOD - big clock is always positioned in the center of the screen.<br><br>The magic of positioning views happens by changing constraints of [NotificationsQuickSettingsContainer][4] and positioning elements vertically in [KeyguardClockPositionAlgorithm][5] | +| Ambient display (AOD), [doze.md][6]<br><br> | UI shown when the device is in a low-powered display state. This is controlled by the doze component. The same lock screen views (ie: clock, notification shade) are used on AOD. The AOSP image on the left shows the usage of a clock that does not support variable weights which is why the clock is thicker in that image than what users see on Pixel devices. | ## General Authentication Terms -| Term | Description | -| ----------- | ----------- | -| Primary Authentication | The strongest form of authentication. Includes: Pin, pattern and password input.| -| Biometric Authentication | Face or fingerprint input. Biometric authentication is categorized into different classes of security. See [Measuring Biometric Security][7].| +| Term | Description | +|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| Primary Authentication | The strongest form of authentication. Includes: Pin, pattern and password input. | +| Biometric Authentication | Face or fingerprint input. Biometric authentication is categorized into different classes of security. See [Measuring Biometric Security][7]. | ## Face Authentication Terms -| Term | Description | -| ----------- | ----------- | -| Passive Authentication | When a user hasn't explicitly requested an authentication method; however, it may still put the device in an unlocked state.<br><br>For example, face authentication is triggered immediately when waking the device; however, users may not have the intent of unlocking their device. Instead, they could have wanted to just check the lock screen. Because of this, SystemUI provides the option for a bypass OR non-bypass face authentication experience which have different user flows.<br><br>In contrast, fingerprint authentication is considered an active authentication method since users need to actively put their finger on the fingerprint sensor to authenticate. Therefore, it's an explicit request for authentication and SystemUI knows the user has the intent for device-entry.| -| Bypass | Used to refer to the face authentication bypass device entry experience. We have this distinction because face auth is a passive authentication method (see above).| -| Bypass User Journey <br><br>| Once the user successfully authenticates with face, the keyguard immediately dismisses and the user is brought to the home screen/last app. This CUJ prioritizes speed of device entry. SystemUI hides interactive views (notifications) on the lock screen to avoid putting users in a state where the lock screen could immediately disappear while they're interacting with affordances on the lock screen.| -| Non-bypass User Journey | Once the user successfully authenticates with face, the device remains on keyguard until the user performs an action to indicate they'd like to enter the device (ie: swipe up on the lock screen or long press on the unlocked icon). This CUJ prioritizes notification visibility.| +| Term | Description | +|-----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Passive Authentication | When a user hasn't explicitly requested an authentication method; however, it may still put the device in an unlocked state.<br><br>For example, face authentication is triggered immediately when waking the device; however, users may not have the intent of unlocking their device. Instead, they could have wanted to just check the lock screen. Because of this, SystemUI provides the option for a bypass OR non-bypass face authentication experience which have different user flows.<br><br>In contrast, fingerprint authentication is considered an active authentication method since users need to actively put their finger on the fingerprint sensor to authenticate. Therefore, it's an explicit request for authentication and SystemUI knows the user has the intent for device-entry. | +| Bypass | Used to refer to the face authentication bypass device entry experience. We have this distinction because face auth is a passive authentication method (see above). | +| Bypass User Journey <br><br> | Once the user successfully authenticates with face, the keyguard immediately dismisses and the user is brought to the home screen/last app. This CUJ prioritizes speed of device entry. SystemUI hides interactive views (notifications) on the lock screen to avoid putting users in a state where the lock screen could immediately disappear while they're interacting with affordances on the lock screen. | +| Non-bypass User Journey | Once the user successfully authenticates with face, the device remains on keyguard until the user performs an action to indicate they'd like to enter the device (ie: swipe up on the lock screen or long press on the unlocked icon). This CUJ prioritizes notification visibility. | ## Fingerprint Authentication Terms -| Term | Description | -| ----------- | ----------- | -| Under-display fingerprint sensor (UDFPS) | References the HW affordance for a fingerprint sensor that is under the display, which requires a software visual affordance. System UI supports showing the UDFPS affordance on the lock screen and on AOD. Users cannot authenticate from the screen-off state.<br><br>Supported SystemUI CUJs include:<ul><li> sliding finger on the screen to the UDFPS area to being authentication (as opposed to directly placing finger in the UDFPS area) </li><li> when a11y services are enabled, there is a haptic played when a touch is detected on UDFPS</li><li>after two hard-fingerprint-failures, the primary authentication bouncer is shown</li><li> when tapping on an affordance that requests to dismiss the lock screen, the user may see the UDFPS icon highlighted - see UDFPS bouncer</li></ul>| -| UDFPS Bouncer | UI that highlights the UDFPS sensor. Users can get into this state after tapping on a notification from the lock screen or locked expanded shade.| +| Term | Description | +|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Under-display fingerprint sensor (UDFPS) | References the HW affordance for a fingerprint sensor that is under the display, which requires a software visual affordance. System UI supports showing the UDFPS affordance on the lock screen and on AOD. Users cannot authenticate from the screen-off state.<br><br>Supported SystemUI CUJs include:<ul><li> sliding finger on the screen to the UDFPS area to being authentication (as opposed to directly placing finger in the UDFPS area) </li><li> when a11y services are enabled, there is a haptic played when a touch is detected on UDFPS</li><li>after multiple consecutive hard-fingerprint-failures, the primary authentication bouncer is shown. The exact number of attempts is defined in: [BiometricUnlockController#UDFPS_ATTEMPTS_BEFORE_SHOW_BOUNCER][4]</li><li> when tapping on an affordance that requests to dismiss the lock screen, the user may see the UDFPS icon highlighted - see UDFPS bouncer</li></ul> | +| UDFPS Bouncer | UI that highlights the UDFPS sensor. Users can get into this state after tapping on a notification from the lock screen or locked expanded shade. | ## Other Authentication Terms -| Term | Description | -| ---------- | ----------- | -| Trust Agents | Provides signals to the keyguard to allow it to lock less frequently.| +| Term | Description | +|--------------|-----------------------------------------------------------------------| +| Trust Agents | Provides signals to the keyguard to allow it to lock less frequently. | [1]: /frameworks/base/packages/SystemUI/docs/device-entry/keyguard.md @@ -46,3 +46,4 @@ [8]: /frameworks/base/packages/SystemUI/res-keyguard/font/clock.xml [9]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java [10]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarState.java +[11]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt index 9f275afa3765..da612a9276a3 100644 --- a/packages/SystemUI/ktfmt_includes.txt +++ b/packages/SystemUI/ktfmt_includes.txt @@ -189,45 +189,8 @@ -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt --packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt --packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt --packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt --packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt --packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt --packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt --packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt --packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt --packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt --packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt --packages/SystemUI/src/com/android/systemui/media/MediaData.kt --packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt --packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt --packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt --packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt --packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt --packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt --packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt --packages/SystemUI/src/com/android/systemui/media/MediaHost.kt --packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt -packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt -packages/SystemUI/src/com/android/systemui/media/MediaProjectionCaptureTarget.kt --packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt --packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt --packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt --packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt --packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt --packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt --packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt --packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt --packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt --packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt --packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt --packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt --packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt --packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt --packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt --packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt --packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt -packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt -packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt -packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt @@ -246,8 +209,6 @@ -packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt -packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt -packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt --packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt --packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt -packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderLogger.kt -packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLogger.kt -packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt @@ -493,7 +454,7 @@ -packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallFlags.kt -packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLogger.kt -packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt --packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt +-packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/ShadeStateListener.kt -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt -packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt @@ -528,6 +489,8 @@ -packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt -packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt -packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +-packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +-packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarRootView.kt -packages/SystemUI/src/com/android/systemui/toast/ToastDefaultAnimation.kt -packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt -packages/SystemUI/src/com/android/systemui/tv/TVSystemUICoreStartableModule.kt @@ -653,32 +616,11 @@ -packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt -packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt -packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt --packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt --packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLoggerTest.kt -packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt -packages/SystemUI/tests/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitorTest.kt @@ -812,7 +754,7 @@ -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometerTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLoggerTest.kt --packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt +-packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/ShadeExpansionStateManagerTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt -packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt index b3dd95553ed0..dee0f5cd1979 100644 --- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt @@ -205,6 +205,13 @@ enum class Style(internal val coreSpec: CoreSpec) { n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)), n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666)) )), + MONOCHROMATIC(CoreSpec( + a1 = TonalSpec(HueSource(), ChromaConstant(.0)), + a2 = TonalSpec(HueSource(), ChromaConstant(.0)), + a3 = TonalSpec(HueSource(), ChromaConstant(.0)), + n1 = TonalSpec(HueSource(), ChromaConstant(.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(.0)) + )), } class ColorScheme( @@ -219,7 +226,7 @@ class ColorScheme( val neutral1: List<Int> val neutral2: List<Int> - constructor(@ColorInt seed: Int, darkTheme: Boolean): + constructor(@ColorInt seed: Int, darkTheme: Boolean) : this(seed, darkTheme, Style.TONAL_SPOT) @JvmOverloads @@ -227,7 +234,7 @@ class ColorScheme( wallpaperColors: WallpaperColors, darkTheme: Boolean, style: Style = Style.TONAL_SPOT - ): + ) : this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) val allAccentColors: List<Int> @@ -472,4 +479,4 @@ class ColorScheme( return huePopulation } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp index cafaaf854eed..7709f210f22f 100644 --- a/packages/SystemUI/plugin/Android.bp +++ b/packages/SystemUI/plugin/Android.bp @@ -33,6 +33,7 @@ java_library { static_libs: [ "androidx.annotation_annotation", + "error_prone_annotations", "PluginCoreLib", "SystemUIAnimationLib", ], diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 01e5d86549eb..89f5c2c80e29 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -14,9 +14,11 @@ package com.android.systemui.plugins import android.content.res.Resources +import android.graphics.Rect import android.graphics.drawable.Drawable import android.view.View import com.android.systemui.plugins.annotations.ProvidesInterface +import com.android.systemui.plugins.log.LogBuffer import java.io.PrintWriter import java.util.Locale import java.util.TimeZone @@ -39,19 +41,19 @@ interface ClockProvider { fun getClocks(): List<ClockMetadata> /** Initializes and returns the target clock design */ - fun createClock(id: ClockId): Clock + fun createClock(id: ClockId): ClockController /** A static thumbnail for rendering in some examples */ fun getClockThumbnail(id: ClockId): Drawable? } /** Interface for controlling an active clock */ -interface Clock { +interface ClockController { /** A small version of the clock, appropriate for smaller viewports */ - val smallClock: View + val smallClock: ClockFaceController /** A large version of the clock, appropriate when a bigger viewport is available */ - val largeClock: View + val largeClock: ClockFaceController /** Events that clocks may need to respond to */ val events: ClockEvents @@ -61,7 +63,7 @@ interface Clock { /** Initializes various rendering parameters. If never called, provides reasonable defaults. */ fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) { - events.onColorPaletteChanged(resources, true, true) + events.onColorPaletteChanged(resources) animations.doze(dozeFraction) animations.fold(foldFraction) events.onTimeTick() @@ -69,12 +71,24 @@ interface Clock { /** Optional method for dumping debug information */ fun dump(pw: PrintWriter) { } + + /** Optional method for debug logging */ + fun setLogBuffer(logBuffer: LogBuffer) { } +} + +/** Interface for a specific clock face version rendered by the clock */ +interface ClockFaceController { + /** View that renders the clock face */ + val view: View + + /** Events specific to this clock face */ + val events: ClockFaceEvents } /** Events that should call when various rendering parameters change */ interface ClockEvents { /** Call every time tick */ - fun onTimeTick() + fun onTimeTick() { } /** Call whenever timezone changes */ fun onTimeZoneChanged(timeZone: TimeZone) { } @@ -89,11 +103,7 @@ interface ClockEvents { fun onFontSettingChanged() { } /** Call whenever the color palette should update */ - fun onColorPaletteChanged( - resources: Resources, - smallClockIsDark: Boolean, - largeClockIsDark: Boolean - ) { } + fun onColorPaletteChanged(resources: Resources) { } } /** Methods which trigger various clock animations */ @@ -109,6 +119,23 @@ interface ClockAnimations { /** Runs the battery animation (if any). */ fun charge() { } + + /** Move the clock, for example, if the notification tray appears in split-shade mode. */ + fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) { } + + /** + * Whether this clock has a custom position update animation. If true, the keyguard will call + * `onPositionUpdated` to notify the clock of a position update animation. If false, a default + * animation will be used (e.g. a simple translation). + */ + val hasCustomPositionUpdatedAnimation + get() = false +} + +/** Events that have specific data about the related face */ +interface ClockFaceEvents { + /** Region Darkness specific to the clock face */ + fun onRegionDarknessChanged(isDark: Boolean) { } } /** Some data about a clock design */ diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java index 506ccf3c2437..5f6f11c16da2 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/NavigationEdgeBackPlugin.java @@ -67,7 +67,6 @@ public interface NavigationEdgeBackPlugin extends Plugin { * * @param triggerBack if back will be triggered in current state. */ - // TODO(b/247883311): Remove default impl once SwipeBackGestureHandler overrides this. - default void setTriggerBack(boolean triggerBack) {} + void setTriggerBack(boolean triggerBack); } } diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt index 6124e10144f2..6436dcb5f613 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt @@ -14,12 +14,11 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import android.os.Trace import android.util.Log -import com.android.systemui.log.dagger.LogModule -import com.android.systemui.util.collection.RingBuffer +import com.android.systemui.plugins.util.RingBuffer import com.google.errorprone.annotations.CompileTimeConstant import java.io.PrintWriter import java.util.concurrent.ArrayBlockingQueue @@ -61,15 +60,18 @@ import kotlin.math.max * In either case, `level` can be any of `verbose`, `debug`, `info`, `warn`, `error`, `assert`, or * the first letter of any of the previous. * - * Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory]. + * In SystemUI, buffers are provided by LogModule. Instances should be created using a SysUI + * LogBufferFactory. * * @param name The name of this buffer, printed when the buffer is dumped and in some other * situations. * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start - * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches - * the maximum, it behaves like a ring buffer. + * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches the + * maximum, it behaves like a ring buffer. */ -class LogBuffer @JvmOverloads constructor( +class LogBuffer +@JvmOverloads +constructor( private val name: String, private val maxSize: Int, private val logcatEchoTracker: LogcatEchoTracker, @@ -78,7 +80,7 @@ class LogBuffer @JvmOverloads constructor( private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() } private val echoMessageQueue: BlockingQueue<LogMessage>? = - if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null + if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null init { if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) { @@ -133,11 +135,11 @@ class LogBuffer @JvmOverloads constructor( */ @JvmOverloads inline fun log( - tag: String, - level: LogLevel, - messageInitializer: MessageInitializer, - noinline messagePrinter: MessagePrinter, - exception: Throwable? = null, + tag: String, + level: LogLevel, + messageInitializer: MessageInitializer, + noinline messagePrinter: MessagePrinter, + exception: Throwable? = null, ) { val message = obtain(tag, level, messagePrinter, exception) messageInitializer(message) @@ -152,14 +154,13 @@ class LogBuffer @JvmOverloads constructor( * log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in * an initializer and a message printer. * - * Log buffers are limited by the number of entries, so logging more frequently - * will limit the time window that the LogBuffer covers in a bug report. Richer logs, on the - * other hand, make a bug report more actionable, so using the [log] with a messagePrinter to - * add more detail to every log may do more to improve overall logging than adding more logs - * with this method. + * Log buffers are limited by the number of entries, so logging more frequently will limit the + * time window that the LogBuffer covers in a bug report. Richer logs, on the other hand, make a + * bug report more actionable, so using the [log] with a messagePrinter to add more detail to + * every log may do more to improve overall logging than adding more logs with this method. */ fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) = - log(tag, level, {str1 = message}, { str1!! }) + log(tag, level, { str1 = message }, { str1!! }) /** * You should call [log] instead of this method. @@ -172,10 +173,10 @@ class LogBuffer @JvmOverloads constructor( */ @Synchronized fun obtain( - tag: String, - level: LogLevel, - messagePrinter: MessagePrinter, - exception: Throwable? = null, + tag: String, + level: LogLevel, + messagePrinter: MessagePrinter, + exception: Throwable? = null, ): LogMessage { if (!mutable) { return FROZEN_MESSAGE @@ -189,8 +190,7 @@ class LogBuffer @JvmOverloads constructor( * You should call [log] instead of this method. * * After acquiring a message via [obtain], call this method to signal to the buffer that you - * have finished filling in its data fields. The message will be echoed to logcat if - * necessary. + * have finished filling in its data fields. The message will be echoed to logcat if necessary. */ @Synchronized fun commit(message: LogMessage) { @@ -213,7 +213,8 @@ class LogBuffer @JvmOverloads constructor( /** Sends message to echo after determining whether to use Logcat and/or systrace. */ private fun echoToDesiredEndpoints(message: LogMessage) { - val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) || + val includeInLogcat = + logcatEchoTracker.isBufferLoggable(name, message.level) || logcatEchoTracker.isTagLoggable(message.tag, message.level) echo(message, toLogcat = includeInLogcat, toSystrace = systrace) } @@ -221,7 +222,12 @@ class LogBuffer @JvmOverloads constructor( /** Converts the entire buffer to a newline-delimited string */ @Synchronized fun dump(pw: PrintWriter, tailLength: Int) { - val iterationStart = if (tailLength <= 0) { 0 } else { max(0, buffer.size - tailLength) } + val iterationStart = + if (tailLength <= 0) { + 0 + } else { + max(0, buffer.size - tailLength) + } for (i in iterationStart until buffer.size) { buffer[i].dump(pw) @@ -229,9 +235,9 @@ class LogBuffer @JvmOverloads constructor( } /** - * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. - * Calls to [log], [obtain], and [commit] will not affect the buffer and will return dummy - * values if necessary. + * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. Calls + * to [log], [obtain], and [commit] will not affect the buffer and will return dummy values if + * necessary. */ @Synchronized fun freeze() { @@ -241,9 +247,7 @@ class LogBuffer @JvmOverloads constructor( } } - /** - * Undoes the effects of calling [freeze]. - */ + /** Undoes the effects of calling [freeze]. */ @Synchronized fun unfreeze() { if (frozen) { @@ -265,8 +269,11 @@ class LogBuffer @JvmOverloads constructor( } private fun echoToSystrace(message: LogMessage, strMessage: String) { - Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events", - "$name - ${message.level.shortString} ${message.tag}: $strMessage") + Trace.instantForTrack( + Trace.TRACE_TAG_APP, + "UI Events", + "$name - ${message.level.shortString} ${message.tag}: $strMessage" + ) } private fun echoToLogcat(message: LogMessage, strMessage: String) { diff --git a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt index 53f231c9f9d2..b036cf0be1d6 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt @@ -14,17 +14,12 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import android.util.Log -/** - * Enum version of @Log.Level - */ -enum class LogLevel( - @Log.Level val nativeLevel: Int, - val shortString: String -) { +/** Enum version of @Log.Level */ +enum class LogLevel(@Log.Level val nativeLevel: Int, val shortString: String) { VERBOSE(Log.VERBOSE, "V"), DEBUG(Log.DEBUG, "D"), INFO(Log.INFO, "I"), diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt index dae2592e116c..9468681289bf 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import java.io.PrintWriter import java.text.SimpleDateFormat @@ -29,9 +29,10 @@ import java.util.Locale * * When a message is logged, the code doing the logging stores data in one or more of the generic * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the - * [messagePrinter] function reads the data stored in the generic fields and converts that to a human- - * readable string. Thus, for every log type there must be a specialized initializer function that - * stores data specific to that log type and a specialized printer function that prints that data. + * [messagePrinter] function reads the data stored in the generic fields and converts that to a + * human- readable string. Thus, for every log type there must be a specialized initializer function + * that stores data specific to that log type and a specialized printer function that prints that + * data. * * See [LogBuffer.log] for more information. */ @@ -55,9 +56,7 @@ interface LogMessage { var bool3: Boolean var bool4: Boolean - /** - * Function that dumps the [LogMessage] to the provided [writer]. - */ + /** Function that dumps the [LogMessage] to the provided [writer]. */ fun dump(writer: PrintWriter) { val formattedTimestamp = DATE_FORMAT.format(timestamp) val shortLevel = level.shortString @@ -68,12 +67,12 @@ interface LogMessage { } /** - * A function that will be called if and when the message needs to be dumped to - * logcat or a bug report. It should read the data stored by the initializer and convert it to - * a human-readable string. The value of `this` will be the LogMessage to be printed. - * **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and NEVER any - * variables in its enclosing scope. Otherwise, the runtime will need to allocate a new instance - * of the printer for each call, thwarting our attempts at avoiding any sort of allocation. + * A function that will be called if and when the message needs to be dumped to logcat or a bug + * report. It should read the data stored by the initializer and convert it to a human-readable + * string. The value of `this` will be the LogMessage to be printed. **IMPORTANT:** The printer + * should ONLY ever reference fields on the LogMessage and NEVER any variables in its enclosing + * scope. Otherwise, the runtime will need to allocate a new instance of the printer for each call, + * thwarting our attempts at avoiding any sort of allocation. */ typealias MessagePrinter = LogMessage.() -> String diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt index 4dd6f652d1c7..f2a6a91adcdf 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt @@ -14,11 +14,9 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log -/** - * Recyclable implementation of [LogMessage]. - */ +/** Recyclable implementation of [LogMessage]. */ data class LogMessageImpl( override var level: LogLevel, override var tag: String, @@ -68,23 +66,24 @@ data class LogMessageImpl( companion object Factory { fun create(): LogMessageImpl { return LogMessageImpl( - LogLevel.DEBUG, - DEFAULT_TAG, - 0, - DEFAULT_PRINTER, - null, - null, - null, - null, - 0, - 0, - 0, - 0, - 0.0, - false, - false, - false, - false) + LogLevel.DEBUG, + DEFAULT_TAG, + 0, + DEFAULT_PRINTER, + null, + null, + null, + null, + 0, + 0, + 0, + 0, + 0.0, + false, + false, + false, + false + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt index 8cda4236bc87..cfe894f276a0 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt @@ -14,24 +14,16 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log -/** - * Keeps track of which [LogBuffer] messages should also appear in logcat. - */ +/** Keeps track of which [LogBuffer] messages should also appear in logcat. */ interface LogcatEchoTracker { - /** - * Whether [bufferName] should echo messages of [level] or higher to logcat. - */ + /** Whether [bufferName] should echo messages of [level] or higher to logcat. */ fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean - /** - * Whether [tagName] should echo messages of [level] or higher to logcat. - */ + /** Whether [tagName] should echo messages of [level] or higher to logcat. */ fun isTagLoggable(tagName: String, level: LogLevel): Boolean - /** - * Whether to log messages in a background thread. - */ + /** Whether to log messages in a background thread. */ val logInBackgroundThread: Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt index 40b0cdc173d8..d3fabaccb6d3 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log import android.content.ContentResolver import android.database.ContentObserver @@ -36,19 +36,15 @@ import android.provider.Settings * $ adb shell settings put global systemui/tag/<tag> <level> * ``` */ -class LogcatEchoTrackerDebug private constructor( - private val contentResolver: ContentResolver -) : LogcatEchoTracker { +class LogcatEchoTrackerDebug private constructor(private val contentResolver: ContentResolver) : + LogcatEchoTracker { private val cachedBufferLevels: MutableMap<String, LogLevel> = mutableMapOf() private val cachedTagLevels: MutableMap<String, LogLevel> = mutableMapOf() override val logInBackgroundThread = true companion object Factory { @JvmStatic - fun create( - contentResolver: ContentResolver, - mainLooper: Looper - ): LogcatEchoTrackerDebug { + fun create(contentResolver: ContentResolver, mainLooper: Looper): LogcatEchoTrackerDebug { val tracker = LogcatEchoTrackerDebug(contentResolver) tracker.attach(mainLooper) return tracker @@ -57,37 +53,35 @@ class LogcatEchoTrackerDebug private constructor( private fun attach(mainLooper: Looper) { contentResolver.registerContentObserver( - Settings.Global.getUriFor(BUFFER_PATH), - true, - object : ContentObserver(Handler(mainLooper)) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - super.onChange(selfChange, uri) - cachedBufferLevels.clear() - } - }) + Settings.Global.getUriFor(BUFFER_PATH), + true, + object : ContentObserver(Handler(mainLooper)) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + cachedBufferLevels.clear() + } + } + ) contentResolver.registerContentObserver( - Settings.Global.getUriFor(TAG_PATH), - true, - object : ContentObserver(Handler(mainLooper)) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - super.onChange(selfChange, uri) - cachedTagLevels.clear() - } - }) + Settings.Global.getUriFor(TAG_PATH), + true, + object : ContentObserver(Handler(mainLooper)) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + cachedTagLevels.clear() + } + } + ) } - /** - * Whether [bufferName] should echo messages of [level] or higher to logcat. - */ + /** Whether [bufferName] should echo messages of [level] or higher to logcat. */ @Synchronized override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean { return level.ordinal >= getLogLevel(bufferName, BUFFER_PATH, cachedBufferLevels).ordinal } - /** - * Whether [tagName] should echo messages of [level] or higher to logcat. - */ + /** Whether [tagName] should echo messages of [level] or higher to logcat. */ @Synchronized override fun isTagLoggable(tagName: String, level: LogLevel): Boolean { return level >= getLogLevel(tagName, TAG_PATH, cachedTagLevels) diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt index 1a4ad1907ff1..3c8bda4a44e0 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt @@ -14,11 +14,9 @@ * limitations under the License. */ -package com.android.systemui.log +package com.android.systemui.plugins.log -/** - * Production version of [LogcatEchoTracker] that isn't configurable. - */ +/** Production version of [LogcatEchoTracker] that isn't configurable. */ class LogcatEchoTrackerProd : LogcatEchoTracker { override val logInBackgroundThread = false diff --git a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt index 97dc842ec699..68d78907f028 100644 --- a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.util.collection +package com.android.systemui.plugins.util import kotlin.math.max @@ -32,19 +32,16 @@ import kotlin.math.max * @param factory A function that creates a fresh instance of T. Used by the buffer while it's * growing to [maxSize]. */ -class RingBuffer<T>( - private val maxSize: Int, - private val factory: () -> T -) : Iterable<T> { +class RingBuffer<T>(private val maxSize: Int, private val factory: () -> T) : Iterable<T> { private val buffer = MutableList<T?>(maxSize) { null } /** * An abstract representation that points to the "end" of the buffer. Increments every time - * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into - * the backing array. Always points to the "next" available slot in the buffer. Before the - * buffer has completely filled, the value pointed to will be null. Afterward, it will be the - * value at the "beginning" of the buffer. + * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into the + * backing array. Always points to the "next" available slot in the buffer. Before the buffer + * has completely filled, the value pointed to will be null. Afterward, it will be the value at + * the "beginning" of the buffer. * * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms, * omega will overflow after a little under three million years of continuous operation. @@ -60,24 +57,23 @@ class RingBuffer<T>( /** * Advances the buffer's position by one and returns the value that is now present at the "end" - * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. - * Otherwise, reuses the value that was previously at the "beginning" of the buffer. + * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. Otherwise, + * reuses the value that was previously at the "beginning" of the buffer. * - * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that - * was previously stored on it. + * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that was + * previously stored on it. */ fun advance(): T { val index = indexOf(omega) omega += 1 - val entry = buffer[index] ?: factory().also { - buffer[index] = it - } + val entry = buffer[index] ?: factory().also { buffer[index] = it } return entry } /** * Returns the value stored at [index], which can range from 0 (the "start", or oldest element - * of the buffer) to [size] - 1 (the "end", or newest element of the buffer). + * of the buffer) to [size] + * - 1 (the "end", or newest element of the buffer). */ operator fun get(index: Int): T { if (index < 0 || index >= size) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt index 56aff3c2fc8b..a39b856f0f49 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt +++ b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt @@ -2,6 +2,7 @@ package com.android.systemui.log import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.log.LogBuffer import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter @@ -18,8 +19,7 @@ class LogBufferTest : SysuiTestCase() { private lateinit var outputWriter: StringWriter - @Mock - private lateinit var logcatEchoTracker: LogcatEchoTracker + @Mock private lateinit var logcatEchoTracker: LogcatEchoTracker @Before fun setup() { @@ -67,15 +67,17 @@ class LogBufferTest : SysuiTestCase() { @Test fun dump_writesCauseAndStacktrace() { buffer = createBuffer() - val exception = createTestException("Exception message", + val exception = + createTestException( + "Exception message", "TestClass", - cause = createTestException("The real cause!", "TestClass")) + cause = createTestException("The real cause!", "TestClass") + ) buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception) val dumpedString = dumpBuffer() - assertThat(dumpedString) - .contains("Caused by: java.lang.RuntimeException: The real cause!") + assertThat(dumpedString).contains("Caused by: java.lang.RuntimeException: The real cause!") assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)") assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)") } @@ -85,49 +87,47 @@ class LogBufferTest : SysuiTestCase() { buffer = createBuffer() val exception = RuntimeException("Root exception message") exception.addSuppressed( - createTestException( - "First suppressed exception", - "FirstClass", - createTestException("Cause of suppressed exp", "ThirdClass") - )) - exception.addSuppressed( - createTestException("Second suppressed exception", "SecondClass")) + createTestException( + "First suppressed exception", + "FirstClass", + createTestException("Cause of suppressed exp", "ThirdClass") + ) + ) + exception.addSuppressed(createTestException("Second suppressed exception", "SecondClass")) buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception) val dumpedStr = dumpBuffer() // first suppressed exception assertThat(dumpedStr) - .contains("Suppressed: " + - "java.lang.RuntimeException: First suppressed exception") + .contains("Suppressed: " + "java.lang.RuntimeException: First suppressed exception") assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:1)") assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:2)") assertThat(dumpedStr) - .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp") + .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp") assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:1)") assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:2)") // second suppressed exception assertThat(dumpedStr) - .contains("Suppressed: " + - "java.lang.RuntimeException: Second suppressed exception") + .contains("Suppressed: " + "java.lang.RuntimeException: Second suppressed exception") assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:1)") assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:2)") } private fun createTestException( - message: String, - errorClass: String, - cause: Throwable? = null, + message: String, + errorClass: String, + cause: Throwable? = null, ): Exception { val exception = RuntimeException(message, cause) - exception.stackTrace = (1..5).map { lineNumber -> - StackTraceElement(errorClass, - "TestMethod", - "$errorClass.java", - lineNumber) - }.toTypedArray() + exception.stackTrace = + (1..5) + .map { lineNumber -> + StackTraceElement(errorClass, "TestMethod", "$errorClass.java", lineNumber) + } + .toTypedArray() return exception } diff --git a/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_header_bg.xml b/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_header_bg.xml index 698696184a33..9d063e9d2533 100644 --- a/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_header_bg.xml +++ b/packages/SystemUI/res-keyguard/drawable/bouncer_user_switcher_header_bg.xml @@ -21,11 +21,12 @@ android:paddingEnd="0dp"> <item> <shape android:shape="rectangle"> - <solid android:color="?androidprv:attr/colorSurface" /> + <solid android:color="?androidprv:attr/colorSurfaceHighlight" /> <corners android:radius="32dp" /> </shape> </item> <item + android:id="@+id/user_switcher_key_down" android:drawable="@drawable/ic_ksh_key_down" android:gravity="end|center_vertical" android:width="32dp" diff --git a/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml new file mode 100644 index 000000000000..de0e526a97c3 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" > + <size android:height="@dimen/bouncer_user_switcher_popup_items_divider_height"/> + <solid android:color="@color/user_switcher_fullscreen_bg"/> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions.xml b/packages/SystemUI/res-keyguard/layout/footer_actions.xml index 1ce106ed2156..2261ae8d7714 100644 --- a/packages/SystemUI/res-keyguard/layout/footer_actions.xml +++ b/packages/SystemUI/res-keyguard/layout/footer_actions.xml @@ -46,7 +46,7 @@ > <com.android.systemui.statusbar.phone.MultiUserSwitch - android:id="@+id/multi_user_switch" + android:id="@id/multi_user_switch" android:layout_width="@dimen/qs_footer_action_button_size" android:layout_height="@dimen/qs_footer_action_button_size" android:background="@drawable/qs_footer_action_circle" @@ -61,7 +61,7 @@ </com.android.systemui.statusbar.phone.MultiUserSwitch> <com.android.systemui.statusbar.AlphaOptimizedFrameLayout - android:id="@+id/settings_button_container" + android:id="@id/settings_button_container" android:layout_width="@dimen/qs_footer_action_button_size" android:layout_height="@dimen/qs_footer_action_button_size" android:background="@drawable/qs_footer_action_circle" @@ -85,7 +85,7 @@ </com.android.systemui.statusbar.AlphaOptimizedFrameLayout> <com.android.systemui.statusbar.AlphaOptimizedImageView - android:id="@+id/pm_lite" + android:id="@id/pm_lite" android:layout_width="@dimen/qs_footer_action_button_size" android:layout_height="@dimen/qs_footer_action_button_size" android:background="@drawable/qs_footer_action_circle_color" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml index 3ad7c8c4369c..d64587dcf362 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml @@ -37,6 +37,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="@dimen/keyguard_large_clock_top_margin" + android:clipChildren="false" android:visibility="gone" /> <!-- Not quite optimal but needed to translate these items as a group. The diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml index 5486adbd2b8e..6ec65cebf840 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml @@ -24,7 +24,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" androidprv:layout_maxWidth="@dimen/keyguard_security_width" - androidprv:layout_maxHeight="@dimen/keyguard_security_height" android:layout_gravity="center_horizontal|bottom" android:gravity="bottom" > diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml index 16a1d944c4d3..647abee9a99b 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml @@ -27,6 +27,7 @@ systemui:layout_constraintEnd_toEndOf="parent" systemui:layout_constraintTop_toTopOf="parent" android:layout_marginHorizontal="@dimen/status_view_margin_horizontal" + android:clipChildren="false" android:layout_width="0dp" android:layout_height="wrap_content"> <LinearLayout diff --git a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml new file mode 100644 index 000000000000..29832a081612 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 2022, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> + +<merge + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:systemui="http://schemas.android.com/apk/res-auto" > + + <com.android.keyguard.AlphaOptimizedLinearLayout + android:id="@+id/mobile_group" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:orientation="horizontal" > + + <FrameLayout + android:id="@+id/inout_container" + android:layout_height="17dp" + android:layout_width="wrap_content" + android:layout_gravity="center_vertical"> + <ImageView + android:id="@+id/mobile_in" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:src="@drawable/ic_activity_down" + android:visibility="gone" + android:paddingEnd="2dp" + /> + <ImageView + android:id="@+id/mobile_out" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:src="@drawable/ic_activity_up" + android:paddingEnd="2dp" + android:visibility="gone" + /> + </FrameLayout> + <ImageView + android:id="@+id/mobile_type" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_gravity="center_vertical" + android:paddingStart="2.5dp" + android:paddingEnd="1dp" + android:visibility="gone" /> + <Space + android:id="@+id/mobile_roaming_space" + android:layout_height="match_parent" + android:layout_width="@dimen/roaming_icon_start_padding" + android:visibility="gone" + /> + <FrameLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical"> + <com.android.systemui.statusbar.AnimatedImageView + android:id="@+id/mobile_signal" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + systemui:hasOverlappingRendering="false" + /> + <ImageView + android:id="@+id/mobile_roaming" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/stat_sys_roaming" + android:contentDescription="@string/data_connection_roaming" + android:visibility="gone" /> + </FrameLayout> + <ImageView + android:id="@+id/mobile_roaming_large" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/stat_sys_roaming_large" + android:contentDescription="@string/data_connection_roaming" + android:visibility="gone" /> + </com.android.keyguard.AlphaOptimizedLinearLayout> +</merge> diff --git a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml new file mode 100644 index 000000000000..1b38fd2c4283 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 2022, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> +<com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/mobile_combo" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" > + + <include layout="@layout/status_bar_mobile_signal_group_inner" /> + +</com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView> + diff --git a/packages/SystemUI/res-keyguard/values/config.xml b/packages/SystemUI/res-keyguard/values/config.xml index b1d33758f1b3..a25ab5109fa8 100644 --- a/packages/SystemUI/res-keyguard/values/config.xml +++ b/packages/SystemUI/res-keyguard/values/config.xml @@ -28,11 +28,6 @@ <!-- Will display the bouncer on one side of the display, and the current user icon and user switcher on the other side --> <bool name="config_enableBouncerUserSwitcher">false</bool> - <!-- Whether to show the face scanning animation on devices with face auth supported. - The face scanning animation renders in a SW layer in ScreenDecorations. - Enabling this will also render the camera protection in the SW layer - (instead of HW, if relevant)."=--> - <bool name="config_enableFaceScanningAnimation">true</bool> <!-- Time to be considered a consecutive fingerprint failure in ms --> <integer name="fp_consecutive_failure_time_ms">3500</integer> </resources> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 46f6ab2399d1..0a55cf779683 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -119,6 +119,7 @@ <dimen name="bouncer_user_switcher_width">248dp</dimen> <dimen name="bouncer_user_switcher_popup_header_height">12dp</dimen> <dimen name="bouncer_user_switcher_popup_divider_height">4dp</dimen> + <dimen name="bouncer_user_switcher_popup_items_divider_height">2dp</dimen> <dimen name="bouncer_user_switcher_item_padding_vertical">10dp</dimen> <dimen name="bouncer_user_switcher_item_padding_horizontal">12dp</dimen> <dimen name="bouncer_user_switcher_header_padding_end">44dp</dimen> diff --git a/packages/SystemUI/res-keyguard/values/ids.xml b/packages/SystemUI/res-keyguard/values/ids.xml new file mode 100644 index 000000000000..0dff4ffa3866 --- /dev/null +++ b/packages/SystemUI/res-keyguard/values/ids.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~ + --> + +<resources> + <item type="id" name="header_footer_views_added_tag_key" /> +</resources> diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml index d90156d451c7..a129fb650ba6 100644 --- a/packages/SystemUI/res-keyguard/values/strings.xml +++ b/packages/SystemUI/res-keyguard/values/strings.xml @@ -201,13 +201,13 @@ <string name="kg_prompt_reason_restart_password">Password required after device restarts</string> <!-- An explanation text that the pattern needs to be solved since the user hasn't used strong authentication since quite some time. [CHAR LIMIT=80] --> - <string name="kg_prompt_reason_timeout_pattern">Pattern required for additional security</string> + <string name="kg_prompt_reason_timeout_pattern">For additional security, use pattern instead</string> <!-- An explanation text that the pin needs to be entered since the user hasn't used strong authentication since quite some time. [CHAR LIMIT=80] --> - <string name="kg_prompt_reason_timeout_pin">PIN required for additional security</string> + <string name="kg_prompt_reason_timeout_pin">For additional security, use PIN instead</string> <!-- An explanation text that the password needs to be entered since the user hasn't used strong authentication since quite some time. [CHAR LIMIT=80] --> - <string name="kg_prompt_reason_timeout_password">Password required for additional security</string> + <string name="kg_prompt_reason_timeout_password">For additional security, use password instead</string> <!-- An explanation text that the credential needs to be entered because a device admin has locked the device. [CHAR LIMIT=80] --> @@ -241,4 +241,6 @@ <string name="clock_title_bubble">Bubble</string> <!-- Name of the "Analog" clock face [CHAR LIMIT=15]--> <string name="clock_title_analog">Analog</string> + <!-- Title of bouncer when we want to authenticate before continuing with action. [CHAR LIMIT=NONE] --> + <string name="keyguard_unlock_to_continue">Unlock your device to continue</string> </resources> diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml index a1d12668d27a..b86929efe406 100644 --- a/packages/SystemUI/res-keyguard/values/styles.xml +++ b/packages/SystemUI/res-keyguard/values/styles.xml @@ -25,7 +25,7 @@ </style> <style name="Keyguard.TextView.EmergencyButton" parent="Theme.SystemUI"> <item name="android:textColor">?androidprv:attr/textColorOnAccent</item> - <item name="android:textSize">14dp</item> + <item name="android:textSize">14sp</item> <item name="android:background">@drawable/kg_emergency_button_background</item> <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> <item name="android:paddingLeft">12dp</item> diff --git a/packages/SystemUI/res/drawable-hdpi/textfield_default_filled.9.png b/packages/SystemUI/res/drawable-hdpi/textfield_default_filled.9.png Binary files differnew file mode 100644 index 000000000000..3dd997fade6c --- /dev/null +++ b/packages/SystemUI/res/drawable-hdpi/textfield_default_filled.9.png diff --git a/packages/SystemUI/res/drawable-mdpi/textfield_default_filled.9.png b/packages/SystemUI/res/drawable-mdpi/textfield_default_filled.9.png Binary files differnew file mode 100644 index 000000000000..80aba01091fe --- /dev/null +++ b/packages/SystemUI/res/drawable-mdpi/textfield_default_filled.9.png diff --git a/packages/SystemUI/res/drawable-xhdpi/textfield_default_filled.9.png b/packages/SystemUI/res/drawable-xhdpi/textfield_default_filled.9.png Binary files differnew file mode 100644 index 000000000000..b3f89ed7ea7a --- /dev/null +++ b/packages/SystemUI/res/drawable-xhdpi/textfield_default_filled.9.png diff --git a/packages/SystemUI/res/drawable-xxhdpi/textfield_default_filled.9.png b/packages/SystemUI/res/drawable-xxhdpi/textfield_default_filled.9.png Binary files differnew file mode 100644 index 000000000000..efa2cb988ac1 --- /dev/null +++ b/packages/SystemUI/res/drawable-xxhdpi/textfield_default_filled.9.png diff --git a/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml index 69390848245d..33c68bf1f6ac 100644 --- a/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml +++ b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml @@ -16,6 +16,6 @@ --> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - <solid android:color="@android:color/white" /> + <solid android:color="@android:color/transparent" /> <corners android:radius="@dimen/qs_media_album_radius" /> </shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/edit_text_filled.xml b/packages/SystemUI/res/drawable/edit_text_filled.xml new file mode 100644 index 000000000000..cca34d456078 --- /dev/null +++ b/packages/SystemUI/res/drawable/edit_text_filled.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:insetLeft="4dp" + android:insetRight="4dp" + android:insetTop="10dp" + android:insetBottom="10dp"> + <selector> + <item android:state_enabled="false"> + <nine-patch android:src="@drawable/textfield_default_filled" + android:tint="?android:attr/colorControlNormal" /> + </item> + <item android:state_pressed="false" android:state_focused="false"> + <nine-patch android:src="@drawable/textfield_default_filled" + android:tint="?android:attr/colorControlNormal" /> + </item> + <item> + <nine-patch android:src="@drawable/textfield_default_filled" + android:tint="?android:attr/colorControlActivated" /> + </item> + </selector> +</inset> diff --git a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml b/packages/SystemUI/res/drawable/media_squiggly_progress.xml index 9e61236aa7df..9cd3f6288b1d 100644 --- a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml +++ b/packages/SystemUI/res/drawable/media_squiggly_progress.xml @@ -14,4 +14,4 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.systemui.media.SquigglyProgress />
\ No newline at end of file +<com.android.systemui.media.controls.ui.SquigglyProgress />
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/overlay_badge_background.xml b/packages/SystemUI/res/drawable/overlay_badge_background.xml new file mode 100644 index 000000000000..857632edcf0d --- /dev/null +++ b/packages/SystemUI/res/drawable/overlay_badge_background.xml @@ -0,0 +1,21 @@ +<?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. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="oval"> + <solid android:color="?androidprv:attr/colorSurface"/> +</shape> diff --git a/packages/SystemUI/res/drawable/qs_media_background.xml b/packages/SystemUI/res/drawable/qs_media_background.xml index 6ed3a0aed091..217656dab022 100644 --- a/packages/SystemUI/res/drawable/qs_media_background.xml +++ b/packages/SystemUI/res/drawable/qs_media_background.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<com.android.systemui.media.IlluminationDrawable +<com.android.systemui.media.controls.ui.IlluminationDrawable xmlns:systemui="http://schemas.android.com/apk/res-auto" systemui:highlight="15" systemui:cornerRadius="@dimen/notification_corner_radius" />
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/qs_media_light_source.xml b/packages/SystemUI/res/drawable/qs_media_light_source.xml index b2647c1f6697..849349a5f100 100644 --- a/packages/SystemUI/res/drawable/qs_media_light_source.xml +++ b/packages/SystemUI/res/drawable/qs_media_light_source.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.systemui.media.LightSourceDrawable +<com.android.systemui.media.controls.ui.LightSourceDrawable xmlns:systemui="http://schemas.android.com/apk/res-auto" systemui:rippleMinSize="25dp" systemui:rippleMaxSize="135dp" />
\ No newline at end of file diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml index da76c8d0b11a..3bcc37a478c9 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml @@ -16,46 +16,74 @@ <com.android.systemui.biometrics.AuthCredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical" - android:gravity="center_horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:orientation="horizontal" + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPinPasswordStyle"> - <TextView - android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Title"/> + <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" + android:layout_width="wrap_content" + android:layout_height="match_parent"> - <TextView - android:id="@+id/subtitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Subtitle"/> + <ImageView + android:id="@+id/icon" + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> - <TextView - android:id="@+id/description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Description"/> + <TextView + android:id="@+id/title" + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> - <ImeAwareEditText - android:id="@+id/lockPassword" - android:layout_width="208dp" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:minHeight="48dp" - android:gravity="center" - android:inputType="textPassword" - android:maxLength="500" - android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" - style="@style/TextAppearance.AuthCredential.PasswordEntry"/> - - <TextView - android:id="@+id/error" - android:layout_width="match_parent" + <TextView + android:id="@+id/subtitle" + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_alignParentLeft="true" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/description" + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" + android:layout_alignParentLeft="true" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + </RelativeLayout> + + <LinearLayout + android:id="@+id/auth_credential_input" + android:layout_width="wrap_content" android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Error"/> + android:orientation="vertical"> + + <ImeAwareEditText + android:id="@+id/lockPassword" + style="?passwordTextAppearance" + android:layout_width="208dp" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" + android:inputType="textPassword" + android:minHeight="48dp" /> + + <TextView + android:id="@+id/error" + style="?errorTextAppearance" + android:layout_gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + </LinearLayout> </com.android.systemui.biometrics.AuthCredentialPasswordView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml index 19a85fec1397..a3dd334bd667 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -16,91 +16,71 @@ <com.android.systemui.biometrics.AuthCredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPatternStyle"> - <LinearLayout + <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" android:layout_width="0dp" android:layout_height="match_parent" - android:layout_weight="1" - android:gravity="center" - android:orientation="vertical"> - - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> + android:layout_weight="1"> <ImageView android:id="@+id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> <TextView android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Title"/> + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> <TextView android:id="@+id/subtitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Subtitle"/> + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_alignParentLeft="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> <TextView android:id="@+id/description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Description"/> + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" + android:layout_alignParentLeft="true" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> + </RelativeLayout> + + <FrameLayout + android:layout_weight="1" + style="?containerStyle" + android:layout_width="0dp" + android:layout_height="match_parent"> + + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_gravity="center" + android:layout_width="match_parent" + android:layout_height="match_parent"/> <TextView android:id="@+id/error" + style="?errorTextAppearance" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/TextAppearance.AuthCredential.Error"/> - - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:layout_weight="1"/> - - </LinearLayout> - - <LinearLayout - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="1" - android:orientation="vertical" - android:gravity="center" - android:paddingLeft="0dp" - android:paddingRight="0dp" - android:paddingTop="0dp" - android:paddingBottom="16dp" - android:clipToPadding="false"> - - <FrameLayout - android:layout_width="wrap_content" - android:layout_height="0dp" - android:layout_weight="1" - style="@style/LockPatternContainerStyle"> - - <com.android.internal.widget.LockPatternView - android:id="@+id/lockPattern" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - style="@style/LockPatternStyleBiometricPrompt"/> - - </FrameLayout> + android:layout_gravity="center_horizontal|bottom"/> - </LinearLayout> + </FrameLayout> </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml index 0ff1db2ef694..774b335f913e 100644 --- a/packages/SystemUI/res/layout/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml @@ -16,74 +16,71 @@ <com.android.systemui.biometrics.AuthCredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:elevation="@dimen/biometric_dialog_elevation" - android:orientation="vertical"> + android:orientation="vertical" + android:theme="?app:attr/lockPinPasswordStyle"> <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <LinearLayout - android:id="@+id/auth_credential_header" - style="@style/AuthCredentialHeaderStyle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_alignParentTop="true"> + android:layout_height="match_parent"> - <ImageView - android:id="@+id/icon" - android:layout_width="48dp" - android:layout_height="48dp" - android:contentDescription="@null" /> + <ImageView + android:id="@+id/icon" + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> - <TextView - android:id="@+id/title" - style="@style/TextAppearance.AuthNonBioCredential.Title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - - <TextView - android:id="@+id/subtitle" - style="@style/TextAppearance.AuthNonBioCredential.Subtitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - - <TextView - android:id="@+id/description" - style="@style/TextAppearance.AuthNonBioCredential.Description" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + <TextView + android:id="@+id/title" + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> - </LinearLayout> + <TextView + android:id="@+id/subtitle" + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> - <LinearLayout + <TextView + android:id="@+id/description" + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" android:layout_width="match_parent" - android:layout_height="match_parent" - android:gravity="center" - android:orientation="vertical" - android:layout_alignParentBottom="true"> + android:layout_height="wrap_content"/> + </RelativeLayout> - <ImeAwareEditText - android:id="@+id/lockPassword" - style="@style/TextAppearance.AuthCredential.PasswordEntry" - android:layout_width="208dp" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" - android:inputType="textPassword" - android:minHeight="48dp" /> + <LinearLayout + android:id="@+id/auth_credential_input" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> - <TextView - android:id="@+id/error" - style="@style/TextAppearance.AuthNonBioCredential.Error" - android:layout_width="match_parent" - android:layout_height="wrap_content" /> + <ImeAwareEditText + android:id="@+id/lockPassword" + style="?passwordTextAppearance" + android:layout_width="208dp" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:imeOptions="actionNext|flagNoFullscreen|flagForceAscii" + android:inputType="textPassword" + android:minHeight="48dp" /> - </LinearLayout> + <TextView + android:id="@+id/error" + style="?errorTextAppearance" + android:layout_gravity="center_horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> - </RelativeLayout> + </LinearLayout> </com.android.systemui.biometrics.AuthCredentialPasswordView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml index dada9813c320..4af997017bba 100644 --- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -16,87 +16,66 @@ <com.android.systemui.biometrics.AuthCredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - android:gravity="center_horizontal" - android:elevation="@dimen/biometric_dialog_elevation"> + android:elevation="@dimen/biometric_dialog_elevation" + android:theme="?app:attr/lockPatternStyle"> <RelativeLayout + android:id="@+id/auth_credential_header" + style="?headerStyle" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <LinearLayout - android:id="@+id/auth_credential_header" - style="@style/AuthCredentialHeaderStyle" - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <ImageView - android:id="@+id/icon" - android:layout_width="48dp" - android:layout_height="48dp" - android:contentDescription="@null" /> - - <TextView - android:id="@+id/title" - style="@style/TextAppearance.AuthNonBioCredential.Title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - - <TextView - android:id="@+id/subtitle" - style="@style/TextAppearance.AuthNonBioCredential.Subtitle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/icon" + style="?headerIconStyle" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:contentDescription="@null"/> + + <TextView + android:id="@+id/title" + style="?titleTextAppearance" + android:layout_below="@id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <TextView + android:id="@+id/subtitle" + style="?subTitleTextAppearance" + android:layout_below="@id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <TextView + android:id="@+id/description" + style="?descriptionTextAppearance" + android:layout_below="@id/subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + </RelativeLayout> - <TextView - android:id="@+id/description" - style="@style/TextAppearance.AuthNonBioCredential.Description" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - </LinearLayout> + <FrameLayout + android:id="@+id/auth_credential_container" + style="?containerStyle" + android:layout_width="match_parent" + android:layout_height="match_parent"> - <LinearLayout + <com.android.internal.widget.LockPatternView + android:id="@+id/lockPattern" + android:layout_gravity="center" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/auth_credential_header" - android:gravity="center" - android:orientation="vertical" - android:paddingBottom="16dp" - android:paddingTop="60dp"> + android:layout_height="match_parent"/> - <FrameLayout - style="@style/LockPatternContainerStyle" - android:layout_width="wrap_content" - android:layout_height="0dp" - android:layout_weight="1"> - - <com.android.internal.widget.LockPatternView - android:id="@+id/lockPattern" - style="@style/LockPatternStyle" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" /> - - </FrameLayout> - - </LinearLayout> - - <LinearLayout + <TextView + android:id="@+id/error" + style="?errorTextAppearance" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentBottom="true"> - - <TextView - android:id="@+id/error" - style="@style/TextAppearance.AuthNonBioCredential.Error" - android:layout_width="match_parent" - android:layout_height="wrap_content" /> - - </LinearLayout> - - </RelativeLayout> + android:layout_gravity="center_horizontal|bottom"/> + </FrameLayout> </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/chipbar.xml index d88680669fe0..bc97e511e7f4 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip.xml +++ b/packages/SystemUI/res/layout/chipbar.xml @@ -16,15 +16,15 @@ <!-- Wrap in a frame layout so that we can update the margins on the inner layout. (Since this view is the root view of a window, we cannot change the root view's margins.) --> <!-- Alphas start as 0 because the view will be animated in. --> -<FrameLayout +<com.android.systemui.temporarydisplay.chipbar.ChipbarRootView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:id="@+id/media_ttt_sender_chip" + android:id="@+id/chipbar_root_view" android:layout_width="wrap_content" android:layout_height="wrap_content"> <LinearLayout - android:id="@+id/media_ttt_sender_chip_inner" + android:id="@+id/chipbar_inner" android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -39,7 +39,7 @@ > <com.android.internal.widget.CachingIconView - android:id="@+id/app_icon" + android:id="@+id/start_icon" android:layout_width="@dimen/media_ttt_app_icon_size" android:layout_height="@dimen/media_ttt_app_icon_size" android:layout_marginEnd="12dp" @@ -69,7 +69,7 @@ /> <ImageView - android:id="@+id/failure_icon" + android:id="@+id/error" android:layout_width="@dimen/media_ttt_status_icon_size" android:layout_height="@dimen/media_ttt_status_icon_size" android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" @@ -78,11 +78,11 @@ android:alpha="0.0" /> + <!-- TODO(b/245610654): Re-name all the media-specific dimens to chipbar dimens instead. --> <TextView - android:id="@+id/undo" + android:id="@+id/end_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/media_transfer_undo" android:textColor="?androidprv:attr/textColorOnAccent" android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" android:textSize="@dimen/media_ttt_text_size" @@ -97,4 +97,4 @@ /> </LinearLayout> -</FrameLayout> +</com.android.systemui.temporarydisplay.chipbar.ChipbarRootView> diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 1a1fc75a41a1..0e9abee2f050 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.systemui.screenshot.DraggableConstraintLayout +<com.android.systemui.clipboardoverlay.ClipboardOverlayView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" @@ -157,4 +157,4 @@ android:layout_margin="@dimen/overlay_dismiss_button_margin" android:src="@drawable/overlay_cancel"/> </FrameLayout> -</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file +</com.android.systemui.clipboardoverlay.ClipboardOverlayView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml new file mode 100644 index 000000000000..1a1fc75a41a1 --- /dev/null +++ b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.systemui.screenshot.DraggableConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/clipboard_ui" + android:theme="@style/FloatingOverlay" + android:alpha="0" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/clipboard_overlay_window_name"> + <ImageView + android:id="@+id/actions_container_background" + android:visibility="gone" + android:layout_height="0dp" + android:layout_width="0dp" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" + app:layout_constraintBottom_toBottomOf="@+id/actions_container" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/actions_container" + app:layout_constraintEnd_toEndOf="@+id/actions_container"/> + <HorizontalScrollView + android:id="@+id/actions_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" + android:paddingEnd="@dimen/overlay_action_container_padding_right" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:scrollbars="none" + android:layout_marginBottom="4dp" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_percent="1.0" + app:layout_constraintWidth_max="wrap" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@+id/preview_border" + app:layout_constraintEnd_toEndOf="parent"> + <LinearLayout + android:id="@+id/actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:animateLayoutChanges="true"> + <include layout="@layout/overlay_action_chip" + android:id="@+id/share_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/remote_copy_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/edit_chip"/> + </LinearLayout> + </HorizontalScrollView> + <View + android:id="@+id/preview_border" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="@dimen/overlay_offset_x" + android:layout_marginBottom="12dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + android:elevation="7dp" + app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end" + app:layout_constraintTop_toTopOf="@id/clipboard_preview_top" + android:background="@drawable/overlay_border"/> + <androidx.constraintlayout.widget.Barrier + android:id="@+id/clipboard_preview_end" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierMargin="@dimen/overlay_border_width" + app:barrierDirection="end" + app:constraint_referenced_ids="clipboard_preview"/> + <androidx.constraintlayout.widget.Barrier + android:id="@+id/clipboard_preview_top" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="top" + app:barrierMargin="@dimen/overlay_border_width_neg" + app:constraint_referenced_ids="clipboard_preview"/> + <FrameLayout + android:id="@+id/clipboard_preview" + android:elevation="7dp" + android:background="@drawable/overlay_preview_background" + android:clipChildren="true" + android:clipToOutline="true" + android:clipToPadding="true" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_margin="@dimen/overlay_border_width" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:layout_constraintBottom_toBottomOf="@id/preview_border" + app:layout_constraintStart_toStartOf="@id/preview_border" + app:layout_constraintEnd_toEndOf="@id/preview_border" + app:layout_constraintTop_toTopOf="@id/preview_border"> + <TextView android:id="@+id/text_preview" + android:textFontWeight="500" + android:padding="8dp" + android:gravity="center|start" + android:ellipsize="end" + android:autoSizeTextType="uniform" + android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font" + android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font" + android:textColor="?attr/overlayButtonTextColor" + android:textColorLink="?attr/overlayButtonTextColor" + android:background="?androidprv:attr/colorAccentSecondary" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="@dimen/clipboard_preview_size"/> + <ImageView + android:id="@+id/image_preview" + android:scaleType="fitCenter" + android:adjustViewBounds="true" + android:contentDescription="@string/clipboard_image_preview" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + <TextView + android:id="@+id/hidden_preview" + android:visibility="gone" + android:textFontWeight="500" + android:padding="8dp" + android:gravity="center" + android:textSize="14sp" + android:textColor="?attr/overlayButtonTextColor" + android:background="?androidprv:attr/colorAccentSecondary" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="@dimen/clipboard_preview_size"/> + </FrameLayout> + <FrameLayout + android:id="@+id/dismiss_button" + android:layout_width="@dimen/overlay_dismiss_button_tappable_size" + android:layout_height="@dimen/overlay_dismiss_button_tappable_size" + android:elevation="10dp" + android:visibility="gone" + android:alpha="0" + app:layout_constraintStart_toEndOf="@id/clipboard_preview" + app:layout_constraintEnd_toEndOf="@id/clipboard_preview" + app:layout_constraintTop_toTopOf="@id/clipboard_preview" + app:layout_constraintBottom_toTopOf="@id/clipboard_preview" + android:contentDescription="@string/clipboard_dismiss_description"> + <ImageView + android:id="@+id/dismiss_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="@dimen/overlay_dismiss_button_margin" + android:src="@drawable/overlay_cancel"/> + </FrameLayout> +</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index 5dc34b9db594..a565988c14ad 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -73,8 +73,8 @@ android:singleLine="true" android:textDirection="locale" android:textAppearance="@style/TextAppearance.QS.Status" - android:transformPivotX="0sp" - android:transformPivotY="20sp" + android:transformPivotX="0dp" + android:transformPivotY="24dp" android:scaleX="1" android:scaleY="1" /> diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index 8df8c49ee057..6120863f23ab 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -59,7 +59,7 @@ </LinearLayout> - <ImageView + <com.android.systemui.common.ui.view.LaunchableImageView android:id="@+id/start_button" android:layout_height="@dimen/keyguard_affordance_fixed_height" android:layout_width="@dimen/keyguard_affordance_fixed_width" @@ -71,7 +71,7 @@ android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset" android:visibility="gone" /> - <ImageView + <com.android.systemui.common.ui.view.LaunchableImageView android:id="@+id/end_button" android:layout_height="@dimen/keyguard_affordance_fixed_height" android:layout_width="@dimen/keyguard_affordance_fixed_width" diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index 50d3cc4e8a7a..715c86957e02 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -24,7 +24,7 @@ android:clipToPadding="false" android:forceHasOverlappingRendering="false" android:theme="@style/MediaPlayer"> - <com.android.systemui.media.MediaScrollView + <com.android.systemui.media.controls.ui.MediaScrollView android:id="@+id/media_carousel_scroller" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -42,7 +42,7 @@ > <!-- QSMediaPlayers will be added here dynamically --> </LinearLayout> - </com.android.systemui.media.MediaScrollView> + </com.android.systemui.media.controls.ui.MediaScrollView> <com.android.systemui.qs.PageIndicator android:id="@+id/media_page_indicator" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/layout/media_projection_app_selector.xml b/packages/SystemUI/res/layout/media_projection_app_selector.xml index 226bc6afc5ab..e4749381243a 100644 --- a/packages/SystemUI/res/layout/media_projection_app_selector.xml +++ b/packages/SystemUI/res/layout/media_projection_app_selector.xml @@ -49,6 +49,8 @@ android:layout_height="wrap_content" android:layout_width="wrap_content" android:textAppearance="?android:attr/textAppearanceLarge" + android:focusable="false" + android:clickable="false" android:gravity="center" android:paddingBottom="@*android:dimen/chooser_view_spacing" android:paddingLeft="24dp" diff --git a/packages/SystemUI/res/layout/media_projection_recent_tasks.xml b/packages/SystemUI/res/layout/media_projection_recent_tasks.xml index a2b3c404f077..31baf26e4a1b 100644 --- a/packages/SystemUI/res/layout/media_projection_recent_tasks.xml +++ b/packages/SystemUI/res/layout/media_projection_recent_tasks.xml @@ -23,8 +23,9 @@ > <FrameLayout + android:id="@+id/media_projection_recent_tasks_container" android:layout_width="match_parent" - android:layout_height="256dp"> + android:layout_height="wrap_content"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/media_projection_recent_tasks_recycler" diff --git a/packages/SystemUI/res/layout/media_projection_task_item.xml b/packages/SystemUI/res/layout/media_projection_task_item.xml index 75f73cd7a1e9..cfa586fb5767 100644 --- a/packages/SystemUI/res/layout/media_projection_task_item.xml +++ b/packages/SystemUI/res/layout/media_projection_task_item.xml @@ -18,7 +18,9 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:gravity="center" - android:orientation="vertical"> + android:orientation="vertical" + android:clickable="true" + > <ImageView android:id="@+id/task_icon" @@ -27,12 +29,12 @@ android:layout_margin="8dp" android:importantForAccessibility="no" /> - <!-- TODO(b/240924926) use a custom view that will handle thumbnail cropping correctly --> - <!-- TODO(b/240924926) dynamically change the view size based on the screen size --> - <ImageView + <!-- This view size will be calculated in runtime --> + <com.android.systemui.mediaprojection.appselector.view.MediaProjectionTaskView android:id="@+id/task_thumbnail" - android:layout_width="100dp" - android:layout_height="216dp" - android:scaleType="centerCrop" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clickable="false" + android:focusable="false" /> </LinearLayout> diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml index e079fd3c5e8f..21d12c278453 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml @@ -29,6 +29,7 @@ <com.android.internal.widget.CachingIconView android:id="@+id/app_icon" + android:background="@drawable/media_ttt_chip_background_receiver" android:layout_width="@dimen/media_ttt_icon_size_receiver" android:layout_height="@dimen/media_ttt_icon_size_receiver" android:layout_gravity="center|bottom" diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml index 2fb6d6cb9bd5..9fc3f409642b 100644 --- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml +++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml @@ -23,6 +23,7 @@ android:layout_height="wrap_content" android:layout_gravity="@integer/notification_panel_layout_gravity" android:background="@android:color/transparent" + android:importantForAccessibility="no" android:baselineAligned="false" android:clickable="false" android:clipChildren="false" @@ -56,7 +57,7 @@ android:clipToPadding="false" android:focusable="true" android:paddingBottom="@dimen/qqs_layout_padding_bottom" - android:importantForAccessibility="yes"> + android:importantForAccessibility="no"> </com.android.systemui.qs.QuickQSPanel> </RelativeLayout> diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml index 60bc3732cde0..8b5d953c3fe7 100644 --- a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml +++ b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml @@ -25,6 +25,7 @@ android:gravity="center" android:layout_gravity="top" android:orientation="horizontal" + android:importantForAccessibility="no" android:clickable="true" android:minHeight="48dp"> diff --git a/packages/SystemUI/res/layout/screenshot.xml b/packages/SystemUI/res/layout/screenshot.xml index c29e11bff624..c134c8e2d339 100644 --- a/packages/SystemUI/res/layout/screenshot.xml +++ b/packages/SystemUI/res/layout/screenshot.xml @@ -35,12 +35,6 @@ android:visibility="gone" android:elevation="7dp" android:src="@android:color/white"/> - <com.android.systemui.screenshot.ScreenshotSelectorView - android:id="@+id/screenshot_selector" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="gone" - android:pointerIcon="crosshair"/> <include layout="@layout/screenshot_static" android:id="@+id/screenshot_static"/> </com.android.systemui.screenshot.ScreenshotView> diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml index 9c027495aa1e..1ac78d491d78 100644 --- a/packages/SystemUI/res/layout/screenshot_static.xml +++ b/packages/SystemUI/res/layout/screenshot_static.xml @@ -103,8 +103,18 @@ app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" app:layout_constraintStart_toStartOf="@id/screenshot_preview_border" app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border" - app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"> - </ImageView> + app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"/> + <ImageView + android:id="@+id/screenshot_badge" + android:layout_width="24dp" + android:layout_height="24dp" + android:padding="4dp" + android:visibility="gone" + android:background="@drawable/overlay_badge_background" + android:elevation="8dp" + android:src="@drawable/overlay_cancel" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/> <FrameLayout android:id="@+id/screenshot_dismiss_button" android:layout_width="@dimen/overlay_dismiss_button_tappable_size" diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml index f0e49d5c2011..92ef3f8201cb 100644 --- a/packages/SystemUI/res/layout/status_bar_expanded.xml +++ b/packages/SystemUI/res/layout/status_bar_expanded.xml @@ -32,41 +32,8 @@ android:layout_height="match_parent" android:layout_width="match_parent" /> - <include - layout="@layout/keyguard_bottom_area" - android:visibility="gone" /> - - <ViewStub - android:id="@+id/keyguard_user_switcher_stub" - android:layout="@layout/keyguard_user_switcher" - android:layout_height="match_parent" - android:layout_width="match_parent" /> - <include layout="@layout/status_bar_expanded_plugin_frame"/> - <include layout="@layout/dock_info_bottom_area_overlay" /> - - <com.android.keyguard.LockIconView - android:id="@+id/lock_icon_view" - android:layout_width="wrap_content" - android:layout_height="wrap_content"> - <!-- Background protection --> - <ImageView - android:id="@+id/lock_icon_bg" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@drawable/fingerprint_bg" - android:visibility="invisible"/> - - <ImageView - android:id="@+id/lock_icon" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - android:scaleType="centerCrop"/> - - </com.android.keyguard.LockIconView> - <com.android.systemui.shade.NotificationsQuickSettingsContainer android:layout_width="match_parent" android:layout_height="match_parent" @@ -145,6 +112,39 @@ /> </com.android.systemui.shade.NotificationsQuickSettingsContainer> + <include + layout="@layout/keyguard_bottom_area" + android:visibility="gone" /> + + <ViewStub + android:id="@+id/keyguard_user_switcher_stub" + android:layout="@layout/keyguard_user_switcher" + android:layout_height="match_parent" + android:layout_width="match_parent" /> + + <include layout="@layout/dock_info_bottom_area_overlay" /> + + <com.android.keyguard.LockIconView + android:id="@+id/lock_icon_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + <!-- Background protection --> + <ImageView + android:id="@+id/lock_icon_bg" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/fingerprint_bg" + android:visibility="invisible"/> + + <ImageView + android:id="@+id/lock_icon" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:scaleType="centerCrop"/> + + </com.android.keyguard.LockIconView> + <FrameLayout android:id="@+id/preview_container" android:layout_width="match_parent" diff --git a/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml b/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml index 10d49b38ae75..d6c63eb4feac 100644 --- a/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml +++ b/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml @@ -18,80 +18,12 @@ --> <com.android.systemui.statusbar.StatusBarMobileView xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:systemui="http://schemas.android.com/apk/res-auto" android:id="@+id/mobile_combo" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center_vertical" > - <com.android.keyguard.AlphaOptimizedLinearLayout - android:id="@+id/mobile_group" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:gravity="center_vertical" - android:orientation="horizontal" > + <include layout="@layout/status_bar_mobile_signal_group_inner" /> - <FrameLayout - android:id="@+id/inout_container" - android:layout_height="17dp" - android:layout_width="wrap_content" - android:layout_gravity="center_vertical"> - <ImageView - android:id="@+id/mobile_in" - android:layout_height="wrap_content" - android:layout_width="wrap_content" - android:src="@drawable/ic_activity_down" - android:visibility="gone" - android:paddingEnd="2dp" - /> - <ImageView - android:id="@+id/mobile_out" - android:layout_height="wrap_content" - android:layout_width="wrap_content" - android:src="@drawable/ic_activity_up" - android:paddingEnd="2dp" - android:visibility="gone" - /> - </FrameLayout> - <ImageView - android:id="@+id/mobile_type" - android:layout_height="wrap_content" - android:layout_width="wrap_content" - android:layout_gravity="center_vertical" - android:paddingStart="2.5dp" - android:paddingEnd="1dp" - android:visibility="gone" /> - <Space - android:id="@+id/mobile_roaming_space" - android:layout_height="match_parent" - android:layout_width="@dimen/roaming_icon_start_padding" - android:visibility="gone" - /> - <FrameLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical"> - <com.android.systemui.statusbar.AnimatedImageView - android:id="@+id/mobile_signal" - android:layout_height="wrap_content" - android:layout_width="wrap_content" - systemui:hasOverlappingRendering="false" - /> - <ImageView - android:id="@+id/mobile_roaming" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/stat_sys_roaming" - android:contentDescription="@string/data_connection_roaming" - android:visibility="gone" /> - </FrameLayout> - <ImageView - android:id="@+id/mobile_roaming_large" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/stat_sys_roaming_large" - android:contentDescription="@string/data_connection_roaming" - android:visibility="gone" /> - </com.android.keyguard.AlphaOptimizedLinearLayout> </com.android.systemui.statusbar.StatusBarMobileView> diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 9d7b01c8d252..49ef330dcc52 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -59,4 +59,5 @@ <dimen name="large_dialog_width">348dp</dimen> <dimen name="qs_panel_padding_top">@dimen/qqs_layout_margin_top</dimen> + <dimen name="qs_panel_padding_top_combined_headers">@dimen/qs_panel_padding_top</dimen> </resources> diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml index 89191984b9e8..aefd9981d02e 100644 --- a/packages/SystemUI/res/values-land/styles.xml +++ b/packages/SystemUI/res/values-land/styles.xml @@ -18,4 +18,42 @@ <style name="BrightnessDialogContainer" parent="@style/BaseBrightnessDialogContainer"> <item name="android:layout_width">360dp</item> </style> + + <style name="AuthCredentialHeaderStyle"> + <item name="android:paddingStart">48dp</item> + <item name="android:paddingEnd">24dp</item> + <item name="android:paddingTop">48dp</item> + <item name="android:paddingBottom">10dp</item> + <item name="android:gravity">top|left</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">320dp</item> + <item name="android:maxWidth">320dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">60dp</item> + <item name="android:paddingVertical">20dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">36dp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:textSize">18sp</item> + </style> + </resources> diff --git a/packages/SystemUI/res/values-sw600dp-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml new file mode 100644 index 000000000000..8148d3dfaf7d --- /dev/null +++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">120dp</item> + <item name="android:paddingVertical">40dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> +</resources> diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml new file mode 100644 index 000000000000..771de08cb360 --- /dev/null +++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialHeaderStyle"> + <item name="android:paddingStart">120dp</item> + <item name="android:paddingEnd">120dp</item> + <item name="android:paddingTop">80dp</item> + <item name="android:paddingBottom">10dp</item> + <item name="android:layout_gravity">top</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">180dp</item> + <item name="android:paddingVertical">80dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index 5dcbeb5c85cf..599bf30a5135 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -68,6 +68,7 @@ <dimen name="qs_security_footer_background_inset">0dp</dimen> <dimen name="qs_panel_padding_top">8dp</dimen> + <dimen name="qs_panel_padding_top_combined_headers">@dimen/qs_panel_padding_top</dimen> <!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) --> <dimen name="large_dialog_width">472dp</dimen> diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml new file mode 100644 index 000000000000..f9ed67d89de7 --- /dev/null +++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">120dp</item> + <item name="android:paddingVertical">40dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Subtitle"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Description"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">16dp</item> + <item name="android:textSize">18sp</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml new file mode 100644 index 000000000000..78d299c483e6 --- /dev/null +++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="AuthCredentialHeaderStyle"> + <item name="android:paddingStart">120dp</item> + <item name="android:paddingEnd">120dp</item> + <item name="android:paddingTop">80dp</item> + <item name="android:paddingBottom">10dp</item> + <item name="android:layout_gravity">top</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:paddingHorizontal">240dp</item> + <item name="android:paddingVertical">120dp</item> + </style> + + <style name="TextAppearance.AuthNonBioCredential.Title"> + <item name="android:fontFamily">google-sans</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36sp</item> + <item name="android:focusable">true</item> + </style> + +</resources> diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index 9a71995383ac..df0659d67afe 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -191,5 +191,18 @@ <declare-styleable name="DelayableMarqueeTextView"> <attr name="marqueeDelay" format="integer" /> </declare-styleable> + + <declare-styleable name="AuthCredentialView"> + <attr name="lockPatternStyle" format="reference" /> + <attr name="lockPinPasswordStyle" format="reference" /> + <attr name="containerStyle" format="reference" /> + <attr name="headerStyle" format="reference" /> + <attr name="headerIconStyle" format="reference" /> + <attr name="titleTextAppearance" format="reference" /> + <attr name="subTitleTextAppearance" format="reference" /> + <attr name="descriptionTextAppearance" format="reference" /> + <attr name="passwordTextAppearance" format="reference" /> + <attr name="errorTextAppearance" format="reference"/> + </declare-styleable> </resources> diff --git a/packages/SystemUI/res/values/bools.xml b/packages/SystemUI/res/values/bools.xml index c67ac8d34aa6..8221d78fbfd7 100644 --- a/packages/SystemUI/res/values/bools.xml +++ b/packages/SystemUI/res/values/bools.xml @@ -18,6 +18,13 @@ <resources> <!-- Whether to show the user switcher in quick settings when only a single user is present. --> <bool name="qs_show_user_switcher_for_single_user">false</bool> + <!-- Whether to show a custom biometric prompt size--> <bool name="use_custom_bp_size">false</bool> + + <!-- Whether to enable clipping on Quick Settings --> + <bool name="qs_enable_clipping">true</bool> + + <!-- Whether to enable transparent background for notification scrims --> + <bool name="notification_scrim_transparent">false</bool> </resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 37549c927a90..9188ce091a3b 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -337,9 +337,6 @@ have been scrolled off-screen. --> <bool name="config_showNotificationShelf">true</bool> - <!-- Whether or not the notifications should always fade as they are dismissed. --> - <bool name="config_fadeNotificationsOnDismiss">false</bool> - <!-- Whether or not the fade on the notification is based on the amount that it has been swiped off-screen. --> <bool name="config_fadeDependingOnAmountSwiped">false</bool> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index f7019dcd06ee..93926ef9e780 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -519,7 +519,7 @@ <dimen name="qs_tile_margin_horizontal">8dp</dimen> <dimen name="qs_tile_margin_vertical">@dimen/qs_tile_margin_horizontal</dimen> <dimen name="qs_tile_margin_top_bottom">4dp</dimen> - <dimen name="qs_brightness_margin_top">8dp</dimen> + <dimen name="qs_brightness_margin_top">12dp</dimen> <dimen name="qs_brightness_margin_bottom">16dp</dimen> <dimen name="qqs_layout_margin_top">16dp</dimen> <dimen name="qqs_layout_padding_bottom">24dp</dimen> @@ -559,7 +559,8 @@ <dimen name="qs_dual_tile_padding_horizontal">6dp</dimen> <dimen name="qs_panel_elevation">4dp</dimen> <dimen name="qs_panel_padding_bottom">@dimen/footer_actions_height</dimen> - <dimen name="qs_panel_padding_top">80dp</dimen> + <dimen name="qs_panel_padding_top">48dp</dimen> + <dimen name="qs_panel_padding_top_combined_headers">80dp</dimen> <dimen name="qs_data_usage_text_size">14sp</dimen> <dimen name="qs_data_usage_usage_text_size">36sp</dimen> @@ -571,6 +572,7 @@ <dimen name="qs_header_row_min_height">48dp</dimen> <dimen name="qs_header_non_clickable_element_height">24dp</dimen> + <dimen name="new_qs_header_non_clickable_element_height">20dp</dimen> <dimen name="qs_footer_padding">20dp</dimen> <dimen name="qs_security_footer_height">88dp</dimen> @@ -948,6 +950,9 @@ <dimen name="biometric_dialog_width">240dp</dimen> <dimen name="biometric_dialog_height">240dp</dimen> + <!-- Biometric Auth Credential values --> + <dimen name="biometric_auth_icon_size">48dp</dimen> + <!-- Starting text size in sp of batteryLevel for wireless charging animation --> <item name="wireless_charging_anim_battery_level_text_size_start" format="float" type="dimen"> 0 @@ -1056,9 +1061,8 @@ <!-- Media tap-to-transfer chip for receiver device --> <dimen name="media_ttt_chip_size_receiver">100dp</dimen> <dimen name="media_ttt_icon_size_receiver">95dp</dimen> - <!-- Since the generic icon isn't circular, we need to scale it down so it still fits within - the circular chip. --> - <dimen name="media_ttt_generic_icon_size_receiver">70dp</dimen> + <!-- Add some padding for the generic icon so it doesn't go all the way to the border. --> + <dimen name="media_ttt_generic_icon_padding">12dp</dimen> <dimen name="media_ttt_receiver_vert_translation">20dp</dimen> <!-- Window magnification --> @@ -1457,6 +1461,9 @@ <dimen name="media_projection_app_selector_icon_size">32dp</dimen> <dimen name="media_projection_app_selector_recents_padding">16dp</dimen> <dimen name="media_projection_app_selector_loader_size">32dp</dimen> + <dimen name="media_projection_app_selector_task_rounded_corners">10dp</dimen> + <dimen name="media_projection_app_selector_task_icon_size">24dp</dimen> + <dimen name="media_projection_app_selector_task_icon_margin">8dp</dimen> <!-- Dream overlay related dimensions --> <dimen name="dream_overlay_status_bar_height">60dp</dimen> @@ -1566,4 +1573,9 @@ <dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen> + + <!-- Default device corner radius, used for assist UI --> + <dimen name="config_rounded_mask_size">0px</dimen> + <dimen name="config_rounded_mask_size_top">0px</dimen> + <dimen name="config_rounded_mask_size_bottom">0px</dimen> </resources> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index f22e79722e78..7ca42f7d7015 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -188,5 +188,11 @@ <item type="id" name="face_scanning_anim"/> <item type="id" name="qqs_tile_layout"/> + + <!-- The buttons in the Quick Settings footer actions.--> + <item type="id" name="multi_user_switch"/> + <item type="id" name="pm_lite"/> + <item type="id" name="settings_button_container"/> + </resources> diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml index 3164ed1e6751..e30d4415a0c4 100644 --- a/packages/SystemUI/res/values/integers.xml +++ b/packages/SystemUI/res/values/integers.xml @@ -28,4 +28,11 @@ <!-- The time it takes for the over scroll release animation to complete, in milli seconds. --> <integer name="lockscreen_shade_over_scroll_release_duration">0</integer> + + <!-- Values for transition of QS Headers --> + <integer name="fade_out_complete_frame">14</integer> + <integer name="fade_in_start_frame">58</integer> + <!-- Percentage of displacement for items in QQS to guarantee matching with bottom of clock at + fade_out_complete_frame --> + <dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 53f1227383b7..212c77b50477 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -246,6 +246,16 @@ <string name="screenrecord_start_label">Start Recording?</string> <!-- Message reminding the user that sensitive information may be captured during a screen recording [CHAR_LIMIT=NONE]--> <string name="screenrecord_description">While recording, Android System can capture any sensitive information that\u2019s visible on your screen or played on your device. This includes passwords, payment info, photos, messages, and audio.</string> + <!-- Dropdown option to record the entire screen [CHAR_LIMIT=30]--> + <string name="screenrecord_option_entire_screen">Record entire screen</string> + <!-- Dropdown option to record a single app [CHAR_LIMIT=30]--> + <string name="screenrecord_option_single_app">Record a single app</string> + <!-- Message reminding the user that sensitive information may be captured during a full screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]--> + <string name="screenrecord_warning_entire_screen">While you\'re recording, Android has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages, or other sensitive information.</string> + <!-- Message reminding the user that sensitive information may be captured during a single app screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]--> + <string name="screenrecord_warning_single_app">While you\'re recording an app, Android has access to anything shown or played on that app. So be careful with passwords, payment details, messages, or other sensitive information.</string> + <!-- Button to start a screen recording in the updated screen record dialog that allows to select an app to record [CHAR LIMIT=50]--> + <string name="screenrecord_start_recording">Start recording</string> <!-- Label for a switch to enable recording audio [CHAR LIMIT=NONE]--> <string name="screenrecord_audio_label">Record audio</string> <!-- Label for the option to record audio from the device [CHAR LIMIT=NONE]--> @@ -631,7 +641,7 @@ <!-- QuickSettings: Label for the toggle that controls whether display color correction is enabled. [CHAR LIMIT=NONE] --> <string name="quick_settings_color_correction_label">Color correction</string> <!-- QuickSettings: Control panel: Label for button that navigates to user settings. [CHAR LIMIT=NONE] --> - <string name="quick_settings_more_user_settings">User settings</string> + <string name="quick_settings_more_user_settings">Manage users</string> <!-- QuickSettings: Control panel: Label for button that dismisses control panel. [CHAR LIMIT=NONE] --> <string name="quick_settings_done">Done</string> <!-- QuickSettings: Control panel: Label for button that dismisses user switcher control panel. [CHAR LIMIT=NONE] --> @@ -958,7 +968,26 @@ <!-- Media projection permission dialog warning title. [CHAR LIMIT=NONE] --> <string name="media_projection_dialog_title">Start recording or casting with <xliff:g id="app_seeking_permission" example="Hangouts">%s</xliff:g>?</string> - <!-- Media projection permission dialog permanent grant check box. [CHAR LIMIT=NONE] --> + <!-- Media projection permission dialog title. [CHAR LIMIT=NONE] --> + <string name="media_projection_permission_dialog_title">Allow <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> to share or record?</string> + + <!-- Media projection permission dropdown option for capturing the whole screen. [CHAR LIMIT=30] --> + <string name="media_projection_permission_dialog_option_entire_screen">Entire screen</string> + + <!-- Media projection permission dropdown option for capturing single app. [CHAR LIMIT=30] --> + <string name="media_projection_permission_dialog_option_single_app">A single app</string> + + <!-- Media projection permission warning for capturing the whole screen. [CHAR LIMIT=350] --> + <string name="media_projection_permission_dialog_warning_entire_screen">When you\'re sharing, recording, or casting, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages, or other sensitive information.</string> + + <!-- Media projection permission warning for capturing an app. [CHAR LIMIT=350] --> + <string name="media_projection_permission_dialog_warning_single_app">When you\'re sharing, recording, or casting an app, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything shown or played on that app. So be careful with passwords, payment details, messages, or other sensitive information.</string> + + <!-- Media projection permission button to continue with app selection or recording [CHAR LIMIT=60] --> + <string name="media_projection_permission_dialog_continue">Continue</string> + + <!-- Title of the dialog that allows to select an app to share or record [CHAR LIMIT=NONE] --> + <string name="media_projection_permission_app_selector_title">Share or record an app</string> <!-- The text to clear all notifications. [CHAR LIMIT=60] --> <string name="clear_all_notifications_text">Clear all</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index ac3eb7e18539..e76887babc50 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -128,11 +128,10 @@ <!-- This is hard coded to be sans-serif-condensed to match the icons --> <style name="TextAppearance.QS.Status"> - <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item> + <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> <item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textSize">14sp</item> <item name="android:letterSpacing">0.01</item> - <item name="android:lineHeight">20sp</item> </style> <style name="TextAppearance.QS.SecurityFooter" parent="@style/TextAppearance.QS.Status"> @@ -143,12 +142,10 @@ <style name="TextAppearance.QS.Status.Carriers" /> <style name="TextAppearance.QS.Status.Carriers.NoCarrierText"> - <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item> <item name="android:textColor">?android:attr/textColorSecondary</item> </style> <style name="TextAppearance.QS.Status.Build"> - <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item> <item name="android:textColor">?android:attr/textColorSecondary</item> </style> @@ -200,8 +197,9 @@ <style name="TextAppearance.AuthNonBioCredential.Title"> <item name="android:fontFamily">google-sans</item> - <item name="android:layout_marginTop">20dp</item> - <item name="android:textSize">36sp</item> + <item name="android:layout_marginTop">24dp</item> + <item name="android:textSize">36dp</item> + <item name="android:focusable">true</item> </style> <style name="TextAppearance.AuthNonBioCredential.Subtitle"> @@ -213,12 +211,10 @@ <style name="TextAppearance.AuthNonBioCredential.Description"> <item name="android:fontFamily">google-sans</item> <item name="android:layout_marginTop">20dp</item> - <item name="android:textSize">16sp</item> + <item name="android:textSize">18sp</item> </style> <style name="TextAppearance.AuthNonBioCredential.Error"> - <item name="android:paddingTop">6dp</item> - <item name="android:paddingBottom">18dp</item> <item name="android:paddingHorizontal">24dp</item> <item name="android:textSize">14sp</item> <item name="android:textColor">?android:attr/colorError</item> @@ -227,20 +223,43 @@ <style name="TextAppearance.AuthCredential.PasswordEntry" parent="@android:style/TextAppearance.DeviceDefault"> <item name="android:gravity">center</item> + <item name="android:paddingTop">28dp</item> <item name="android:singleLine">true</item> <item name="android:textColor">?android:attr/colorForeground</item> <item name="android:textSize">24sp</item> + <item name="android:background">@drawable/edit_text_filled</item> </style> <style name="AuthCredentialHeaderStyle"> <item name="android:paddingStart">48dp</item> - <item name="android:paddingEnd">24dp</item> - <item name="android:paddingTop">28dp</item> - <item name="android:paddingBottom">20dp</item> - <item name="android:orientation">vertical</item> + <item name="android:paddingEnd">48dp</item> + <item name="android:paddingTop">48dp</item> + <item name="android:paddingBottom">10dp</item> <item name="android:layout_gravity">top</item> </style> + <style name="AuthCredentialIconStyle"> + <item name="android:layout_width">@dimen/biometric_auth_icon_size</item> + <item name="android:layout_height">@dimen/biometric_auth_icon_size</item> + </style> + + <style name="AuthCredentialPatternContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">420dp</item> + <item name="android:maxWidth">420dp</item> + <item name="android:minHeight">200dp</item> + <item name="android:minWidth">200dp</item> + <item name="android:padding">20dp</item> + </style> + + <style name="AuthCredentialPinPasswordContainerStyle"> + <item name="android:gravity">center</item> + <item name="android:maxHeight">48dp</item> + <item name="android:maxWidth">600dp</item> + <item name="android:minHeight">48dp</item> + <item name="android:minWidth">200dp</item> + </style> + <style name="DeviceManagementDialogTitle"> <item name="android:gravity">center</item> <item name="android:textAppearance">@style/TextAppearance.DeviceManagementDialog.Title</item> @@ -278,7 +297,9 @@ <item name="wallpaperTextColorSecondary">@*android:color/secondary_text_material_dark</item> <item name="wallpaperTextColorAccent">@color/material_dynamic_primary90</item> <item name="android:colorError">@*android:color/error_color_material_dark</item> - <item name="*android:lockPatternStyle">@style/LockPatternStyle</item> + <item name="*android:lockPatternStyle">@style/LockPatternViewStyle</item> + <item name="lockPatternStyle">@style/LockPatternContainerStyle</item> + <item name="lockPinPasswordStyle">@style/LockPinPasswordContainerStyle</item> <item name="passwordStyle">@style/PasswordTheme</item> <item name="numPadKeyStyle">@style/NumPadKey</item> <item name="backgroundProtectedStyle">@style/BackgroundProtectedStyle</item> @@ -304,27 +325,33 @@ <item name="android:textColor">?attr/wallpaperTextColor</item> </style> - <style name="LockPatternContainerStyle"> - <item name="android:maxHeight">400dp</item> - <item name="android:maxWidth">420dp</item> - <item name="android:minHeight">0dp</item> - <item name="android:minWidth">0dp</item> - <item name="android:paddingHorizontal">60dp</item> - <item name="android:paddingBottom">40dp</item> + <style name="AuthCredentialStyle"> + <item name="*android:regularColor">?android:attr/colorForeground</item> + <item name="*android:successColor">?android:attr/colorForeground</item> + <item name="*android:errorColor">?android:attr/colorError</item> + <item name="*android:dotColor">?android:attr/textColorSecondary</item> + <item name="headerStyle">@style/AuthCredentialHeaderStyle</item> + <item name="headerIconStyle">@style/AuthCredentialIconStyle</item> + <item name="titleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Title</item> + <item name="subTitleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Subtitle</item> + <item name="descriptionTextAppearance">@style/TextAppearance.AuthNonBioCredential.Description</item> + <item name="passwordTextAppearance">@style/TextAppearance.AuthCredential.PasswordEntry</item> + <item name="errorTextAppearance">@style/TextAppearance.AuthNonBioCredential.Error</item> </style> - <style name="LockPatternStyle"> + <style name="LockPatternViewStyle" > <item name="*android:regularColor">?android:attr/colorAccent</item> <item name="*android:successColor">?android:attr/textColorPrimary</item> <item name="*android:errorColor">?android:attr/colorError</item> <item name="*android:dotColor">?android:attr/textColorSecondary</item> </style> - <style name="LockPatternStyleBiometricPrompt"> - <item name="*android:regularColor">?android:attr/colorForeground</item> - <item name="*android:successColor">?android:attr/colorForeground</item> - <item name="*android:errorColor">?android:attr/colorError</item> - <item name="*android:dotColor">?android:attr/textColorSecondary</item> + <style name="LockPatternContainerStyle" parent="@style/AuthCredentialStyle"> + <item name="containerStyle">@style/AuthCredentialPatternContainerStyle</item> + </style> + + <style name="LockPinPasswordContainerStyle" parent="@style/AuthCredentialStyle"> + <item name="containerStyle">@style/AuthCredentialPinPasswordContainerStyle</item> </style> <style name="Theme.SystemUI.QuickSettings" parent="@*android:style/Theme.DeviceDefault"> diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml index f3866c08cbfc..de855e275f5f 100644 --- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml +++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml @@ -27,67 +27,60 @@ <KeyPosition app:keyPositionType="deltaRelative" app:percentX="0" - app:percentY="0" - app:framePosition="49" + app:percentY="@dimen/percent_displacement_at_fade_out" + app:framePosition="@integer/fade_out_complete_frame" app:sizePercent="0" app:curveFit="linear" app:motionTarget="@id/date" /> <KeyPosition app:keyPositionType="deltaRelative" app:percentX="1" - app:percentY="0.51" + app:percentY="0.5" app:sizePercent="1" - app:framePosition="51" + app:framePosition="50" app:curveFit="linear" app:motionTarget="@id/date" /> <KeyAttribute app:motionTarget="@id/date" - app:framePosition="30" + app:framePosition="14" android:alpha="0" /> <KeyAttribute app:motionTarget="@id/date" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> <KeyPosition - app:keyPositionType="pathRelative" - app:percentX="0" - app:percentY="0" - app:framePosition="0" - app:curveFit="linear" - app:motionTarget="@id/statusIcons" /> - <KeyPosition - app:keyPositionType="pathRelative" + app:keyPositionType="deltaRelative" app:percentX="0" - app:percentY="0" - app:framePosition="50" + app:percentY="@dimen/percent_displacement_at_fade_out" + app:framePosition="@integer/fade_out_complete_frame" app:sizePercent="0" app:curveFit="linear" app:motionTarget="@id/statusIcons" /> <KeyPosition app:keyPositionType="deltaRelative" app:percentX="1" - app:percentY="0.51" - app:framePosition="51" + app:percentY="0.5" + app:framePosition="50" app:sizePercent="1" app:curveFit="linear" app:motionTarget="@id/statusIcons" /> <KeyAttribute app:motionTarget="@id/statusIcons" - app:framePosition="30" + app:framePosition="@integer/fade_out_complete_frame" android:alpha="0" /> <KeyAttribute app:motionTarget="@id/statusIcons" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> <KeyPosition app:keyPositionType="deltaRelative" app:percentX="0" - app:percentY="0" - app:framePosition="50" + app:percentY="@dimen/percent_displacement_at_fade_out" + app:framePosition="@integer/fade_out_complete_frame" app:percentWidth="1" app:percentHeight="1" app:curveFit="linear" @@ -95,27 +88,27 @@ <KeyPosition app:keyPositionType="deltaRelative" app:percentX="1" - app:percentY="0.51" - app:framePosition="51" + app:percentY="0.5" + app:framePosition="50" app:percentWidth="1" app:percentHeight="1" app:curveFit="linear" app:motionTarget="@id/batteryRemainingIcon" /> <KeyAttribute app:motionTarget="@id/batteryRemainingIcon" - app:framePosition="30" + app:framePosition="@integer/fade_out_complete_frame" android:alpha="0" /> <KeyAttribute app:motionTarget="@id/batteryRemainingIcon" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> <KeyPosition app:motionTarget="@id/carrier_group" app:percentX="1" - app:percentY="0.51" - app:framePosition="51" + app:percentY="0.5" + app:framePosition="50" app:percentWidth="1" app:percentHeight="1" app:curveFit="linear" @@ -126,7 +119,7 @@ android:alpha="0" /> <KeyAttribute app:motionTarget="@id/carrier_group" - app:framePosition="70" + app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> </KeyFrameSet> </Transition> diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml index a82684d0358b..88b4f43b440b 100644 --- a/packages/SystemUI/res/xml/qqs_header.xml +++ b/packages/SystemUI/res/xml/qqs_header.xml @@ -43,7 +43,8 @@ android:id="@+id/date"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + android:layout_marginStart="8dp" app:layout_constrainedWidth="true" app:layout_constraintStart_toEndOf="@id/clock" app:layout_constraintEnd_toStartOf="@id/barrier" @@ -57,8 +58,8 @@ android:id="@+id/statusIcons"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" - app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toEndOf="@id/date" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" app:layout_constraintTop_toTopOf="parent" @@ -71,9 +72,9 @@ android:id="@+id/batteryRemainingIcon"> <Layout android:layout_width="wrap_content" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" app:layout_constrainedWidth="true" - app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toEndOf="@id/statusIcons" app:layout_constraintEnd_toEndOf="@id/end_guide" app:layout_constraintTop_toTopOf="parent" diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml index f39e6bd65b86..d8a4e7752960 100644 --- a/packages/SystemUI/res/xml/qs_header_new.xml +++ b/packages/SystemUI/res/xml/qs_header_new.xml @@ -40,13 +40,13 @@ android:layout_height="@dimen/large_screen_shade_header_min_height" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/privacy_container" - app:layout_constraintBottom_toTopOf="@id/date" + app:layout_constraintBottom_toBottomOf="@id/carrier_group" app:layout_constraintEnd_toStartOf="@id/carrier_group" app:layout_constraintHorizontal_bias="0" /> <Transform - android:scaleX="2.4" - android:scaleY="2.4" + android:scaleX="2.57" + android:scaleY="2.57" /> </Constraint> @@ -54,11 +54,11 @@ android:id="@+id/date"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/space" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintTop_toBottomOf="@id/clock" + app:layout_constraintTop_toBottomOf="@id/carrier_group" app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_chainStyle="spread_inside" /> @@ -87,7 +87,7 @@ android:id="@+id/statusIcons"> <Layout android:layout_width="0dp" - android:layout_height="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" app:layout_constrainedWidth="true" app:layout_constraintStart_toEndOf="@id/space" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" @@ -101,8 +101,8 @@ android:id="@+id/batteryRemainingIcon"> <Layout android:layout_width="wrap_content" - android:layout_height="@dimen/qs_header_non_clickable_element_height" - app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height" + android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toEndOf="@id/statusIcons" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/date" diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt index 2e391c7aacbe..49cc48321d77 100644 --- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt @@ -19,6 +19,7 @@ package com.android.systemui.testing.screenshot import android.app.Activity import android.graphics.Color import android.view.View +import android.view.Window import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat @@ -51,13 +52,14 @@ class ExternalViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestR /** * Compare the content of the [view] with the golden image identified by [goldenIdentifier] in - * the context of [emulationSpec]. + * the context of [emulationSpec]. Window must be specified to capture views that render + * hardware buffers. */ - fun screenshotTest(goldenIdentifier: String, view: View) { + fun screenshotTest(goldenIdentifier: String, view: View, window: Window? = null) { view.removeElevationRecursively() ScreenshotRuleAsserter.Builder(screenshotRule) - .setScreenshotProvider { view.toBitmap() } + .setScreenshotProvider { view.toBitmap(window) } .withMatcher(matcher) .build() .assertGoldenImage(goldenIdentifier) @@ -94,6 +96,6 @@ class ExternalViewScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestR activity.currentFocus?.clearFocus() } - screenshotTest(goldenIdentifier, rootView) + screenshotTest(goldenIdentifier, rootView, activity.window) } } diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 3543cfc83b69..ffaeaaadb1cf 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -59,7 +59,9 @@ android_library { resource_dirs: [ "res", ], - java_version: "1.8", + optimize: { + proguard_flags_files: ["proguard.flags"], + }, min_sdk_version: "current", plugins: ["dagger2-compiler"], } diff --git a/packages/SystemUI/shared/proguard.flags b/packages/SystemUI/shared/proguard.flags new file mode 100644 index 000000000000..5eda04500190 --- /dev/null +++ b/packages/SystemUI/shared/proguard.flags @@ -0,0 +1,4 @@ +# Retain signatures of TypeToken and its subclasses for gson usage in ClockRegistry +-keepattributes Signature +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt index ffab3cd79d7f..12e0b9ab835a 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt @@ -16,6 +16,7 @@ package com.android.systemui.shared.animation import android.view.View +import android.view.View.LAYOUT_DIRECTION_RTL import android.view.ViewGroup import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate import com.android.systemui.unfold.UnfoldTransitionProgressProvider @@ -58,9 +59,15 @@ class UnfoldConstantTranslateAnimator( // progress == 0 -> -translationMax // progress == 1 -> 0 val xTrans = (progress - 1f) * translationMax + val rtlMultiplier = + if (rootView.getLayoutDirection() == LAYOUT_DIRECTION_RTL) { + -1 + } else { + 1 + } viewsToTranslate.forEach { (view, direction, shouldBeAnimated) -> if (shouldBeAnimated()) { - view.get()?.translationX = xTrans * direction.multiplier + view.get()?.translationX = xTrans * direction.multiplier * rtlMultiplier } } } @@ -90,7 +97,7 @@ class UnfoldConstantTranslateAnimator( /** Direction of the animation. */ enum class Direction(val multiplier: Float) { - LEFT(-1f), - RIGHT(1f), + START(-1f), + END(1f), } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt index d7a0b473ead6..3efdc5acb00c 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt @@ -17,6 +17,7 @@ package com.android.systemui.shared.animation import android.graphics.Point import android.view.Surface +import android.view.Surface.Rotation import android.view.View import android.view.WindowManager import com.android.systemui.unfold.UnfoldTransitionProgressProvider @@ -58,14 +59,14 @@ class UnfoldMoveFromCenterAnimator @JvmOverloads constructor( * Updates display properties in order to calculate the initial position for the views * Must be called before [registerViewForAnimation] */ - fun updateDisplayProperties() { + @JvmOverloads + fun updateDisplayProperties(@Rotation rotation: Int = windowManager.defaultDisplay.rotation) { windowManager.defaultDisplay.getSize(screenSize) // Simple implementation to get current fold orientation, // this might not be correct on all devices // TODO: use JetPack WindowManager library to get the fold orientation - isVerticalFold = windowManager.defaultDisplay.rotation == Surface.ROTATION_0 || - windowManager.defaultDisplay.rotation == Surface.ROTATION_180 + isVerticalFold = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 } /** diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index c2e74456c032..236aa669eaa9 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -20,25 +20,28 @@ import android.annotation.ColorInt import android.annotation.FloatRange import android.annotation.IntRange import android.annotation.SuppressLint -import android.app.compat.ChangeIdStateCache.invalidate import android.content.Context import android.graphics.Canvas +import android.graphics.Rect import android.text.Layout import android.text.TextUtils import android.text.format.DateFormat import android.util.AttributeSet +import android.util.MathUtils import android.widget.TextView -import com.android.internal.R.attr.contentDescription -import com.android.internal.R.attr.format import com.android.internal.annotations.VisibleForTesting import com.android.systemui.animation.GlyphCallback import com.android.systemui.animation.Interpolators import com.android.systemui.animation.TextAnimator +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG import com.android.systemui.shared.R import java.io.PrintWriter import java.util.Calendar import java.util.Locale import java.util.TimeZone +import kotlin.math.max +import kotlin.math.min /** * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30) @@ -51,13 +54,8 @@ class AnimatableClockView @JvmOverloads constructor( defStyleAttr: Int = 0, defStyleRes: Int = 0 ) : TextView(context, attrs, defStyleAttr, defStyleRes) { - - private var lastMeasureCall: CharSequence? = null - private var lastDraw: CharSequence? = null - private var lastTextUpdate: CharSequence? = null - private var lastOnTextChanged: CharSequence? = null - private var lastInvalidate: CharSequence? = null - private var lastTimeZoneChange: CharSequence? = null + var tag: String = "UnnamedClockView" + var logBuffer: LogBuffer? = null private val time = Calendar.getInstance() @@ -134,6 +132,7 @@ class AnimatableClockView @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() + logBuffer?.log(tag, DEBUG, "onAttachedToWindow") refreshFormat() } @@ -149,27 +148,39 @@ class AnimatableClockView @JvmOverloads constructor( time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis() contentDescription = DateFormat.format(descFormat, time) val formattedText = DateFormat.format(format, time) + logBuffer?.log(tag, DEBUG, + { str1 = formattedText?.toString() }, + { "refreshTime: new formattedText=$str1" } + ) // Setting text actually triggers a layout pass (because the text view is set to // wrap_content width and TextView always relayouts for this). Avoid needless // relayout if the text didn't actually change. if (!TextUtils.equals(text, formattedText)) { text = formattedText + logBuffer?.log(tag, DEBUG, + { str1 = formattedText?.toString() }, + { "refreshTime: done setting new time text to: $str1" } + ) // Because the TextLayout may mutate under the hood as a result of the new text, we // notify the TextAnimator that it may have changed and request a measure/layout. A // crash will occur on the next invocation of setTextStyle if the layout is mutated // without being notified TextInterpolator being notified. if (layout != null) { textAnimator?.updateLayout(layout) + logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout") } requestLayout() - lastTextUpdate = getTimestamp() + logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout") } } fun onTimeZoneChanged(timeZone: TimeZone?) { time.timeZone = timeZone refreshFormat() - lastTimeZoneChange = "${getTimestamp()} timeZone=${time.timeZone}" + logBuffer?.log(tag, DEBUG, + { str1 = timeZone?.toString() }, + { "onTimeZoneChanged newTimeZone=$str1" } + ) } @SuppressLint("DrawAllocation") @@ -183,22 +194,24 @@ class AnimatableClockView @JvmOverloads constructor( } else { animator.updateLayout(layout) } - lastMeasureCall = getTimestamp() + logBuffer?.log(tag, DEBUG, "onMeasure") } override fun onDraw(canvas: Canvas) { - lastDraw = getTimestamp() - // intentionally doesn't call super.onDraw here or else the text will be rendered twice - textAnimator?.draw(canvas) + // Use textAnimator to render text if animation is enabled. + // Otherwise default to using standard draw functions. + if (isAnimationEnabled) { + // intentionally doesn't call super.onDraw here or else the text will be rendered twice + textAnimator?.draw(canvas) + } else { + super.onDraw(canvas) + } + logBuffer?.log(tag, DEBUG, "onDraw lastDraw") } override fun invalidate() { super.invalidate() - lastInvalidate = getTimestamp() - } - - private fun getTimestamp(): CharSequence { - return "${DateFormat.format("HH:mm:ss", System.currentTimeMillis())} text=$text" + logBuffer?.log(tag, DEBUG, "invalidate") } override fun onTextChanged( @@ -208,7 +221,10 @@ class AnimatableClockView @JvmOverloads constructor( lengthAfter: Int ) { super.onTextChanged(text, start, lengthBefore, lengthAfter) - lastOnTextChanged = "${getTimestamp()}" + logBuffer?.log(tag, DEBUG, + { str1 = text.toString() }, + { "onTextChanged text=$str1" } + ) } fun setLineSpacingScale(scale: Float) { @@ -222,6 +238,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateAppearOnLockscreen() { + logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen") setTextStyle( weight = dozingWeight, textSize = -1f, @@ -246,6 +263,7 @@ class AnimatableClockView @JvmOverloads constructor( if (isAnimationEnabled && textAnimator == null) { return } + logBuffer?.log(tag, DEBUG, "animateFoldAppear") setTextStyle( weight = lockScreenWeightInternal, textSize = -1f, @@ -272,6 +290,7 @@ class AnimatableClockView @JvmOverloads constructor( // Skip charge animation if dozing animation is already playing. return } + logBuffer?.log(tag, DEBUG, "animateCharge") val startAnimPhase2 = Runnable { setTextStyle( weight = if (isDozing()) dozingWeight else lockScreenWeight, @@ -295,6 +314,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateDoze(isDozing: Boolean, animate: Boolean) { + logBuffer?.log(tag, DEBUG, "animateDoze") setTextStyle( weight = if (isDozing) dozingWeight else lockScreenWeight, textSize = -1f, @@ -306,7 +326,24 @@ class AnimatableClockView @JvmOverloads constructor( ) } - private val glyphFilter: GlyphCallback? = null // Add text animation tweak here. + // The offset of each glyph from where it should be. + private var glyphOffsets = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f) + + private var lastSeenAnimationProgress = 1.0f + + // If the animation is being reversed, the target offset for each glyph for the "stop". + private var animationCancelStartPosition = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f) + private var animationCancelStopPosition = 0.0f + + // Whether the currently playing animation needed a stop (and thus, is shortened). + private var currentAnimationNeededStop = false + + private val glyphFilter: GlyphCallback = { positionedGlyph, _ -> + val offset = positionedGlyph.lineNo * DIGITS_PER_LINE + positionedGlyph.glyphIndex + if (offset < glyphOffsets.size) { + positionedGlyph.x += glyphOffsets[offset] + } + } /** * Set text style with an optional animation. @@ -340,6 +377,9 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter + if (color != null && !isAnimationEnabled) { + setTextColor(color) + } } else { // when the text animator is set, update its start values onTextAnimatorInitialized = Runnable { @@ -354,6 +394,9 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter + if (color != null && !isAnimationEnabled) { + setTextColor(color) + } } } } @@ -389,9 +432,12 @@ class AnimatableClockView @JvmOverloads constructor( isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12 else -> DOUBLE_LINE_FORMAT_12_HOUR } + logBuffer?.log(tag, DEBUG, + { str1 = format?.toString() }, + { "refreshFormat format=$str1" } + ) descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12 - refreshTime() } @@ -400,17 +446,133 @@ class AnimatableClockView @JvmOverloads constructor( pw.println(" measuredWidth=$measuredWidth") pw.println(" measuredHeight=$measuredHeight") pw.println(" singleLineInternal=$isSingleLineInternal") - pw.println(" lastTextUpdate=$lastTextUpdate") - pw.println(" lastOnTextChanged=$lastOnTextChanged") - pw.println(" lastInvalidate=$lastInvalidate") - pw.println(" lastMeasureCall=$lastMeasureCall") - pw.println(" lastDraw=$lastDraw") - pw.println(" lastTimeZoneChange=$lastTimeZoneChange") pw.println(" currText=$text") pw.println(" currTimeContextDesc=$contentDescription") + pw.println(" dozingWeightInternal=$dozingWeightInternal") + pw.println(" lockScreenWeightInternal=$lockScreenWeightInternal") + pw.println(" dozingColor=$dozingColor") + pw.println(" lockScreenColor=$lockScreenColor") pw.println(" time=$time") } + fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) { + // Do we need to cancel an in-flight animation? + // Need to also check against 0.0f here; we can sometimes get two calls with fraction == 0, + // which trips up the check otherwise. + if (lastSeenAnimationProgress != 1.0f && + lastSeenAnimationProgress != 0.0f && + fraction == 0.0f) { + // New animation, but need to stop the old one. Figure out where each glyph currently + // is in relation to the box position. After that, use the leading digit's current + // position as the stop target. + currentAnimationNeededStop = true + + // We assume that the current glyph offsets would be relative to the "from" position. + val moveAmount = toRect.left - fromRect.left + + // Remap the current glyph offsets to be relative to the new "end" position, and figure + // out the start/end positions for the stop animation. + for (i in 0 until NUM_DIGITS) { + glyphOffsets[i] = -moveAmount + glyphOffsets[i] + animationCancelStartPosition[i] = glyphOffsets[i] + } + + // Use the leading digit's offset as the stop position. + if (toRect.left > fromRect.left) { + // It _was_ moving left + animationCancelStopPosition = glyphOffsets[0] + } else { + // It was moving right + animationCancelStopPosition = glyphOffsets[1] + } + } + + // Is there a cancellation in progress? + if (currentAnimationNeededStop && fraction < ANIMATION_CANCELLATION_TIME) { + val animationStopProgress = MathUtils.constrainedMap( + 0.0f, 1.0f, 0.0f, ANIMATION_CANCELLATION_TIME, fraction + ) + + // One of the digits has already stopped. + val animationStopStep = 1.0f / (NUM_DIGITS - 1) + + for (i in 0 until NUM_DIGITS) { + val stopAmount = if (toRect.left > fromRect.left) { + // It was moving left (before flipping) + MOVE_LEFT_DELAYS[i] * animationStopStep + } else { + // It was moving right (before flipping) + MOVE_RIGHT_DELAYS[i] * animationStopStep + } + + // Leading digit stops immediately. + if (stopAmount == 0.0f) { + glyphOffsets[i] = animationCancelStopPosition + } else { + val actualStopAmount = MathUtils.constrainedMap( + 0.0f, 1.0f, 0.0f, stopAmount, animationStopProgress + ) + val easedProgress = MOVE_INTERPOLATOR.getInterpolation(actualStopAmount) + val glyphMoveAmount = + animationCancelStopPosition - animationCancelStartPosition[i] + glyphOffsets[i] = + animationCancelStartPosition[i] + glyphMoveAmount * easedProgress + } + } + } else { + // Normal part of the animation. + // Do we need to remap the animation progress to take account of the cancellation? + val actualFraction = if (currentAnimationNeededStop) { + MathUtils.constrainedMap( + 0.0f, 1.0f, ANIMATION_CANCELLATION_TIME, 1.0f, fraction + ) + } else { + fraction + } + + val digitFractions = (0 until NUM_DIGITS).map { + // The delay for each digit, in terms of fraction (i.e. the digit should not move + // during 0.0 - 0.1). + val initialDelay = if (toRect.left > fromRect.left) { + MOVE_RIGHT_DELAYS[it] * MOVE_DIGIT_STEP + } else { + MOVE_LEFT_DELAYS[it] * MOVE_DIGIT_STEP + } + + val f = MathUtils.constrainedMap( + 0.0f, 1.0f, + initialDelay, initialDelay + AVAILABLE_ANIMATION_TIME, + actualFraction + ) + MOVE_INTERPOLATOR.getInterpolation(max(min(f, 1.0f), 0.0f)) + } + + // Was there an animation halt? + val moveAmount = if (currentAnimationNeededStop) { + // Only need to animate over the remaining space if the animation was aborted. + -animationCancelStopPosition + } else { + toRect.left.toFloat() - fromRect.left.toFloat() + } + + for (i in 0 until NUM_DIGITS) { + glyphOffsets[i] = -moveAmount + (moveAmount * digitFractions[i]) + } + } + + invalidate() + + if (fraction == 1.0f) { + // Reset + currentAnimationNeededStop = false + } + + lastSeenAnimationProgress = fraction + + // Ensure that the actual clock container is always in the "end" position. + this.setLeftTopRightBottom(toRect.left, toRect.top, toRect.right, toRect.bottom) + } + // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. // This is an optimization to ensure we only recompute the patterns when the inputs change. private object Patterns { @@ -434,6 +596,7 @@ class AnimatableClockView @JvmOverloads constructor( if (!clockView12Skel.contains("a")) { sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' } } + sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel) sCacheKey = key } @@ -448,5 +611,36 @@ class AnimatableClockView @JvmOverloads constructor( private const val APPEAR_ANIM_DURATION: Long = 350 private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500 private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000 + + // Constants for the animation + private val MOVE_INTERPOLATOR = Interpolators.STANDARD + + // Calculate the positions of all of the digits... + // Offset each digit by, say, 0.1 + // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should + // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3 + // from 0.3 - 1.0. + private const val NUM_DIGITS = 4 + private const val DIGITS_PER_LINE = 2 + + // How much of "fraction" to spend on canceling the animation, if needed + private const val ANIMATION_CANCELLATION_TIME = 0.4f + + // Delays. Each digit's animation should have a slight delay, so we get a nice + // "stepping" effect. When moving right, the second digit of the hour should move first. + // When moving left, the first digit of the hour should move first. The lists encode + // the delay for each digit (hour[0], hour[1], minute[0], minute[1]), to be multiplied + // by delayMultiplier. + private val MOVE_LEFT_DELAYS = listOf(0, 1, 2, 3) + private val MOVE_RIGHT_DELAYS = listOf(1, 0, 3, 2) + + // How much delay to apply to each subsequent digit. This is measured in terms of "fraction" + // (i.e. a value of 0.1 would cause a digit to wait until fraction had hit 0.1, or 0.2 etc + // before moving). + private const val MOVE_DIGIT_STEP = 0.1f + + // Total available transition time for each digit, taking into account the step. If step is + // 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7. + private val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1) } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt index 38a312448bab..48821e8d0bd3 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -18,11 +18,10 @@ import android.database.ContentObserver import android.graphics.drawable.Drawable import android.net.Uri import android.os.Handler -import android.os.UserHandle import android.provider.Settings import android.util.Log -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.plugins.Clock +import com.android.internal.annotations.Keep +import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockId import com.android.systemui.plugins.ClockMetadata import com.android.systemui.plugins.ClockProvider @@ -30,32 +29,24 @@ import com.android.systemui.plugins.ClockProviderPlugin import com.android.systemui.plugins.PluginListener import com.android.systemui.shared.plugins.PluginManager import com.google.gson.Gson -import javax.inject.Inject private val TAG = ClockRegistry::class.simpleName -private val DEBUG = true +private const val DEBUG = true /** ClockRegistry aggregates providers and plugins */ open class ClockRegistry( val context: Context, val pluginManager: PluginManager, val handler: Handler, - defaultClockProvider: ClockProvider + val isEnabled: Boolean, + userHandle: Int, + defaultClockProvider: ClockProvider, ) { - @Inject constructor( - context: Context, - pluginManager: PluginManager, - @Main handler: Handler, - defaultClockProvider: DefaultClockProvider - ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { } - // Usually this would be a typealias, but a SAM provides better java interop fun interface ClockChangeListener { fun onClockChanged() } - var isEnabled: Boolean = false - private val gson = Gson() private val availableClocks = mutableMapOf<ClockId, ClockInfo>() private val clockChangeListeners = mutableListOf<ClockChangeListener>() @@ -105,14 +96,19 @@ open class ClockRegistry( ) } - pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java, - true /* allowMultiple */) - context.contentResolver.registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), - false, - settingObserver, - UserHandle.USER_ALL - ) + if (isEnabled) { + pluginManager.addPluginListener( + pluginListener, + ClockProviderPlugin::class.java, + /*allowMultiple=*/ true + ) + context.contentResolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), + /*notifyForDescendants=*/ false, + settingObserver, + userHandle + ) + } } private fun connectClocks(provider: ClockProvider) { @@ -130,6 +126,10 @@ open class ClockRegistry( } availableClocks[id] = ClockInfo(clock, provider) + if (DEBUG) { + Log.i(TAG, "Added ${clock.clockId}") + } + if (currentId == id) { if (DEBUG) { Log.i(TAG, "Current clock ($currentId) was connected") @@ -143,6 +143,9 @@ open class ClockRegistry( val currentId = currentClockId for (clock in provider.getClocks()) { availableClocks.remove(clock.clockId) + if (DEBUG) { + Log.i(TAG, "Removed ${clock.clockId}") + } if (currentId == clock.clockId) { Log.w(TAG, "Current clock ($currentId) was disconnected") @@ -161,7 +164,7 @@ open class ClockRegistry( fun getClockThumbnail(clockId: ClockId): Drawable? = availableClocks[clockId]?.provider?.getClockThumbnail(clockId) - fun createExampleClock(clockId: ClockId): Clock? = createClock(clockId) + fun createExampleClock(clockId: ClockId): ClockController? = createClock(clockId) fun registerClockChangeListener(listener: ClockChangeListener) = clockChangeListeners.add(listener) @@ -169,11 +172,14 @@ open class ClockRegistry( fun unregisterClockChangeListener(listener: ClockChangeListener) = clockChangeListeners.remove(listener) - fun createCurrentClock(): Clock { + fun createCurrentClock(): ClockController { val clockId = currentClockId if (isEnabled && clockId.isNotEmpty()) { val clock = createClock(clockId) if (clock != null) { + if (DEBUG) { + Log.i(TAG, "Rendering clock $clockId") + } return clock } else { Log.e(TAG, "Clock $clockId not found; using default") @@ -183,7 +189,7 @@ open class ClockRegistry( return createClock(DEFAULT_CLOCK_ID)!! } - private fun createClock(clockId: ClockId): Clock? = + private fun createClock(clockId: ClockId): ClockController? = availableClocks[clockId]?.provider?.createClock(clockId) private data class ClockInfo( @@ -191,6 +197,7 @@ open class ClockRegistry( val provider: ClockProvider ) + @Keep private data class ClockSetting( val clockId: ClockId, val _applied_timestamp: Long? diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt new file mode 100644 index 000000000000..da1d233949cf --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.android.systemui.shared.clocks + +import android.content.Context +import android.content.res.Resources +import android.graphics.Color +import android.graphics.Rect +import android.icu.text.NumberFormat +import android.util.TypedValue +import android.view.LayoutInflater +import android.widget.FrameLayout +import androidx.annotation.VisibleForTesting +import com.android.systemui.plugins.ClockAnimations +import com.android.systemui.plugins.ClockController +import com.android.systemui.plugins.ClockEvents +import com.android.systemui.plugins.ClockFaceController +import com.android.systemui.plugins.ClockFaceEvents +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.shared.R +import java.io.PrintWriter +import java.util.Locale +import java.util.TimeZone + +private val TAG = DefaultClockController::class.simpleName + +/** + * Controls the default clock visuals. + * + * This serves as an adapter between the clock interface and the AnimatableClockView used by the + * existing lockscreen clock. + */ +class DefaultClockController( + ctx: Context, + private val layoutInflater: LayoutInflater, + private val resources: Resources, +) : ClockController { + override val smallClock: DefaultClockFaceController + override val largeClock: LargeClockFaceController + private val clocks: List<AnimatableClockView> + + private val burmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my")) + private val burmeseNumerals = burmeseNf.format(FORMAT_NUMBER.toLong()) + private val burmeseLineSpacing = + resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese) + private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale) + + override val events: DefaultClockEvents + override lateinit var animations: DefaultClockAnimations + private set + + init { + val parent = FrameLayout(ctx) + smallClock = + DefaultClockFaceController( + layoutInflater.inflate(R.layout.clock_default_small, parent, false) + as AnimatableClockView + ) + largeClock = + LargeClockFaceController( + layoutInflater.inflate(R.layout.clock_default_large, parent, false) + as AnimatableClockView + ) + clocks = listOf(smallClock.view, largeClock.view) + + events = DefaultClockEvents() + animations = DefaultClockAnimations(0f, 0f) + events.onLocaleChanged(Locale.getDefault()) + } + + override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) { + largeClock.recomputePadding() + animations = DefaultClockAnimations(dozeFraction, foldFraction) + events.onColorPaletteChanged(resources) + events.onTimeZoneChanged(TimeZone.getDefault()) + events.onTimeTick() + } + + override fun setLogBuffer(logBuffer: LogBuffer) { + smallClock.view.tag = "smallClockView" + largeClock.view.tag = "largeClockView" + smallClock.view.logBuffer = logBuffer + largeClock.view.logBuffer = logBuffer + } + + open inner class DefaultClockFaceController( + override val view: AnimatableClockView, + ) : ClockFaceController { + + // MAGENTA is a placeholder, and will be assigned correctly in initialize + private var currentColor = Color.MAGENTA + private var isRegionDark = false + + init { + view.setColors(currentColor, currentColor) + } + + override val events = + object : ClockFaceEvents { + override fun onRegionDarknessChanged(isRegionDark: Boolean) { + this@DefaultClockFaceController.isRegionDark = isRegionDark + updateColor() + } + } + + fun updateColor() { + val color = + if (isRegionDark) { + resources.getColor(android.R.color.system_accent1_100) + } else { + resources.getColor(android.R.color.system_accent2_600) + } + + if (currentColor == color) { + return + } + + currentColor = color + view.setColors(DOZE_COLOR, color) + view.animateAppearOnLockscreen() + } + } + + inner class LargeClockFaceController( + view: AnimatableClockView, + ) : DefaultClockFaceController(view) { + fun recomputePadding() { + val lp = view.getLayoutParams() as FrameLayout.LayoutParams + lp.topMargin = (-0.5f * view.bottom).toInt() + view.setLayoutParams(lp) + } + + fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) { + view.moveForSplitShade(fromRect, toRect, fraction) + } + } + + inner class DefaultClockEvents : ClockEvents { + override fun onTimeTick() = clocks.forEach { it.refreshTime() } + + override fun onTimeFormatChanged(is24Hr: Boolean) = + clocks.forEach { it.refreshFormat(is24Hr) } + + override fun onTimeZoneChanged(timeZone: TimeZone) = + clocks.forEach { it.onTimeZoneChanged(timeZone) } + + override fun onFontSettingChanged() { + smallClock.view.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat() + ) + largeClock.view.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat() + ) + largeClock.recomputePadding() + } + + override fun onColorPaletteChanged(resources: Resources) { + largeClock.updateColor() + smallClock.updateColor() + } + + override fun onLocaleChanged(locale: Locale) { + val nf = NumberFormat.getInstance(locale) + if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) { + clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) } + } else { + clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) } + } + + clocks.forEach { it.refreshFormat() } + } + } + + inner class DefaultClockAnimations( + dozeFraction: Float, + foldFraction: Float, + ) : ClockAnimations { + private var foldState = AnimationState(0f) + private var dozeState = AnimationState(0f) + + init { + dozeState = AnimationState(dozeFraction) + foldState = AnimationState(foldFraction) + + if (foldState.isActive) { + clocks.forEach { it.animateFoldAppear(false) } + } else { + clocks.forEach { it.animateDoze(dozeState.isActive, false) } + } + } + + override fun enter() { + if (!dozeState.isActive) { + clocks.forEach { it.animateAppearOnLockscreen() } + } + } + + override fun charge() = clocks.forEach { it.animateCharge { dozeState.isActive } } + + override fun fold(fraction: Float) { + val (hasChanged, hasJumped) = foldState.update(fraction) + if (hasChanged) { + clocks.forEach { it.animateFoldAppear(!hasJumped) } + } + } + + override fun doze(fraction: Float) { + val (hasChanged, hasJumped) = dozeState.update(fraction) + if (hasChanged) { + clocks.forEach { it.animateDoze(dozeState.isActive, !hasJumped) } + } + } + + override fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) { + largeClock.moveForSplitShade(fromRect, toRect, fraction) + } + + override val hasCustomPositionUpdatedAnimation: Boolean + get() = true + } + + private class AnimationState( + var fraction: Float, + ) { + var isActive: Boolean = fraction < 0.5f + fun update(newFraction: Float): Pair<Boolean, Boolean> { + val wasActive = isActive + val hasJumped = + (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f) + isActive = newFraction > fraction + fraction = newFraction + return Pair(wasActive != isActive, hasJumped) + } + } + + override fun dump(pw: PrintWriter) { + pw.print("smallClock=") + smallClock.view.dump(pw) + + pw.print("largeClock=") + largeClock.view.dump(pw) + } + + companion object { + @VisibleForTesting const val DOZE_COLOR = Color.WHITE + private const val FORMAT_NUMBER = 1234567890 + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index 19ac2e479bcb..6627c5d8a1c5 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -15,24 +15,14 @@ package com.android.systemui.shared.clocks import android.content.Context import android.content.res.Resources -import android.graphics.Color import android.graphics.drawable.Drawable -import android.icu.text.NumberFormat -import android.util.TypedValue import android.view.LayoutInflater -import android.widget.FrameLayout -import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.plugins.Clock -import com.android.systemui.plugins.ClockAnimations -import com.android.systemui.plugins.ClockEvents +import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockId import com.android.systemui.plugins.ClockMetadata import com.android.systemui.plugins.ClockProvider import com.android.systemui.shared.R -import java.io.PrintWriter -import java.util.Locale -import java.util.TimeZone import javax.inject.Inject private val TAG = DefaultClockProvider::class.simpleName @@ -48,11 +38,12 @@ class DefaultClockProvider @Inject constructor( override fun getClocks(): List<ClockMetadata> = listOf(ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME)) - override fun createClock(id: ClockId): Clock { + override fun createClock(id: ClockId): ClockController { if (id != DEFAULT_CLOCK_ID) { throw IllegalArgumentException("$id is unsupported by $TAG") } - return DefaultClock(ctx, layoutInflater, resources) + + return DefaultClockController(ctx, layoutInflater, resources) } override fun getClockThumbnail(id: ClockId): Drawable? { @@ -64,190 +55,3 @@ class DefaultClockProvider @Inject constructor( return resources.getDrawable(R.drawable.clock_default_thumbnail, null) } } - -/** - * Controls the default clock visuals. - * - * This serves as an adapter between the clock interface and the - * AnimatableClockView used by the existing lockscreen clock. - */ -class DefaultClock( - ctx: Context, - private val layoutInflater: LayoutInflater, - private val resources: Resources -) : Clock { - override val smallClock: AnimatableClockView - override val largeClock: AnimatableClockView - private val clocks get() = listOf(smallClock, largeClock) - - private val burmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my")) - private val burmeseNumerals = burmeseNf.format(FORMAT_NUMBER.toLong()) - private val burmeseLineSpacing = - resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese) - private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale) - - override val events: ClockEvents - override lateinit var animations: ClockAnimations - private set - - private var smallRegionDarkness = false - private var largeRegionDarkness = false - - init { - val parent = FrameLayout(ctx) - - smallClock = layoutInflater.inflate( - R.layout.clock_default_small, - parent, - false - ) as AnimatableClockView - - largeClock = layoutInflater.inflate( - R.layout.clock_default_large, - parent, - false - ) as AnimatableClockView - - events = DefaultClockEvents() - animations = DefaultClockAnimations(0f, 0f) - - events.onLocaleChanged(Locale.getDefault()) - - // DOZE_COLOR is a placeholder, and will be assigned correctly in initialize - clocks.forEach { it.setColors(DOZE_COLOR, DOZE_COLOR) } - } - - override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) { - recomputePadding() - animations = DefaultClockAnimations(dozeFraction, foldFraction) - events.onColorPaletteChanged(resources, true, true) - events.onTimeZoneChanged(TimeZone.getDefault()) - events.onTimeTick() - } - - inner class DefaultClockEvents() : ClockEvents { - override fun onTimeTick() = clocks.forEach { it.refreshTime() } - - override fun onTimeFormatChanged(is24Hr: Boolean) = - clocks.forEach { it.refreshFormat(is24Hr) } - - override fun onTimeZoneChanged(timeZone: TimeZone) = - clocks.forEach { it.onTimeZoneChanged(timeZone) } - - override fun onFontSettingChanged() { - smallClock.setTextSize( - TypedValue.COMPLEX_UNIT_PX, - resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat() - ) - largeClock.setTextSize( - TypedValue.COMPLEX_UNIT_PX, - resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat() - ) - recomputePadding() - } - - override fun onColorPaletteChanged( - resources: Resources, - smallClockIsDark: Boolean, - largeClockIsDark: Boolean - ) { - if (smallRegionDarkness != smallClockIsDark) { - smallRegionDarkness = smallClockIsDark - updateClockColor(smallClock, smallClockIsDark) - } - if (largeRegionDarkness != largeClockIsDark) { - largeRegionDarkness = largeClockIsDark - updateClockColor(largeClock, largeClockIsDark) - } - } - - override fun onLocaleChanged(locale: Locale) { - val nf = NumberFormat.getInstance(locale) - if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) { - clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) } - } else { - clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) } - } - - clocks.forEach { it.refreshFormat() } - } - } - - inner class DefaultClockAnimations( - dozeFraction: Float, - foldFraction: Float - ) : ClockAnimations { - private var foldState = AnimationState(0f) - private var dozeState = AnimationState(0f) - - init { - dozeState = AnimationState(dozeFraction) - foldState = AnimationState(foldFraction) - - if (foldState.isActive) { - clocks.forEach { it.animateFoldAppear(false) } - } else { - clocks.forEach { it.animateDoze(dozeState.isActive, false) } - } - } - - override fun enter() { - if (!dozeState.isActive) { - clocks.forEach { it.animateAppearOnLockscreen() } - } - } - - override fun charge() = clocks.forEach { it.animateCharge { dozeState.isActive } } - - override fun fold(fraction: Float) { - val (hasChanged, hasJumped) = foldState.update(fraction) - if (hasChanged) { - clocks.forEach { it.animateFoldAppear(!hasJumped) } - } - } - - override fun doze(fraction: Float) { - val (hasChanged, hasJumped) = dozeState.update(fraction) - if (hasChanged) { - clocks.forEach { it.animateDoze(dozeState.isActive, !hasJumped) } - } - } - } - - private class AnimationState( - var fraction: Float - ) { - var isActive: Boolean = fraction < 0.5f - fun update(newFraction: Float): Pair<Boolean, Boolean> { - val wasActive = isActive - val hasJumped = (fraction == 0f && newFraction == 1f) || - (fraction == 1f && newFraction == 0f) - isActive = newFraction > fraction - fraction = newFraction - return Pair(wasActive != isActive, hasJumped) - } - } - - private fun updateClockColor(clock: AnimatableClockView, isRegionDark: Boolean) { - val color = if (isRegionDark) { - resources.getColor(android.R.color.system_accent1_100) - } else { - resources.getColor(android.R.color.system_accent2_600) - } - clock.setColors(DOZE_COLOR, color) - clock.animateAppearOnLockscreen() - } - - private fun recomputePadding() { - val lp = largeClock.getLayoutParams() as FrameLayout.LayoutParams - lp.topMargin = (-0.5f * largeClock.bottom).toInt() - largeClock.setLayoutParams(lp) - } - - override fun dump(pw: PrintWriter) = clocks.forEach { it.dump(pw) } - - companion object { - @VisibleForTesting const val DOZE_COLOR = Color.WHITE - private const val FORMAT_NUMBER = 1234567890 - } -} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/RegionSamplingHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/RegionSamplingHelper.java index c7d5ffe1b84c..023ef319352e 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/RegionSamplingHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/RegionSamplingHelper.java @@ -217,12 +217,16 @@ public class RegionSamplingHelper implements View.OnAttachStateChangeListener, unregisterSamplingListener(); mSamplingListenerRegistered = true; SurfaceControl wrappedStopLayer = wrap(stopLayerControl); + + // pass this to background thread to avoid empty Rect race condition + final Rect boundsCopy = new Rect(mSamplingRequestBounds); + mBackgroundExecutor.execute(() -> { if (wrappedStopLayer != null && !wrappedStopLayer.isValid()) { return; } mCompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY, - wrappedStopLayer, mSamplingRequestBounds); + wrappedStopLayer, boundsCopy); }); mRegisteredSamplingBounds.set(mSamplingRequestBounds); mRegisteredStopLayer = stopLayerControl; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl index e77c65079456..2b2b05ce2fbf 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl @@ -81,11 +81,6 @@ interface ISystemUiProxy { */ void stopScreenPinning() = 17; - /* - * Notifies that the swipe-to-home (recents animation) is finished. - */ - void notifySwipeToHomeFinished() = 23; - /** * Notifies that quickstep will switch to a new task * @param rotation indicates which Surface.Rotation the gesture was started in diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java index 2111df501415..647dd47159e0 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java @@ -27,6 +27,8 @@ import android.app.ActivityManager.TaskDescription; import android.app.TaskInfo; import android.content.ComponentName; import android.content.Intent; +import android.graphics.Point; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; @@ -233,17 +235,14 @@ public class Task { @ViewDebug.ExportedProperty(category="recents") public boolean isLocked; + public Point positionInParent; + + public Rect appBounds; + // Last snapshot data, only used for recent tasks public ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData = new ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData(); - /** - * Indicates that this task for the desktop tile in recents. - * - * Used when desktop mode feature is enabled. - */ - public boolean desktopTile; - public Task() { // Do nothing } @@ -274,7 +273,8 @@ public class Task { this(other.key, other.colorPrimary, other.colorBackground, other.isDockable, other.isLocked, other.taskDescription, other.topActivity); lastSnapshotData.set(other.lastSnapshotData); - desktopTile = other.desktopTile; + positionInParent = other.positionInParent; + appBounds = other.appBounds; } /** diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java new file mode 100644 index 000000000000..40c8774d4f34 --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java @@ -0,0 +1,242 @@ +package com.android.systemui.shared.recents.utilities; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; + +import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.wm.shell.util.SplitBounds; + +/** + * Utility class to position the thumbnail in the TaskView + */ +public class PreviewPositionHelper { + + public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f; + + /** + * Specifies that a stage is positioned at the top half of the screen if + * in portrait mode or at the left half of the screen if in landscape mode. + * TODO(b/254378592): Remove after consolidation + */ + public static final int STAGE_POSITION_TOP_OR_LEFT = 0; + + /** + * Specifies that a stage is positioned at the bottom half of the screen if + * in portrait mode or at the right half of the screen if in landscape mode. + * TODO(b/254378592): Remove after consolidation + */ + public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1; + + // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1. + private final RectF mClippedInsets = new RectF(); + private final Matrix mMatrix = new Matrix(); + private boolean mIsOrientationChanged; + private SplitBounds mSplitBounds; + private int mDesiredStagePosition; + + public Matrix getMatrix() { + return mMatrix; + } + + public void setOrientationChanged(boolean orientationChanged) { + mIsOrientationChanged = orientationChanged; + } + + public boolean isOrientationChanged() { + return mIsOrientationChanged; + } + + public void setSplitBounds(SplitBounds splitBounds, int desiredStagePosition) { + mSplitBounds = splitBounds; + mDesiredStagePosition = desiredStagePosition; + } + + /** + * Updates the matrix based on the provided parameters + */ + public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData, + int canvasWidth, int canvasHeight, int screenWidthPx, int taskbarSize, boolean isTablet, + int currentRotation, boolean isRtl) { + boolean isRotated = false; + boolean isOrientationDifferent; + + float fullscreenTaskWidth = screenWidthPx; + if (mSplitBounds != null && !mSplitBounds.appsStackedVertically) { + // For landscape, scale the width + float taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? mSplitBounds.leftTaskPercent + : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent)); + // Scale landscape width to that of actual screen + fullscreenTaskWidth = screenWidthPx * taskPercent; + } + int thumbnailRotation = thumbnailData.rotation; + int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation); + RectF thumbnailClipHint = new RectF(); + float canvasScreenRatio = canvasWidth / fullscreenTaskWidth; + float scaledTaskbarSize = taskbarSize * canvasScreenRatio; + thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0; + + float scale = thumbnailData.scale; + final float thumbnailScale; + + // Landscape vs portrait change. + // Note: Disable rotation in grid layout. + boolean windowingModeSupportsRotation = + thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN && !isTablet; + isOrientationDifferent = isOrientationChange(deltaRotate) + && windowingModeSupportsRotation; + if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) { + // If we haven't measured , skip the thumbnail drawing and only draw the background + // color + thumbnailScale = 0f; + } else { + // Rotate the screenshot if not in multi-window mode + isRotated = deltaRotate > 0 && windowingModeSupportsRotation; + + float surfaceWidth = thumbnailBounds.width() / scale; + float surfaceHeight = thumbnailBounds.height() / scale; + float availableWidth = surfaceWidth + - (thumbnailClipHint.left + thumbnailClipHint.right); + float availableHeight = surfaceHeight + - (thumbnailClipHint.top + thumbnailClipHint.bottom); + + float canvasAspect = canvasWidth / (float) canvasHeight; + float availableAspect = isRotated + ? availableHeight / availableWidth + : availableWidth / availableHeight; + boolean isAspectLargelyDifferent = + Utilities.isRelativePercentDifferenceGreaterThan(canvasAspect, + availableAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT); + if (isRotated && isAspectLargelyDifferent) { + // Do not rotate thumbnail if it would not improve fit + isRotated = false; + isOrientationDifferent = false; + } + + if (isAspectLargelyDifferent) { + // Crop letterbox insets if insets isn't already clipped + thumbnailClipHint.left = thumbnailData.letterboxInsets.left; + thumbnailClipHint.right = thumbnailData.letterboxInsets.right; + thumbnailClipHint.top = thumbnailData.letterboxInsets.top; + thumbnailClipHint.bottom = thumbnailData.letterboxInsets.bottom; + availableWidth = surfaceWidth + - (thumbnailClipHint.left + thumbnailClipHint.right); + availableHeight = surfaceHeight + - (thumbnailClipHint.top + thumbnailClipHint.bottom); + } + + final float targetW, targetH; + if (isOrientationDifferent) { + targetW = canvasHeight; + targetH = canvasWidth; + } else { + targetW = canvasWidth; + targetH = canvasHeight; + } + float targetAspect = targetW / targetH; + + // Update the clipHint such that + // > the final clipped position has same aspect ratio as requested by canvas + // > first fit the width and crop the extra height + // > if that will leave empty space, fit the height and crop the width instead + float croppedWidth = availableWidth; + float croppedHeight = croppedWidth / targetAspect; + if (croppedHeight > availableHeight) { + croppedHeight = availableHeight; + if (croppedHeight < targetH) { + croppedHeight = Math.min(targetH, surfaceHeight); + } + croppedWidth = croppedHeight * targetAspect; + + // One last check in case the task aspect radio messed up something + if (croppedWidth > surfaceWidth) { + croppedWidth = surfaceWidth; + croppedHeight = croppedWidth / targetAspect; + } + } + + // Update the clip hints. Align to 0,0, crop the remaining. + if (isRtl) { + thumbnailClipHint.left += availableWidth - croppedWidth; + if (thumbnailClipHint.right < 0) { + thumbnailClipHint.left += thumbnailClipHint.right; + thumbnailClipHint.right = 0; + } + } else { + thumbnailClipHint.right += availableWidth - croppedWidth; + if (thumbnailClipHint.left < 0) { + thumbnailClipHint.right += thumbnailClipHint.left; + thumbnailClipHint.left = 0; + } + } + thumbnailClipHint.bottom += availableHeight - croppedHeight; + if (thumbnailClipHint.top < 0) { + thumbnailClipHint.bottom += thumbnailClipHint.top; + thumbnailClipHint.top = 0; + } else if (thumbnailClipHint.bottom < 0) { + thumbnailClipHint.top += thumbnailClipHint.bottom; + thumbnailClipHint.bottom = 0; + } + + thumbnailScale = targetW / (croppedWidth * scale); + } + + if (!isRotated) { + mMatrix.setTranslate( + -thumbnailClipHint.left * scale, + -thumbnailClipHint.top * scale); + } else { + setThumbnailRotation(deltaRotate, thumbnailBounds); + } + + mClippedInsets.set(0, 0, 0, scaledTaskbarSize); + + mMatrix.postScale(thumbnailScale, thumbnailScale); + mIsOrientationChanged = isOrientationDifferent; + } + + private int getRotationDelta(int oldRotation, int newRotation) { + int delta = newRotation - oldRotation; + if (delta < 0) delta += 4; + return delta; + } + + /** + * @param deltaRotation the number of 90 degree turns from the current orientation + * @return {@code true} if the change in rotation results in a shift from landscape to + * portrait or vice versa, {@code false} otherwise + */ + private boolean isOrientationChange(int deltaRotation) { + return deltaRotation == ROTATION_90 || deltaRotation == ROTATION_270; + } + + private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) { + float translateX = 0; + float translateY = 0; + + mMatrix.setRotate(90 * deltaRotate); + switch (deltaRotate) { /* Counter-clockwise */ + case ROTATION_90: + translateX = thumbnailPosition.height(); + break; + case ROTATION_270: + translateY = thumbnailPosition.width(); + break; + case ROTATION_180: + translateX = thumbnailPosition.width(); + translateY = thumbnailPosition.height(); + break; + } + mMatrix.postTranslate(translateX, translateY); + } + + public RectF getClippedInsets() { + return mClippedInsets; + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java index 56326e36ff5e..77a13bd91b90 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java @@ -62,6 +62,16 @@ public class Utilities { return false; // Default } + /** + * Compares the ratio of two quantities and returns whether that ratio is greater than the + * provided bound. Order of quantities does not matter. Bound should be a decimal representation + * of a percentage. + */ + public static boolean isRelativePercentDifferenceGreaterThan(float first, float second, + float bound) { + return (Math.abs(first - second) / Math.abs((first + second) / 2.0f)) > bound; + } + /** Calculates the constrast between two colors, using the algorithm provided by the WCAG v2. */ public static float computeContrastBetweenColors(int bg, int fg) { float bgR = Color.red(bg) / 255f; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt index dd2e55d4e7d7..cd4b9994ccca 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.shared.regionsampling +import android.graphics.Color import android.graphics.Rect import android.view.View import androidx.annotation.VisibleForTesting @@ -33,18 +34,19 @@ open class RegionSamplingInstance( regionSamplingEnabled: Boolean, updateFun: UpdateColorCallback ) { - private var isDark = RegionDarkness.DEFAULT + private var regionDarkness = RegionDarkness.DEFAULT private var samplingBounds = Rect() private val tmpScreenLocation = IntArray(2) @VisibleForTesting var regionSampler: RegionSamplingHelper? = null - + private var lightForegroundColor = Color.WHITE + private var darkForegroundColor = Color.BLACK /** * Interface for method to be passed into RegionSamplingHelper */ @FunctionalInterface interface UpdateColorCallback { /** - * Method to update the text colors after clock darkness changed. + * Method to update the foreground colors after clock darkness changed. */ fun updateColors() } @@ -59,6 +61,30 @@ open class RegionSamplingInstance( return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor) } + /** + * Sets the colors to be used for Dark and Light Foreground. + * + * @param lightColor The color used for Light Foreground. + * @param darkColor The color used for Dark Foreground. + */ + fun setForegroundColors(lightColor: Int, darkColor: Int) { + lightForegroundColor = lightColor + darkForegroundColor = darkColor + } + + /** + * Determines which foreground color to use based on region darkness. + * + * @return the determined foreground color + */ + fun currentForegroundColor(): Int{ + return if (regionDarkness.isDark) { + lightForegroundColor + } else { + darkForegroundColor + } + } + private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness { return if (isRegionDark) { RegionDarkness.DARK @@ -68,7 +94,7 @@ open class RegionSamplingInstance( } fun currentRegionDarkness(): RegionDarkness { - return isDark + return regionDarkness } /** @@ -97,7 +123,7 @@ open class RegionSamplingInstance( regionSampler = createRegionSamplingHelper(sampledView, object : SamplingCallback { override fun onRegionDarknessChanged(isRegionDark: Boolean) { - isDark = convertToClockDarkness(isRegionDark) + regionDarkness = convertToClockDarkness(isRegionDark) updateFun.updateColors() } /** diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java index 22bffda33918..6087655615a6 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java @@ -132,8 +132,11 @@ public class RotationButtonController { mMainThreadHandler.postAtFrontOfQueue(() -> { // If the screen rotation changes while locked, potentially update lock to flow with // new screen rotation and hide any showing suggestions. - if (isRotationLocked()) { - if (shouldOverrideUserLockPrefs(rotation)) { + boolean rotationLocked = isRotationLocked(); + // The isVisible check makes the rotation button disappear when we are not locked + // (e.g. for tabletop auto-rotate). + if (rotationLocked || mRotationButton.isVisible()) { + if (shouldOverrideUserLockPrefs(rotation) && rotationLocked) { setRotationLockedAtAngle(rotation); } setRotateSuggestionButtonState(false /* visible */, true /* forced */); diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index 916526d0efac..fd41cb0630dd 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -19,7 +19,6 @@ package com.android.systemui.shared.system; import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED; import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; -import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE; import static android.app.ActivityTaskManager.getService; import android.annotation.NonNull; @@ -27,7 +26,6 @@ import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityClient; import android.app.ActivityManager; -import android.app.ActivityManager.RecentTaskInfo; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityOptions; import android.app.ActivityTaskManager; @@ -196,12 +194,8 @@ public class ActivityManagerWrapper { Rect homeContentInsets, Rect minimizedHomeBounds) { final RecentsAnimationControllerCompat controllerCompat = new RecentsAnimationControllerCompat(controller); - final RemoteAnimationTargetCompat[] appsCompat = - RemoteAnimationTargetCompat.wrap(apps); - final RemoteAnimationTargetCompat[] wallpapersCompat = - RemoteAnimationTargetCompat.wrap(wallpapers); - animationHandler.onAnimationStart(controllerCompat, appsCompat, - wallpapersCompat, homeContentInsets, minimizedHomeBounds); + animationHandler.onAnimationStart(controllerCompat, apps, + wallpapers, homeContentInsets, minimizedHomeBounds); } @Override @@ -212,12 +206,7 @@ public class ActivityManagerWrapper { @Override public void onTasksAppeared(RemoteAnimationTarget[] apps) { - final RemoteAnimationTargetCompat[] compats = - new RemoteAnimationTargetCompat[apps.length]; - for (int i = 0; i < apps.length; ++i) { - compats[i] = new RemoteAnimationTargetCompat(apps[i]); - } - animationHandler.onTasksAppeared(compats); + animationHandler.onTasksAppeared(apps); } }; } @@ -252,8 +241,9 @@ public class ActivityManagerWrapper { public boolean startActivityFromRecents(int taskId, ActivityOptions options) { try { Bundle optsBundle = options == null ? null : options.toBundle(); - getService().startActivityFromRecents(taskId, optsBundle); - return true; + return ActivityManager.isStartResultSuccessful( + getService().startActivityFromRecents( + taskId, optsBundle)); } catch (Exception e) { return false; } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java index 5d6598d63a1b..8a2509610310 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java @@ -51,6 +51,8 @@ public final class InteractionJankMonitorWrapper { InteractionJankMonitor.CUJ_SPLIT_SCREEN_ENTER; public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = InteractionJankMonitor.CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION; + public static final int CUJ_RECENTS_SCROLLING = + InteractionJankMonitor.CUJ_RECENTS_SCROLLING; @IntDef({ CUJ_APP_LAUNCH_FROM_RECENTS, @@ -59,7 +61,8 @@ public final class InteractionJankMonitorWrapper { CUJ_APP_CLOSE_TO_PIP, CUJ_QUICK_SWITCH, CUJ_APP_LAUNCH_FROM_WIDGET, - CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION + CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, + CUJ_RECENTS_SCROLLING }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 85278dd4b883..766266d9cc94 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -43,27 +43,8 @@ public class QuickStepContract { public static final String KEY_EXTRA_SYSUI_PROXY = "extra_sysui_proxy"; public static final String KEY_EXTRA_WINDOW_CORNER_RADIUS = "extra_window_corner_radius"; public static final String KEY_EXTRA_SUPPORTS_WINDOW_CORNERS = "extra_supports_window_corners"; - // See IPip.aidl - public static final String KEY_EXTRA_SHELL_PIP = "extra_shell_pip"; - // See ISplitScreen.aidl - public static final String KEY_EXTRA_SHELL_SPLIT_SCREEN = "extra_shell_split_screen"; - // See IFloatingTasks.aidl - public static final String KEY_EXTRA_SHELL_FLOATING_TASKS = "extra_shell_floating_tasks"; - // See IOneHanded.aidl - public static final String KEY_EXTRA_SHELL_ONE_HANDED = "extra_shell_one_handed"; - // See IShellTransitions.aidl - public static final String KEY_EXTRA_SHELL_SHELL_TRANSITIONS = - "extra_shell_shell_transitions"; - // See IStartingWindow.aidl - public static final String KEY_EXTRA_SHELL_STARTING_WINDOW = - "extra_shell_starting_window"; // See ISysuiUnlockAnimationController.aidl public static final String KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER = "unlock_animation"; - // See IRecentTasks.aidl - public static final String KEY_EXTRA_RECENT_TASKS = "recent_tasks"; - public static final String KEY_EXTRA_SHELL_BACK_ANIMATION = "extra_shell_back_animation"; - // See IDesktopMode.aidl - public static final String KEY_EXTRA_SHELL_DESKTOP_MODE = "extra_shell_desktop_mode"; public static final String NAV_BAR_MODE_3BUTTON_OVERLAY = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY; @@ -129,6 +110,9 @@ public class QuickStepContract { public static final int SYSUI_STATE_IMMERSIVE_MODE = 1 << 24; // The voice interaction session window is showing public static final int SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING = 1 << 25; + // Freeform windows are showing in desktop mode + public static final int SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1 << 26; + @Retention(RetentionPolicy.SOURCE) @IntDef({SYSUI_STATE_SCREEN_PINNING, @@ -156,7 +140,8 @@ public class QuickStepContract { SYSUI_STATE_BACK_DISABLED, SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED, SYSUI_STATE_IMMERSIVE_MODE, - SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING + SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING, + SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE }) public @interface SystemUiStateFlags {} @@ -192,6 +177,8 @@ public class QuickStepContract { ? "bubbles_mange_menu_expanded" : ""); str.add((flags & SYSUI_STATE_IMMERSIVE_MODE) != 0 ? "immersive_mode" : ""); str.add((flags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0 ? "vis_win_showing" : ""); + str.add((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0 + ? "freeform_active_in_desktop_mode" : ""); return str.toString(); } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java index 5cca4a6a65f0..8bddf217ccb4 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java @@ -17,6 +17,7 @@ package com.android.systemui.shared.system; import android.graphics.Rect; +import android.view.RemoteAnimationTarget; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -27,7 +28,7 @@ public interface RecentsAnimationListener { * Called when the animation into Recents can start. This call is made on the binder thread. */ void onAnimationStart(RecentsAnimationControllerCompat controller, - RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers, + RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, Rect homeContentInsets, Rect minimizedHomeBounds); /** @@ -39,7 +40,7 @@ public interface RecentsAnimationListener { * Called when the task of an activity that has been started while the recents animation * was running becomes ready for control. */ - void onTasksAppeared(RemoteAnimationTargetCompat[] app); + void onTasksAppeared(RemoteAnimationTarget[] app); /** * Called to request that the current task tile be switched out for a screenshot (if not diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java index 09cf7c57c08a..37e706a9a4c9 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java @@ -83,12 +83,6 @@ public class RemoteAnimationAdapterCompat { RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, final IRemoteAnimationFinishedCallback finishedCallback) { - final RemoteAnimationTargetCompat[] appsCompat = - RemoteAnimationTargetCompat.wrap(apps); - final RemoteAnimationTargetCompat[] wallpapersCompat = - RemoteAnimationTargetCompat.wrap(wallpapers); - final RemoteAnimationTargetCompat[] nonAppsCompat = - RemoteAnimationTargetCompat.wrap(nonApps); final Runnable animationFinishedCallback = new Runnable() { @Override public void run() { @@ -100,8 +94,8 @@ public class RemoteAnimationAdapterCompat { } } }; - remoteAnimationAdapter.onAnimationStart(transit, appsCompat, wallpapersCompat, - nonAppsCompat, animationFinishedCallback); + remoteAnimationAdapter.onAnimationStart(transit, apps, wallpapers, + nonApps, animationFinishedCallback); } @Override @@ -121,12 +115,12 @@ public class RemoteAnimationAdapterCompat { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) { final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>(); - final RemoteAnimationTargetCompat[] appsCompat = + final RemoteAnimationTarget[] apps = RemoteAnimationTargetCompat.wrapApps(info, t, leashMap); - final RemoteAnimationTargetCompat[] wallpapersCompat = + final RemoteAnimationTarget[] wallpapers = RemoteAnimationTargetCompat.wrapNonApps( info, true /* wallpapers */, t, leashMap); - final RemoteAnimationTargetCompat[] nonAppsCompat = + final RemoteAnimationTarget[] nonApps = RemoteAnimationTargetCompat.wrapNonApps( info, false /* wallpapers */, t, leashMap); @@ -189,9 +183,9 @@ public class RemoteAnimationAdapterCompat { } } // Make wallpaper visible immediately since launcher apparently won't do this. - for (int i = wallpapersCompat.length - 1; i >= 0; --i) { - t.show(wallpapersCompat[i].leash); - t.setAlpha(wallpapersCompat[i].leash, 1.f); + for (int i = wallpapers.length - 1; i >= 0; --i) { + t.show(wallpapers[i].leash); + t.setAlpha(wallpapers[i].leash, 1.f); } } else { if (launcherTask != null) { @@ -237,7 +231,7 @@ public class RemoteAnimationAdapterCompat { } // TODO(bc-unlcok): Pass correct transit type. remoteAnimationAdapter.onAnimationStart(TRANSIT_OLD_NONE, - appsCompat, wallpapersCompat, nonAppsCompat, () -> { + apps, wallpapers, nonApps, () -> { synchronized (mFinishRunnables) { if (mFinishRunnables.remove(token) == null) return; } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java index 007629254c7c..5809c8124946 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java @@ -16,11 +16,12 @@ package com.android.systemui.shared.system; +import android.view.RemoteAnimationTarget; import android.view.WindowManager; public interface RemoteAnimationRunnerCompat { void onAnimationStart(@WindowManager.TransitionOldType int transit, - RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers, - RemoteAnimationTargetCompat[] nonApps, Runnable finishedCallback); + RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, Runnable finishedCallback); void onAnimationCancelled(); }
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java index 7c3b5fc52f0a..e1e806319ba0 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java @@ -11,106 +11,50 @@ * 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 + * limitations under the License. */ package com.android.systemui.shared.system; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.view.RemoteAnimationTarget.MODE_CHANGING; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; +import android.app.TaskInfo; import android.app.WindowConfiguration; -import android.graphics.Point; import android.graphics.Rect; import android.util.ArrayMap; -import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; +import android.window.TransitionInfo.Change; import java.util.ArrayList; +import java.util.function.BiPredicate; /** - * @see RemoteAnimationTarget + * Some utility methods for creating {@link RemoteAnimationTarget} instances. */ public class RemoteAnimationTargetCompat { - public static final int MODE_OPENING = RemoteAnimationTarget.MODE_OPENING; - public static final int MODE_CLOSING = RemoteAnimationTarget.MODE_CLOSING; - public static final int MODE_CHANGING = RemoteAnimationTarget.MODE_CHANGING; - public final int mode; - - public static final int ACTIVITY_TYPE_UNDEFINED = WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; - public static final int ACTIVITY_TYPE_STANDARD = WindowConfiguration.ACTIVITY_TYPE_STANDARD; - public static final int ACTIVITY_TYPE_HOME = WindowConfiguration.ACTIVITY_TYPE_HOME; - public static final int ACTIVITY_TYPE_RECENTS = WindowConfiguration.ACTIVITY_TYPE_RECENTS; - public static final int ACTIVITY_TYPE_ASSISTANT = WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; - public final int activityType; - - public int taskId; - public final SurfaceControl leash; - public final boolean isTranslucent; - public final Rect clipRect; - public final int prefixOrderIndex; - public final Point position; - public final Rect localBounds; - public final Rect sourceContainerBounds; - public final Rect screenSpaceBounds; - public final Rect startScreenSpaceBounds; - public final boolean isNotInRecents; - public final Rect contentInsets; - public ActivityManager.RunningTaskInfo taskInfo; - public final boolean allowEnterPip; - public final int rotationChange; - public final int windowType; - public final WindowConfiguration windowConfiguration; - - private final SurfaceControl mStartLeash; - - // Fields used only to unwrap into RemoteAnimationTarget - private final Rect startBounds; - - public final boolean willShowImeOnTarget; - - public RemoteAnimationTargetCompat(RemoteAnimationTarget app) { - taskId = app.taskId; - mode = app.mode; - leash = app.leash; - isTranslucent = app.isTranslucent; - clipRect = app.clipRect; - position = app.position; - localBounds = app.localBounds; - sourceContainerBounds = app.sourceContainerBounds; - screenSpaceBounds = app.screenSpaceBounds; - startScreenSpaceBounds = screenSpaceBounds; - prefixOrderIndex = app.prefixOrderIndex; - isNotInRecents = app.isNotInRecents; - contentInsets = app.contentInsets; - activityType = app.windowConfiguration.getActivityType(); - taskInfo = app.taskInfo; - allowEnterPip = app.allowEnterPip; - rotationChange = 0; - - mStartLeash = app.startLeash; - windowType = app.windowType; - windowConfiguration = app.windowConfiguration; - startBounds = app.startBounds; - willShowImeOnTarget = app.willShowImeOnTarget; - } - private static int newModeToLegacyMode(int newMode) { switch (newMode) { case WindowManager.TRANSIT_OPEN: @@ -120,20 +64,10 @@ public class RemoteAnimationTargetCompat { case WindowManager.TRANSIT_TO_BACK: return MODE_CLOSING; default: - return 2; // MODE_CHANGING + return MODE_CHANGING; } } - public RemoteAnimationTarget unwrap() { - final RemoteAnimationTarget target = new RemoteAnimationTarget( - taskId, mode, leash, isTranslucent, clipRect, contentInsets, - prefixOrderIndex, position, localBounds, screenSpaceBounds, windowConfiguration, - isNotInRecents, mStartLeash, startBounds, taskInfo, allowEnterPip, windowType - ); - target.setWillShowImeOnTarget(willShowImeOnTarget); - return target; - } - /** * Almost a copy of Transitions#setupStartState. * TODO: remove when there is proper cross-process transaction sync. @@ -205,54 +139,61 @@ public class RemoteAnimationTargetCompat { return leashSurface; } - public RemoteAnimationTargetCompat(TransitionInfo.Change change, int order, - TransitionInfo info, SurfaceControl.Transaction t) { - mode = newModeToLegacyMode(change.getMode()); + /** + * Creates a new RemoteAnimationTarget from the provided change info + */ + public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order, + TransitionInfo info, SurfaceControl.Transaction t, + @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) { + int taskId; + boolean isNotInRecents; + ActivityManager.RunningTaskInfo taskInfo; + WindowConfiguration windowConfiguration; + taskInfo = change.getTaskInfo(); if (taskInfo != null) { taskId = taskInfo.taskId; isNotInRecents = !taskInfo.isRunning; - activityType = taskInfo.getActivityType(); windowConfiguration = taskInfo.configuration.windowConfiguration; } else { taskId = INVALID_TASK_ID; isNotInRecents = true; - activityType = ACTIVITY_TYPE_UNDEFINED; windowConfiguration = new WindowConfiguration(); } - // TODO: once we can properly sync transactions across process, then get rid of this leash. - leash = createLeash(info, change, order, t); - - isTranslucent = (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0; - clipRect = null; - position = null; - localBounds = new Rect(change.getEndAbsBounds()); + Rect localBounds = new Rect(change.getEndAbsBounds()); localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y); - sourceContainerBounds = null; - screenSpaceBounds = new Rect(change.getEndAbsBounds()); - startScreenSpaceBounds = new Rect(change.getStartAbsBounds()); - - prefixOrderIndex = order; - // TODO(shell-transitions): I guess we need to send content insets? evaluate how its used. - contentInsets = new Rect(0, 0, 0, 0); - allowEnterPip = change.getAllowEnterPip(); - mStartLeash = null; - rotationChange = change.getEndRotation() - change.getStartRotation(); - windowType = (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0 - ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE; - - startBounds = change.getStartAbsBounds(); - willShowImeOnTarget = (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0; - } - public static RemoteAnimationTargetCompat[] wrap(RemoteAnimationTarget[] apps) { - final int length = apps != null ? apps.length : 0; - final RemoteAnimationTargetCompat[] appsCompat = new RemoteAnimationTargetCompat[length]; - for (int i = 0; i < length; i++) { - appsCompat[i] = new RemoteAnimationTargetCompat(apps[i]); + RemoteAnimationTarget target = new RemoteAnimationTarget( + taskId, + newModeToLegacyMode(change.getMode()), + // TODO: once we can properly sync transactions across process, + // then get rid of this leash. + createLeash(info, change, order, t), + (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0, + null, + // TODO(shell-transitions): we need to send content insets? evaluate how its used. + new Rect(0, 0, 0, 0), + order, + null, + localBounds, + new Rect(change.getEndAbsBounds()), + windowConfiguration, + isNotInRecents, + null, + new Rect(change.getStartAbsBounds()), + taskInfo, + change.getAllowEnterPip(), + (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0 + ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE + ); + target.setWillShowImeOnTarget( + (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0); + target.setRotationChange(change.getEndRotation() - change.getStartRotation()); + if (leashMap != null) { + leashMap.put(change.getLeash(), target.leash); } - return appsCompat; + return target; } /** @@ -261,35 +202,20 @@ public class RemoteAnimationTargetCompat { * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be * populated by this function. If null, it is ignored. */ - public static RemoteAnimationTargetCompat[] wrapApps(TransitionInfo info, + public static RemoteAnimationTarget[] wrapApps(TransitionInfo info, SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) { - final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>(); - final SparseArray<TransitionInfo.Change> childTaskTargets = new SparseArray<>(); - for (int i = 0; i < info.getChanges().size(); i++) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() == null) continue; - - final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + SparseBooleanArray childTaskTargets = new SparseBooleanArray(); + return wrap(info, t, leashMap, (change, taskInfo) -> { // Children always come before parent since changes are in top-to-bottom z-order. - if (taskInfo != null) { - if (childTaskTargets.contains(taskInfo.taskId)) { - // has children, so not a leaf. Skip. - continue; - } - if (taskInfo.hasParentTask()) { - childTaskTargets.put(taskInfo.parentTaskId, change); - } + if ((taskInfo == null) || childTaskTargets.get(taskInfo.taskId)) { + // has children, so not a leaf. Skip. + return false; } - - final RemoteAnimationTargetCompat targetCompat = - new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t); - if (leashMap != null) { - leashMap.put(change.getLeash(), targetCompat.leash); + if (taskInfo.hasParentTask()) { + childTaskTargets.put(taskInfo.parentTaskId, true); } - out.add(targetCompat); - } - - return out.toArray(new RemoteAnimationTargetCompat[out.size()]); + return true; + }); } /** @@ -300,38 +226,23 @@ public class RemoteAnimationTargetCompat { * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be * populated by this function. If null, it is ignored. */ - public static RemoteAnimationTargetCompat[] wrapNonApps(TransitionInfo info, boolean wallpapers, + public static RemoteAnimationTarget[] wrapNonApps(TransitionInfo info, boolean wallpapers, SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) { - final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>(); + return wrap(info, t, leashMap, (change, taskInfo) -> (taskInfo == null) + && wallpapers == change.hasFlags(FLAG_IS_WALLPAPER) + && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)); + } + private static RemoteAnimationTarget[] wrap(TransitionInfo info, + SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap, + BiPredicate<Change, TaskInfo> filter) { + final ArrayList<RemoteAnimationTarget> out = new ArrayList<>(); for (int i = 0; i < info.getChanges().size(); i++) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getTaskInfo() != null) continue; - - final boolean changeIsWallpaper = - (change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0; - if (wallpapers != changeIsWallpaper) continue; - - final RemoteAnimationTargetCompat targetCompat = - new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t); - if (leashMap != null) { - leashMap.put(change.getLeash(), targetCompat.leash); + TransitionInfo.Change change = info.getChanges().get(i); + if (filter.test(change, change.getTaskInfo())) { + out.add(newTarget(change, info.getChanges().size() - i, info, t, leashMap)); } - out.add(targetCompat); - } - - return out.toArray(new RemoteAnimationTargetCompat[out.size()]); - } - - /** - * @see SurfaceControl#release() - */ - public void release() { - if (leash != null) { - leash.release(); - } - if (mStartLeash != null) { - mStartLeash.release(); } + return out.toArray(new RemoteAnimationTarget[out.size()]); } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java index f6792251d282..d6655a74219c 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java @@ -17,7 +17,9 @@ package com.android.systemui.shared.system; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; @@ -27,8 +29,7 @@ import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionFilter.CONTAINER_ORDER_TOP; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.newTarget; import android.annotation.NonNull; import android.annotation.Nullable; @@ -45,6 +46,7 @@ import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; import android.view.IRecentsAnimationController; +import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; @@ -127,9 +129,9 @@ public class RemoteTransitionCompat implements Parcelable { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishedCallback) { final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>(); - final RemoteAnimationTargetCompat[] apps = + final RemoteAnimationTarget[] apps = RemoteAnimationTargetCompat.wrapApps(info, t, leashMap); - final RemoteAnimationTargetCompat[] wallpapers = + final RemoteAnimationTarget[] wallpapers = RemoteAnimationTargetCompat.wrapNonApps( info, true /* wallpapers */, t, leashMap); // TODO(b/177438007): Move this set-up logic into launcher's animation impl. @@ -230,7 +232,7 @@ public class RemoteTransitionCompat implements Parcelable { private PictureInPictureSurfaceTransaction mPipTransaction = null; private IBinder mTransition = null; private boolean mKeyguardLocked = false; - private RemoteAnimationTargetCompat[] mAppearedTargets; + private RemoteAnimationTarget[] mAppearedTargets; private boolean mWillFinishToHome = false; void setup(RecentsAnimationControllerCompat wrapped, TransitionInfo info, @@ -325,18 +327,15 @@ public class RemoteTransitionCompat implements Parcelable { final int layer = mInfo.getChanges().size() * 3; mOpeningLeashes = new ArrayList<>(); mOpeningHome = cancelRecents; - final RemoteAnimationTargetCompat[] targets = - new RemoteAnimationTargetCompat[openingTasks.size()]; + final RemoteAnimationTarget[] targets = + new RemoteAnimationTarget[openingTasks.size()]; for (int i = 0; i < openingTasks.size(); ++i) { final TransitionInfo.Change change = openingTasks.valueAt(i); mOpeningLeashes.add(change.getLeash()); // We are receiving new opening tasks, so convert to onTasksAppeared. - final RemoteAnimationTargetCompat target = new RemoteAnimationTargetCompat( - change, layer, info, t); - mLeashMap.put(mOpeningLeashes.get(i), target.leash); - t.reparent(target.leash, mInfo.getRootLeash()); - t.setLayer(target.leash, layer); - targets[i] = target; + targets[i] = newTarget(change, layer, info, t, mLeashMap); + t.reparent(targets[i].leash, mInfo.getRootLeash()); + t.setLayer(targets[i].leash, layer); } t.apply(); mAppearedTargets = targets; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java deleted file mode 100644 index 30c062b66da9..000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.shared.system; - -import android.graphics.HardwareRenderer; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.os.Handler; -import android.os.Handler.Callback; -import android.os.Message; -import android.os.Trace; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; -import android.view.View; -import android.view.ViewRootImpl; - -import java.util.function.Consumer; - -/** - * Helper class to apply surface transactions in sync with RenderThread. - * - * NOTE: This is a modification of {@link android.view.SyncRtSurfaceTransactionApplier}, we can't - * currently reference that class from the shared lib as it is hidden. - */ -public class SyncRtSurfaceTransactionApplierCompat { - - public static final int FLAG_ALL = 0xffffffff; - public static final int FLAG_ALPHA = 1; - public static final int FLAG_MATRIX = 1 << 1; - public static final int FLAG_WINDOW_CROP = 1 << 2; - public static final int FLAG_LAYER = 1 << 3; - public static final int FLAG_CORNER_RADIUS = 1 << 4; - public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5; - public static final int FLAG_VISIBILITY = 1 << 6; - public static final int FLAG_RELATIVE_LAYER = 1 << 7; - public static final int FLAG_SHADOW_RADIUS = 1 << 8; - - private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0; - - private final SurfaceControl mBarrierSurfaceControl; - private final ViewRootImpl mTargetViewRootImpl; - private final Handler mApplyHandler; - - private int mSequenceNumber = 0; - private int mPendingSequenceNumber = 0; - private Runnable mAfterApplyCallback; - - /** - * @param targetView The view in the surface that acts as synchronization anchor. - */ - public SyncRtSurfaceTransactionApplierCompat(View targetView) { - mTargetViewRootImpl = targetView != null ? targetView.getViewRootImpl() : null; - mBarrierSurfaceControl = mTargetViewRootImpl != null - ? mTargetViewRootImpl.getSurfaceControl() : null; - - mApplyHandler = new Handler(new Callback() { - @Override - public boolean handleMessage(Message msg) { - if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) { - onApplyMessage(msg.arg1); - return true; - } - return false; - } - }); - } - - private void onApplyMessage(int seqNo) { - mSequenceNumber = seqNo; - if (mSequenceNumber == mPendingSequenceNumber && mAfterApplyCallback != null) { - Runnable r = mAfterApplyCallback; - mAfterApplyCallback = null; - r.run(); - } - } - - /** - * Schedules applying surface parameters on the next frame. - * - * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into - * this method to avoid synchronization issues. - */ - public void scheduleApply(final SyncRtSurfaceTransactionApplierCompat.SurfaceParams... params) { - if (mTargetViewRootImpl == null || mTargetViewRootImpl.getView() == null) { - return; - } - - mPendingSequenceNumber++; - final int toApplySeqNo = mPendingSequenceNumber; - mTargetViewRootImpl.registerRtFrameCallback(new HardwareRenderer.FrameDrawingCallback() { - @Override - public void onFrameDraw(long frame) { - if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) { - Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0) - .sendToTarget(); - return; - } - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Sync transaction frameNumber=" + frame); - Transaction t = new Transaction(); - for (int i = params.length - 1; i >= 0; i--) { - SyncRtSurfaceTransactionApplierCompat.SurfaceParams surfaceParams = - params[i]; - surfaceParams.applyTo(t); - } - if (mTargetViewRootImpl != null) { - mTargetViewRootImpl.mergeWithNextTransaction(t, frame); - } else { - t.apply(); - } - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0) - .sendToTarget(); - } - }); - - // Make sure a frame gets scheduled. - mTargetViewRootImpl.getView().invalidate(); - } - - /** - * Calls the runnable when any pending apply calls have completed - */ - public void addAfterApplyCallback(final Runnable afterApplyCallback) { - if (mSequenceNumber == mPendingSequenceNumber) { - afterApplyCallback.run(); - } else { - if (mAfterApplyCallback == null) { - mAfterApplyCallback = afterApplyCallback; - } else { - final Runnable oldCallback = mAfterApplyCallback; - mAfterApplyCallback = new Runnable() { - @Override - public void run() { - afterApplyCallback.run(); - oldCallback.run(); - } - }; - } - } - } - - public static void applyParams(TransactionCompat t, - SyncRtSurfaceTransactionApplierCompat.SurfaceParams params) { - params.applyTo(t.mTransaction); - } - - /** - * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is - * attached if necessary. - */ - public static void create(final View targetView, - final Consumer<SyncRtSurfaceTransactionApplierCompat> callback) { - if (targetView == null) { - // No target view, no applier - callback.accept(null); - } else if (targetView.getViewRootImpl() != null) { - // Already attached, we're good to go - callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView)); - } else { - // Haven't been attached before we can get the view root - targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - targetView.removeOnAttachStateChangeListener(this); - callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView)); - } - - @Override - public void onViewDetachedFromWindow(View v) { - // Do nothing - } - }); - } - } - - public static class SurfaceParams { - public static class Builder { - final SurfaceControl surface; - int flags; - float alpha; - float cornerRadius; - int backgroundBlurRadius; - Matrix matrix; - Rect windowCrop; - int layer; - SurfaceControl relativeTo; - int relativeLayer; - boolean visible; - float shadowRadius; - - /** - * @param surface The surface to modify. - */ - public Builder(SurfaceControl surface) { - this.surface = surface; - } - - /** - * @param alpha The alpha value to apply to the surface. - * @return this Builder - */ - public Builder withAlpha(float alpha) { - this.alpha = alpha; - flags |= FLAG_ALPHA; - return this; - } - - /** - * @param matrix The matrix to apply to the surface. - * @return this Builder - */ - public Builder withMatrix(Matrix matrix) { - this.matrix = new Matrix(matrix); - flags |= FLAG_MATRIX; - return this; - } - - /** - * @param windowCrop The window crop to apply to the surface. - * @return this Builder - */ - public Builder withWindowCrop(Rect windowCrop) { - this.windowCrop = new Rect(windowCrop); - flags |= FLAG_WINDOW_CROP; - return this; - } - - /** - * @param layer The layer to assign the surface. - * @return this Builder - */ - public Builder withLayer(int layer) { - this.layer = layer; - flags |= FLAG_LAYER; - return this; - } - - /** - * @param relativeTo The surface that's set relative layer to. - * @param relativeLayer The relative layer. - * @return this Builder - */ - public Builder withRelativeLayerTo(SurfaceControl relativeTo, int relativeLayer) { - this.relativeTo = relativeTo; - this.relativeLayer = relativeLayer; - flags |= FLAG_RELATIVE_LAYER; - return this; - } - - /** - * @param radius the Radius for rounded corners to apply to the surface. - * @return this Builder - */ - public Builder withCornerRadius(float radius) { - this.cornerRadius = radius; - flags |= FLAG_CORNER_RADIUS; - return this; - } - - /** - * @param radius the Radius for the shadows to apply to the surface. - * @return this Builder - */ - public Builder withShadowRadius(float radius) { - this.shadowRadius = radius; - flags |= FLAG_SHADOW_RADIUS; - return this; - } - - /** - * @param radius the Radius for blur to apply to the background surfaces. - * @return this Builder - */ - public Builder withBackgroundBlur(int radius) { - this.backgroundBlurRadius = radius; - flags |= FLAG_BACKGROUND_BLUR_RADIUS; - return this; - } - - /** - * @param visible The visibility to apply to the surface. - * @return this Builder - */ - public Builder withVisibility(boolean visible) { - this.visible = visible; - flags |= FLAG_VISIBILITY; - return this; - } - - /** - * @return a new SurfaceParams instance - */ - public SurfaceParams build() { - return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer, - relativeTo, relativeLayer, cornerRadius, backgroundBlurRadius, visible, - shadowRadius); - } - } - - private SurfaceParams(SurfaceControl surface, int flags, float alpha, Matrix matrix, - Rect windowCrop, int layer, SurfaceControl relativeTo, int relativeLayer, - float cornerRadius, int backgroundBlurRadius, boolean visible, float shadowRadius) { - this.flags = flags; - this.surface = surface; - this.alpha = alpha; - this.matrix = matrix; - this.windowCrop = windowCrop; - this.layer = layer; - this.relativeTo = relativeTo; - this.relativeLayer = relativeLayer; - this.cornerRadius = cornerRadius; - this.backgroundBlurRadius = backgroundBlurRadius; - this.visible = visible; - this.shadowRadius = shadowRadius; - } - - private final int flags; - private final float[] mTmpValues = new float[9]; - - public final SurfaceControl surface; - public final float alpha; - public final float cornerRadius; - public final int backgroundBlurRadius; - public final Matrix matrix; - public final Rect windowCrop; - public final int layer; - public final SurfaceControl relativeTo; - public final int relativeLayer; - public final boolean visible; - public final float shadowRadius; - - public void applyTo(SurfaceControl.Transaction t) { - if ((flags & FLAG_MATRIX) != 0) { - t.setMatrix(surface, matrix, mTmpValues); - } - if ((flags & FLAG_WINDOW_CROP) != 0) { - t.setWindowCrop(surface, windowCrop); - } - if ((flags & FLAG_ALPHA) != 0) { - t.setAlpha(surface, alpha); - } - if ((flags & FLAG_LAYER) != 0) { - t.setLayer(surface, layer); - } - if ((flags & FLAG_CORNER_RADIUS) != 0) { - t.setCornerRadius(surface, cornerRadius); - } - if ((flags & FLAG_BACKGROUND_BLUR_RADIUS) != 0) { - t.setBackgroundBlurRadius(surface, backgroundBlurRadius); - } - if ((flags & FLAG_VISIBILITY) != 0) { - if (visible) { - t.show(surface); - } else { - t.hide(surface); - } - } - if ((flags & FLAG_RELATIVE_LAYER) != 0) { - t.setRelativeLayer(surface, relativeTo, relativeLayer); - } - if ((flags & FLAG_SHADOW_RADIUS) != 0) { - t.setShadowRadius(surface, shadowRadius); - } - } - } -} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java deleted file mode 100644 index 43a882a5f6be..000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.shared.system; - -import android.graphics.Matrix; -import android.graphics.Rect; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; - -public class TransactionCompat { - - final Transaction mTransaction; - - final float[] mTmpValues = new float[9]; - - public TransactionCompat() { - mTransaction = new Transaction(); - } - - public void apply() { - mTransaction.apply(); - } - - public TransactionCompat show(SurfaceControl surfaceControl) { - mTransaction.show(surfaceControl); - return this; - } - - public TransactionCompat hide(SurfaceControl surfaceControl) { - mTransaction.hide(surfaceControl); - return this; - } - - public TransactionCompat setPosition(SurfaceControl surfaceControl, float x, float y) { - mTransaction.setPosition(surfaceControl, x, y); - return this; - } - - public TransactionCompat setSize(SurfaceControl surfaceControl, int w, int h) { - mTransaction.setBufferSize(surfaceControl, w, h); - return this; - } - - public TransactionCompat setLayer(SurfaceControl surfaceControl, int z) { - mTransaction.setLayer(surfaceControl, z); - return this; - } - - public TransactionCompat setAlpha(SurfaceControl surfaceControl, float alpha) { - mTransaction.setAlpha(surfaceControl, alpha); - return this; - } - - public TransactionCompat setOpaque(SurfaceControl surfaceControl, boolean opaque) { - mTransaction.setOpaque(surfaceControl, opaque); - return this; - } - - public TransactionCompat setMatrix(SurfaceControl surfaceControl, float dsdx, float dtdx, - float dtdy, float dsdy) { - mTransaction.setMatrix(surfaceControl, dsdx, dtdx, dtdy, dsdy); - return this; - } - - public TransactionCompat setMatrix(SurfaceControl surfaceControl, Matrix matrix) { - mTransaction.setMatrix(surfaceControl, matrix, mTmpValues); - return this; - } - - public TransactionCompat setWindowCrop(SurfaceControl surfaceControl, Rect crop) { - mTransaction.setWindowCrop(surfaceControl, crop); - return this; - } - - public TransactionCompat setCornerRadius(SurfaceControl surfaceControl, float radius) { - mTransaction.setCornerRadius(surfaceControl, radius); - return this; - } - - public TransactionCompat setBackgroundBlurRadius(SurfaceControl surfaceControl, int radius) { - mTransaction.setBackgroundBlurRadius(surfaceControl, radius); - return this; - } - - public TransactionCompat setColor(SurfaceControl surfaceControl, float[] color) { - mTransaction.setColor(surfaceControl, color); - return this; - } - - public static void setRelativeLayer(Transaction t, SurfaceControl surfaceControl, - SurfaceControl relativeTo, int z) { - t.setRelativeLayer(surfaceControl, relativeTo, z); - } -} diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt index ec938b219933..aca9907fec1b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProvider.kt @@ -15,12 +15,11 @@ package com.android.systemui.unfold.util import android.content.Context -import android.os.RemoteException -import android.view.IRotationWatcher -import android.view.IWindowManager import android.view.Surface import com.android.systemui.unfold.UnfoldTransitionProgressProvider import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import com.android.systemui.unfold.updates.RotationChangeProvider +import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener /** * [UnfoldTransitionProgressProvider] that emits transition progress only when the display has @@ -29,27 +28,21 @@ import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionPr */ class NaturalRotationUnfoldProgressProvider( private val context: Context, - private val windowManagerInterface: IWindowManager, + private val rotationChangeProvider: RotationChangeProvider, unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider ) : UnfoldTransitionProgressProvider { private val scopedUnfoldTransitionProgressProvider = ScopedUnfoldTransitionProgressProvider(unfoldTransitionProgressProvider) - private val rotationWatcher = RotationWatcher() private var isNaturalRotation: Boolean = false fun init() { - try { - windowManagerInterface.watchRotation(rotationWatcher, context.display.displayId) - } catch (e: RemoteException) { - throw e.rethrowFromSystemServer() - } - - onRotationChanged(context.display.rotation) + rotationChangeProvider.addCallback(rotationListener) + rotationListener.onRotationChanged(context.display.rotation) } - private fun onRotationChanged(rotation: Int) { + private val rotationListener = RotationListener { rotation -> val isNewRotationNatural = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 @@ -60,12 +53,7 @@ class NaturalRotationUnfoldProgressProvider( } override fun destroy() { - try { - windowManagerInterface.removeRotationWatcher(rotationWatcher) - } catch (e: RemoteException) { - e.rethrowFromSystemServer() - } - + rotationChangeProvider.removeCallback(rotationListener) scopedUnfoldTransitionProgressProvider.destroy() } @@ -76,10 +64,4 @@ class NaturalRotationUnfoldProgressProvider( override fun removeCallback(listener: TransitionProgressListener) { scopedUnfoldTransitionProgressProvider.removeCallback(listener) } - - private inner class RotationWatcher : IRotationWatcher.Stub() { - override fun onRotationChanged(rotation: Int) { - this@NaturalRotationUnfoldProgressProvider.onRotationChanged(rotation) - } - } } diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt index 74bd9c6c287d..bb3df8f0358a 100644 --- a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt +++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.flags import android.content.Context import android.os.Handler +import com.android.internal.statusbar.IStatusBarService import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlagsDebug.ALL_FLAGS import com.android.systemui.util.settings.SettingsUtilModule @@ -27,6 +28,7 @@ import dagger.Provides import javax.inject.Named @Module(includes = [ + FeatureFlagsDebugStartableModule::class, ServerFlagReaderModule::class, SettingsUtilModule::class, ]) @@ -46,5 +48,15 @@ abstract class FlagsModule { @Provides @Named(ALL_FLAGS) fun providesAllFlags(): Map<Int, Flag<*>> = Flags.collectFlags() + + @JvmStatic + @Provides + fun providesRestarter(barService: IStatusBarService): Restarter { + return object: Restarter { + override fun restart() { + barService.restart() + } + } + } } } diff --git a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt index 38b5c9a9fa79..0f7e732fceb1 100644 --- a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt +++ b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt @@ -16,11 +16,29 @@ package com.android.systemui.flags +import com.android.internal.statusbar.IStatusBarService import dagger.Binds import dagger.Module +import dagger.Provides -@Module(includes = [ServerFlagReaderModule::class]) +@Module(includes = [ + FeatureFlagsReleaseStartableModule::class, + ServerFlagReaderModule::class +]) abstract class FlagsModule { @Binds abstract fun bindsFeatureFlagRelease(impl: FeatureFlagsRelease): FeatureFlags + + @Module + companion object { + @JvmStatic + @Provides + fun providesRestarter(barService: IStatusBarService): Restarter { + return object: Restarter { + override fun restart() { + barService.restart() + } + } + } + } } diff --git a/packages/SystemUI/src/com/android/keyguard/AdminSecondaryLockScreenController.java b/packages/SystemUI/src/com/android/keyguard/AdminSecondaryLockScreenController.java index 00f1c0108d0b..207f3440d38b 100644 --- a/packages/SystemUI/src/com/android/keyguard/AdminSecondaryLockScreenController.java +++ b/packages/SystemUI/src/com/android/keyguard/AdminSecondaryLockScreenController.java @@ -15,6 +15,12 @@ */ package com.android.keyguard; +import static androidx.constraintlayout.widget.ConstraintSet.BOTTOM; +import static androidx.constraintlayout.widget.ConstraintSet.END; +import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; +import static androidx.constraintlayout.widget.ConstraintSet.START; +import static androidx.constraintlayout.widget.ConstraintSet.TOP; + import android.annotation.Nullable; import android.app.admin.IKeyguardCallback; import android.app.admin.IKeyguardClient; @@ -30,7 +36,10 @@ import android.util.Log; import android.view.SurfaceControlViewHost; import android.view.SurfaceHolder; import android.view.SurfaceView; -import android.view.ViewGroup; +import android.view.View; + +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintSet; import com.android.internal.annotations.VisibleForTesting; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; @@ -49,7 +58,7 @@ public class AdminSecondaryLockScreenController { private static final int REMOTE_CONTENT_READY_TIMEOUT_MILLIS = 500; private final KeyguardUpdateMonitor mUpdateMonitor; private final Context mContext; - private final ViewGroup mParent; + private final ConstraintLayout mParent; private AdminSecurityView mView; private Handler mHandler; private IKeyguardClient mClient; @@ -156,6 +165,7 @@ public class AdminSecondaryLockScreenController { mUpdateMonitor = updateMonitor; mKeyguardCallback = callback; mView = new AdminSecurityView(mContext, mSurfaceHolderCallback); + mView.setId(View.generateViewId()); } /** @@ -167,6 +177,15 @@ public class AdminSecondaryLockScreenController { } if (!mView.isAttachedToWindow()) { mParent.addView(mView); + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.clone(mParent); + constraintSet.connect(mView.getId(), TOP, PARENT_ID, TOP); + constraintSet.connect(mView.getId(), START, PARENT_ID, START); + constraintSet.connect(mView.getId(), END, PARENT_ID, END); + constraintSet.connect(mView.getId(), BOTTOM, PARENT_ID, BOTTOM); + constraintSet.constrainHeight(mView.getId(), ConstraintSet.MATCH_CONSTRAINT); + constraintSet.constrainWidth(mView.getId(), ConstraintSet.MATCH_CONSTRAINT); + constraintSet.applyTo(mParent); } } diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java deleted file mode 100644 index 6064be94bc0a..000000000000 --- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java +++ /dev/null @@ -1,302 +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. - */ - -package com.android.keyguard; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.Rect; -import android.icu.text.NumberFormat; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - -import com.android.settingslib.Utils; -import com.android.systemui.R; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shared.clocks.AnimatableClockView; -import com.android.systemui.shared.navigationbar.RegionSamplingHelper; -import com.android.systemui.statusbar.policy.BatteryController; -import com.android.systemui.util.ViewController; - -import java.io.PrintWriter; -import java.util.Locale; -import java.util.Objects; -import java.util.Optional; -import java.util.TimeZone; -import java.util.concurrent.Executor; - -import javax.inject.Inject; - -/** - * Controller for an AnimatableClockView on the keyguard. Instantiated by - * {@link KeyguardClockSwitchController}. - */ -public class AnimatableClockController extends ViewController<AnimatableClockView> { - private static final String TAG = "AnimatableClockCtrl"; - private static final int FORMAT_NUMBER = 1234567890; - - private final StatusBarStateController mStatusBarStateController; - private final BroadcastDispatcher mBroadcastDispatcher; - private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private final BatteryController mBatteryController; - private final int mDozingColor = Color.WHITE; - private Optional<RegionSamplingHelper> mRegionSamplingHelper = Optional.empty(); - private Rect mSamplingBounds = new Rect(); - private int mLockScreenColor; - private final boolean mRegionSamplingEnabled; - - private boolean mIsDozing; - private boolean mIsCharging; - private float mDozeAmount; - boolean mKeyguardShowing; - private Locale mLocale; - - private final NumberFormat mBurmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my")); - private final String mBurmeseNumerals; - private final float mBurmeseLineSpacing; - private final float mDefaultLineSpacing; - - @Inject - public AnimatableClockController( - AnimatableClockView view, - StatusBarStateController statusBarStateController, - BroadcastDispatcher broadcastDispatcher, - BatteryController batteryController, - KeyguardUpdateMonitor keyguardUpdateMonitor, - @Main Resources resources, - @Main Executor mainExecutor, - @Background Executor bgExecutor, - FeatureFlags featureFlags - ) { - super(view); - mStatusBarStateController = statusBarStateController; - mBroadcastDispatcher = broadcastDispatcher; - mKeyguardUpdateMonitor = keyguardUpdateMonitor; - mBatteryController = batteryController; - - mBurmeseNumerals = mBurmeseNf.format(FORMAT_NUMBER); - mBurmeseLineSpacing = resources.getFloat( - R.dimen.keyguard_clock_line_spacing_scale_burmese); - mDefaultLineSpacing = resources.getFloat( - R.dimen.keyguard_clock_line_spacing_scale); - - mRegionSamplingEnabled = featureFlags.isEnabled(Flags.REGION_SAMPLING); - if (!mRegionSamplingEnabled) { - return; - } - - mRegionSamplingHelper = Optional.of(new RegionSamplingHelper(mView, - new RegionSamplingHelper.SamplingCallback() { - @Override - public void onRegionDarknessChanged(boolean isRegionDark) { - if (isRegionDark) { - mLockScreenColor = Color.WHITE; - } else { - mLockScreenColor = Color.BLACK; - } - initColors(); - } - - @Override - public Rect getSampledRegion(View sampledView) { - mSamplingBounds = new Rect(sampledView.getLeft(), sampledView.getTop(), - sampledView.getRight(), sampledView.getBottom()); - return mSamplingBounds; - } - - @Override - public boolean isSamplingEnabled() { - return mRegionSamplingEnabled; - } - }, mainExecutor, bgExecutor) - ); - mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { - regionSamplingHelper.setWindowVisible(true); - }); - } - - private void reset() { - mView.animateDoze(mIsDozing, false); - } - - private final BatteryController.BatteryStateChangeCallback mBatteryCallback = - new BatteryController.BatteryStateChangeCallback() { - @Override - public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { - if (mKeyguardShowing && !mIsCharging && charging) { - mView.animateCharge(mStatusBarStateController::isDozing); - } - mIsCharging = charging; - } - }; - - private final BroadcastReceiver mLocaleBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - updateLocale(); - } - }; - - private final StatusBarStateController.StateListener mStatusBarStateListener = - new StatusBarStateController.StateListener() { - @Override - public void onDozeAmountChanged(float linear, float eased) { - boolean noAnimation = (mDozeAmount == 0f && linear == 1f) - || (mDozeAmount == 1f && linear == 0f); - boolean isDozing = linear > mDozeAmount; - mDozeAmount = linear; - if (mIsDozing != isDozing) { - mIsDozing = isDozing; - mView.animateDoze(mIsDozing, !noAnimation); - } - } - }; - - private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = - new KeyguardUpdateMonitorCallback() { - @Override - public void onKeyguardVisibilityChanged(boolean showing) { - mKeyguardShowing = showing; - if (!mKeyguardShowing) { - // reset state (ie: after weight animations) - reset(); - } - } - - @Override - public void onTimeFormatChanged(String timeFormat) { - mView.refreshFormat(); - } - - @Override - public void onTimeZoneChanged(TimeZone timeZone) { - mView.onTimeZoneChanged(timeZone); - } - - @Override - public void onUserSwitchComplete(int userId) { - mView.refreshFormat(); - } - }; - - @Override - protected void onInit() { - mIsDozing = mStatusBarStateController.isDozing(); - } - - @Override - protected void onViewAttached() { - updateLocale(); - mBroadcastDispatcher.registerReceiver(mLocaleBroadcastReceiver, - new IntentFilter(Intent.ACTION_LOCALE_CHANGED)); - mDozeAmount = mStatusBarStateController.getDozeAmount(); - mIsDozing = mStatusBarStateController.isDozing() || mDozeAmount != 0; - mBatteryController.addCallback(mBatteryCallback); - mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); - - mStatusBarStateController.addCallback(mStatusBarStateListener); - - mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { - regionSamplingHelper.start(mSamplingBounds); - }); - - mView.onTimeZoneChanged(TimeZone.getDefault()); - initColors(); - mView.animateDoze(mIsDozing, false); - } - - @Override - protected void onViewDetached() { - mBroadcastDispatcher.unregisterReceiver(mLocaleBroadcastReceiver); - mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback); - mBatteryController.removeCallback(mBatteryCallback); - mStatusBarStateController.removeCallback(mStatusBarStateListener); - mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { - regionSamplingHelper.stop(); - }); - } - - /** Animate the clock appearance */ - public void animateAppear() { - if (!mIsDozing) mView.animateAppearOnLockscreen(); - } - - /** Animate the clock appearance when a foldable device goes from fully-open/half-open state to - * fully folded state and it goes to sleep (always on display screen) */ - public void animateFoldAppear() { - mView.animateFoldAppear(true); - } - - /** - * Updates the time for the view. - */ - public void refreshTime() { - mView.refreshTime(); - } - - /** - * Return locallly stored dozing state. - */ - @VisibleForTesting - public boolean isDozing() { - return mIsDozing; - } - - private void updateLocale() { - Locale currLocale = Locale.getDefault(); - if (!Objects.equals(currLocale, mLocale)) { - mLocale = currLocale; - NumberFormat nf = NumberFormat.getInstance(mLocale); - if (nf.format(FORMAT_NUMBER).equals(mBurmeseNumerals)) { - mView.setLineSpacingScale(mBurmeseLineSpacing); - } else { - mView.setLineSpacingScale(mDefaultLineSpacing); - } - mView.refreshFormat(); - } - } - - private void initColors() { - if (!mRegionSamplingEnabled) { - mLockScreenColor = Utils.getColorAttrDefaultColor(getContext(), - com.android.systemui.R.attr.wallpaperTextColorAccent); - } - mView.setColors(mDozingColor, mLockScreenColor); - mView.animateDoze(mIsDozing, false); - } - - /** - * Dump information for debugging - */ - public void dump(@NonNull PrintWriter pw) { - pw.println(this); - mView.dump(pw); - mRegionSamplingHelper.ifPresent((regionSamplingHelper) -> { - regionSamplingHelper.dump(pw); - }); - } -} diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt index 0075ddd73cd3..450784ea8f03 100644 --- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt +++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt @@ -16,19 +16,29 @@ package com.android.keyguard +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator import android.content.Context import android.content.res.ColorStateList import android.content.res.TypedArray import android.graphics.Color import android.util.AttributeSet +import android.view.View import com.android.settingslib.Utils +import com.android.systemui.animation.Interpolators /** Displays security messages for the keyguard bouncer. */ -class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : +open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : KeyguardMessageArea(context, attrs) { private val DEFAULT_COLOR = -1 private var mDefaultColorState: ColorStateList? = null private var mNextMessageColorState: ColorStateList? = ColorStateList.valueOf(DEFAULT_COLOR) + private val animatorSet = AnimatorSet() + private var textAboutToShow: CharSequence? = null + protected open val SHOW_DURATION_MILLIS = 150L + protected open val HIDE_DURATION_MILLIS = 200L override fun updateTextColor() { var colorState = mDefaultColorState @@ -58,4 +68,46 @@ class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) : mDefaultColorState = Utils.getColorAttr(context, android.R.attr.textColorPrimary) super.reloadColor() } + + override fun setMessage(msg: CharSequence?) { + if ((msg == textAboutToShow && msg != null) || msg == text) { + return + } + textAboutToShow = msg + + if (animatorSet.isRunning) { + animatorSet.cancel() + textAboutToShow = null + } + + val hideAnimator = + ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f).apply { + duration = HIDE_DURATION_MILLIS + interpolator = Interpolators.STANDARD_ACCELERATE + } + + hideAnimator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + super@BouncerKeyguardMessageArea.setMessage(msg) + } + } + ) + val showAnimator = + ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f).apply { + duration = SHOW_DURATION_MILLIS + interpolator = Interpolators.STANDARD_DECELERATE + } + + showAnimator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + textAboutToShow = null + } + } + ) + + animatorSet.playSequentially(hideAnimator, showAnimator) + animatorSet.start() + } } diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java index 907943a9203d..7971e84769a2 100644 --- a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java +++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java @@ -293,7 +293,7 @@ public class CarrierTextManager { } protected List<SubscriptionInfo> getSubscriptionInfo() { - return mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(false); + return mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(); } protected void updateCarrierText() { diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index b444f4c1110b..40a96b060bc0 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -23,12 +23,21 @@ import android.content.res.Resources import android.text.format.DateFormat import android.util.TypedValue import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags -import com.android.systemui.plugins.Clock -import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.flags.Flags.DOZING_MIGRATION_1 +import com.android.systemui.flags.Flags.REGION_SAMPLING +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.log.dagger.KeyguardClockLog +import com.android.systemui.plugins.ClockController +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.shared.regionsampling.RegionSamplingInstance import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -38,27 +47,36 @@ import java.util.Locale import java.util.TimeZone import java.util.concurrent.Executor import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch /** * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController]. */ open class ClockEventController @Inject constructor( - private val statusBarStateController: StatusBarStateController, - private val broadcastDispatcher: BroadcastDispatcher, - private val batteryController: BatteryController, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - private val configurationController: ConfigurationController, - @Main private val resources: Resources, - private val context: Context, - @Main private val mainExecutor: Executor, - @Background private val bgExecutor: Executor, - private val featureFlags: FeatureFlags + private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val broadcastDispatcher: BroadcastDispatcher, + private val batteryController: BatteryController, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val configurationController: ConfigurationController, + @Main private val resources: Resources, + private val context: Context, + @Main private val mainExecutor: Executor, + @Background private val bgExecutor: Executor, + @KeyguardClockLog private val logBuffer: LogBuffer, + private val featureFlags: FeatureFlags ) { - var clock: Clock? = null + var clock: ClockController? = null set(value) { field = value if (value != null) { + value.setLogBuffer(logBuffer) value.initialize(resources, dozeAmount, 0f) updateRegionSamplers(value) } @@ -69,47 +87,50 @@ open class ClockEventController @Inject constructor( private var isCharging = false private var dozeAmount = 0f - private var isKeyguardShowing = false - - private val regionSamplingEnabled = - featureFlags.isEnabled(com.android.systemui.flags.Flags.REGION_SAMPLING) - - private val updateFun = object : RegionSamplingInstance.UpdateColorCallback { - override fun updateColors() { - if (regionSamplingEnabled) { - smallClockIsDark = smallRegionSamplingInstance.currentRegionDarkness().isDark - largeClockIsDark = largeRegionSamplingInstance.currentRegionDarkness().isDark - } else { - val isLightTheme = TypedValue() - context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true) - smallClockIsDark = isLightTheme.data == 0 - largeClockIsDark = isLightTheme.data == 0 - } - clock?.events?.onColorPaletteChanged(resources, smallClockIsDark, largeClockIsDark) + private var isKeyguardVisible = false + private var isRegistered = false + private var disposableHandle: DisposableHandle? = null + private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING) + + private fun updateColors() { + if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) { + smallClockIsDark = smallRegionSampler!!.currentRegionDarkness().isDark + largeClockIsDark = largeRegionSampler!!.currentRegionDarkness().isDark + } else { + val isLightTheme = TypedValue() + context.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true) + smallClockIsDark = isLightTheme.data == 0 + largeClockIsDark = isLightTheme.data == 0 } + + clock?.smallClock?.events?.onRegionDarknessChanged(smallClockIsDark) + clock?.largeClock?.events?.onRegionDarknessChanged(largeClockIsDark) } - fun updateRegionSamplers(currentClock: Clock?) { - smallRegionSamplingInstance = createRegionSampler( - currentClock?.smallClock, + private fun updateRegionSamplers(currentClock: ClockController?) { + smallRegionSampler?.stopRegionSampler() + largeRegionSampler?.stopRegionSampler() + + smallRegionSampler = createRegionSampler( + currentClock?.smallClock?.view, mainExecutor, bgExecutor, regionSamplingEnabled, - updateFun + ::updateColors ) - largeRegionSamplingInstance = createRegionSampler( - currentClock?.largeClock, + largeRegionSampler = createRegionSampler( + currentClock?.largeClock?.view, mainExecutor, bgExecutor, regionSamplingEnabled, - updateFun + ::updateColors ) - smallRegionSamplingInstance.startRegionSampler() - largeRegionSamplingInstance.startRegionSampler() + smallRegionSampler!!.startRegionSampler() + largeRegionSampler!!.startRegionSampler() - updateFun.updateColors() + updateColors() } protected open fun createRegionSampler( @@ -117,25 +138,29 @@ open class ClockEventController @Inject constructor( mainExecutor: Executor?, bgExecutor: Executor?, regionSamplingEnabled: Boolean, - updateFun: RegionSamplingInstance.UpdateColorCallback + updateColors: () -> Unit ): RegionSamplingInstance { return RegionSamplingInstance( sampledView, mainExecutor, bgExecutor, regionSamplingEnabled, - updateFun) + object : RegionSamplingInstance.UpdateColorCallback { + override fun updateColors() { + updateColors() + } + }) } - lateinit var smallRegionSamplingInstance: RegionSamplingInstance - lateinit var largeRegionSamplingInstance: RegionSamplingInstance + var smallRegionSampler: RegionSamplingInstance? = null + var largeRegionSampler: RegionSamplingInstance? = null private var smallClockIsDark = true private var largeClockIsDark = true private val configListener = object : ConfigurationController.ConfigurationListener { override fun onThemeChanged() { - updateFun.updateColors() + clock?.events?.onColorPaletteChanged(resources) } override fun onDensityOrFontScaleChanged() { @@ -145,7 +170,7 @@ open class ClockEventController @Inject constructor( private val batteryCallback = object : BatteryStateChangeCallback { override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) { - if (isKeyguardShowing && !isCharging && charging) { + if (isKeyguardVisible && !isCharging && charging) { clock?.animations?.charge() } isCharging = charging @@ -158,19 +183,10 @@ open class ClockEventController @Inject constructor( } } - private val statusBarStateListener = object : StatusBarStateController.StateListener { - override fun onDozeAmountChanged(linear: Float, eased: Float) { - clock?.animations?.doze(linear) - - isDozing = linear > dozeAmount - dozeAmount = linear - } - } - private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { - override fun onKeyguardVisibilityChanged(showing: Boolean) { - isKeyguardShowing = showing - if (!isKeyguardShowing) { + override fun onKeyguardVisibilityChanged(visible: Boolean) { + isKeyguardVisible = visible + if (!isKeyguardVisible) { clock?.animations?.doze(if (isDozing) 1f else 0f) } } @@ -188,13 +204,11 @@ open class ClockEventController @Inject constructor( } } - init { - isDozing = statusBarStateController.isDozing - } - - fun registerListeners() { - dozeAmount = statusBarStateController.dozeAmount - isDozing = statusBarStateController.isDozing || dozeAmount != 0f + fun registerListeners(parent: View) { + if (isRegistered) { + return + } + isRegistered = true broadcastDispatcher.registerReceiver( localeBroadcastReceiver, @@ -203,19 +217,33 @@ open class ClockEventController @Inject constructor( configurationController.addCallback(configListener) batteryController.addCallback(batteryCallback) keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) - statusBarStateController.addCallback(statusBarStateListener) - smallRegionSamplingInstance.startRegionSampler() - largeRegionSamplingInstance.startRegionSampler() + smallRegionSampler?.startRegionSampler() + largeRegionSampler?.startRegionSampler() + disposableHandle = parent.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + listenForDozing(this) + if (featureFlags.isEnabled(DOZING_MIGRATION_1)) { + listenForDozeAmountTransition(this) + } else { + listenForDozeAmount(this) + } + } + } } fun unregisterListeners() { + if (!isRegistered) { + return + } + isRegistered = false + + disposableHandle?.dispose() broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver) configurationController.removeCallback(configListener) batteryController.removeCallback(batteryCallback) keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) - statusBarStateController.removeCallback(statusBarStateListener) - smallRegionSamplingInstance.stopRegionSampler() - largeRegionSamplingInstance.stopRegionSampler() + smallRegionSampler?.stopRegionSampler() + largeRegionSampler?.stopRegionSampler() } /** @@ -224,12 +252,42 @@ open class ClockEventController @Inject constructor( fun dump(pw: PrintWriter) { pw.println(this) clock?.dump(pw) - smallRegionSamplingInstance.dump(pw) - largeRegionSamplingInstance.dump(pw) + smallRegionSampler?.dump(pw) + largeRegionSampler?.dump(pw) } - companion object { - private val TAG = ClockEventController::class.simpleName - private const val FORMAT_NUMBER = 1234567890 + @VisibleForTesting + internal fun listenForDozeAmount(scope: CoroutineScope): Job { + return scope.launch { + keyguardInteractor.dozeAmount.collect { + dozeAmount = it + clock?.animations?.doze(dozeAmount) + } + } + } + + @VisibleForTesting + internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job { + return scope.launch { + keyguardTransitionInteractor.dozeAmountTransition.collect { + dozeAmount = it.value + clock?.animations?.doze(dozeAmount) + } + } + } + + @VisibleForTesting + internal fun listenForDozing(scope: CoroutineScope): Job { + return scope.launch { + combine ( + keyguardInteractor.dozeAmount, + keyguardInteractor.isDozing, + ) { localDozeAmount, localIsDozing -> + localDozeAmount > dozeAmount || localIsDozing + } + .collect { localIsDozing -> + isDozing = localIsDozing + } + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt index 692fe83ee2b8..e6a2bfa1af12 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt @@ -17,7 +17,6 @@ package com.android.keyguard import android.app.StatusBarManager.SESSION_KEYGUARD -import android.content.Context import android.hardware.biometrics.BiometricSourceType import com.android.internal.annotations.VisibleForTesting import com.android.internal.logging.UiEvent @@ -41,11 +40,10 @@ import javax.inject.Inject */ @SysUISingleton class KeyguardBiometricLockoutLogger @Inject constructor( - context: Context?, private val uiEventLogger: UiEventLogger, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val sessionTracker: SessionTracker -) : CoreStartable(context) { +) : CoreStartable { private var fingerprintLockedOut = false private var faceLockedOut = false private var encryptedOrLockdown = false @@ -169,4 +167,4 @@ class KeyguardBiometricLockoutLogger @Inject constructor( return strongAuthFlags and flagCheck != 0 } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index d7cd1d0dbc6f..8ebad6c0fdbf 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -17,7 +17,7 @@ import androidx.annotation.VisibleForTesting; import com.android.keyguard.dagger.KeyguardStatusViewScope; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; -import com.android.systemui.plugins.Clock; +import com.android.systemui.plugins.ClockController; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -94,7 +94,7 @@ public class KeyguardClockSwitch extends RelativeLayout { onDensityOrFontScaleChanged(); } - void setClock(Clock clock, int statusBarState) { + void setClock(ClockController clock, int statusBarState) { // Disconnect from existing plugin. mSmallClockFrame.removeAllViews(); mLargeClockFrame.removeAllViews(); @@ -105,11 +105,14 @@ public class KeyguardClockSwitch extends RelativeLayout { } // Attach small and big clock views to hierarchy. - mSmallClockFrame.addView(clock.getSmallClock()); - mLargeClockFrame.addView(clock.getLargeClock()); + Log.i(TAG, "Attached new clock views to switch"); + mSmallClockFrame.addView(clock.getSmallClock().getView()); + mLargeClockFrame.addView(clock.getLargeClock().getView()); } private void updateClockViews(boolean useLargeClock, boolean animate) { + Log.i(TAG, "updateClockViews; useLargeClock=" + useLargeClock + "; animate=" + animate + + "; mChildrenAreLaidOut=" + mChildrenAreLaidOut); if (mClockInAnim != null) mClockInAnim.cancel(); if (mClockOutAnim != null) mClockOutAnim.cancel(); if (mStatusAreaAnim != null) mStatusAreaAnim.cancel(); @@ -124,7 +127,7 @@ public class KeyguardClockSwitch extends RelativeLayout { if (useLargeClock) { out = mSmallClockFrame; in = mLargeClockFrame; - if (indexOfChild(in) == -1) addView(in); + if (indexOfChild(in) == -1) addView(in, 0); direction = -1; statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop() + mSmartspaceTopOffset; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 2165099b474e..d3cc7ed08a82 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -37,10 +37,9 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.plugins.Clock; +import com.android.systemui.plugins.ClockAnimations; +import com.android.systemui.plugins.ClockController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.ClockRegistry; import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; @@ -119,8 +118,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS SecureSettings secureSettings, @Main Executor uiExecutor, DumpManager dumpManager, - ClockEventController clockEventController, - FeatureFlags featureFlags) { + ClockEventController clockEventController) { super(keyguardClockSwitch); mStatusBarStateController = statusBarStateController; mClockRegistry = clockRegistry; @@ -133,7 +131,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mDumpManager = dumpManager; mClockEventController = clockEventController; - mClockRegistry.setEnabled(featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS)); mClockChangedListener = () -> { setClock(mClockRegistry.createCurrentClock()); }; @@ -164,7 +161,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS protected void onViewAttached() { mClockRegistry.registerClockChangeListener(mClockChangedListener); setClock(mClockRegistry.createCurrentClock()); - mClockEventController.registerListeners(); + mClockEventController.registerListeners(mView); mKeyguardClockTopMargin = mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); @@ -262,7 +259,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mCurrentClockSize = clockSize; - Clock clock = getClock(); + ClockController clock = getClock(); boolean appeared = mView.switchToClock(clockSize, animate); if (clock != null && animate && appeared && clockSize == LARGE) { clock.getAnimations().enter(); @@ -273,7 +270,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS * Animates the clock view between folded and unfolded states */ public void animateFoldToAod(float foldFraction) { - Clock clock = getClock(); + ClockController clock = getClock(); if (clock != null) { clock.getAnimations().fold(foldFraction); } @@ -286,7 +283,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS if (mSmartspaceController != null) { mSmartspaceController.requestSmartspaceUpdate(); } - Clock clock = getClock(); + ClockController clock = getClock(); if (clock != null) { clock.getEvents().onTimeTick(); } @@ -319,17 +316,17 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS * We can't directly getBottom() because clock changes positions in AOD for burn-in */ int getClockBottom(int statusBarHeaderHeight) { - Clock clock = getClock(); + ClockController clock = getClock(); if (clock == null) { return 0; } if (mLargeClockFrame.getVisibility() == View.VISIBLE) { int frameHeight = mLargeClockFrame.getHeight(); - int clockHeight = clock.getLargeClock().getHeight(); + int clockHeight = clock.getLargeClock().getView().getHeight(); return frameHeight / 2 + clockHeight / 2; } else { - int clockHeight = clock.getSmallClock().getHeight(); + int clockHeight = clock.getSmallClock().getView().getHeight(); return clockHeight + statusBarHeaderHeight + mKeyguardClockTopMargin; } } @@ -338,15 +335,15 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS * Get the height of the currently visible clock on the keyguard. */ int getClockHeight() { - Clock clock = getClock(); + ClockController clock = getClock(); if (clock == null) { return 0; } if (mLargeClockFrame.getVisibility() == View.VISIBLE) { - return clock.getLargeClock().getHeight(); + return clock.getLargeClock().getView().getHeight(); } else { - return clock.getSmallClock().getHeight(); + return clock.getSmallClock().getView().getHeight(); } } @@ -361,12 +358,12 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mNotificationIconAreaController.setupAodIcons(nic); } - private void setClock(Clock clock) { + private void setClock(ClockController clock) { mClockEventController.setClock(clock); mView.setClock(clock, mStatusBarStateController.getState()); } - private Clock getClock() { + private ClockController getClock() { return mClockEventController.getClock(); } @@ -398,10 +395,16 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("currentClockSizeLarge=" + (mCurrentClockSize == LARGE)); pw.println("mCanShowDoubleLineClock=" + mCanShowDoubleLineClock); - Clock clock = getClock(); + mView.dump(pw, args); + ClockController clock = getClock(); if (clock != null) { clock.dump(pw); } } + + /** Gets the animations for the current clock. */ + public ClockAnimations getClockAnimations() { + return getClock().getAnimations(); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index f26b9057dc7c..73229c321079 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -152,6 +152,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> } public void startAppearAnimation() { + mMessageAreaController.setMessage(getInitialMessageResId()); mView.startAppearAnimation(); } @@ -169,6 +170,11 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> return view.indexOfChild(mView); } + /** Determines the message to show in the bouncer when it first appears. */ + protected int getInitialMessageResId() { + return 0; + } + /** Factory for a {@link KeyguardInputViewController}. */ public static class Factory { private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java index c2802f7b6843..2bd3ca59b740 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java @@ -18,7 +18,6 @@ package com.android.keyguard; import android.content.res.ColorStateList; import android.content.res.Configuration; -import android.text.TextUtils; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -100,15 +99,6 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mView.setMessage(resId); } - /** - * Set Text if KeyguardMessageArea is empty. - */ - public void setMessageIfEmpty(int resId) { - if (TextUtils.isEmpty(mView.getText())) { - setMessage(resId); - } - } - public void setNextMessageColor(ColorStateList colorState) { mView.setNextMessageColor(colorState); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java index 453072bc42da..5d86ccd5409e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java @@ -176,6 +176,8 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { @Override public void startAppearAnimation() { + setAlpha(1f); + setTranslationY(0); if (mAppearAnimator.isRunning()) { mAppearAnimator.cancel(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java index 1e5c53de4446..2cc5ccdc3fa1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java @@ -24,7 +24,6 @@ import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT; -import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST; import android.animation.Animator; @@ -107,8 +106,6 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView { return R.string.kg_prompt_reason_timeout_password; case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: return R.string.kg_prompt_reason_timeout_password; - case PROMPT_REASON_TRUSTAGENT_EXPIRED: - return R.string.kg_prompt_reason_timeout_password; case PROMPT_REASON_NONE: return 0; default: diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java index 29e912fdab32..0025986c0e5c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java @@ -187,7 +187,7 @@ public class KeyguardPasswordViewController @Override void resetState() { mPasswordEntry.setTextOperationUser(UserHandle.of(KeyguardUpdateMonitor.getCurrentUser())); - mMessageAreaController.setMessage(""); + mMessageAreaController.setMessage(getInitialMessageResId()); final boolean wasDisabled = mPasswordEntry.isEnabled(); mView.setPasswordEntryEnabled(true); mView.setPasswordEntryInputEnabled(true); @@ -207,7 +207,6 @@ public class KeyguardPasswordViewController if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) { showInput(); } - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_password); } private void showInput() { @@ -324,4 +323,9 @@ public class KeyguardPasswordViewController //enabled input method subtype (The current IME should be LatinIME.) || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1; } + + @Override + protected int getInitialMessageResId() { + return R.string.keyguard_enter_your_password; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index 5b223242670c..1f0bd54f8e09 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -298,12 +298,6 @@ public class KeyguardPatternViewController } @Override - public void onResume(int reason) { - super.onResume(reason); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pattern); - } - - @Override public boolean needsInput() { return false; } @@ -330,9 +324,6 @@ public class KeyguardPatternViewController case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern); break; - case PROMPT_REASON_TRUSTAGENT_EXPIRED: - mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern); - break; case PROMPT_REASON_NONE: break; default: @@ -364,7 +355,7 @@ public class KeyguardPatternViewController } private void displayDefaultSecurityMessage() { - mMessageAreaController.setMessage(""); + mMessageAreaController.setMessage(getInitialMessageResId()); } private void handleAttemptLockout(long elapsedRealtimeDeadline) { @@ -395,4 +386,9 @@ public class KeyguardPatternViewController }.start(); } + + @Override + protected int getInitialMessageResId() { + return R.string.keyguard_enter_your_pattern; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java index 0a91150e6c39..c46e33d9fd53 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -22,7 +22,6 @@ import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT; -import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST; import android.animation.Animator; @@ -124,8 +123,6 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView return R.string.kg_prompt_reason_timeout_pin; case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: return R.string.kg_prompt_reason_timeout_pin; - case PROMPT_REASON_TRUSTAGENT_EXPIRED: - return R.string.kg_prompt_reason_timeout_pin; case PROMPT_REASON_NONE: return 0; default: diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 59a018ad51df..f7423ed12e68 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -127,7 +127,6 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB public void onResume(int reason) { super.onResume(reason); mPasswordEntry.requestFocus(); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index 89fcc47caf57..7876f071fdf5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -76,20 +76,13 @@ public class KeyguardPinViewController } @Override - void resetState() { - super.resetState(); - mMessageAreaController.setMessage(""); - } - - @Override - public void startAppearAnimation() { - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); - super.startAppearAnimation(); - } - - @Override public boolean startDisappearAnimation(Runnable finishRunnable) { return mView.startDisappearAnimation( mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable); } + + @Override + protected int getInitialMessageResId() { + return R.string.keyguard_enter_your_pin; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 2bdb1b894c4a..93ee151f26c5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -21,15 +21,23 @@ import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.systemBars; import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; +import static androidx.constraintlayout.widget.ConstraintSet.BOTTOM; +import static androidx.constraintlayout.widget.ConstraintSet.CHAIN_SPREAD; +import static androidx.constraintlayout.widget.ConstraintSet.END; +import static androidx.constraintlayout.widget.ConstraintSet.LEFT; +import static androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT; +import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; +import static androidx.constraintlayout.widget.ConstraintSet.RIGHT; +import static androidx.constraintlayout.widget.ConstraintSet.START; +import static androidx.constraintlayout.widget.ConstraintSet.TOP; +import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT; + import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY; import static java.lang.Integer.max; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; import android.app.Activity; import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; @@ -44,12 +52,12 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.os.UserManager; import android.provider.Settings; +import android.transition.TransitionManager; import android.util.AttributeSet; import android.util.Log; import android.util.MathUtils; import android.util.TypedValue; import android.view.GestureDetector; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -59,17 +67,15 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowInsetsAnimation; import android.view.WindowManager; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; -import android.widget.AdapterView; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.IntDef; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintSet; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringAnimation; @@ -92,9 +98,9 @@ import com.android.systemui.util.settings.GlobalSettings; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; -public class KeyguardSecurityContainer extends FrameLayout { +/** Determines how the bouncer is displayed to the user. */ +public class KeyguardSecurityContainer extends ConstraintLayout { static final int USER_TYPE_PRIMARY = 1; static final int USER_TYPE_WORK_PROFILE = 2; static final int USER_TYPE_SECONDARY_USER = 3; @@ -125,15 +131,6 @@ public class KeyguardSecurityContainer extends FrameLayout { // How much to scale the default slop by, to avoid accidental drags. private static final float SLOP_SCALE = 4f; - private static final long IME_DISAPPEAR_DURATION_MS = 125; - - // The duration of the animation to switch security sides. - private static final long SECURITY_SHIFT_ANIMATION_DURATION_MS = 500; - - // How much of the switch sides animation should be dedicated to fading the security out. The - // remainder will fade it back in again. - private static final float SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION = 0.2f; - @VisibleForTesting KeyguardSecurityViewFlipper mSecurityViewFlipper; private GlobalSettings mGlobalSettings; @@ -320,7 +317,8 @@ public class KeyguardSecurityContainer extends FrameLayout { } void initMode(@Mode int mode, GlobalSettings globalSettings, FalsingManager falsingManager, - UserSwitcherController userSwitcherController) { + UserSwitcherController userSwitcherController, + UserSwitcherViewMode.UserSwitcherCallback userSwitcherCallback) { if (mCurrentMode == mode) return; Log.i(TAG, "Switching mode from " + modeToString(mCurrentMode) + " to " + modeToString(mode)); @@ -332,7 +330,7 @@ public class KeyguardSecurityContainer extends FrameLayout { mViewMode = new OneHandedViewMode(); break; case MODE_USER_SWITCHER: - mViewMode = new UserSwitcherViewMode(); + mViewMode = new UserSwitcherViewMode(userSwitcherCallback); break; default: mViewMode = new DefaultViewMode(); @@ -538,7 +536,7 @@ public class KeyguardSecurityContainer extends FrameLayout { } /** - * Runs after a succsssful authentication only + * Runs after a successful authentication only */ public void startDisappearAnimation(SecurityMode securitySelection) { mDisappearAnimRunning = true; @@ -649,47 +647,8 @@ public class KeyguardSecurityContainer extends FrameLayout { } @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int maxHeight = 0; - int maxWidth = 0; - int childState = 0; - - for (int i = 0; i < getChildCount(); i++) { - final View view = getChildAt(i); - if (view.getVisibility() != GONE) { - int updatedWidthMeasureSpec = mViewMode.getChildWidthMeasureSpec(widthMeasureSpec); - final LayoutParams lp = (LayoutParams) view.getLayoutParams(); - - // When using EXACTLY spec, measure will use the layout width if > 0. Set before - // measuring the child - lp.width = MeasureSpec.getSize(updatedWidthMeasureSpec); - measureChildWithMargins(view, updatedWidthMeasureSpec, 0, - heightMeasureSpec, 0); - - maxWidth = Math.max(maxWidth, - view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); - maxHeight = Math.max(maxHeight, - view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); - childState = combineMeasuredStates(childState, view.getMeasuredState()); - } - } - - maxWidth += getPaddingLeft() + getPaddingRight(); - maxHeight += getPaddingTop() + getPaddingBottom(); - - // Check against our minimum height and width - maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); - maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); - - setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), - resolveSizeAndState(maxHeight, heightMeasureSpec, - childState << MEASURED_HEIGHT_STATE_SHIFT)); - } - - @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - int width = right - left; if (changed && mWidth != width) { mWidth = width; @@ -761,7 +720,7 @@ public class KeyguardSecurityContainer extends FrameLayout { * Enscapsulates the differences between bouncer modes for the container. */ interface ViewMode { - default void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings, + default void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings, @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, @NonNull UserSwitcherController userSwitcherController) {}; @@ -787,11 +746,6 @@ public class KeyguardSecurityContainer extends FrameLayout { /** On notif tap, this animation will run */ default void startAppearAnimation(SecurityMode securityMode) {}; - /** Override to alter the width measure spec to perhaps limit the ViewFlipper size */ - default int getChildWidthMeasureSpec(int parentWidthMeasureSpec) { - return parentWidthMeasureSpec; - } - /** Called when we are setting a new ViewMode */ default void onDestroy() {}; } @@ -801,13 +755,12 @@ public class KeyguardSecurityContainer extends FrameLayout { * screen devices */ abstract static class SidedSecurityMode implements ViewMode { - @Nullable private ValueAnimator mRunningSecurityShiftAnimator; private KeyguardSecurityViewFlipper mViewFlipper; - private ViewGroup mView; + private ConstraintLayout mView; private GlobalSettings mGlobalSettings; private int mDefaultSideSetting; - public void init(ViewGroup v, KeyguardSecurityViewFlipper viewFlipper, + public void init(ConstraintLayout v, KeyguardSecurityViewFlipper viewFlipper, GlobalSettings globalSettings, boolean leftAlignedByDefault) { mView = v; mViewFlipper = viewFlipper; @@ -850,127 +803,6 @@ public class KeyguardSecurityContainer extends FrameLayout { protected abstract void updateSecurityViewLocation(boolean leftAlign, boolean animate); - protected void translateSecurityViewLocation(boolean leftAlign, boolean animate) { - translateSecurityViewLocation(leftAlign, animate, i -> {}); - } - - /** - * Moves the inner security view to the correct location with animation. This is triggered - * when the user double taps on the side of the screen that is not currently occupied by - * the security view. - */ - protected void translateSecurityViewLocation(boolean leftAlign, boolean animate, - Consumer<Float> securityAlphaListener) { - if (mRunningSecurityShiftAnimator != null) { - mRunningSecurityShiftAnimator.cancel(); - mRunningSecurityShiftAnimator = null; - } - - int targetTranslation = leftAlign - ? 0 : mView.getMeasuredWidth() - mViewFlipper.getWidth(); - - if (animate) { - // This animation is a bit fun to implement. The bouncer needs to move, and fade - // in/out at the same time. The issue is, the bouncer should only move a short - // amount (120dp or so), but obviously needs to go from one side of the screen to - // the other. This needs a pretty custom animation. - // - // This works as follows. It uses a ValueAnimation to simply drive the animation - // progress. This animator is responsible for both the translation of the bouncer, - // and the current fade. It will fade the bouncer out while also moving it along the - // 120dp path. Once the bouncer is fully faded out though, it will "snap" the - // bouncer closer to its destination, then fade it back in again. The effect is that - // the bouncer will move from 0 -> X while fading out, then - // (destination - X) -> destination while fading back in again. - // TODO(b/208250221): Make this animation properly abortable. - Interpolator positionInterpolator = AnimationUtils.loadInterpolator( - mView.getContext(), android.R.interpolator.fast_out_extra_slow_in); - Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN; - Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; - - mRunningSecurityShiftAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); - mRunningSecurityShiftAnimator.setDuration(SECURITY_SHIFT_ANIMATION_DURATION_MS); - mRunningSecurityShiftAnimator.setInterpolator(Interpolators.LINEAR); - - int initialTranslation = (int) mViewFlipper.getTranslationX(); - int totalTranslation = (int) mView.getResources().getDimension( - R.dimen.security_shift_animation_translation); - - final boolean shouldRestoreLayerType = mViewFlipper.hasOverlappingRendering() - && mViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE; - if (shouldRestoreLayerType) { - mViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null); - } - - float initialAlpha = mViewFlipper.getAlpha(); - - mRunningSecurityShiftAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mRunningSecurityShiftAnimator = null; - } - }); - mRunningSecurityShiftAnimator.addUpdateListener(animation -> { - float switchPoint = SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION; - boolean isFadingOut = animation.getAnimatedFraction() < switchPoint; - - int currentTranslation = (int) (positionInterpolator.getInterpolation( - animation.getAnimatedFraction()) * totalTranslation); - int translationRemaining = totalTranslation - currentTranslation; - - // Flip the sign if we're going from right to left. - if (leftAlign) { - currentTranslation = -currentTranslation; - translationRemaining = -translationRemaining; - } - - float opacity; - if (isFadingOut) { - // The bouncer fades out over the first X%. - float fadeOutFraction = MathUtils.constrainedMap( - /* rangeMin= */1.0f, - /* rangeMax= */0.0f, - /* valueMin= */0.0f, - /* valueMax= */switchPoint, - animation.getAnimatedFraction()); - opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction); - - // When fading out, the alpha needs to start from the initial opacity of the - // view flipper, otherwise we get a weird bit of jank as it ramps back to - // 100%. - mViewFlipper.setAlpha(opacity * initialAlpha); - - // Animate away from the source. - mViewFlipper.setTranslationX(initialTranslation + currentTranslation); - } else { - // And in again over the remaining (100-X)%. - float fadeInFraction = MathUtils.constrainedMap( - /* rangeMin= */0.0f, - /* rangeMax= */1.0f, - /* valueMin= */switchPoint, - /* valueMax= */1.0f, - animation.getAnimatedFraction()); - - opacity = fadeInInterpolator.getInterpolation(fadeInFraction); - mViewFlipper.setAlpha(opacity); - - // Fading back in, animate towards the destination. - mViewFlipper.setTranslationX(targetTranslation - translationRemaining); - } - securityAlphaListener.accept(opacity); - - if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) { - mViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null); - } - }); - - mRunningSecurityShiftAnimator.start(); - } else { - mViewFlipper.setTranslationX(targetTranslation); - } - } - - boolean isLeftAligned() { return mGlobalSettings.getInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE, mDefaultSideSetting) @@ -989,11 +821,11 @@ public class KeyguardSecurityContainer extends FrameLayout { * Default bouncer is centered within the space */ static class DefaultViewMode implements ViewMode { - private ViewGroup mView; + private ConstraintLayout mView; private KeyguardSecurityViewFlipper mViewFlipper; @Override - public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings, + public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings, @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, @NonNull UserSwitcherController userSwitcherController) { @@ -1005,11 +837,14 @@ public class KeyguardSecurityContainer extends FrameLayout { } private void updateSecurityViewGroup() { - FrameLayout.LayoutParams lp = - (FrameLayout.LayoutParams) mViewFlipper.getLayoutParams(); - lp.gravity = Gravity.CENTER_HORIZONTAL; - mViewFlipper.setLayoutParams(lp); - mViewFlipper.setTranslationX(0); + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.connect(mViewFlipper.getId(), START, PARENT_ID, START); + constraintSet.connect(mViewFlipper.getId(), END, PARENT_ID, END); + constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM); + constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP); + constraintSet.constrainHeight(mViewFlipper.getId(), MATCH_CONSTRAINT); + constraintSet.constrainWidth(mViewFlipper.getId(), MATCH_CONSTRAINT); + constraintSet.applyTo(mView); } } @@ -1018,7 +853,7 @@ public class KeyguardSecurityContainer extends FrameLayout { * a user switcher, in both portrait and landscape modes. */ static class UserSwitcherViewMode extends SidedSecurityMode { - private ViewGroup mView; + private ConstraintLayout mView; private ViewGroup mUserSwitcherViewGroup; private KeyguardSecurityViewFlipper mViewFlipper; private TextView mUserSwitcher; @@ -1029,11 +864,14 @@ public class KeyguardSecurityContainer extends FrameLayout { private UserSwitcherController.UserSwitchCallback mUserSwitchCallback = this::setupUserSwitcher; - private float mAnimationLastAlpha = 1f; - private boolean mAnimationWaitsToShift = true; + private UserSwitcherCallback mUserSwitcherCallback; + + UserSwitcherViewMode(UserSwitcherCallback userSwitcherCallback) { + mUserSwitcherCallback = userSwitcherCallback; + } @Override - public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings, + public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings, @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, @NonNull UserSwitcherController userSwitcherController) { @@ -1063,6 +901,7 @@ public class KeyguardSecurityContainer extends FrameLayout { mPopup.dismiss(); mPopup = null; } + setupUserSwitcher(); } @Override @@ -1073,6 +912,11 @@ public class KeyguardSecurityContainer extends FrameLayout { android.R.attr.textColorPrimary)); header.setBackground(mView.getContext().getDrawable( R.drawable.bouncer_user_switcher_header_bg)); + Drawable keyDownDrawable = + ((LayerDrawable) header.getBackground().mutate()).findDrawableByLayerId( + R.id.user_switcher_key_down); + keyDownDrawable.setTintList(Utils.getColorAttr(mView.getContext(), + android.R.attr.textColorPrimary)); } } @@ -1096,7 +940,6 @@ public class KeyguardSecurityContainer extends FrameLayout { return; } - mView.setAlpha(1f); mUserSwitcherViewGroup.setAlpha(0f); ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mUserSwitcherViewGroup, View.ALPHA, 1f); @@ -1203,120 +1046,82 @@ public class KeyguardSecurityContainer extends FrameLayout { } }; - if (adapter.getCount() < 2) { - // The drop down arrow is at index 1 - ((LayerDrawable) mUserSwitcher.getBackground()).getDrawable(1).setAlpha(0); - anchor.setClickable(false); - return; - } else { - ((LayerDrawable) mUserSwitcher.getBackground()).getDrawable(1).setAlpha(255); - } - anchor.setOnClickListener((v) -> { if (mFalsingManager.isFalseTap(LOW_PENALTY)) return; mPopup = new KeyguardUserSwitcherPopupMenu(v.getContext(), mFalsingManager); mPopup.setAnchorView(anchor); mPopup.setAdapter(adapter); - mPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() { - public void onItemClick(AdapterView parent, View view, int pos, long id) { - if (mFalsingManager.isFalseTap(LOW_PENALTY)) return; - if (!view.isEnabled()) return; - - // Subtract one for the header - UserRecord user = adapter.getItem(pos - 1); - if (!user.isCurrent) { - adapter.onUserListItemClicked(user); - } - mPopup.dismiss(); - mPopup = null; - } - }); + mPopup.setOnItemClickListener((parent, view, pos, id) -> { + if (mFalsingManager.isFalseTap(LOW_PENALTY)) return; + if (!view.isEnabled()) return; + // Subtract one for the header + UserRecord user = adapter.getItem(pos - 1); + if (user.isManageUsers || user.isAddSupervisedUser) { + mUserSwitcherCallback.showUnlockToContinueMessage(); + } + if (!user.isCurrent) { + adapter.onUserListItemClicked(user); + } + mPopup.dismiss(); + mPopup = null; + }); mPopup.show(); }); } - /** - * Each view will get half the width. Yes, it would be easier to use something other than - * FrameLayout but it was too disruptive to downstream projects to change. - */ - @Override - public int getChildWidthMeasureSpec(int parentWidthMeasureSpec) { - return MeasureSpec.makeMeasureSpec( - MeasureSpec.getSize(parentWidthMeasureSpec) / 2, - MeasureSpec.EXACTLY); - } - @Override public void updateSecurityViewLocation() { updateSecurityViewLocation(isLeftAligned(), /* animate= */false); } public void updateSecurityViewLocation(boolean leftAlign, boolean animate) { - setYTranslation(); - setGravity(); - setXTranslation(leftAlign, animate); - } - - private void setXTranslation(boolean leftAlign, boolean animate) { - if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - mUserSwitcherViewGroup.setTranslationX(0); - mViewFlipper.setTranslationX(0); - } else { - int switcherTargetTranslation = leftAlign - ? mView.getMeasuredWidth() - mViewFlipper.getWidth() : 0; - if (animate) { - mAnimationWaitsToShift = true; - mAnimationLastAlpha = 1f; - translateSecurityViewLocation(leftAlign, animate, securityAlpha -> { - // During the animation security view fades out - alpha goes from 1 to - // (almost) 0 - and then fades in - alpha grows back to 1. - // If new alpha is bigger than previous one it means we're at inflection - // point and alpha is zero or almost zero. That's when we want to do - // translation of user switcher, so that it's not visible to the user. - boolean fullyFadeOut = securityAlpha == 0.0f - || securityAlpha > mAnimationLastAlpha; - if (fullyFadeOut && mAnimationWaitsToShift) { - mUserSwitcherViewGroup.setTranslationX(switcherTargetTranslation); - mAnimationWaitsToShift = false; - } - mUserSwitcherViewGroup.setAlpha(securityAlpha); - mAnimationLastAlpha = securityAlpha; - }); - } else { - translateSecurityViewLocation(leftAlign, animate); - mUserSwitcherViewGroup.setTranslationX(switcherTargetTranslation); - } - } - - } - - private void setGravity() { - if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - updateViewGravity(mUserSwitcherViewGroup, Gravity.CENTER_HORIZONTAL); - updateViewGravity(mViewFlipper, Gravity.CENTER_HORIZONTAL); - } else { - // horizontal gravity is the same because we translate these views anyway - updateViewGravity(mViewFlipper, Gravity.LEFT | Gravity.BOTTOM); - updateViewGravity(mUserSwitcherViewGroup, Gravity.LEFT | Gravity.CENTER_VERTICAL); + if (animate) { + TransitionManager.beginDelayedTransition(mView, + new KeyguardSecurityViewTransition()); } - } - - private void setYTranslation() { int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans); if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - mUserSwitcherViewGroup.setTranslationY(yTrans); + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.connect(mUserSwitcherViewGroup.getId(), TOP, PARENT_ID, TOP, yTrans); + constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP); + constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM); + constraintSet.centerHorizontally(mViewFlipper.getId(), PARENT_ID); + constraintSet.centerHorizontally(mUserSwitcherViewGroup.getId(), PARENT_ID); + constraintSet.setVerticalChainStyle(mViewFlipper.getId(), CHAIN_SPREAD); + constraintSet.setVerticalChainStyle(mUserSwitcherViewGroup.getId(), CHAIN_SPREAD); + constraintSet.constrainHeight(mUserSwitcherViewGroup.getId(), WRAP_CONTENT); + constraintSet.constrainWidth(mUserSwitcherViewGroup.getId(), WRAP_CONTENT); + constraintSet.constrainHeight(mViewFlipper.getId(), MATCH_CONSTRAINT); + constraintSet.applyTo(mView); } else { - // Attempt to reposition a bit higher to make up for this frame being a bit lower - // on the device - mUserSwitcherViewGroup.setTranslationY(-yTrans); - mViewFlipper.setTranslationY(0); + int leftElement = leftAlign ? mViewFlipper.getId() : mUserSwitcherViewGroup.getId(); + int rightElement = + leftAlign ? mUserSwitcherViewGroup.getId() : mViewFlipper.getId(); + + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.connect(leftElement, LEFT, PARENT_ID, LEFT); + constraintSet.connect(leftElement, RIGHT, rightElement, LEFT); + constraintSet.connect(rightElement, LEFT, leftElement, RIGHT); + constraintSet.connect(rightElement, RIGHT, PARENT_ID, RIGHT); + constraintSet.connect(mUserSwitcherViewGroup.getId(), TOP, PARENT_ID, TOP); + constraintSet.connect(mUserSwitcherViewGroup.getId(), BOTTOM, PARENT_ID, BOTTOM, + yTrans); + constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP); + constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM); + constraintSet.setHorizontalChainStyle(mUserSwitcherViewGroup.getId(), CHAIN_SPREAD); + constraintSet.setHorizontalChainStyle(mViewFlipper.getId(), CHAIN_SPREAD); + constraintSet.constrainHeight(mUserSwitcherViewGroup.getId(), + MATCH_CONSTRAINT); + constraintSet.constrainWidth(mUserSwitcherViewGroup.getId(), + MATCH_CONSTRAINT); + constraintSet.constrainWidth(mViewFlipper.getId(), MATCH_CONSTRAINT); + constraintSet.constrainHeight(mViewFlipper.getId(), MATCH_CONSTRAINT); + constraintSet.applyTo(mView); } } - private void updateViewGravity(View v, int gravity) { - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); - lp.gravity = gravity; - v.setLayoutParams(lp); + interface UserSwitcherCallback { + void showUnlockToContinueMessage(); } } @@ -1325,11 +1130,11 @@ public class KeyguardSecurityContainer extends FrameLayout { * between alternate sides of the display. */ static class OneHandedViewMode extends SidedSecurityMode { - private ViewGroup mView; + private ConstraintLayout mView; private KeyguardSecurityViewFlipper mViewFlipper; @Override - public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings, + public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings, @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, @NonNull UserSwitcherController userSwitcherController) { @@ -1337,28 +1142,10 @@ public class KeyguardSecurityContainer extends FrameLayout { mView = v; mViewFlipper = viewFlipper; - updateSecurityViewGravity(); updateSecurityViewLocation(isLeftAligned(), /* animate= */false); } /** - * One-handed mode contains the child to half of the available space. - */ - @Override - public int getChildWidthMeasureSpec(int parentWidthMeasureSpec) { - return MeasureSpec.makeMeasureSpec( - MeasureSpec.getSize(parentWidthMeasureSpec) / 2, - MeasureSpec.EXACTLY); - } - - private void updateSecurityViewGravity() { - FrameLayout.LayoutParams lp = - (FrameLayout.LayoutParams) mViewFlipper.getLayoutParams(); - lp.gravity = Gravity.LEFT | Gravity.BOTTOM; - mViewFlipper.setLayoutParams(lp); - } - - /** * Moves the bouncer to align with a tap (most likely in the shade), so the bouncer * appears on the same side as a touch. */ @@ -1375,7 +1162,20 @@ public class KeyguardSecurityContainer extends FrameLayout { } protected void updateSecurityViewLocation(boolean leftAlign, boolean animate) { - translateSecurityViewLocation(leftAlign, animate); + if (animate) { + TransitionManager.beginDelayedTransition(mView, + new KeyguardSecurityViewTransition()); + } + ConstraintSet constraintSet = new ConstraintSet(); + if (leftAlign) { + constraintSet.connect(mViewFlipper.getId(), LEFT, PARENT_ID, LEFT); + } else { + constraintSet.connect(mViewFlipper.getId(), RIGHT, PARENT_ID, RIGHT); + } + constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP); + constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM); + constraintSet.constrainPercentWidth(mViewFlipper.getId(), 0.5f); + constraintSet.applyTo(mView); } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 57058b76ce8f..81305f90e2b8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -219,13 +219,16 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard }; - private SwipeListener mSwipeListener = new SwipeListener() { + private final SwipeListener mSwipeListener = new SwipeListener() { @Override public void onSwipeUp() { if (!mUpdateMonitor.isFaceDetectionRunning()) { - mUpdateMonitor.requestFaceAuth(true, FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); + boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); mKeyguardSecurityCallback.userActivity(); - showMessage(null, null); + if (didFaceAuthRun) { + showMessage(null, null); + } } if (mUpdateMonitor.isFaceEnrolled()) { mUpdateMonitor.requestActiveUnlock( @@ -234,7 +237,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } } }; - private ConfigurationController.ConfigurationListener mConfigurationListener = + private final ConfigurationController.ConfigurationListener mConfigurationListener = new ConfigurationController.ConfigurationListener() { @Override public void onThemeChanged() { @@ -427,6 +430,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard public void startAppearAnimation() { if (mCurrentSecurityMode != SecurityMode.None) { + mView.setAlpha(1f); mView.startAppearAnimation(mCurrentSecurityMode); getCurrentSecurityController().startAppearAnimation(); } @@ -619,7 +623,9 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mode = KeyguardSecurityContainer.MODE_ONE_HANDED; } - mView.initMode(mode, mGlobalSettings, mFalsingManager, mUserSwitcherController); + mView.initMode(mode, mGlobalSettings, mFalsingManager, mUserSwitcherController, + () -> showMessage(getContext().getString(R.string.keyguard_unlock_to_continue), + null)); } public void reportFailedUnlockAttempt(int userId, int timeoutMs) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java index 9d0a8acf02b4..ac00e9453c97 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java @@ -61,12 +61,6 @@ public interface KeyguardSecurityView { int PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT = 7; /** - * Some auth is required because the trustagent expired either from timeout or manually by - * the user - */ - int PROMPT_REASON_TRUSTAGENT_EXPIRED = 8; - - /** * Reset the view and prepare to take input. This should do things like clearing the * password or pattern and clear error messages. */ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt new file mode 100644 index 000000000000..c9128e5881cc --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewTransition.kt @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.graphics.Rect +import android.transition.Transition +import android.transition.TransitionValues +import android.util.MathUtils +import android.view.View +import android.view.ViewGroup +import android.view.animation.AnimationUtils +import com.android.internal.R.interpolator.fast_out_extra_slow_in +import com.android.systemui.R +import com.android.systemui.animation.Interpolators + +/** Animates constraint layout changes for the security view. */ +class KeyguardSecurityViewTransition : Transition() { + + companion object { + const val PROP_BOUNDS = "securityViewLocation:bounds" + + // The duration of the animation to switch security sides. + const val SECURITY_SHIFT_ANIMATION_DURATION_MS = 500L + + // How much of the switch sides animation should be dedicated to fading the security out. + // The remainder will fade it back in again. + const val SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION = 0.2f + } + + private fun captureValues(values: TransitionValues) { + val boundsRect = Rect() + boundsRect.left = values.view.left + boundsRect.top = values.view.top + boundsRect.right = values.view.right + boundsRect.bottom = values.view.bottom + values.values[PROP_BOUNDS] = boundsRect + } + + override fun getTransitionProperties(): Array<String>? { + return arrayOf(PROP_BOUNDS) + } + + override fun captureEndValues(transitionValues: TransitionValues?) { + transitionValues?.let { captureValues(it) } + } + + override fun captureStartValues(transitionValues: TransitionValues?) { + transitionValues?.let { captureValues(it) } + } + + override fun createAnimator( + sceneRoot: ViewGroup?, + startValues: TransitionValues?, + endValues: TransitionValues? + ): Animator? { + if (sceneRoot == null || startValues == null || endValues == null) { + return null + } + + // This animation is a bit fun to implement. The bouncer needs to move, and fade + // in/out at the same time. The issue is, the bouncer should only move a short + // amount (120dp or so), but obviously needs to go from one side of the screen to + // the other. This needs a pretty custom animation. + // + // This works as follows. It uses a ValueAnimation to simply drive the animation + // progress. This animator is responsible for both the translation of the bouncer, + // and the current fade. It will fade the bouncer out while also moving it along the + // 120dp path. Once the bouncer is fully faded out though, it will "snap" the + // bouncer closer to its destination, then fade it back in again. The effect is that + // the bouncer will move from 0 -> X while fading out, then + // (destination - X) -> destination while fading back in again. + // TODO(b/208250221): Make this animation properly abortable. + val positionInterpolator = + AnimationUtils.loadInterpolator(sceneRoot.context, fast_out_extra_slow_in) + val fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN + val fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN + var runningSecurityShiftAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) + runningSecurityShiftAnimator.duration = SECURITY_SHIFT_ANIMATION_DURATION_MS + runningSecurityShiftAnimator.interpolator = Interpolators.LINEAR + val startRect = startValues.values[PROP_BOUNDS] as Rect + val endRect = endValues.values[PROP_BOUNDS] as Rect + val v = startValues.view + val totalTranslation: Int = + sceneRoot.resources.getDimension(R.dimen.security_shift_animation_translation).toInt() + val shouldRestoreLayerType = + (v.hasOverlappingRendering() && v.layerType != View.LAYER_TYPE_HARDWARE) + if (shouldRestoreLayerType) { + v.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */ null) + } + val initialAlpha: Float = v.alpha + runningSecurityShiftAnimator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + runningSecurityShiftAnimator = null + if (shouldRestoreLayerType) { + v.setLayerType(View.LAYER_TYPE_NONE, /* paint= */ null) + } + } + } + ) + + runningSecurityShiftAnimator.addUpdateListener { animation: ValueAnimator -> + val switchPoint = SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION + val isFadingOut = animation.animatedFraction < switchPoint + val opacity: Float + var currentTranslation = + (positionInterpolator.getInterpolation(animation.animatedFraction) * + totalTranslation) + .toInt() + var translationRemaining = totalTranslation - currentTranslation + val leftAlign = endRect.left < startRect.left + if (leftAlign) { + currentTranslation = -currentTranslation + translationRemaining = -translationRemaining + } + + if (isFadingOut) { + // The bouncer fades out over the first X%. + val fadeOutFraction = + MathUtils.constrainedMap( + /* rangeMin= */ 1.0f, + /* rangeMax= */ 0.0f, + /* valueMin= */ 0.0f, + /* valueMax= */ switchPoint, + animation.animatedFraction + ) + opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction) + + // When fading out, the alpha needs to start from the initial opacity of the + // view flipper, otherwise we get a weird bit of jank as it ramps back to + // 100%. + v.alpha = opacity * initialAlpha + if (v is KeyguardSecurityViewFlipper) { + v.setLeftTopRightBottom( + startRect.left + currentTranslation, + startRect.top, + startRect.right + currentTranslation, + startRect.bottom + ) + } else { + v.setLeftTopRightBottom( + startRect.left, + startRect.top, + startRect.right, + startRect.bottom + ) + } + } else { + // And in again over the remaining (100-X)%. + val fadeInFraction = + MathUtils.constrainedMap( + /* rangeMin= */ 0.0f, + /* rangeMax= */ 1.0f, + /* valueMin= */ switchPoint, + /* valueMax= */ 1.0f, + animation.animatedFraction + ) + opacity = fadeInInterpolator.getInterpolation(fadeInFraction) + v.alpha = opacity + + // Fading back in, animate towards the destination. + if (v is KeyguardSecurityViewFlipper) { + v.setLeftTopRightBottom( + endRect.left - translationRemaining, + endRect.top, + endRect.right - translationRemaining, + endRect.bottom + ) + } else { + v.setLeftTopRightBottom( + endRect.left, + endRect.top, + endRect.right, + endRect.bottom + ) + } + } + } + runningSecurityShiftAnimator.start() + return runningSecurityShiftAnimator + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index c715a4eaef2b..784974778af5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -20,6 +20,7 @@ import android.graphics.Rect; import android.util.Slog; import com.android.keyguard.KeyguardClockSwitch.ClockSize; +import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.stack.AnimationProperties; @@ -212,9 +213,9 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV } @Override - public void onKeyguardVisibilityChanged(boolean showing) { - if (showing) { - if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing); + public void onKeyguardVisibilityChanged(boolean visible) { + if (visible) { + if (DEBUG) Slog.v(TAG, "refresh statusview visible:true"); refreshTime(); } } @@ -232,4 +233,9 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV mView.setClipBounds(null); } } + + /** Gets the animations for the current clock. */ + public ClockAnimations getClockAnimations() { + return mKeyguardClockSwitchController.getClockAnimations(); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt index 7d6f377d5287..f974e27a99ec 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt @@ -20,8 +20,8 @@ import android.content.Context import android.view.ViewGroup import com.android.systemui.R import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator -import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.LEFT -import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.RIGHT +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate import com.android.systemui.unfold.SysUIUnfoldScope import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider @@ -49,13 +49,14 @@ constructor( UnfoldConstantTranslateAnimator( viewsIdToTranslate = setOf( - ViewIdToTranslate(R.id.keyguard_status_area, LEFT, filterNever), - ViewIdToTranslate(R.id.lockscreen_clock_view_large, LEFT, filterSplitShadeOnly), - ViewIdToTranslate(R.id.lockscreen_clock_view, LEFT, filterNever), + ViewIdToTranslate(R.id.keyguard_status_area, START, filterNever), ViewIdToTranslate( - R.id.notification_stack_scroller, RIGHT, filterSplitShadeOnly), - ViewIdToTranslate(R.id.start_button, LEFT, filterNever), - ViewIdToTranslate(R.id.end_button, RIGHT, filterNever)), + R.id.lockscreen_clock_view_large, START, filterSplitShadeOnly), + ViewIdToTranslate(R.id.lockscreen_clock_view, START, filterNever), + ViewIdToTranslate( + R.id.notification_stack_scroller, END, filterSplitShadeOnly), + ViewIdToTranslate(R.id.start_button, START, filterNever), + ViewIdToTranslate(R.id.end_button, END, filterNever)), progressProvider = unfoldProgressProvider) } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 6eef3b33cf8f..cb1330dbd53d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -69,8 +69,7 @@ import static com.android.systemui.DejankUtils.whitelistIpcs; import android.annotation.AnyThread; import android.annotation.MainThread; -import android.annotation.NonNull; -import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.ActivityTaskManager.RootTaskInfo; @@ -113,7 +112,6 @@ import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import android.service.dreams.DreamService; import android.service.dreams.IDreamManager; import android.telephony.CarrierConfigManager; import android.telephony.ServiceState; @@ -126,6 +124,9 @@ import android.text.TextUtils; import android.util.SparseArray; import android.util.SparseBooleanArray; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; @@ -281,9 +282,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final KeyguardUpdateMonitorLogger mLogger; private final boolean mIsPrimaryUser; private final AuthController mAuthController; - private final StatusBarStateController mStatusBarStateController; private final UiEventLogger mUiEventLogger; private final Set<Integer> mFaceAcquiredInfoIgnoreList; + private final PackageManager mPackageManager; private int mStatusBarState; private final StatusBarStateController.StateListener mStatusBarStateControllerListener = new StatusBarStateController.StateListener() { @@ -304,10 +305,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab }; HashMap<Integer, SimData> mSimDatas = new HashMap<>(); - HashMap<Integer, ServiceState> mServiceStates = new HashMap<Integer, ServiceState>(); + HashMap<Integer, ServiceState> mServiceStates = new HashMap<>(); private int mPhoneState; - private boolean mKeyguardIsVisible; + private boolean mKeyguardShowing; + private boolean mKeyguardOccluded; private boolean mCredentialAttempted; private boolean mKeyguardGoingAway; private boolean mGoingToSleep; @@ -317,7 +319,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private boolean mAuthInterruptActive; private boolean mNeedsSlowUnlockTransition; private boolean mAssistantVisible; - private boolean mKeyguardOccluded; private boolean mOccludingAppRequestingFp; private boolean mOccludingAppRequestingFace; private boolean mSecureCameraLaunched; @@ -336,35 +337,42 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>> mCallbacks = Lists.newArrayList(); private ContentObserver mDeviceProvisionedObserver; - private ContentObserver mTimeFormatChangeObserver; + private final ContentObserver mTimeFormatChangeObserver; private boolean mSwitchingUser; private boolean mDeviceInteractive; - private SubscriptionManager mSubscriptionManager; + private final SubscriptionManager mSubscriptionManager; private final TelephonyListenerManager mTelephonyListenerManager; - private List<SubscriptionInfo> mSubscriptionInfo; - private TrustManager mTrustManager; - private UserManager mUserManager; - private KeyguardBypassController mKeyguardBypassController; - private int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; - private int mFaceRunningState = BIOMETRIC_STATE_STOPPED; - private LockPatternUtils mLockPatternUtils; - private final IDreamManager mDreamManager; - private boolean mIsDreaming; + private final TrustManager mTrustManager; + private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; private final BroadcastDispatcher mBroadcastDispatcher; private final InteractionJankMonitor mInteractionJankMonitor; private final LatencyTracker mLatencyTracker; - private boolean mLogoutEnabled; - private boolean mIsFaceEnrolled; - private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + private final StatusBarStateController mStatusBarStateController; private final Executor mBackgroundExecutor; - private SensorPrivacyManager mSensorPrivacyManager; + private final SensorPrivacyManager mSensorPrivacyManager; private final ActiveUnlockConfig mActiveUnlockConfig; private final PowerManager mPowerManager; + private final IDreamManager mDreamManager; + private final TelephonyManager mTelephonyManager; + @Nullable + private final FingerprintManager mFpm; + @Nullable + private final FaceManager mFaceManager; + private final LockPatternUtils mLockPatternUtils; private final boolean mWakeOnFingerprintAcquiredStart; + private KeyguardBypassController mKeyguardBypassController; + private List<SubscriptionInfo> mSubscriptionInfo; + private int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; + private int mFaceRunningState = BIOMETRIC_STATE_STOPPED; + private boolean mIsDreaming; + private boolean mLogoutEnabled; + private boolean mIsFaceEnrolled; + private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + /** * Short delay before restarting fingerprint authentication after a successful try. This should * be slightly longer than the time between onFingerprintAuthenticated and @@ -390,12 +398,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } private final Handler mHandler; - private SparseBooleanArray mBiometricEnabledForUser = new SparseBooleanArray(); - private BiometricManager mBiometricManager; - private IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback = + private final IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback = new IBiometricEnabledOnKeyguardCallback.Stub() { @Override - public void onChanged(boolean enabled, int userId) throws RemoteException { + public void onChanged(boolean enabled, int userId) { mHandler.post(() -> { mBiometricEnabledForUser.put(userId, enabled); updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, @@ -414,7 +420,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } }; - private OnSubscriptionsChangedListener mSubscriptionListener = + private final OnSubscriptionsChangedListener mSubscriptionListener = new OnSubscriptionsChangedListener() { @Override public void onSubscriptionsChanged() { @@ -433,11 +439,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } - private SparseBooleanArray mUserIsUnlocked = new SparseBooleanArray(); - private SparseBooleanArray mUserHasTrust = new SparseBooleanArray(); - private SparseBooleanArray mUserTrustIsManaged = new SparseBooleanArray(); - private SparseBooleanArray mUserTrustIsUsuallyManaged = new SparseBooleanArray(); - private Map<Integer, Intent> mSecondaryLockscreenRequirement = new HashMap<Integer, Intent>(); + private final SparseBooleanArray mUserIsUnlocked = new SparseBooleanArray(); + private final SparseBooleanArray mUserHasTrust = new SparseBooleanArray(); + private final SparseBooleanArray mUserTrustIsManaged = new SparseBooleanArray(); + private final SparseBooleanArray mUserTrustIsUsuallyManaged = new SparseBooleanArray(); + private final SparseBooleanArray mBiometricEnabledForUser = new SparseBooleanArray(); + private final Map<Integer, Intent> mSecondaryLockscreenRequirement = new HashMap<>(); @VisibleForTesting SparseArray<BiometricAuthenticated> mUserFingerprintAuthenticated = new SparseArray<>(); @@ -598,7 +605,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } if (sil == null) { // getCompleteActiveSubscriptionInfoList was null callers expect an empty list. - mSubscriptionInfo = new ArrayList<SubscriptionInfo>(); + mSubscriptionInfo = new ArrayList<>(); } else { mSubscriptionInfo = sil; } @@ -613,7 +620,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * of them based on carrier config. e.g. In this case we should only show one carrier name * on the status bar and quick settings. */ - public List<SubscriptionInfo> getFilteredSubscriptionInfo(boolean forceReload) { + public List<SubscriptionInfo> getFilteredSubscriptionInfo() { List<SubscriptionInfo> subscriptions = getSubscriptionInfo(false); if (subscriptions.size() == 2) { SubscriptionInfo info1 = subscriptions.get(0); @@ -674,14 +681,42 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** - * Updates KeyguardUpdateMonitor's internal state to know if keyguard is occluded + * Updates KeyguardUpdateMonitor's internal state to know if keyguard is showing and if + * its occluded. The keyguard is considered visible if its showing and NOT occluded. */ - public void setKeyguardOccluded(boolean occluded) { + public void setKeyguardShowing(boolean showing, boolean occluded) { + final boolean occlusionChanged = mKeyguardOccluded != occluded; + final boolean showingChanged = mKeyguardShowing != showing; + if (!occlusionChanged && !showingChanged) { + return; + } + + final boolean wasKeyguardVisible = isKeyguardVisible(); + mKeyguardShowing = showing; mKeyguardOccluded = occluded; - updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, - FACE_AUTH_UPDATED_KEYGUARD_OCCLUSION_CHANGED); - } + final boolean isKeyguardVisible = isKeyguardVisible(); + mLogger.logKeyguardShowingChanged(showing, occluded, isKeyguardVisible); + if (isKeyguardVisible != wasKeyguardVisible) { + if (isKeyguardVisible) { + mSecureCameraLaunched = false; + } + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onKeyguardVisibilityChanged(isKeyguardVisible); + } + } + } + + if (occlusionChanged) { + updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, + FACE_AUTH_UPDATED_KEYGUARD_OCCLUSION_CHANGED); + } else if (showingChanged) { + updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, + FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED); + } + } /** * Request to listen for face authentication when an app is occluding keyguard. @@ -733,7 +768,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * If the device is dreaming, awakens the device */ public void awakenFromDream() { - if (mIsDreaming && mDreamManager != null) { + if (mIsDreaming) { try { mDreamManager.awaken(); } catch (RemoteException e) { @@ -778,12 +813,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } private void reportSuccessfulBiometricUnlock(boolean isStrongBiometric, int userId) { - mBackgroundExecutor.execute(new Runnable() { - @Override - public void run() { - mLockPatternUtils.reportSuccessfulBiometricUnlock(isStrongBiometric, userId); - } - }); + mBackgroundExecutor.execute( + () -> mLockPatternUtils.reportSuccessfulBiometricUnlock(isStrongBiometric, userId)); } private void handleFingerprintAuthFailed() { @@ -866,7 +897,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } - private Runnable mRetryFingerprintAuthentication = new Runnable() { + private final Runnable mRetryFingerprintAuthentication = new Runnable() { + @SuppressLint("MissingPermission") @Override public void run() { mLogger.logRetryAfterFpHwUnavailable(mHardwareFingerprintUnavailableRetryCount); @@ -910,7 +942,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab boolean lockedOutStateChanged = false; if (msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT) { - lockedOutStateChanged |= !mFingerprintLockedOutPermanent; + lockedOutStateChanged = !mFingerprintLockedOutPermanent; mFingerprintLockedOutPermanent = true; mLogger.d("Fingerprint locked out - requiring strong auth"); mLockPatternUtils.requireStrongAuth( @@ -955,9 +987,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // that the events will arrive in a particular order. Add a delay here in case // an unlock is in progress. In this is a normal unlock the extra delay won't // be noticeable. - mHandler.postDelayed(() -> { - updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); - }, getBiometricLockoutDelay()); + mHandler.postDelayed( + () -> updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE), + getBiometricLockoutDelay()); } else { updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } @@ -1091,7 +1123,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } - private Runnable mRetryFaceAuthentication = new Runnable() { + private final Runnable mRetryFaceAuthentication = new Runnable() { @Override public void run() { mLogger.logRetryingAfterFaceHwUnavailable(mHardwareFaceUnavailableRetryCount); @@ -1117,12 +1149,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // Error is always the end of authentication lifecycle mFaceCancelSignal = null; - boolean cameraPrivacyEnabled = false; - if (mSensorPrivacyManager != null) { - cameraPrivacyEnabled = mSensorPrivacyManager - .isSensorPrivacyEnabled(SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, - SensorPrivacyManager.Sensors.CAMERA); - } + boolean cameraPrivacyEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled( + SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, SensorPrivacyManager.Sensors.CAMERA); if (msgId == FaceManager.FACE_ERROR_CANCELED && mFaceRunningState == BIOMETRIC_STATE_CANCELLING_RESTARTING) { @@ -1173,10 +1201,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mFaceLockedOutPermanent = (mode == BIOMETRIC_LOCKOUT_PERMANENT); final boolean changed = (mFaceLockedOutPermanent != wasLockoutPermanent); - mHandler.postDelayed(() -> { - updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, - FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET); - }, getBiometricLockoutDelay()); + mHandler.postDelayed(() -> updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, + FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET), getBiometricLockoutDelay()); if (changed) { notifyLockedOutStateChanged(BiometricSourceType.FACE); @@ -1215,28 +1241,24 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return mFaceRunningState == BIOMETRIC_STATE_RUNNING; } - private boolean isTrustDisabled(int userId) { + private boolean isTrustDisabled() { // Don't allow trust agent if device is secured with a SIM PIN. This is here // mainly because there's no other way to prompt the user to enter their SIM PIN // once they get past the keyguard screen. - final boolean disabledBySimPin = isSimPinSecure(); - return disabledBySimPin; + return isSimPinSecure(); // Disabled by SIM PIN } private boolean isFingerprintDisabled(int userId) { - final DevicePolicyManager dpm = - (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); - return dpm != null && (dpm.getKeyguardDisabledFeatures(null, userId) - & DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) != 0 + return (mDevicePolicyManager.getKeyguardDisabledFeatures(null, userId) + & DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) != 0 || isSimPinSecure(); } private boolean isFaceDisabled(int userId) { - final DevicePolicyManager dpm = - (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); // TODO(b/140035044) - return whitelistIpcs(() -> dpm != null && (dpm.getKeyguardDisabledFeatures(null, userId) - & DevicePolicyManager.KEYGUARD_DISABLE_FACE) != 0 + return whitelistIpcs(() -> + (mDevicePolicyManager.getKeyguardDisabledFeatures(null, userId) + & DevicePolicyManager.KEYGUARD_DISABLE_FACE) != 0 || isSimPinSecure()); } @@ -1258,7 +1280,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } public boolean getUserHasTrust(int userId) { - return !isTrustDisabled(userId) && mUserHasTrust.get(userId); + return !isTrustDisabled() && mUserHasTrust.get(userId); } /** @@ -1290,7 +1312,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } public boolean getUserTrustIsManaged(int userId) { - return mUserTrustIsManaged.get(userId) && !isTrustDisabled(userId); + return mUserTrustIsManaged.get(userId) && !isTrustDisabled(); } private void updateSecondaryLockscreenRequirement(int userId) { @@ -1308,7 +1330,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Intent intent = new Intent(DevicePolicyManager.ACTION_BIND_SECONDARY_LOCKSCREEN_SERVICE) .setPackage(supervisorComponent.getPackageName()); - ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, 0); + ResolveInfo resolveInfo = mPackageManager.resolveService(intent, 0); if (resolveInfo != null && resolveInfo.serviceInfo != null) { Intent launchIntent = new Intent().setComponent(resolveInfo.serviceInfo.getComponentName()); @@ -1406,6 +1428,16 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } + private void notifyNonStrongBiometricStateChanged(int userId) { + Assert.isMainThread(); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onNonStrongBiometricAllowedChanged(userId); + } + } + } + private void dispatchErrorMessage(CharSequence message) { Assert.isMainThread(); for (int i = 0; i < mCallbacks.size(); i++) { @@ -1677,22 +1709,19 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab CancellationSignal mFingerprintCancelSignal; @VisibleForTesting CancellationSignal mFaceCancelSignal; - private FingerprintManager mFpm; - private FaceManager mFaceManager; private List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties; private List<FaceSensorPropertiesInternal> mFaceSensorProperties; private boolean mFingerprintLockedOut; private boolean mFingerprintLockedOutPermanent; private boolean mFaceLockedOutPermanent; - private HashMap<Integer, Boolean> mIsUnlockWithFingerprintPossible = new HashMap<>(); - private TelephonyManager mTelephonyManager; + private final HashMap<Integer, Boolean> mIsUnlockWithFingerprintPossible = new HashMap<>(); /** * When we receive a * {@link com.android.internal.telephony.TelephonyIntents#ACTION_SIM_STATE_CHANGED} broadcast, * and then pass a result via our handler to {@link KeyguardUpdateMonitor#handleSimStateChange}, * we need a single object to pass to the handler. This class helps decode - * the intent and provide a {@link SimCard.State} result. + * the intent and provide a {@link SimData} result. */ private static class SimData { public int simState; @@ -1759,11 +1788,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab public static class StrongAuthTracker extends LockPatternUtils.StrongAuthTracker { private final Consumer<Integer> mStrongAuthRequiredChangedCallback; + private final Consumer<Integer> mNonStrongBiometricAllowedChanged; public StrongAuthTracker(Context context, - Consumer<Integer> strongAuthRequiredChangedCallback) { + Consumer<Integer> strongAuthRequiredChangedCallback, + Consumer<Integer> nonStrongBiometricAllowedChanged) { super(context); mStrongAuthRequiredChangedCallback = strongAuthRequiredChangedCallback; + mNonStrongBiometricAllowedChanged = nonStrongBiometricAllowedChanged; } public boolean isUnlockingWithBiometricAllowed(boolean isStrongBiometric) { @@ -1781,6 +1813,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab public void onStrongAuthRequiredChanged(int userId) { mStrongAuthRequiredChangedCallback.accept(userId); } + + // TODO(b/247091681): Renaming the inappropriate onIsNonStrongBiometricAllowedChanged + // callback wording for Weak/Convenience idle timeout constraint that only allow + // Strong-Auth + @Override + public void onIsNonStrongBiometricAllowedChanged(int userId) { + mNonStrongBiometricAllowedChanged.accept(userId); + } } protected void handleStartedWakingUp() { @@ -1913,12 +1953,24 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab UiEventLogger uiEventLogger, // This has to be a provider because SessionTracker depends on KeyguardUpdateMonitor :( Provider<SessionTracker> sessionTrackerProvider, - PowerManager powerManager) { + PowerManager powerManager, + TrustManager trustManager, + SubscriptionManager subscriptionManager, + UserManager userManager, + IDreamManager dreamManager, + DevicePolicyManager devicePolicyManager, + SensorPrivacyManager sensorPrivacyManager, + TelephonyManager telephonyManager, + PackageManager packageManager, + @Nullable FaceManager faceManager, + @Nullable FingerprintManager fingerprintManager, + @Nullable BiometricManager biometricManager) { mContext = context; - mSubscriptionManager = SubscriptionManager.from(context); + mSubscriptionManager = subscriptionManager; mTelephonyListenerManager = telephonyListenerManager; mDeviceProvisioned = isDeviceProvisionedInSettingsDb(); - mStrongAuthTracker = new StrongAuthTracker(context, this::notifyStrongAuthStateChanged); + mStrongAuthTracker = new StrongAuthTracker(context, this::notifyStrongAuthStateChanged, + this::notifyNonStrongBiometricStateChanged); mBackgroundExecutor = backgroundExecutor; mBroadcastDispatcher = broadcastDispatcher; mInteractionJankMonitor = interactionJankMonitor; @@ -1929,12 +1981,20 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mLockPatternUtils = lockPatternUtils; mAuthController = authController; dumpManager.registerDumpable(getClass().getName(), this); - mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class); + mSensorPrivacyManager = sensorPrivacyManager; mActiveUnlockConfig = activeUnlockConfiguration; mLogger = logger; mUiEventLogger = uiEventLogger; mSessionTrackerProvider = sessionTrackerProvider; mPowerManager = powerManager; + mTrustManager = trustManager; + mUserManager = userManager; + mDreamManager = dreamManager; + mTelephonyManager = telephonyManager; + mDevicePolicyManager = devicePolicyManager; + mPackageManager = packageManager; + mFpm = fingerprintManager; + mFaceManager = faceManager; mActiveUnlockConfig.setKeyguardUpdateMonitor(this); mWakeOnFingerprintAcquiredStart = context.getResources() .getBoolean(com.android.internal.R.bool.kg_wake_on_acquire_start); @@ -2079,8 +2139,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // listener now with the service state from the default sub. mBackgroundExecutor.execute(() -> { int subId = SubscriptionManager.getDefaultSubscriptionId(); - ServiceState serviceState = mContext.getSystemService(TelephonyManager.class) - .getServiceStateForSubscriber(subId); + ServiceState serviceState = mTelephonyManager.getServiceStateForSubscriber(subId); mHandler.sendMessage( mHandler.obtainMessage(MSG_SERVICE_STATE_CHANGE, subId, 0, serviceState)); }); @@ -2102,26 +2161,21 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab e.rethrowAsRuntimeException(); } - mTrustManager = context.getSystemService(TrustManager.class); mTrustManager.registerTrustListener(this); setStrongAuthTracker(mStrongAuthTracker); - mDreamManager = IDreamManager.Stub.asInterface( - ServiceManager.getService(DreamService.DREAM_SERVICE)); - - if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { - mFpm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE); + if (mFpm != null) { mFingerprintSensorProperties = mFpm.getSensorPropertiesInternal(); + mFpm.addLockoutResetCallback(mFingerprintLockoutResetCallback); } - if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)) { - mFaceManager = (FaceManager) context.getSystemService(Context.FACE_SERVICE); + if (mFaceManager != null) { mFaceSensorProperties = mFaceManager.getSensorPropertiesInternal(); + mFaceManager.addLockoutResetCallback(mFaceLockoutResetCallback); } - if (mFpm != null || mFaceManager != null) { - mBiometricManager = context.getSystemService(BiometricManager.class); - mBiometricManager.registerEnabledOnKeyguardCallback(mBiometricEnabledCallback); + if (biometricManager != null) { + biometricManager.registerEnabledOnKeyguardCallback(mBiometricEnabledCallback); } // in case authenticators aren't registered yet at this point: @@ -2139,19 +2193,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } }); updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_ON_KEYGUARD_INIT); - if (mFpm != null) { - mFpm.addLockoutResetCallback(mFingerprintLockoutResetCallback); - } - if (mFaceManager != null) { - mFaceManager.addLockoutResetCallback(mFaceLockoutResetCallback); - } TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); - mUserManager = context.getSystemService(UserManager.class); mIsPrimaryUser = mUserManager.isPrimaryUser(); int user = ActivityManager.getCurrentUser(); mUserIsUnlocked.put(user, mUserManager.isUserUnlocked(user)); - mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class); mLogoutEnabled = mDevicePolicyManager.isLogoutEnabled(); updateSecondaryLockscreenRequirement(user); List<UserInfo> allUsers = mUserManager.getUsers(); @@ -2161,22 +2207,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } updateAirplaneModeState(); - mTelephonyManager = - (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - if (mTelephonyManager != null) { - mTelephonyListenerManager.addActiveDataSubscriptionIdListener(mPhoneStateListener); - // Set initial sim states values. - for (int slot = 0; slot < mTelephonyManager.getActiveModemCount(); slot++) { - int state = mTelephonyManager.getSimState(slot); - int[] subIds = mSubscriptionManager.getSubscriptionIds(slot); - if (subIds != null) { - for (int subId : subIds) { - mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, subId, slot, state) - .sendToTarget(); - } - } - } - } + mTelephonyListenerManager.addActiveDataSubscriptionIdListener(mPhoneStateListener); + initializeSimState(); mTimeFormatChangeObserver = new ContentObserver(mHandler) { @Override @@ -2193,6 +2225,20 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab false, mTimeFormatChangeObserver, UserHandle.USER_ALL); } + private void initializeSimState() { + // Set initial sim states values. + for (int slot = 0; slot < mTelephonyManager.getActiveModemCount(); slot++) { + int state = mTelephonyManager.getSimState(slot); + int[] subIds = mSubscriptionManager.getSubscriptionIds(slot); + if (subIds != null) { + for (int subId : subIds) { + mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, subId, slot, state) + .sendToTarget(); + } + } + } + } + private void updateFaceEnrolled(int userId) { mIsFaceEnrolled = whitelistIpcs( () -> mFaceManager != null && mFaceManager.isHardwareDetected() @@ -2235,7 +2281,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } @Override - public void onUserSwitchComplete(int newUserId) throws RemoteException { + public void onUserSwitchComplete(int newUserId) { mHandler.sendMessage(mHandler.obtainMessage(MSG_USER_SWITCH_COMPLETE, newUserId, 0)); } @@ -2320,11 +2366,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * @param userInitiatedRequest true if the user explicitly requested face auth * @param reason One of the reasons {@link FaceAuthApiRequestReason} on why this API is being * invoked. + * @return current face auth detection state, true if it is running. */ - public void requestFaceAuth(boolean userInitiatedRequest, + public boolean requestFaceAuth(boolean userInitiatedRequest, @FaceAuthApiRequestReason String reason) { - mLogger.logFaceAuthRequested(userInitiatedRequest); + mLogger.logFaceAuthRequested(userInitiatedRequest, reason); updateFaceListeningState(BIOMETRIC_ACTION_START, apiRequestReasonToUiEvent(reason)); + return isFaceDetectionRunning(); } /** @@ -2334,10 +2382,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab stopListeningForFace(FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER); } - public boolean isFaceScanning() { - return mFaceRunningState == BIOMETRIC_STATE_RUNNING; - } - private void updateFaceListeningState(int action, @NonNull FaceAuthUiEvent faceAuthUiEvent) { // If this message exists, we should not authenticate again until this message is // consumed by the handler @@ -2385,7 +2429,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * Attempts to trigger active unlock from trust agent. */ private void requestActiveUnlock( - ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, + @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, String reason, boolean dismissKeyguard ) { @@ -2415,7 +2459,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * Only dismisses the keyguard under certain conditions. */ public void requestActiveUnlock( - ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, + @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, String extraReason ) { final boolean canFaceBypass = isFaceEnrolled() && mKeyguardBypassController != null @@ -2446,7 +2490,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // Triggers: final boolean triggerActiveUnlockForAssistant = shouldTriggerActiveUnlockForAssistant(); final boolean awakeKeyguard = mBouncerFullyShown || mUdfpsBouncerShowing - || (mKeyguardIsVisible && !mGoingToSleep + || (isKeyguardVisible() && !mGoingToSleep && mStatusBarState != StatusBarState.SHADE_LOCKED); // Gates: @@ -2522,7 +2566,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean userDoesNotHaveTrust = !getUserHasTrust(user); final boolean shouldListenForFingerprintAssistant = shouldListenForFingerprintAssistant(); final boolean shouldListenKeyguardState = - mKeyguardIsVisible + isKeyguardVisible() || !mDeviceInteractive || (mBouncerIsOrWillBeShowing && !mKeyguardGoingAway) || mGoingToSleep @@ -2571,7 +2615,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mFingerprintLockedOut, mGoingToSleep, mKeyguardGoingAway, - mKeyguardIsVisible, + isKeyguardVisible(), mKeyguardOccluded, mOccludingAppRequestingFp, mIsPrimaryUser, @@ -2593,7 +2637,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } final boolean statusBarShadeLocked = mStatusBarState == StatusBarState.SHADE_LOCKED; - final boolean awakeKeyguard = mKeyguardIsVisible && mDeviceInteractive && !mGoingToSleep + final boolean awakeKeyguard = isKeyguardVisible() && mDeviceInteractive && !statusBarShadeLocked; final int user = getCurrentUser(); final int strongAuth = mStrongAuthTracker.getStrongAuthForUser(user); @@ -2639,7 +2683,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware. final boolean shouldListen = - (mBouncerFullyShown && !mGoingToSleep + (mBouncerFullyShown || mAuthInterruptActive || mOccludingAppRequestingFace || awakeKeyguard @@ -2651,6 +2695,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab && strongAuthAllowsScanning && mIsPrimaryUser && (!mSecureCameraLaunched || mOccludingAppRequestingFace) && !faceAuthenticated + && !mGoingToSleep && !fpOrFaceIsLockedOut; // Aggregate relevant fields for debug logging. @@ -2681,7 +2726,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return shouldListen; } - private void maybeLogListenerModelData(KeyguardListenModel model) { + private void maybeLogListenerModelData(@NonNull KeyguardListenModel model) { mLogger.logKeyguardListenerModel(model); if (model instanceof KeyguardActiveUnlockModel) { @@ -2794,6 +2839,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return isUnlockWithFacePossible(userId) || isUnlockWithFingerprintPossible(userId); } + @SuppressLint("MissingPermission") @VisibleForTesting boolean isUnlockWithFingerprintPossible(int userId) { // TODO (b/242022358), make this rely on onEnrollmentChanged event and update it only once. @@ -2924,6 +2970,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab try { reply.sendResult(null); } catch (RemoteException e) { + mLogger.logException(e, "Ignored exception while userSwitching"); } } @@ -3147,32 +3194,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab callbacksRefreshCarrierInfo(); } - public boolean isKeyguardVisible() { - return mKeyguardIsVisible; - } - /** - * Notifies that the visibility state of Keyguard has changed. - * - * <p>Needs to be called from the main thread. + * Whether the keyguard is showing and not occluded. */ - public void onKeyguardVisibilityChanged(boolean showing) { - Assert.isMainThread(); - mLogger.logKeyguardVisibilityChanged(showing); - mKeyguardIsVisible = showing; - - if (showing) { - mSecureCameraLaunched = false; - } - - for (int i = 0; i < mCallbacks.size(); i++) { - KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); - if (cb != null) { - cb.onKeyguardVisibilityChangedRaw(showing); - } - } - updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, - FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED); + public boolean isKeyguardVisible() { + return mKeyguardShowing && !mKeyguardOccluded; } /** @@ -3190,7 +3216,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return false; } Intent homeIntent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME); - ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivityAsUser(homeIntent, + ResolveInfo resolveInfo = mPackageManager.resolveActivityAsUser(homeIntent, 0 /* flags */, getCurrentUser()); if (resolveInfo == null) { @@ -3320,11 +3346,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } // change in battery overheat - if (current.health != old.health) { - return true; - } - - return false; + return current.health != old.health; } /** @@ -3375,10 +3397,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab public void setSwitchingUser(boolean switching) { mSwitchingUser = switching; // Since this comes in on a binder thread, we need to post if first - mHandler.post(() -> { - updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, - FACE_AUTH_UPDATED_USER_SWITCHING); - }); + mHandler.post(() -> updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, + FACE_AUTH_UPDATED_USER_SWITCHING)); } private void sendUpdates(KeyguardUpdateMonitorCallback callback) { @@ -3387,7 +3407,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab callback.onTimeChanged(); callback.onPhoneStateChanged(mPhoneState); callback.onRefreshCarrierInfo(); - callback.onKeyguardVisibilityChangedRaw(mKeyguardIsVisible); + callback.onKeyguardVisibilityChanged(isKeyguardVisible()); callback.onTelephonyCapable(mTelephonyCapable); for (Entry<Integer, SimData> data : mSimDatas.entrySet()) { @@ -3487,7 +3507,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab /** * If any SIM cards are currently secure. * - * @see #isSimPinSecure(State) + * @see #isSimPinSecure(int) */ public boolean isSimPinSecure() { // True if any SIM is pin secure @@ -3534,10 +3554,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * @return true if and only if the state has changed for the specified {@code slotId} */ private boolean refreshSimState(int subId, int slotId) { - final TelephonyManager tele = - (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); - int state = (tele != null) ? - tele.getSimState(slotId) : TelephonyManager.SIM_STATE_UNKNOWN; + int state = mTelephonyManager.getSimState(slotId); SimData data = mSimDatas.get(subId); final boolean changed; if (data == null) { @@ -3680,13 +3697,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * Unregister all listeners. */ public void destroy() { - // TODO: inject these dependencies: - TelephonyManager telephony = - (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); - if (telephony != null) { - mTelephonyListenerManager.removeActiveDataSubscriptionIdListener(mPhoneStateListener); - } - + mStatusBarStateController.removeCallback(mStatusBarStateControllerListener); + mTelephonyListenerManager.removeActiveDataSubscriptionIdListener(mPhoneStateListener); mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionListener); if (mDeviceProvisionedObserver != null) { @@ -3716,8 +3728,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mHandler.removeCallbacksAndMessages(null); } + @SuppressLint("MissingPermission") @Override - public void dump(PrintWriter pw, String[] args) { + public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("KeyguardUpdateMonitor state:"); pw.println(" getUserHasTrust()=" + getUserHasTrust(getCurrentUser())); pw.println(" getUserUnlockedWithBiometric()=" diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index 7a42803859b5..c06e1dcf08c2 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -16,7 +16,6 @@ package com.android.keyguard; import android.hardware.biometrics.BiometricSourceType; -import android.os.SystemClock; import android.telephony.TelephonyManager; import android.view.WindowManagerPolicyConstants; @@ -32,10 +31,6 @@ import java.util.TimeZone; */ public class KeyguardUpdateMonitorCallback { - private static final long VISIBILITY_CHANGED_COLLAPSE_MS = 1000; - private long mVisibilityChangedCalled; - private boolean mShowing; - /** * Called when the battery status changes, e.g. when plugged in or unplugged, charge * level, etc. changes. @@ -75,21 +70,6 @@ public class KeyguardUpdateMonitorCallback { public void onPhoneStateChanged(int phoneState) { } /** - * Called when the visibility of the keyguard changes. - * @param showing Indicates if the keyguard is now visible. - */ - public void onKeyguardVisibilityChanged(boolean showing) { } - - public void onKeyguardVisibilityChangedRaw(boolean showing) { - final long now = SystemClock.elapsedRealtime(); - if (showing == mShowing - && (now - mVisibilityChangedCalled) < VISIBILITY_CHANGED_COLLAPSE_MS) return; - onKeyguardVisibilityChanged(showing); - mVisibilityChangedCalled = now; - mShowing = showing; - } - - /** * Called when the keyguard enters or leaves bouncer mode. * @param bouncerIsOrWillBeShowing if true, keyguard is showing the bouncer or transitioning * from/to bouncer mode. @@ -97,6 +77,12 @@ public class KeyguardUpdateMonitorCallback { public void onKeyguardBouncerStateChanged(boolean bouncerIsOrWillBeShowing) { } /** + * Called when the keyguard visibility changes. + * @param visible whether the keyguard is showing and is NOT occluded + */ + public void onKeyguardVisibilityChanged(boolean visible) { } + + /** * Called when the keyguard fully transitions to the bouncer or is no longer the bouncer * @param bouncerIsFullyShowing if true, keyguard is fully showing the bouncer */ @@ -305,4 +291,9 @@ public class KeyguardUpdateMonitorCallback { * Called when the notification shade is expanded or collapsed. */ public void onShadeExpandedChanged(boolean expanded) { } + + /** + * Called when the non-strong biometric state changed. + */ + public void onNonStrongBiometricAllowedChanged(int userId) { } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java index efa5558f5088..b793fd22aed1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java @@ -66,10 +66,13 @@ public class KeyguardUserSwitcherPopupMenu extends ListPopupWindow { listView.setDividerHeight(mContext.getResources().getDimensionPixelSize( R.dimen.bouncer_user_switcher_popup_divider_height)); - int height = mContext.getResources().getDimensionPixelSize( - R.dimen.bouncer_user_switcher_popup_header_height); - listView.addHeaderView(createSpacer(height), null, false); - listView.addFooterView(createSpacer(height), null, false); + if (listView.getTag(R.id.header_footer_views_added_tag_key) == null) { + int height = mContext.getResources().getDimensionPixelSize( + R.dimen.bouncer_user_switcher_popup_header_height); + listView.addHeaderView(createSpacer(height), null, false); + listView.addFooterView(createSpacer(height), null, false); + listView.setTag(R.id.header_footer_views_added_tag_key, new Object()); + } listView.setOnTouchListener((v, ev) -> { if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java index 8293c74c5e75..90f0446ee34d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java @@ -24,10 +24,10 @@ import androidx.annotation.Nullable; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.shade.NotificationPanelViewController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.phone.BiometricUnlockController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.KeyguardBypassController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; /** * Interface to control Keyguard View. It should be implemented by KeyguardViewManagers, which @@ -94,11 +94,6 @@ public interface KeyguardViewController { void setOccluded(boolean occluded, boolean animate); /** - * @return Whether the keyguard is showing - */ - boolean isShowing(); - - /** * Dismisses the keyguard by going to the next screen or making it gone. */ void dismissAndCollapse(); @@ -185,7 +180,7 @@ public interface KeyguardViewController { */ void registerCentralSurfaces(CentralSurfaces centralSurfaces, NotificationPanelViewController notificationPanelViewController, - @Nullable PanelExpansionStateManager panelExpansionStateManager, + @Nullable ShadeExpansionStateManager shadeExpansionStateManager, BiometricUnlockController biometricUnlockController, View notificationContainer, KeyguardBypassController bypassController); diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index 2a3667610f9c..fe7c70ae4c7e 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -23,6 +23,8 @@ import static com.android.keyguard.LockIconView.ICON_LOCK; import static com.android.keyguard.LockIconView.ICON_UNLOCK; import static com.android.systemui.classifier.Classifier.LOCK_ICON; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; +import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.content.res.Configuration; import android.content.res.Resources; @@ -44,6 +46,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.android.systemui.Dumpable; @@ -53,6 +56,10 @@ import com.android.systemui.biometrics.AuthRippleController; import com.android.systemui.biometrics.UdfpsController; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; @@ -65,6 +72,7 @@ import com.android.systemui.util.concurrency.DelayableExecutor; import java.io.PrintWriter; import java.util.Objects; +import java.util.function.Consumer; import javax.inject.Inject; @@ -101,6 +109,9 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @NonNull private CharSequence mLockedLabel; @NonNull private final VibratorHelper mVibrator; @Nullable private final AuthRippleController mAuthRippleController; + @NonNull private final FeatureFlags mFeatureFlags; + @NonNull private final KeyguardTransitionInteractor mTransitionInteractor; + @NonNull private final KeyguardInteractor mKeyguardInteractor; // Tracks the velocity of a touch to help filter out the touches that move too fast. private VelocityTracker mVelocityTracker; @@ -137,6 +148,20 @@ public class LockIconViewController extends ViewController<LockIconView> impleme private boolean mDownDetected; private final Rect mSensorTouchLocation = new Rect(); + @VisibleForTesting + final Consumer<TransitionStep> mDozeTransitionCallback = (TransitionStep step) -> { + mInterpolatedDarkAmount = step.getValue(); + mView.setDozeAmount(step.getValue()); + updateBurnInOffsets(); + }; + + @VisibleForTesting + final Consumer<Boolean> mIsDozingCallback = (Boolean isDozing) -> { + mIsDozing = isDozing; + updateBurnInOffsets(); + updateVisibility(); + }; + @Inject public LockIconViewController( @Nullable LockIconView view, @@ -152,7 +177,10 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @NonNull @Main DelayableExecutor executor, @NonNull VibratorHelper vibrator, @Nullable AuthRippleController authRippleController, - @NonNull @Main Resources resources + @NonNull @Main Resources resources, + @NonNull KeyguardTransitionInteractor transitionInteractor, + @NonNull KeyguardInteractor keyguardInteractor, + @NonNull FeatureFlags featureFlags ) { super(view); mStatusBarStateController = statusBarStateController; @@ -166,6 +194,9 @@ public class LockIconViewController extends ViewController<LockIconView> impleme mExecutor = executor; mVibrator = vibrator; mAuthRippleController = authRippleController; + mTransitionInteractor = transitionInteractor; + mKeyguardInteractor = keyguardInteractor; + mFeatureFlags = featureFlags; mMaxBurnInOffsetX = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x); mMaxBurnInOffsetY = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y); @@ -182,6 +213,12 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @Override protected void onInit() { mView.setAccessibilityDelegate(mAccessibilityDelegate); + + if (mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + collectFlow(mView, mTransitionInteractor.getDozeAmountTransition(), + mDozeTransitionCallback); + collectFlow(mView, mKeyguardInteractor.isDozing(), mIsDozingCallback); + } } @Override @@ -377,14 +414,17 @@ public class LockIconViewController extends ViewController<LockIconView> impleme pw.println(" mShowUnlockIcon: " + mShowUnlockIcon); pw.println(" mShowLockIcon: " + mShowLockIcon); pw.println(" mShowAodUnlockedIcon: " + mShowAodUnlockedIcon); - pw.println(" mIsDozing: " + mIsDozing); - pw.println(" mIsBouncerShowing: " + mIsBouncerShowing); - pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric); - pw.println(" mRunningFPS: " + mRunningFPS); - pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); - pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState)); - pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount); - pw.println(" mSensorTouchLocation: " + mSensorTouchLocation); + pw.println(); + pw.println(" mIsDozing: " + mIsDozing); + pw.println(" isFlagEnabled(DOZING_MIGRATION_1): " + + mFeatureFlags.isEnabled(DOZING_MIGRATION_1)); + pw.println(" mIsBouncerShowing: " + mIsBouncerShowing); + pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric); + pw.println(" mRunningFPS: " + mRunningFPS); + pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); + pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState)); + pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount); + pw.println(" mSensorTouchLocation: " + mSensorTouchLocation); if (mView != null) { mView.dump(pw, args); @@ -425,16 +465,20 @@ public class LockIconViewController extends ViewController<LockIconView> impleme new StatusBarStateController.StateListener() { @Override public void onDozeAmountChanged(float linear, float eased) { - mInterpolatedDarkAmount = eased; - mView.setDozeAmount(eased); - updateBurnInOffsets(); + if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + mInterpolatedDarkAmount = eased; + mView.setDozeAmount(eased); + updateBurnInOffsets(); + } } @Override public void onDozingChanged(boolean isDozing) { - mIsDozing = isDozing; - updateBurnInOffsets(); - updateVisibility(); + if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + mIsDozing = isDozing; + updateBurnInOffsets(); + updateVisibility(); + } } @Override @@ -447,14 +491,6 @@ public class LockIconViewController extends ViewController<LockIconView> impleme private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() { @Override - public void onKeyguardVisibilityChanged(boolean showing) { - // reset mIsBouncerShowing state in case it was preemptively set - // onLongPress - mIsBouncerShowing = mKeyguardViewController.isBouncerShowing(); - updateVisibility(); - } - - @Override public void onKeyguardBouncerStateChanged(boolean bouncer) { mIsBouncerShowing = bouncer; updateVisibility(); @@ -507,6 +543,11 @@ public class LockIconViewController extends ViewController<LockIconView> impleme // If biometrics were removed, local vars mCanDismissLockScreen and // mUserUnlockedWithBiometric may not be updated. mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen(); + + // reset mIsBouncerShowing state in case it was preemptively set + // onLongPress + mIsBouncerShowing = mKeyguardViewController.isBouncerShowing(); + updateKeyguardShowing(); if (mIsKeyguardShowing) { mUserUnlockedWithBiometric = diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java index c4be1ba53503..72a44bd198f2 100644 --- a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java +++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java @@ -21,9 +21,14 @@ import java.util.List; import dagger.Module; import dagger.Provides; -/** Dagger Module for clock package. */ +/** + * Dagger Module for clock package. + * + * @deprecated Migrate to ClockRegistry + */ @Module -public abstract class ClockModule { +@Deprecated +public abstract class ClockInfoModule { /** */ @Provides diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java new file mode 100644 index 000000000000..9767313331d3 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard.dagger; + +import android.content.Context; +import android.os.Handler; +import android.os.UserHandle; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.shared.clocks.ClockRegistry; +import com.android.systemui.shared.clocks.DefaultClockProvider; +import com.android.systemui.shared.plugins.PluginManager; + +import dagger.Module; +import dagger.Provides; + +/** Dagger Module for clocks. */ +@Module +public abstract class ClockRegistryModule { + /** Provide the ClockRegistry as a singleton so that it is not instantiated more than once. */ + @Provides + @SysUISingleton + public static ClockRegistry getClockRegistry( + @Application Context context, + PluginManager pluginManager, + @Main Handler handler, + DefaultClockProvider defaultClockProvider, + FeatureFlags featureFlags) { + return new ClockRegistry( + context, + pluginManager, + handler, + featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS), + UserHandle.USER_ALL, + defaultClockProvider); + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt index 2c2ab7b39161..6264ce7273f1 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt @@ -17,9 +17,9 @@ package com.android.keyguard.logging import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG import com.android.systemui.log.dagger.BiometricMessagesLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG import javax.inject.Inject /** Helper class for logging for [com.android.systemui.biometrics.FaceHelpMessageDeferral] */ diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt new file mode 100644 index 000000000000..46f3d4e5f6aa --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard.logging + +import com.android.systemui.log.dagger.KeyguardLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogLevel.WARNING +import com.android.systemui.plugins.log.MessageInitializer +import com.android.systemui.plugins.log.MessagePrinter +import com.google.errorprone.annotations.CompileTimeConstant +import javax.inject.Inject + +private const val TAG = "KeyguardLog" + +/** + * Generic logger for keyguard that's wrapping [LogBuffer]. This class should be used for adding + * temporary logs or logs for smaller classes when creating whole new [LogBuffer] wrapper might be + * an overkill. + */ +class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) { + fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG) + + fun e(@CompileTimeConstant msg: String) = log(msg, ERROR) + + fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE) + + fun w(@CompileTimeConstant msg: String) = log(msg, WARNING) + + fun log(msg: String, level: LogLevel) = buffer.log(TAG, level, msg) + + private fun debugLog(messageInitializer: MessageInitializer, messagePrinter: MessagePrinter) { + buffer.log(TAG, DEBUG, messageInitializer, messagePrinter) + } + + // TODO: remove after b/237743330 is fixed + fun logStatusBarCalculatedAlpha(alpha: Float) { + debugLog({ double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" }) + } + + // TODO: remove after b/237743330 is fixed + fun logStatusBarExplicitAlpha(alpha: Float) { + debugLog({ double1 = alpha.toDouble() }, { "new mExplicitAlpha value: $double1" }) + } + + // TODO: remove after b/237743330 is fixed + fun logStatusBarAlphaVisibility(visibility: Int, alpha: Float, state: String) { + debugLog( + { + int1 = visibility + double1 = alpha.toDouble() + str1 = state + }, + { "changing visibility to $int1 with alpha $double1 in state: $str1" } + ) + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 7a00cd930f2a..2f79e30a0b5b 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -22,13 +22,13 @@ import android.telephony.SubscriptionInfo import com.android.keyguard.ActiveUnlockConfig import com.android.keyguard.KeyguardListenModel import com.android.keyguard.KeyguardUpdateMonitorCallback -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.VERBOSE -import com.android.systemui.log.LogLevel.WARNING +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject @@ -45,13 +45,13 @@ class KeyguardUpdateMonitorLogger @Inject constructor( fun e(@CompileTimeConstant msg: String) = log(msg, ERROR) - fun v(@CompileTimeConstant msg: String) = log(msg, ERROR) + fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE) fun w(@CompileTimeConstant msg: String) = log(msg, WARNING) fun log(@CompileTimeConstant msg: String, level: LogLevel) = logBuffer.log(TAG, level, msg) - fun logActiveUnlockTriggered(reason: String) { + fun logActiveUnlockTriggered(reason: String?) { logBuffer.log("ActiveUnlock", DEBUG, { str1 = reason }, { "initiate active unlock triggerReason=$str1" }) @@ -101,17 +101,18 @@ class KeyguardUpdateMonitorLogger @Inject constructor( { "Face authenticated for wrong user: $int1" }) } - fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String) { + fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String?) { logBuffer.log(TAG, DEBUG, { int1 = msgId str1 = helpMsg }, { "Face help received, msgId: $int1 msg: $str1" }) } - fun logFaceAuthRequested(userInitiatedRequest: Boolean) { - logBuffer.log(TAG, DEBUG, - { bool1 = userInitiatedRequest }, - { "requestFaceAuth() userInitiated=$bool1" }) + fun logFaceAuthRequested(userInitiatedRequest: Boolean, reason: String?) { + logBuffer.log(TAG, DEBUG, { + bool1 = userInitiatedRequest + str1 = reason + }, { "requestFaceAuth() userInitiated=$bool1 reason=$str1" }) } fun logFaceAuthSuccess(userId: Int) { @@ -170,8 +171,14 @@ class KeyguardUpdateMonitorLogger @Inject constructor( logBuffer.log(TAG, VERBOSE, { str1 = "$model" }, { str1!! }) } - fun logKeyguardVisibilityChanged(showing: Boolean) { - logBuffer.log(TAG, DEBUG, { bool1 = showing }, { "onKeyguardVisibilityChanged($bool1)" }) + fun logKeyguardShowingChanged(showing: Boolean, occluded: Boolean, visible: Boolean) { + logBuffer.log(TAG, DEBUG, { + bool1 = showing + bool2 = occluded + bool3 = visible + }, { + "keyguardShowingChanged(showing=$bool1 occluded=$bool2 visible=$bool3)" + }) } fun logMissingSupervisorAppError(userId: Int) { @@ -180,7 +187,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( { "No Profile Owner or Device Owner supervision app found for User $int1" }) } - fun logPhoneStateChanged(newState: String) { + fun logPhoneStateChanged(newState: String?) { logBuffer.log(TAG, DEBUG, { str1 = newState }, { "handlePhoneStateChanged($str1)" }) @@ -233,7 +240,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( }, { "handleServiceStateChange(subId=$int1, serviceState=$str1)" }) } - fun logServiceStateIntent(action: String, serviceState: ServiceState?, subId: Int) { + fun logServiceStateIntent(action: String?, serviceState: ServiceState?, subId: Int) { logBuffer.log(TAG, VERBOSE, { str1 = action str2 = "$serviceState" @@ -249,7 +256,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( }, { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" }) } - fun logSimStateFromIntent(action: String, extraSimState: String, slotId: Int, subId: Int) { + fun logSimStateFromIntent(action: String?, extraSimState: String?, slotId: Int, subId: Int) { logBuffer.log(TAG, VERBOSE, { str1 = action str2 = extraSimState @@ -282,7 +289,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( { "SubInfo:$str1" }) } - fun logTimeFormatChanged(newTimeFormat: String) { + fun logTimeFormatChanged(newTimeFormat: String?) { logBuffer.log(TAG, DEBUG, { str1 = newTimeFormat }, { "handleTimeFormatUpdate timeFormat=$str1" }) @@ -331,18 +338,18 @@ class KeyguardUpdateMonitorLogger @Inject constructor( fun logUserRequestedUnlock( requestOrigin: ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN, - reason: String, + reason: String?, dismissKeyguard: Boolean ) { logBuffer.log("ActiveUnlock", DEBUG, { - str1 = requestOrigin.name + str1 = requestOrigin?.name str2 = reason bool1 = dismissKeyguard }, { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" }) } fun logShowTrustGrantedMessage( - message: String + message: String? ) { logBuffer.log(TAG, DEBUG, { str1 = message diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt index 37829f25d179..a89cbf57f95b 100644 --- a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt +++ b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt @@ -19,11 +19,11 @@ import kotlinx.coroutines.withContext @SysUISingleton class ChooserSelector @Inject constructor( - context: Context, + private val context: Context, private val featureFlags: FeatureFlags, @Application private val coroutineScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher -) : CoreStartable(context) { +) : CoreStartable { private val packageManager = context.packageManager private val chooserComponent = ComponentName.unflattenFromString( diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java index 0201cdc25319..929ebea37eef 100644 --- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java +++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java @@ -16,39 +16,41 @@ package com.android.systemui; -import android.content.Context; import android.content.res.Configuration; import androidx.annotation.NonNull; -import com.android.internal.annotations.VisibleForTesting; - import java.io.PrintWriter; /** - * A top-level module of system UI code (sometimes called "system UI services" elsewhere in code). - * Which CoreStartable modules are loaded can be controlled via a config resource. + * Code that needs to be run when SystemUI is started. + * + * Which CoreStartable modules are loaded is controlled via the dagger graph. Bind them into the + * CoreStartable map with code such as: + * + * <pre> + * @Binds + * @IntoMap + * @ClassKey(FoobarStartable::class) + * abstract fun bind(impl: FoobarStartable): CoreStartable + * </pre> * * @see SystemUIApplication#startServicesIfNeeded() */ -public abstract class CoreStartable implements Dumpable { - protected final Context mContext; - - public CoreStartable(Context context) { - mContext = context; - } +public interface CoreStartable extends Dumpable { /** Main entry point for implementations. Called shortly after app startup. */ - public abstract void start(); + void start(); - protected void onConfigurationChanged(Configuration newConfig) { + /** */ + default void onConfigurationChanged(Configuration newConfig) { } @Override - public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { + default void dump(@NonNull PrintWriter pw, @NonNull String[] args) { } - @VisibleForTesting - protected void onBootCompleted() { + /** Called when the device reports BOOT_COMPLETED. */ + default void onBootCompleted() { } } diff --git a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt index a3351e1a6440..5d52056d8b17 100644 --- a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt +++ b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt @@ -86,30 +86,38 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView { onUpdate() } - fun onDisplayChanged(newDisplayUniqueId: String?) { + fun updateConfiguration(newDisplayUniqueId: String?) { + val info = DisplayInfo() + context.display?.getDisplayInfo(info) val oldMode: Display.Mode? = displayMode - val display: Display? = context.display - displayMode = display?.mode + displayMode = info.mode - if (displayUniqueId != display?.uniqueId) { - displayUniqueId = display?.uniqueId - shouldDrawCutout = DisplayCutout.getFillBuiltInDisplayCutout( - context.resources, displayUniqueId - ) - } + updateDisplayUniqueId(info.uniqueId) // Skip if display mode or cutout hasn't changed. if (!displayModeChanged(oldMode, displayMode) && - display?.cutout == displayInfo.displayCutout) { + displayInfo.displayCutout == info.displayCutout && + displayRotation == info.rotation) { return } - if (newDisplayUniqueId == display?.uniqueId) { + if (newDisplayUniqueId == info.uniqueId) { + displayRotation = info.rotation updateCutout() updateProtectionBoundingPath() onUpdate() } } + open fun updateDisplayUniqueId(newDisplayUniqueId: String?) { + if (displayUniqueId != newDisplayUniqueId) { + displayUniqueId = newDisplayUniqueId + shouldDrawCutout = DisplayCutout.getFillBuiltInDisplayCutout( + context.resources, displayUniqueId + ) + invalidate() + } + } + open fun updateRotation(rotation: Int) { displayRotation = rotation updateCutout() diff --git a/packages/SystemUI/src/com/android/systemui/Dumpable.java b/packages/SystemUI/src/com/android/systemui/Dumpable.java index 652595100c0f..73fdce6c9045 100644 --- a/packages/SystemUI/src/com/android/systemui/Dumpable.java +++ b/packages/SystemUI/src/com/android/systemui/Dumpable.java @@ -30,7 +30,6 @@ public interface Dumpable { /** * Called when it's time to dump the internal state - * @param fd A file descriptor. * @param pw Where to write your dump to. * @param args Arguments. */ diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt index c5955860aebf..3e0fa455d39e 100644 --- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt @@ -19,6 +19,7 @@ package com.android.systemui import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet +import android.animation.TimeInterpolator import android.animation.ValueAnimator import android.content.Context import android.graphics.Canvas @@ -55,7 +56,7 @@ class FaceScanningOverlay( private val rimRect = RectF() private var cameraProtectionColor = Color.BLACK var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context, - com.android.systemui.R.attr.wallpaperTextColorAccent) + R.attr.wallpaperTextColorAccent) private var cameraProtectionAnimator: ValueAnimator? = null var hideOverlayRunnable: Runnable? = null var faceAuthSucceeded = false @@ -84,46 +85,19 @@ class FaceScanningOverlay( } override fun drawCutoutProtection(canvas: Canvas) { - if (rimProgress > HIDDEN_RIM_SCALE && !protectionRect.isEmpty) { - val rimPath = Path(protectionPath) - val scaleMatrix = Matrix().apply { - val rimBounds = RectF() - rimPath.computeBounds(rimBounds, true) - setScale(rimProgress, rimProgress, rimBounds.centerX(), rimBounds.centerY()) - } - rimPath.transform(scaleMatrix) - rimPaint.style = Paint.Style.FILL - val rimPaintAlpha = rimPaint.alpha - rimPaint.color = ColorUtils.blendARGB( - faceScanningAnimColor, - Color.WHITE, - statusBarStateController.dozeAmount) - rimPaint.alpha = rimPaintAlpha - canvas.drawPath(rimPath, rimPaint) + if (protectionRect.isEmpty) { + return } - - if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE && - !protectionRect.isEmpty) { - val scaledProtectionPath = Path(protectionPath) - val scaleMatrix = Matrix().apply { - val protectionPathRect = RectF() - scaledProtectionPath.computeBounds(protectionPathRect, true) - setScale(cameraProtectionProgress, cameraProtectionProgress, - protectionPathRect.centerX(), protectionPathRect.centerY()) - } - scaledProtectionPath.transform(scaleMatrix) - paint.style = Paint.Style.FILL - paint.color = cameraProtectionColor - canvas.drawPath(scaledProtectionPath, paint) + if (rimProgress > HIDDEN_RIM_SCALE) { + drawFaceScanningRim(canvas) + } + if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE) { + drawCameraProtection(canvas) } - } - - override fun updateVisOnUpdateCutout(): Boolean { - return false // instead, we always update the visibility whenever face scanning starts/ends } override fun enableShowProtection(show: Boolean) { - val showScanningAnimNow = keyguardUpdateMonitor.isFaceScanning && show + val showScanningAnimNow = keyguardUpdateMonitor.isFaceDetectionRunning && show if (showScanningAnimNow == showScanningAnim) { return } @@ -152,91 +126,26 @@ class FaceScanningOverlay( if (showScanningAnim) Interpolators.STANDARD_ACCELERATE else if (faceAuthSucceeded) Interpolators.STANDARD else Interpolators.STANDARD_DECELERATE - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - cameraProtectionProgress = animation.animatedValue as Float - invalidate() - }) + addUpdateListener(this@FaceScanningOverlay::updateCameraProtectionProgress) addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { cameraProtectionAnimator = null if (!showScanningAnim) { - visibility = View.INVISIBLE - hideOverlayRunnable?.run() - hideOverlayRunnable = null - requestLayout() + hide() } } }) } rimAnimator?.cancel() - rimAnimator = AnimatorSet().apply { - if (showScanningAnim) { - val rimAppearAnimator = ValueAnimator.ofFloat(SHOW_CAMERA_PROTECTION_SCALE, - PULSE_RADIUS_OUT).apply { - duration = PULSE_APPEAR_DURATION - interpolator = Interpolators.STANDARD_DECELERATE - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - } - - // animate in camera protection, rim, and then pulse in/out - playSequentially(cameraProtectionAnimator, rimAppearAnimator, - createPulseAnimator(), createPulseAnimator(), - createPulseAnimator(), createPulseAnimator(), - createPulseAnimator(), createPulseAnimator()) - } else { - val rimDisappearAnimator = ValueAnimator.ofFloat( - rimProgress, - if (faceAuthSucceeded) PULSE_RADIUS_SUCCESS - else SHOW_CAMERA_PROTECTION_SCALE - ).apply { - duration = - if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION - else PULSE_ERROR_DISAPPEAR_DURATION - interpolator = - if (faceAuthSucceeded) Interpolators.STANDARD_DECELERATE - else Interpolators.STANDARD - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - rimProgress = HIDDEN_RIM_SCALE - invalidate() - } - }) - } - if (faceAuthSucceeded) { - val successOpacityAnimator = ValueAnimator.ofInt(255, 0).apply { - duration = PULSE_SUCCESS_DISAPPEAR_DURATION - interpolator = Interpolators.LINEAR - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimPaint.alpha = animation.animatedValue as Int - invalidate() - }) - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - rimPaint.alpha = 255 - invalidate() - } - }) - } - val rimSuccessAnimator = AnimatorSet() - rimSuccessAnimator.playTogether(rimDisappearAnimator, successOpacityAnimator) - playTogether(rimSuccessAnimator, cameraProtectionAnimator) - } else { - playTogether(rimDisappearAnimator, cameraProtectionAnimator) - } - } - + rimAnimator = if (showScanningAnim) { + createFaceScanningRimAnimator() + } else if (faceAuthSucceeded) { + createFaceSuccessRimAnimator() + } else { + createFaceNotSuccessRimAnimator() + } + rimAnimator?.apply { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { rimAnimator = null @@ -245,34 +154,12 @@ class FaceScanningOverlay( } } }) - start() } + rimAnimator?.start() } - fun createPulseAnimator(): AnimatorSet { - return AnimatorSet().apply { - val pulseInwards = ValueAnimator.ofFloat( - PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply { - duration = PULSE_DURATION_INWARDS - interpolator = Interpolators.STANDARD - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - } - val pulseOutwards = ValueAnimator.ofFloat( - PULSE_RADIUS_IN, PULSE_RADIUS_OUT).apply { - duration = PULSE_DURATION_OUTWARDS - interpolator = Interpolators.STANDARD - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - } - playSequentially(pulseInwards, pulseOutwards) - } + override fun updateVisOnUpdateCutout(): Boolean { + return false // instead, we always update the visibility whenever face scanning starts/ends } override fun updateProtectionBoundingPath() { @@ -290,17 +177,153 @@ class FaceScanningOverlay( // Make sure that our measured height encompasses the extra space for the animation mTotalBounds.union(mBoundingRect) mTotalBounds.union( - rimRect.left.toInt(), - rimRect.top.toInt(), - rimRect.right.toInt(), - rimRect.bottom.toInt()) + rimRect.left.toInt(), + rimRect.top.toInt(), + rimRect.right.toInt(), + rimRect.bottom.toInt()) setMeasuredDimension( - resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0), - resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)) + resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0), + resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)) } else { setMeasuredDimension( - resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0), - resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0)) + resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0), + resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0)) + } + } + + private fun drawFaceScanningRim(canvas: Canvas) { + val rimPath = Path(protectionPath) + scalePath(rimPath, rimProgress) + rimPaint.style = Paint.Style.FILL + val rimPaintAlpha = rimPaint.alpha + rimPaint.color = ColorUtils.blendARGB( + faceScanningAnimColor, + Color.WHITE, + statusBarStateController.dozeAmount + ) + rimPaint.alpha = rimPaintAlpha + canvas.drawPath(rimPath, rimPaint) + } + + private fun drawCameraProtection(canvas: Canvas) { + val scaledProtectionPath = Path(protectionPath) + scalePath(scaledProtectionPath, cameraProtectionProgress) + paint.style = Paint.Style.FILL + paint.color = cameraProtectionColor + canvas.drawPath(scaledProtectionPath, paint) + } + + private fun createFaceSuccessRimAnimator(): AnimatorSet { + val rimSuccessAnimator = AnimatorSet() + rimSuccessAnimator.playTogether( + createRimDisappearAnimator( + PULSE_RADIUS_SUCCESS, + PULSE_SUCCESS_DISAPPEAR_DURATION, + Interpolators.STANDARD_DECELERATE + ), + createSuccessOpacityAnimator(), + ) + return AnimatorSet().apply { + playTogether(rimSuccessAnimator, cameraProtectionAnimator) + } + } + + private fun createFaceNotSuccessRimAnimator(): AnimatorSet { + return AnimatorSet().apply { + playTogether( + createRimDisappearAnimator( + SHOW_CAMERA_PROTECTION_SCALE, + PULSE_ERROR_DISAPPEAR_DURATION, + Interpolators.STANDARD + ), + cameraProtectionAnimator, + ) + } + } + + private fun createRimDisappearAnimator( + endValue: Float, + animDuration: Long, + timeInterpolator: TimeInterpolator + ): ValueAnimator { + return ValueAnimator.ofFloat(rimProgress, endValue).apply { + duration = animDuration + interpolator = timeInterpolator + addUpdateListener(this@FaceScanningOverlay::updateRimProgress) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimProgress = HIDDEN_RIM_SCALE + invalidate() + } + }) + } + } + + private fun createSuccessOpacityAnimator(): ValueAnimator { + return ValueAnimator.ofInt(255, 0).apply { + duration = PULSE_SUCCESS_DISAPPEAR_DURATION + interpolator = Interpolators.LINEAR + addUpdateListener(this@FaceScanningOverlay::updateRimAlpha) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimPaint.alpha = 255 + invalidate() + } + }) + } + } + + private fun createFaceScanningRimAnimator(): AnimatorSet { + return AnimatorSet().apply { + playSequentially( + cameraProtectionAnimator, + createRimAppearAnimator(), + createPulseAnimator() + ) + } + } + + private fun createRimAppearAnimator(): ValueAnimator { + return ValueAnimator.ofFloat( + SHOW_CAMERA_PROTECTION_SCALE, + PULSE_RADIUS_OUT + ).apply { + duration = PULSE_APPEAR_DURATION + interpolator = Interpolators.STANDARD_DECELERATE + addUpdateListener(this@FaceScanningOverlay::updateRimProgress) + } + } + + private fun hide() { + visibility = INVISIBLE + hideOverlayRunnable?.run() + hideOverlayRunnable = null + requestLayout() + } + + private fun updateRimProgress(animator: ValueAnimator) { + rimProgress = animator.animatedValue as Float + invalidate() + } + + private fun updateCameraProtectionProgress(animator: ValueAnimator) { + cameraProtectionProgress = animator.animatedValue as Float + invalidate() + } + + private fun updateRimAlpha(animator: ValueAnimator) { + rimPaint.alpha = animator.animatedValue as Int + invalidate() + } + + private fun createPulseAnimator(): ValueAnimator { + return ValueAnimator.ofFloat( + PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply { + duration = HALF_PULSE_DURATION + interpolator = Interpolators.STANDARD + repeatCount = 11 // Pulse inwards and outwards, reversing direction, 6 times + repeatMode = ValueAnimator.REVERSE + addUpdateListener(this@FaceScanningOverlay::updateRimProgress) } } @@ -363,13 +386,24 @@ class FaceScanningOverlay( private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L private const val PULSE_APPEAR_DURATION = 250L // without start delay - private const val PULSE_DURATION_INWARDS = 500L - private const val PULSE_DURATION_OUTWARDS = 500L + private const val HALF_PULSE_DURATION = 500L private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay + + private fun scalePath(path: Path, scalingFactor: Float) { + val scaleMatrix = Matrix().apply { + val boundingRectangle = RectF() + path.computeBounds(boundingRectangle, true) + setScale( + scalingFactor, scalingFactor, + boundingRectangle.centerX(), boundingRectangle.centerY() + ) + } + path.transform(scaleMatrix) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/LatencyTester.java b/packages/SystemUI/src/com/android/systemui/LatencyTester.java index 9cdce6400e56..8f419560c78d 100644 --- a/packages/SystemUI/src/com/android/systemui/LatencyTester.java +++ b/packages/SystemUI/src/com/android/systemui/LatencyTester.java @@ -46,7 +46,7 @@ import javax.inject.Inject; * system that are used for testing the latency. */ @SysUISingleton -public class LatencyTester extends CoreStartable { +public class LatencyTester implements CoreStartable { private static final boolean DEFAULT_ENABLED = Build.IS_ENG; private static final String ACTION_FINGERPRINT_WAKE = @@ -62,13 +62,11 @@ public class LatencyTester extends CoreStartable { @Inject public LatencyTester( - Context context, BiometricUnlockController biometricUnlockController, BroadcastDispatcher broadcastDispatcher, DeviceConfigProxy deviceConfigProxy, @Main DelayableExecutor mainExecutor ) { - super(context); mBiometricUnlockController = biometricUnlockController; mBroadcastDispatcher = broadcastDispatcher; mDeviceConfigProxy = deviceConfigProxy; diff --git a/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt new file mode 100644 index 000000000000..4c3a7ff4e2eb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt @@ -0,0 +1,7 @@ +package com.android.systemui + +import com.android.systemui.dump.nano.SystemUIProtoDump + +interface ProtoDumpable : Dumpable { + fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>) +} diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 2e13903814a5..11d579d481c1 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -105,7 +105,7 @@ import kotlin.Pair; * for antialiasing and emulation purposes. */ @SysUISingleton -public class ScreenDecorations extends CoreStartable implements Tunable , Dumpable { +public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { private static final boolean DEBUG = false; private static final String TAG = "ScreenDecorations"; @@ -130,6 +130,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab @VisibleForTesting protected boolean mIsRegistered; private final BroadcastDispatcher mBroadcastDispatcher; + private final Context mContext; private final Executor mMainExecutor; private final TunerService mTunerService; private final SecureSettings mSecureSettings; @@ -308,7 +309,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab ThreadFactory threadFactory, PrivacyDotDecorProviderFactory dotFactory, FaceScanningProviderFactory faceScanningFactory) { - super(context); + mContext = context; mMainExecutor = mainExecutor; mSecureSettings = secureSettings; mBroadcastDispatcher = broadcastDispatcher; @@ -455,7 +456,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } } - boolean needToUpdateProviderViews = false; final String newUniqueId = mDisplayInfo.uniqueId; if (!Objects.equals(newUniqueId, mDisplayUniqueId)) { mDisplayUniqueId = newUniqueId; @@ -473,37 +473,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab setupDecorations(); return; } - - if (mScreenDecorHwcLayer != null) { - updateHwLayerRoundedCornerDrawable(); - updateHwLayerRoundedCornerExistAndSize(); - } - needToUpdateProviderViews = true; - } - - final float newRatio = getPhysicalPixelDisplaySizeRatio(); - if (mRoundedCornerResDelegate.getPhysicalPixelDisplaySizeRatio() != newRatio) { - mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(newRatio); - if (mScreenDecorHwcLayer != null) { - updateHwLayerRoundedCornerExistAndSize(); - } - needToUpdateProviderViews = true; - } - - if (needToUpdateProviderViews) { - updateOverlayProviderViews(null); - } else { - updateOverlayProviderViews(new Integer[] { - mFaceScanningViewId, - R.id.display_cutout, - R.id.display_cutout_left, - R.id.display_cutout_right, - R.id.display_cutout_bottom, - }); - } - - if (mScreenDecorHwcLayer != null) { - mScreenDecorHwcLayer.onDisplayChanged(newUniqueId); } } }; @@ -973,7 +942,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } @Override - protected void onConfigurationChanged(Configuration newConfig) { + public void onConfigurationChanged(Configuration newConfig) { if (DEBUG_DISABLE_SCREEN_DECORATIONS) { Log.i(TAG, "ScreenDecorations is disabled"); return; @@ -1069,9 +1038,11 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab && (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod))) { mRotation = newRotation; mDisplayMode = newMod; + mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio( + getPhysicalPixelDisplaySizeRatio()); if (mScreenDecorHwcLayer != null) { mScreenDecorHwcLayer.pendingConfigChange = false; - mScreenDecorHwcLayer.updateRotation(mRotation); + mScreenDecorHwcLayer.updateConfiguration(mDisplayUniqueId); updateHwLayerRoundedCornerExistAndSize(); updateHwLayerRoundedCornerDrawable(); } @@ -1110,7 +1081,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab context.getResources(), context.getDisplay().getUniqueId()); } - private void updateOverlayProviderViews(@Nullable Integer[] filterIds) { + @VisibleForTesting + void updateOverlayProviderViews(@Nullable Integer[] filterIds) { if (mOverlays == null) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java b/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java index 1f2de4cfc346..5bd85a72b06f 100644 --- a/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java +++ b/packages/SystemUI/src/com/android/systemui/SliceBroadcastRelayHandler.java @@ -38,16 +38,17 @@ import javax.inject.Inject; * @see SliceBroadcastRelay */ @SysUISingleton -public class SliceBroadcastRelayHandler extends CoreStartable { +public class SliceBroadcastRelayHandler implements CoreStartable { private static final String TAG = "SliceBroadcastRelay"; private static final boolean DEBUG = false; private final ArrayMap<Uri, BroadcastRelay> mRelays = new ArrayMap<>(); + private final Context mContext; private final BroadcastDispatcher mBroadcastDispatcher; @Inject public SliceBroadcastRelayHandler(Context context, BroadcastDispatcher broadcastDispatcher) { - super(context); + mContext = context; mBroadcastDispatcher = broadcastDispatcher; } diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index fe6dbe5de8f0..873a695ecd93 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -38,6 +38,8 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; +import androidx.annotation.VisibleForTesting; + import com.android.systemui.animation.Interpolators; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; @@ -66,7 +68,7 @@ public class SwipeHelper implements Gefingerpoken { private static final int MAX_DISMISS_VELOCITY = 4000; // dp/sec private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms - static final float SWIPE_PROGRESS_FADE_END = 0.5f; // fraction of thumbnail width + public static final float SWIPE_PROGRESS_FADE_END = 0.6f; // fraction of thumbnail width // beyond which swipe progress->0 public static final float SWIPED_FAR_ENOUGH_SIZE_FRACTION = 0.6f; static final float MAX_SCROLL_SIZE_FRACTION = 0.3f; @@ -235,7 +237,11 @@ public class SwipeHelper implements Gefingerpoken { return Math.min(Math.max(mMinSwipeProgress, result), mMaxSwipeProgress); } - private float getSwipeAlpha(float progress) { + /** + * Returns the alpha value depending on the progress of the swipe. + */ + @VisibleForTesting + public float getSwipeAlpha(float progress) { if (mFadeDependingOnAmountSwiped) { // The more progress has been fade, the lower the alpha value so that the view fades. return Math.max(1 - progress, 0); @@ -260,7 +266,7 @@ public class SwipeHelper implements Gefingerpoken { animView.setLayerType(View.LAYER_TYPE_NONE, null); } } - animView.setAlpha(getSwipeAlpha(swipeProgress)); + updateSwipeProgressAlpha(animView, getSwipeAlpha(swipeProgress)); } } invalidateGlobalRegion(animView); @@ -561,6 +567,14 @@ public class SwipeHelper implements Gefingerpoken { mCallback.onChildSnappedBack(animView, targetLeft); } + + /** + * Called to update the content alpha while the view is swiped + */ + protected void updateSwipeProgressAlpha(View animView, float alpha) { + animView.setAlpha(alpha); + } + /** * Give the swipe helper itself a chance to do something on snap back so NSSL doesn't have * to tell us what to do diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 9cfd3999a0ec..d9f44cdecf40 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -45,8 +45,6 @@ import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dump.DumpManager; import com.android.systemui.util.NotificationChannels; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.util.Comparator; import java.util.Map; import java.util.TreeMap; @@ -287,14 +285,10 @@ public class SystemUIApplication extends Application implements CoreStartable startable; if (DEBUG) Log.d(TAG, "loading: " + clsName); try { - Constructor<?> constructor = Class.forName(clsName).getConstructor( - Context.class); - startable = (CoreStartable) constructor.newInstance(this); + startable = (CoreStartable) Class.forName(clsName).newInstance(); } catch (ClassNotFoundException - | NoSuchMethodException | IllegalAccessException - | InstantiationException - | InvocationTargetException ex) { + | InstantiationException ex) { throw new RuntimeException(ex); } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java index 7bcba3cc1c46..50e03992df49 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java @@ -121,6 +121,6 @@ public class SystemUIService extends Service { DumpHandler.PRIORITY_ARG_CRITICAL}; } - mDumpHandler.dump(pw, massagedArgs); + mDumpHandler.dump(fd, pw, massagedArgs); } } diff --git a/packages/SystemUI/src/com/android/systemui/VendorServices.java b/packages/SystemUI/src/com/android/systemui/VendorServices.java index 139448c0fab4..a3209396ac27 100644 --- a/packages/SystemUI/src/com/android/systemui/VendorServices.java +++ b/packages/SystemUI/src/com/android/systemui/VendorServices.java @@ -16,15 +16,12 @@ package com.android.systemui; -import android.content.Context; - /** * Placeholder for any vendor-specific services. */ -public class VendorServices extends CoreStartable { +public class VendorServices implements CoreStartable { - public VendorServices(Context context) { - super(context); + public VendorServices() { } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java b/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java index 2ba2bb6edc18..ed6fbecd19fe 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java @@ -45,7 +45,7 @@ class SimpleMirrorWindowControl extends MirrorWindowControl implements View.OnCl private boolean mShouldSetTouchStart; @Nullable private MoveWindowTask mMoveWindowTask; - private PointF mLastDrag = new PointF(); + private final PointF mLastDrag = new PointF(); private final Handler mHandler; SimpleMirrorWindowControl(Context context, Handler handler) { @@ -92,8 +92,7 @@ class SimpleMirrorWindowControl extends MirrorWindowControl implements View.OnCl } private Point findOffset(View v, int moveFrameAmount) { - final Point offset = mTmpPoint; - offset.set(0, 0); + mTmpPoint.set(0, 0); if (v.getId() == R.id.left_control) { mTmpPoint.x = -moveFrameAmount; } else if (v.getId() == R.id.up_control) { @@ -184,7 +183,7 @@ class SimpleMirrorWindowControl extends MirrorWindowControl implements View.OnCl private final int mYOffset; private final Handler mHandler; /** Time in milliseconds between successive task executions.*/ - private long mPeriod; + private final long mPeriod; private boolean mCancel; MoveWindowTask(@NonNull MirrorWindowDelegate windowDelegate, Handler handler, int xOffset, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java index a1288b531955..9f1c9b45e6cd 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java @@ -69,7 +69,7 @@ import dagger.Lazy; * Class to register system actions with accessibility framework. */ @SysUISingleton -public class SystemActions extends CoreStartable { +public class SystemActions implements CoreStartable { private static final String TAG = "SystemActions"; /** @@ -177,6 +177,7 @@ public class SystemActions extends CoreStartable { private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; private final SystemActionsBroadcastReceiver mReceiver; + private final Context mContext; private final Optional<Recents> mRecentsOptional; private Locale mLocale; private final AccessibilityManager mA11yManager; @@ -190,7 +191,7 @@ public class SystemActions extends CoreStartable { NotificationShadeWindowController notificationShadeController, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, Optional<Recents> recentsOptional) { - super(context); + mContext = context; mRecentsOptional = recentsOptional; mReceiver = new SystemActionsBroadcastReceiver(); mLocale = mContext.getResources().getConfiguration().getLocales().get(0); @@ -219,7 +220,6 @@ public class SystemActions extends CoreStartable { @Override public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); final Locale locale = mContext.getResources().getConfiguration().getLocales().get(0); if (!locale.equals(mLocale)) { mLocale = locale; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java index 6e14ddc671a3..ab11fcea7b28 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java @@ -52,11 +52,12 @@ import javax.inject.Inject; * when {@code IStatusBar#requestWindowMagnificationConnection(boolean)} is called. */ @SysUISingleton -public class WindowMagnification extends CoreStartable implements WindowMagnifierCallback, +public class WindowMagnification implements CoreStartable, WindowMagnifierCallback, CommandQueue.Callbacks { private static final String TAG = "WindowMagnification"; private final ModeSwitchesController mModeSwitchesController; + private final Context mContext; private final Handler mHandler; private final AccessibilityManager mAccessibilityManager; private final CommandQueue mCommandQueue; @@ -102,7 +103,7 @@ public class WindowMagnification extends CoreStartable implements WindowMagnifie public WindowMagnification(Context context, @Main Handler mainHandler, CommandQueue commandQueue, ModeSwitchesController modeSwitchesController, SysUiState sysUiState, OverviewProxyService overviewProxyService) { - super(context); + mContext = context; mHandler = mainHandler; mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mCommandQueue = commandQueue; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index ae73e34f5fda..8ded268ed5e0 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -94,7 +94,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private final Context mContext; private final Resources mResources; private final Handler mHandler; - private Rect mWindowBounds; + private final Rect mWindowBounds; private final int mDisplayId; @Surface.Rotation @VisibleForTesting @@ -174,16 +174,16 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; private final MagnificationGestureDetector mGestureDetector; private final int mBounceEffectDuration; - private Choreographer.FrameCallback mMirrorViewGeometryVsyncCallback; + private final Choreographer.FrameCallback mMirrorViewGeometryVsyncCallback; private Locale mLocale; private NumberFormat mPercentFormat; private float mBounceEffectAnimationScale; - private SysUiState mSysUiState; + private final SysUiState mSysUiState; // Set it to true when the view is overlapped with the gesture insets at the bottom. private boolean mOverlapWithGestureInsets; @Nullable - private MirrorWindowControl mMirrorWindowControl; + private final MirrorWindowControl mMirrorWindowControl; WindowMagnificationController(@UiContext Context context, @NonNull Handler handler, @NonNull WindowMagnificationAnimationController animationController, @@ -489,9 +489,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold /** Returns the rotation degree change of two {@link Surface.Rotation} */ private int getDegreeFromRotation(@Surface.Rotation int newRotation, @Surface.Rotation int oldRotation) { - final int rotationDiff = oldRotation - newRotation; - final int degree = (rotationDiff + 4) % 4 * 90; - return degree; + return (oldRotation - newRotation + 4) % 4 * 90; } private void createMirrorWindow() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java index 11353f67a799..403941f8e639 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java @@ -61,8 +61,8 @@ public class AccessibilityFloatingMenuController implements } @Override - public void onKeyguardVisibilityChanged(boolean showing) { - mIsKeyguardVisible = showing; + public void onKeyguardVisibilityChanged(boolean visible) { + mIsKeyguardVisible = visible; handleFloatingMenuVisibility(mIsKeyguardVisible, mBtnMode, mBtnTargets); } diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java index 33e6ca49ddd5..9b441ad1d75c 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java @@ -21,6 +21,8 @@ import android.util.DisplayMetrics; import android.view.Display; import android.view.Surface; +import com.android.systemui.R; + /** * Utility class for determining screen and corner dimensions. */ @@ -82,17 +84,13 @@ public class DisplayUtils { * where the curve ends), in pixels. */ public static int getCornerRadiusBottom(Context context) { - int radius = 0; - - int resourceId = context.getResources().getIdentifier("config_rounded_mask_size_bottom", - "dimen", "com.android.systemui"); - if (resourceId > 0) { - radius = context.getResources().getDimensionPixelSize(resourceId); - } + int radius = context.getResources().getDimensionPixelSize( + R.dimen.config_rounded_mask_size_bottom); if (radius == 0) { radius = getCornerRadiusDefault(context); } + return radius; } @@ -101,28 +99,17 @@ public class DisplayUtils { * the curve ends), in pixels. */ public static int getCornerRadiusTop(Context context) { - int radius = 0; - - int resourceId = context.getResources().getIdentifier("config_rounded_mask_size_top", - "dimen", "com.android.systemui"); - if (resourceId > 0) { - radius = context.getResources().getDimensionPixelSize(resourceId); - } + int radius = context.getResources().getDimensionPixelSize( + R.dimen.config_rounded_mask_size_top); if (radius == 0) { radius = getCornerRadiusDefault(context); } + return radius; } private static int getCornerRadiusDefault(Context context) { - int radius = 0; - - int resourceId = context.getResources().getIdentifier("config_rounded_mask_size", - "dimen", "com.android.systemui"); - if (resourceId > 0) { - radius = context.getResources().getDimensionPixelSize(resourceId); - } - return radius; + return context.getResources().getDimensionPixelSize(R.dimen.config_rounded_mask_size); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 8f5cbb76222f..80c6c48cb7ee 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -127,7 +127,7 @@ public class AuthContainerView extends LinearLayout private final ScrollView mBiometricScrollView; private final View mPanelView; private final float mTranslationY; - @ContainerState private int mContainerState = STATE_UNKNOWN; + @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN; private final Set<Integer> mFailedModalities = new HashSet<Integer>(); private final OnBackInvokedCallback mBackCallback = this::onBackInvoked; @@ -485,45 +485,18 @@ public class AuthContainerView extends LinearLayout mContainerState = STATE_SHOWING; } else { mContainerState = STATE_ANIMATING_IN; - // The background panel and content are different views since we need to be able to - // animate them separately in other places. - mPanelView.setY(mTranslationY); - mBiometricScrollView.setY(mTranslationY); - + setY(mTranslationY); setAlpha(0f); final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS; postOnAnimation(() -> { - mPanelView.animate() - .translationY(0) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mPanelView, SHOW, animateDuration)) - .withLayer() - .withEndAction(this::onDialogAnimatedIn) - .start(); - mBiometricScrollView.animate() - .translationY(0) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mBiometricScrollView, SHOW, animateDuration)) - .withLayer() - .start(); - if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { - mCredentialView.setY(mTranslationY); - mCredentialView.animate() - .translationY(0) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mCredentialView, SHOW, animateDuration)) - .withLayer() - .start(); - } animate() .alpha(1f) + .translationY(0) .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .withLayer() .setListener(getJankListener(this, SHOW, animateDuration)) + .withEndAction(this::onDialogAnimatedIn) .start(); }); } @@ -657,11 +630,25 @@ public class AuthContainerView extends LinearLayout wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle())); } + private void forceExecuteAnimatedIn() { + if (mContainerState == STATE_ANIMATING_IN) { + //clear all animators + if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { + mCredentialView.animate().cancel(); + } + mPanelView.animate().cancel(); + mBiometricView.animate().cancel(); + animate().cancel(); + onDialogAnimatedIn(); + } + } + @Override public void dismissWithoutCallback(boolean animate) { if (animate) { animateAway(false /* sendReason */, 0 /* reason */); } else { + forceExecuteAnimatedIn(); removeWindowIfAttached(); } } @@ -788,32 +775,9 @@ public class AuthContainerView extends LinearLayout final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS; postOnAnimation(() -> { - mPanelView.animate() - .translationY(mTranslationY) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mPanelView, DISMISS, animateDuration)) - .withLayer() - .withEndAction(endActionRunnable) - .start(); - mBiometricScrollView.animate() - .translationY(mTranslationY) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mBiometricScrollView, DISMISS, animateDuration)) - .withLayer() - .start(); - if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { - mCredentialView.animate() - .translationY(mTranslationY) - .setDuration(animateDuration) - .setInterpolator(mLinearOutSlowIn) - .setListener(getJankListener(mCredentialView, DISMISS, animateDuration)) - .withLayer() - .start(); - } animate() .alpha(0f) + .translationY(mTranslationY) .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .setListener(getJankListener(this, DISMISS, animateDuration)) @@ -828,6 +792,7 @@ public class AuthContainerView extends LinearLayout mWindowManager.updateViewLayout(this, lp); }) .withLayer() + .withEndAction(endActionRunnable) .start(); }); } @@ -849,7 +814,7 @@ public class AuthContainerView extends LinearLayout } mContainerState = STATE_GONE; if (isAttachedToWindow()) { - mWindowManager.removeView(this); + mWindowManager.removeViewImmediate(this); } } @@ -875,6 +840,7 @@ public class AuthContainerView extends LinearLayout static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) { final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_SECURE + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_DIM_BEHIND; final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index aae92adc5880..c015a21c7db4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -102,7 +102,7 @@ import kotlin.Unit; * {@link com.android.keyguard.KeyguardUpdateMonitor} */ @SysUISingleton -public class AuthController extends CoreStartable implements CommandQueue.Callbacks, +public class AuthController implements CoreStartable, CommandQueue.Callbacks, AuthDialogCallback, DozeReceiver { private static final String TAG = "AuthController"; @@ -110,6 +110,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba private static final int SENSOR_PRIVACY_DELAY = 500; private final Handler mHandler; + private final Context mContext; private final Execution mExecution; private final CommandQueue mCommandQueue; private final StatusBarStateController mStatusBarStateController; @@ -293,6 +294,8 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba } }); mUdfpsController.setAuthControllerUpdateUdfpsLocation(this::updateUdfpsLocation); + mUdfpsController.setUdfpsDisplayMode(new UdfpsDisplayMode(mContext, mExecution, + this)); mUdfpsBounds = mUdfpsProps.get(0).getLocation().getRect(); } @@ -625,17 +628,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba mUdfpsController.onAodInterrupt(screenX, screenY, major, minor); } - /** - * Cancel a fingerprint scan manually. This will get rid of the white circle on the udfps - * sensor area even if the user hasn't explicitly lifted their finger yet. - */ - public void onCancelUdfps() { - if (mUdfpsController == null) { - return; - } - mUdfpsController.onCancelUdfps(); - } - private void sendResultAndCleanUp(@DismissedReason int reason, @Nullable byte[] credentialAttestation) { if (mReceiver == null) { @@ -669,7 +661,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba @NonNull InteractionJankMonitor jankMonitor, @Main Handler handler, @Background DelayableExecutor bgExecutor) { - super(context); + mContext = context; mExecution = execution; mUserManager = userManager; mLockPatternUtils = lockPatternUtils; @@ -963,8 +955,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba } else { Log.w(TAG, "onBiometricError callback but dialog is gone"); } - - onCancelUdfps(); } @Override @@ -1099,8 +1089,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba } @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); + public void onConfigurationChanged(Configuration newConfig) { updateSensorLocations(); // Save the state of the current dialog (buttons showing, etc) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java index 0892612d1825..76cd3f4c4f1d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java @@ -16,12 +16,21 @@ package com.android.systemui.biometrics; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.view.WindowInsets.Type.ime; + import android.annotation.NonNull; import android.content.Context; +import android.graphics.Insets; import android.os.UserHandle; import android.text.InputType; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnApplyWindowInsetsListener; +import android.view.ViewGroup; +import android.view.WindowInsets; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.ImeAwareEditText; @@ -31,18 +40,24 @@ import com.android.internal.widget.LockPatternChecker; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockscreenCredential; import com.android.internal.widget.VerifyCredentialResponse; +import com.android.systemui.Dumpable; import com.android.systemui.R; +import java.io.PrintWriter; + /** * Pin and Password UI */ public class AuthCredentialPasswordView extends AuthCredentialView - implements TextView.OnEditorActionListener { + implements TextView.OnEditorActionListener, OnApplyWindowInsetsListener, Dumpable { private static final String TAG = "BiometricPrompt/AuthCredentialPasswordView"; private final InputMethodManager mImm; private ImeAwareEditText mPasswordField; + private ViewGroup mAuthCredentialHeader; + private ViewGroup mAuthCredentialInput; + private int mBottomInset = 0; public AuthCredentialPasswordView(Context context, AttributeSet attrs) { @@ -53,6 +68,9 @@ public class AuthCredentialPasswordView extends AuthCredentialView @Override protected void onFinishInflate() { super.onFinishInflate(); + + mAuthCredentialHeader = findViewById(R.id.auth_credential_header); + mAuthCredentialInput = findViewById(R.id.auth_credential_input); mPasswordField = findViewById(R.id.lockPassword); mPasswordField.setOnEditorActionListener(this); // TODO: De-dupe the logic with AuthContainerView @@ -66,6 +84,8 @@ public class AuthCredentialPasswordView extends AuthCredentialView } return true; }); + + setOnApplyWindowInsetsListener(this); } @Override @@ -127,4 +147,92 @@ public class AuthCredentialPasswordView extends AuthCredentialView mPasswordField.setText(""); } } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (mAuthCredentialInput == null || mAuthCredentialHeader == null || mSubtitleView == null + || mDescriptionView == null || mPasswordField == null || mErrorView == null) { + return; + } + + int inputLeftBound; + int inputTopBound; + int headerRightBound = right; + int headerTopBounds = top; + final int subTitleBottom = (mSubtitleView.getVisibility() == GONE) ? mTitleView.getBottom() + : mSubtitleView.getBottom(); + final int descBottom = (mDescriptionView.getVisibility() == GONE) ? subTitleBottom + : mDescriptionView.getBottom(); + if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { + inputTopBound = (bottom - mAuthCredentialInput.getHeight()) / 2; + inputLeftBound = (right - left) / 2; + headerRightBound = inputLeftBound; + headerTopBounds -= Math.min(mIconView.getBottom(), mBottomInset); + } else { + inputTopBound = + descBottom + (bottom - descBottom - mAuthCredentialInput.getHeight()) / 2; + inputLeftBound = (right - left - mAuthCredentialInput.getWidth()) / 2; + } + + if (mDescriptionView.getBottom() > mBottomInset) { + mAuthCredentialHeader.layout(left, headerTopBounds, headerRightBound, bottom); + } + mAuthCredentialInput.layout(inputLeftBound, inputTopBound, right, bottom); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final int newWidth = MeasureSpec.getSize(widthMeasureSpec); + final int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mBottomInset; + + setMeasuredDimension(newWidth, newHeight); + + final int halfWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() / 2, + MeasureSpec.AT_MOST); + final int fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED); + if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { + measureChildren(halfWidthSpec, fullHeightSpec); + } else { + measureChildren(widthMeasureSpec, fullHeightSpec); + } + } + + @NonNull + @Override + public WindowInsets onApplyWindowInsets(@NonNull View v, WindowInsets insets) { + + final Insets bottomInset = insets.getInsets(ime()); + if (v instanceof AuthCredentialPasswordView && mBottomInset != bottomInset.bottom) { + mBottomInset = bottomInset.bottom; + if (mBottomInset > 0 + && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { + mTitleView.setSingleLine(true); + mTitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE); + mTitleView.setMarqueeRepeatLimit(-1); + // select to enable marquee unless a screen reader is enabled + mTitleView.setSelected(!mAccessibilityManager.isEnabled() + || !mAccessibilityManager.isTouchExplorationEnabled()); + } else { + mTitleView.setSingleLine(false); + mTitleView.setEllipsize(null); + // select to enable marquee unless a screen reader is enabled + mTitleView.setSelected(false); + } + requestLayout(); + } + return insets; + } + + @Override + public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { + pw.println(TAG + "State:"); + pw.println(" mBottomInset=" + mBottomInset); + pw.println(" mAuthCredentialHeader size=(" + mAuthCredentialHeader.getWidth() + "," + + mAuthCredentialHeader.getHeight()); + pw.println(" mAuthCredentialInput size=(" + mAuthCredentialInput.getWidth() + "," + + mAuthCredentialInput.getHeight()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java index 11498dbc0b83..f9e44a0c1724 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java @@ -93,7 +93,9 @@ public class AuthCredentialPatternView extends AuthCredentialView { @Override protected void onErrorTimeoutFinish() { super.onErrorTimeoutFinish(); - mLockPatternView.setEnabled(true); + // select to enable marquee unless a screen reader is enabled + mLockPatternView.setEnabled(!mAccessibilityManager.isEnabled() + || !mAccessibilityManager.isTouchExplorationEnabled()); } public AuthCredentialPatternView(Context context, AttributeSet attrs) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java index 4fa835e038ec..5958e6a436f1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java @@ -30,7 +30,6 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.pm.UserInfo; import android.graphics.drawable.Drawable; -import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.PromptInfo; import android.os.AsyncTask; import android.os.CountDownTimer; @@ -77,7 +76,7 @@ public abstract class AuthCredentialView extends LinearLayout { protected final Handler mHandler; protected final LockPatternUtils mLockPatternUtils; - private final AccessibilityManager mAccessibilityManager; + protected final AccessibilityManager mAccessibilityManager; private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; @@ -86,10 +85,10 @@ public abstract class AuthCredentialView extends LinearLayout { private boolean mShouldAnimatePanel; private boolean mShouldAnimateContents; - private TextView mTitleView; - private TextView mSubtitleView; - private TextView mDescriptionView; - private ImageView mIconView; + protected TextView mTitleView; + protected TextView mSubtitleView; + protected TextView mDescriptionView; + protected ImageView mIconView; protected TextView mErrorView; protected @Utils.CredentialType int mCredentialType; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index 4fee0837a52c..4363b88bf21e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -117,7 +117,7 @@ class AuthRippleController @Inject constructor( } fun showUnlockRipple(biometricSourceType: BiometricSourceType?) { - if (!(keyguardUpdateMonitor.isKeyguardVisible || keyguardUpdateMonitor.isDreaming) || + if (!keyguardStateController.isShowing || keyguardUpdateMonitor.userNeedsStrongAuth()) { return } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt index 3ad2bef97ac3..4130cf589310 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt @@ -22,9 +22,9 @@ import com.android.systemui.Dumpable import com.android.systemui.animation.Interpolators import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionListener +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.phone.SystemUIDialogManager -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager import com.android.systemui.util.ViewController import java.io.PrintWriter @@ -41,7 +41,7 @@ import java.io.PrintWriter abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>( view: T, protected val statusBarStateController: StatusBarStateController, - protected val panelExpansionStateManager: PanelExpansionStateManager, + protected val shadeExpansionStateManager: ShadeExpansionStateManager, protected val dialogManager: SystemUIDialogManager, private val dumpManager: DumpManager ) : ViewController<T>(view), Dumpable { @@ -54,7 +54,7 @@ abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>( private var dialogAlphaAnimator: ValueAnimator? = null private val dialogListener = SystemUIDialogManager.Listener { runDialogAlphaAnimator() } - private val panelExpansionListener = PanelExpansionListener { event -> + private val shadeExpansionListener = ShadeExpansionListener { event -> // Notification shade can be expanded but not visible (fraction: 0.0), for example // when a heads-up notification (HUN) is showing. notificationShadeVisible = event.expanded && event.fraction > 0f @@ -108,13 +108,13 @@ abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>( } override fun onViewAttached() { - panelExpansionStateManager.addExpansionListener(panelExpansionListener) + shadeExpansionStateManager.addExpansionListener(shadeExpansionListener) dialogManager.registerListener(dialogListener) dumpManager.registerDumpable(dumpTag, this) } override fun onViewDetached() { - panelExpansionStateManager.removeExpansionListener(panelExpansionListener) + shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener) dialogManager.unregisterListener(dialogListener) dumpManager.unregisterDumpable(dumpTag) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt index 4cd40d2f186b..e6aeb43d9d9e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt @@ -17,8 +17,8 @@ package com.android.systemui.biometrics import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.phone.SystemUIDialogManager -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager /** * Class that coordinates non-HBM animations for biometric prompt. @@ -26,13 +26,13 @@ import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManage class UdfpsBpViewController( view: UdfpsBpView, statusBarStateController: StatusBarStateController, - panelExpansionStateManager: PanelExpansionStateManager, + shadeExpansionStateManager: ShadeExpansionStateManager, systemUIDialogManager: SystemUIDialogManager, dumpManager: DumpManager ) : UdfpsAnimationViewController<UdfpsBpView>( view, statusBarStateController, - panelExpansionStateManager, + shadeExpansionStateManager, systemUIDialogManager, dumpManager ) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 412dc0577876..b49d4523a765 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -63,12 +63,12 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.SystemUIDialogManager; import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -111,7 +111,7 @@ public class UdfpsController implements DozeReceiver { private final WindowManager mWindowManager; private final DelayableExecutor mFgExecutor; @NonNull private final Executor mBiometricExecutor; - @NonNull private final PanelExpansionStateManager mPanelExpansionStateManager; + @NonNull private final ShadeExpansionStateManager mShadeExpansionStateManager; @NonNull private final StatusBarStateController mStatusBarStateController; @NonNull private final KeyguardStateController mKeyguardStateController; @NonNull private final StatusBarKeyguardViewManager mKeyguardViewManager; @@ -123,7 +123,6 @@ public class UdfpsController implements DozeReceiver { @NonNull private final PowerManager mPowerManager; @NonNull private final AccessibilityManager mAccessibilityManager; @NonNull private final LockscreenShadeTransitionController mLockscreenShadeTransitionController; - @Nullable private final UdfpsDisplayModeProvider mUdfpsDisplayMode; @NonNull private final ConfigurationController mConfigurationController; @NonNull private final SystemClock mSystemClock; @NonNull private final UnlockedScreenOffAnimationController @@ -139,6 +138,7 @@ public class UdfpsController implements DozeReceiver { // TODO(b/229290039): UDFPS controller should manage its dimensions on its own. Remove this. @Nullable private Runnable mAuthControllerUpdateUdfpsLocation; @Nullable private final AlternateUdfpsTouchProvider mAlternateTouchProvider; + @Nullable private UdfpsDisplayMode mUdfpsDisplayMode; // Tracks the velocity of a touch to help filter out the touches that move too fast. @Nullable private VelocityTracker mVelocityTracker; @@ -205,7 +205,7 @@ public class UdfpsController implements DozeReceiver { mFgExecutor.execute(() -> UdfpsController.this.showUdfpsOverlay( new UdfpsControllerOverlay(mContext, mFingerprintManager, mInflater, mWindowManager, mAccessibilityManager, mStatusBarStateController, - mPanelExpansionStateManager, mKeyguardViewManager, + mShadeExpansionStateManager, mKeyguardViewManager, mKeyguardUpdateMonitor, mDialogManager, mDumpManager, mLockscreenShadeTransitionController, mConfigurationController, mSystemClock, mKeyguardStateController, @@ -245,7 +245,7 @@ public class UdfpsController implements DozeReceiver { mAcquiredReceived = true; final UdfpsView view = mOverlay.getOverlayView(); if (view != null) { - view.unconfigureDisplay(); + unconfigureDisplay(view); } if (acquiredGood) { mOverlay.onAcquiredGood(); @@ -319,6 +319,10 @@ public class UdfpsController implements DozeReceiver { mAuthControllerUpdateUdfpsLocation = r; } + public void setUdfpsDisplayMode(UdfpsDisplayMode udfpsDisplayMode) { + mUdfpsDisplayMode = udfpsDisplayMode; + } + /** * Calculate the pointer speed given a velocity tracker and the pointer id. * This assumes that the velocity tracker has already been passed all relevant motion events. @@ -582,7 +586,7 @@ public class UdfpsController implements DozeReceiver { @NonNull WindowManager windowManager, @NonNull StatusBarStateController statusBarStateController, @Main DelayableExecutor fgExecutor, - @NonNull PanelExpansionStateManager panelExpansionStateManager, + @NonNull ShadeExpansionStateManager shadeExpansionStateManager, @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager, @NonNull DumpManager dumpManager, @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -594,7 +598,6 @@ public class UdfpsController implements DozeReceiver { @NonNull VibratorHelper vibrator, @NonNull UdfpsHapticsSimulator udfpsHapticsSimulator, @NonNull UdfpsShell udfpsShell, - @NonNull Optional<UdfpsDisplayModeProvider> udfpsDisplayMode, @NonNull KeyguardStateController keyguardStateController, @NonNull DisplayManager displayManager, @Main Handler mainHandler, @@ -615,7 +618,7 @@ public class UdfpsController implements DozeReceiver { mFingerprintManager = checkNotNull(fingerprintManager); mWindowManager = windowManager; mFgExecutor = fgExecutor; - mPanelExpansionStateManager = panelExpansionStateManager; + mShadeExpansionStateManager = shadeExpansionStateManager; mStatusBarStateController = statusBarStateController; mKeyguardStateController = keyguardStateController; mKeyguardViewManager = statusBarKeyguardViewManager; @@ -626,7 +629,6 @@ public class UdfpsController implements DozeReceiver { mPowerManager = powerManager; mAccessibilityManager = accessibilityManager; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; - mUdfpsDisplayMode = udfpsDisplayMode.orElse(null); screenLifecycle.addObserver(mScreenObserver); mScreenOn = screenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_ON; mConfigurationController = configurationController; @@ -735,6 +737,19 @@ public class UdfpsController implements DozeReceiver { mOverlay = null; mOrientationListener.disable(); + + } + + private void unconfigureDisplay(@NonNull UdfpsView view) { + if (view.isDisplayConfigured()) { + view.unconfigureDisplay(); + + if (mCancelAodTimeoutAction != null) { + mCancelAodTimeoutAction.run(); + mCancelAodTimeoutAction = null; + } + mIsAodInterruptActive = false; + } } /** @@ -773,7 +788,7 @@ public class UdfpsController implements DozeReceiver { // ACTION_UP/ACTION_CANCEL, we need to be careful about not letting the screen // accidentally remain in high brightness mode. As a mitigation, queue a call to // cancel the fingerprint scan. - mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::onCancelUdfps, + mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::cancelAodInterrupt, AOD_INTERRUPT_TIMEOUT_MILLIS); // using a hard-coded value for major and minor until it is available from the sensor onFingerDown(requestId, screenX, screenY, minor, major); @@ -800,26 +815,22 @@ public class UdfpsController implements DozeReceiver { } /** - * Cancel UDFPS affordances - ability to hide the UDFPS overlay before the user explicitly - * lifts their finger. Generally, this should be called on errors in the authentication flow. - * - * The sensor that triggers an AOD fingerprint interrupt (see onAodInterrupt) doesn't give - * ACTION_UP/ACTION_CANCEL events, so and AOD interrupt scan needs to be cancelled manually. + * The sensor that triggers {@link #onAodInterrupt} doesn't emit ACTION_UP or ACTION_CANCEL + * events, which means the fingerprint gesture created by the AOD interrupt needs to be + * cancelled manually. * This should be called when authentication either succeeds or fails. Failing to cancel the * scan will leave the display in the UDFPS mode until the user lifts their finger. On optical * sensors, this can result in illumination persisting for longer than necessary. */ - void onCancelUdfps() { - if (mOverlay != null && mOverlay.getOverlayView() != null) { - onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView()); - } + @VisibleForTesting + void cancelAodInterrupt() { if (!mIsAodInterruptActive) { return; } - if (mCancelAodTimeoutAction != null) { - mCancelAodTimeoutAction.run(); - mCancelAodTimeoutAction = null; + if (mOverlay != null && mOverlay.getOverlayView() != null) { + onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView()); } + mCancelAodTimeoutAction = null; mIsAodInterruptActive = false; } @@ -909,15 +920,8 @@ public class UdfpsController implements DozeReceiver { } } mOnFingerDown = false; - if (view.isDisplayConfigured()) { - view.unconfigureDisplay(); - } + unconfigureDisplay(view); - if (mCancelAodTimeoutAction != null) { - mCancelAodTimeoutAction.run(); - mCancelAodTimeoutAction = null; - } - mIsAodInterruptActive = false; } /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 1c62f8a4e508..7d0109686351 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -21,13 +21,18 @@ import android.annotation.UiThread import android.content.Context import android.graphics.PixelFormat import android.graphics.Rect -import android.hardware.biometrics.BiometricOverlayConstants +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR import android.hardware.biometrics.BiometricOverlayConstants.ShowReason import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.IUdfpsOverlayControllerCallback +import android.os.Build import android.os.RemoteException +import android.provider.Settings import android.util.Log import android.util.RotationUtils import android.view.LayoutInflater @@ -38,36 +43,40 @@ import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener import androidx.annotation.LayoutRes +import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.time.SystemClock private const val TAG = "UdfpsControllerOverlay" +@VisibleForTesting +const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui" + /** * Keeps track of the overlay state and UI resources associated with a single FingerprintService * request. This state can persist across configuration changes via the [show] and [hide] * methods. */ @UiThread -class UdfpsControllerOverlay( +class UdfpsControllerOverlay @JvmOverloads constructor( private val context: Context, fingerprintManager: FingerprintManager, private val inflater: LayoutInflater, private val windowManager: WindowManager, private val accessibilityManager: AccessibilityManager, private val statusBarStateController: StatusBarStateController, - private val panelExpansionStateManager: PanelExpansionStateManager, + private val shadeExpansionStateManager: ShadeExpansionStateManager, private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val dialogManager: SystemUIDialogManager, @@ -82,7 +91,8 @@ class UdfpsControllerOverlay( @ShowReason val requestReason: Int, private val controllerCallback: IUdfpsOverlayControllerCallback, private val onTouch: (View, MotionEvent, Boolean) -> Boolean, - private val activityLaunchAnimator: ActivityLaunchAnimator + private val activityLaunchAnimator: ActivityLaunchAnimator, + private val isDebuggable: Boolean = Build.IS_DEBUGGABLE ) { /** The view, when [isShowing], or null. */ var overlayView: UdfpsView? = null @@ -102,18 +112,19 @@ class UdfpsControllerOverlay( gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or - WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) + WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY // Avoid announcing window title. accessibilityTitle = " " } /** A helper if the [requestReason] was due to enrollment. */ - val enrollHelper: UdfpsEnrollHelper? = if (requestReason.isEnrollmentReason()) { - UdfpsEnrollHelper(context, fingerprintManager, requestReason) - } else { - null - } + val enrollHelper: UdfpsEnrollHelper? = + if (requestReason.isEnrollmentReason() && !shouldRemoveEnrollmentUi()) { + UdfpsEnrollHelper(context, fingerprintManager, requestReason) + } else { + null + } /** If the overlay is currently showing. */ val isShowing: Boolean @@ -129,6 +140,17 @@ class UdfpsControllerOverlay( private var touchExplorationEnabled = false + private fun shouldRemoveEnrollmentUi(): Boolean { + if (isDebuggable) { + return Settings.Global.getInt( + context.contentResolver, + SETTING_REMOVE_ENROLLMENT_UI, + 0 /* def */ + ) != 0 + } + return false + } + /** Show the overlay or return false and do nothing if it is already showing. */ @SuppressLint("ClickableViewAccessibility") fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean { @@ -183,7 +205,18 @@ class UdfpsControllerOverlay( view: UdfpsView, controller: UdfpsController ): UdfpsAnimationViewController<*>? { - return when (requestReason) { + val isEnrollment = when (requestReason) { + REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true + else -> false + } + + val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) { + REASON_AUTH_OTHER + } else { + requestReason + } + + return when (filteredRequestReason) { REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> { UdfpsEnrollViewController( @@ -192,17 +225,17 @@ class UdfpsControllerOverlay( }, enrollHelper ?: throw IllegalStateException("no enrollment helper"), statusBarStateController, - panelExpansionStateManager, + shadeExpansionStateManager, dialogManager, dumpManager, overlayParams.scaleFactor ) } - BiometricOverlayConstants.REASON_AUTH_KEYGUARD -> { + REASON_AUTH_KEYGUARD -> { UdfpsKeyguardViewController( view.addUdfpsView(R.layout.udfps_keyguard_view), statusBarStateController, - panelExpansionStateManager, + shadeExpansionStateManager, statusBarKeyguardViewManager, keyguardUpdateMonitor, dumpManager, @@ -216,22 +249,22 @@ class UdfpsControllerOverlay( activityLaunchAnimator ) } - BiometricOverlayConstants.REASON_AUTH_BP -> { + REASON_AUTH_BP -> { // note: empty controller, currently shows no visual affordance UdfpsBpViewController( view.addUdfpsView(R.layout.udfps_bp_view), statusBarStateController, - panelExpansionStateManager, + shadeExpansionStateManager, dialogManager, dumpManager ) } - BiometricOverlayConstants.REASON_AUTH_OTHER, - BiometricOverlayConstants.REASON_AUTH_SETTINGS -> { + REASON_AUTH_OTHER, + REASON_AUTH_SETTINGS -> { UdfpsFpmOtherViewController( view.addUdfpsView(R.layout.udfps_fpm_other_view), statusBarStateController, - panelExpansionStateManager, + shadeExpansionStateManager, dialogManager, dumpManager ) @@ -440,4 +473,4 @@ private fun Int.isEnrollmentReason() = private fun Int.isImportantForAccessibility() = this == REASON_ENROLL_FIND_SENSOR || this == REASON_ENROLL_ENROLLING || - this == BiometricOverlayConstants.REASON_AUTH_BP + this == REASON_AUTH_BP diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt new file mode 100644 index 000000000000..e9de7ccc7b4f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt @@ -0,0 +1,88 @@ +package com.android.systemui.biometrics + +import android.content.Context +import android.os.RemoteException +import android.os.Trace +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.util.concurrency.Execution +import javax.inject.Inject + +private const val TAG = "UdfpsDisplayMode" + +/** + * UdfpsDisplayMode that encapsulates pixel-specific code, such as enabling the high-brightness mode + * (HBM) in a display-specific way and freezing the display's refresh rate. + */ +@SysUISingleton +class UdfpsDisplayMode +@Inject +constructor( + private val context: Context, + private val execution: Execution, + private val authController: AuthController +) : UdfpsDisplayModeProvider { + + // The request is reset to null after it's processed. + private var currentRequest: Request? = null + + override fun enable(onEnabled: Runnable?) { + execution.isMainThread() + Log.v(TAG, "enable") + + if (currentRequest != null) { + Log.e(TAG, "enable | already requested") + return + } + if (authController.udfpsHbmListener == null) { + Log.e(TAG, "enable | mDisplayManagerCallback is null") + return + } + + Trace.beginSection("UdfpsDisplayMode.enable") + + // Track this request in one object. + val request = Request(context.displayId) + currentRequest = request + + try { + // This method is a misnomer. It has nothing to do with HBM, its purpose is to set + // the appropriate display refresh rate. + authController.udfpsHbmListener!!.onHbmEnabled(request.displayId) + Log.v(TAG, "enable | requested optimal refresh rate for UDFPS") + } catch (e: RemoteException) { + Log.e(TAG, "enable", e) + } + + onEnabled?.run() ?: Log.w(TAG, "enable | onEnabled is null") + Trace.endSection() + } + + override fun disable(onDisabled: Runnable?) { + execution.isMainThread() + Log.v(TAG, "disable") + + val request = currentRequest + if (request == null) { + Log.w(TAG, "disable | already disabled") + return + } + + Trace.beginSection("UdfpsDisplayMode.disable") + + try { + // Allow DisplayManager to unset the UDFPS refresh rate. + authController.udfpsHbmListener!!.onHbmDisabled(request.displayId) + Log.v(TAG, "disable | removed the UDFPS refresh rate request") + } catch (e: RemoteException) { + Log.e(TAG, "disable", e) + } + + currentRequest = null + onDisabled?.run() ?: Log.w(TAG, "disable | onDisabled is null") + Trace.endSection() + } +} + +/** Tracks a request to enable the UDFPS mode. */ +private data class Request(val displayId: Int) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java index 0b7bddee41fa..e01273f2a092 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java @@ -22,8 +22,8 @@ import android.graphics.PointF; import com.android.systemui.R; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.phone.SystemUIDialogManager; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; /** * Class that coordinates non-HBM animations during enrollment. @@ -54,11 +54,11 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController<Udfp @NonNull UdfpsEnrollView view, @NonNull UdfpsEnrollHelper enrollHelper, @NonNull StatusBarStateController statusBarStateController, - @NonNull PanelExpansionStateManager panelExpansionStateManager, + @NonNull ShadeExpansionStateManager shadeExpansionStateManager, @NonNull SystemUIDialogManager systemUIDialogManager, @NonNull DumpManager dumpManager, float scaleFactor) { - super(view, statusBarStateController, panelExpansionStateManager, systemUIDialogManager, + super(view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager, dumpManager); mEnrollProgressBarRadius = (int) (scaleFactor * getContext().getResources().getInteger( R.integer.config_udfpsEnrollProgressBar)); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt index 98205cfb7966..7c232789bcac 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt @@ -17,8 +17,8 @@ package com.android.systemui.biometrics import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.phone.SystemUIDialogManager -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager /** * Class that coordinates non-HBM animations for non keyguard, enrollment or biometric prompt @@ -29,13 +29,13 @@ import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManage class UdfpsFpmOtherViewController( view: UdfpsFpmOtherView, statusBarStateController: StatusBarStateController, - panelExpansionStateManager: PanelExpansionStateManager, + shadeExpansionStateManager: ShadeExpansionStateManager, systemUIDialogManager: SystemUIDialogManager, dumpManager: DumpManager ) : UdfpsAnimationViewController<UdfpsFpmOtherView>( view, statusBarStateController, - panelExpansionStateManager, + shadeExpansionStateManager, systemUIDialogManager, dumpManager ) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java index 24b893340ae0..4d7f89d7b727 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java @@ -31,6 +31,9 @@ import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.Interpolators; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionChangeEvent; +import com.android.systemui.shade.ShadeExpansionListener; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; @@ -38,9 +41,6 @@ import com.android.systemui.statusbar.phone.KeyguardBouncer; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.SystemUIDialogManager; import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.time.SystemClock; @@ -88,7 +88,7 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud protected UdfpsKeyguardViewController( @NonNull UdfpsKeyguardView view, @NonNull StatusBarStateController statusBarStateController, - @NonNull PanelExpansionStateManager panelExpansionStateManager, + @NonNull ShadeExpansionStateManager shadeExpansionStateManager, @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager, @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, @NonNull DumpManager dumpManager, @@ -100,7 +100,7 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud @NonNull SystemUIDialogManager systemUIDialogManager, @NonNull UdfpsController udfpsController, @NonNull ActivityLaunchAnimator activityLaunchAnimator) { - super(view, statusBarStateController, panelExpansionStateManager, systemUIDialogManager, + super(view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager, dumpManager); mKeyguardViewManager = statusBarKeyguardViewManager; mKeyguardUpdateMonitor = keyguardUpdateMonitor; @@ -153,7 +153,7 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud mQsExpansion = mKeyguardViewManager.getQsExpansion(); updateGenericBouncerVisibility(); mConfigurationController.addCallback(mConfigurationListener); - getPanelExpansionStateManager().addExpansionListener(mPanelExpansionListener); + getShadeExpansionStateManager().addExpansionListener(mShadeExpansionListener); updateScaleFactor(); mView.updatePadding(); updateAlpha(); @@ -174,7 +174,7 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud mKeyguardViewManager.removeAlternateAuthInterceptor(mAlternateAuthInterceptor); mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false); mConfigurationController.removeCallback(mConfigurationListener); - getPanelExpansionStateManager().removeExpansionListener(mPanelExpansionListener); + getShadeExpansionStateManager().removeExpansionListener(mShadeExpansionListener); if (mLockScreenShadeTransitionController.getUdfpsKeyguardViewController() == this) { mLockScreenShadeTransitionController.setUdfpsKeyguardViewController(null); } @@ -219,7 +219,7 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud mView.animateInUdfpsBouncer(null); } - if (mKeyguardViewManager.isOccluded()) { + if (mKeyguardStateController.isOccluded()) { mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true); } @@ -502,9 +502,9 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<Ud } }; - private final PanelExpansionListener mPanelExpansionListener = new PanelExpansionListener() { + private final ShadeExpansionListener mShadeExpansionListener = new ShadeExpansionListener() { @Override - public void onPanelExpansionChanged(PanelExpansionChangeEvent event) { + public void onPanelExpansionChanged(ShadeExpansionChangeEvent event) { float fraction = event.getFraction(); mPanelExpansionFraction = mKeyguardViewManager.isBouncerInTransit() ? BouncerPanelExpansionCalculator diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt index 96af42bfda22..d99625a9fbf2 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt @@ -17,9 +17,9 @@ package com.android.systemui.bluetooth import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.BluetoothLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Helper class for logging bluetooth events. */ diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java index 9b7d49883222..8e062bd69d63 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java @@ -17,15 +17,11 @@ package com.android.systemui.bluetooth; import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.os.Bundle; -import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.Window; -import android.view.WindowManager; import android.widget.Button; import android.widget.TextView; @@ -33,7 +29,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; -import com.android.systemui.media.MediaDataUtils; +import com.android.systemui.media.controls.util.MediaDataUtils; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.statusbar.phone.SystemUIDialog; diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/ActionReceiver.kt b/packages/SystemUI/src/com/android/systemui/broadcast/ActionReceiver.kt index ca36375c2cde..379c81907acc 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/ActionReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/ActionReceiver.kt @@ -52,7 +52,7 @@ class ActionReceiver( private val userId: Int, private val registerAction: BroadcastReceiver.(IntentFilter) -> Unit, private val unregisterAction: BroadcastReceiver.() -> Unit, - private val bgExecutor: Executor, + private val workerExecutor: Executor, private val logger: BroadcastDispatcherLogger, private val testPendingRemovalAction: (BroadcastReceiver, Int) -> Boolean ) : BroadcastReceiver(), Dumpable { @@ -112,7 +112,7 @@ class ActionReceiver( val id = index.getAndIncrement() logger.logBroadcastReceived(id, userId, intent) // Immediately return control to ActivityManager - bgExecutor.execute { + workerExecutor.execute { receiverDatas.forEach { if (it.filter.matchCategories(intent.categories) == null && !testPendingRemovalAction(it.receiver, userId)) { @@ -138,4 +138,4 @@ class ActionReceiver( println("Categories: ${activeCategories.joinToString(", ")}") } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt index eb8cb47c2671..537cbc5a267d 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt @@ -34,7 +34,8 @@ import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.BroadcastRunning +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.settings.UserTracker import java.io.PrintWriter @@ -55,7 +56,6 @@ private const val MSG_ADD_RECEIVER = 0 private const val MSG_REMOVE_RECEIVER = 1 private const val MSG_REMOVE_RECEIVER_FOR_USER = 2 private const val TAG = "BroadcastDispatcher" -private const val DEBUG = true /** * SystemUI master Broadcast Dispatcher. @@ -73,15 +73,16 @@ private const val DEBUG = true @SysUISingleton open class BroadcastDispatcher @Inject constructor( private val context: Context, - @Background private val bgLooper: Looper, - @Background private val bgExecutor: Executor, + @Main private val mainExecutor: Executor, + @BroadcastRunning private val broadcastLooper: Looper, + @BroadcastRunning private val broadcastExecutor: Executor, private val dumpManager: DumpManager, private val logger: BroadcastDispatcherLogger, private val userTracker: UserTracker, private val removalPendingStore: PendingRemovalStore ) : Dumpable { - // Only modify in BG thread + // Only modify in BroadcastRunning thread private val receiversByUser = SparseArray<UserBroadcastDispatcher>(20) fun initialize() { @@ -148,7 +149,7 @@ open class BroadcastDispatcher @Inject constructor( val data = ReceiverData( receiver, filter, - executor ?: context.mainExecutor, + executor ?: mainExecutor, user ?: context.user, permission ) @@ -181,7 +182,7 @@ open class BroadcastDispatcher @Inject constructor( registerReceiver( receiver, filter, - bgExecutor, + broadcastExecutor, user, flags, permission, @@ -246,8 +247,8 @@ open class BroadcastDispatcher @Inject constructor( UserBroadcastDispatcher( context, userId, - bgLooper, - bgExecutor, + broadcastLooper, + broadcastExecutor, logger, removalPendingStore ) @@ -265,7 +266,7 @@ open class BroadcastDispatcher @Inject constructor( ipw.decreaseIndent() } - private val handler = object : Handler(bgLooper) { + private val handler = object : Handler(broadcastLooper) { override fun handleMessage(msg: Message) { when (msg.what) { diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt index d7b263a323ca..c536e81ef20a 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcherStartable.kt @@ -16,16 +16,14 @@ package com.android.systemui.broadcast -import android.content.Context import com.android.systemui.CoreStartable import javax.inject.Inject class BroadcastDispatcherStartable @Inject constructor( - context: Context, val broadcastDispatcher: BroadcastDispatcher -) : CoreStartable(context) { +) : CoreStartable { override fun start() { broadcastDispatcher.initialize() } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt index 6b15188cdb2a..22dc94a2c3f9 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt @@ -16,6 +16,7 @@ package com.android.systemui.broadcast +import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.os.Handler @@ -46,8 +47,8 @@ private const val DEBUG = false open class UserBroadcastDispatcher( private val context: Context, private val userId: Int, - private val bgLooper: Looper, - private val bgExecutor: Executor, + private val workerLooper: Looper, + private val workerExecutor: Executor, private val logger: BroadcastDispatcherLogger, private val removalPendingStore: PendingRemovalStore ) : Dumpable { @@ -66,9 +67,11 @@ open class UserBroadcastDispatcher( val permission: String? ) - private val bgHandler = Handler(bgLooper) + private val wrongThreadErrorMsg = "This method should only be called from the worker thread " + + "(which is expected to be the BroadcastRunning thread)" + private val workerHandler = Handler(workerLooper) - // Only modify in BG thread + // Only modify in BroadcastRunning thread @VisibleForTesting internal val actionsToActionsReceivers = ArrayMap<ReceiverProperties, ActionReceiver>() private val receiverToActions = ArrayMap<BroadcastReceiver, MutableSet<String>>() @@ -97,8 +100,7 @@ open class UserBroadcastDispatcher( } private fun handleRegisterReceiver(receiverData: ReceiverData, flags: Int) { - Preconditions.checkState(bgLooper.isCurrentThread, - "This method should only be called from BG thread") + Preconditions.checkState(workerLooper.isCurrentThread, wrongThreadErrorMsg) if (DEBUG) Log.w(TAG, "Register receiver: ${receiverData.receiver}") receiverToActions .getOrPut(receiverData.receiver, { ArraySet() }) @@ -113,6 +115,7 @@ open class UserBroadcastDispatcher( logger.logReceiverRegistered(userId, receiverData.receiver, flags) } + @SuppressLint("RegisterReceiverViaContextDetector") @VisibleForTesting internal open fun createActionReceiver( action: String, @@ -128,7 +131,7 @@ open class UserBroadcastDispatcher( UserHandle.of(userId), it, permission, - bgHandler, + workerHandler, flags ) logger.logContextReceiverRegistered(userId, flags, it) @@ -143,15 +146,14 @@ open class UserBroadcastDispatcher( IllegalStateException(e)) } }, - bgExecutor, + workerExecutor, logger, removalPendingStore::isPendingRemoval ) } private fun handleUnregisterReceiver(receiver: BroadcastReceiver) { - Preconditions.checkState(bgLooper.isCurrentThread, - "This method should only be called from BG thread") + Preconditions.checkState(workerLooper.isCurrentThread, wrongThreadErrorMsg) if (DEBUG) Log.w(TAG, "Unregister receiver: $receiver") receiverToActions.getOrDefault(receiver, mutableSetOf()).forEach { actionsToActionsReceivers.forEach { (key, value) -> diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt index 5b3a982ab5e2..d27708fc04d7 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt @@ -20,11 +20,11 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogMessage +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogMessage import com.android.systemui.log.dagger.BroadcastDispatcherLog import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index d53e56f7b852..500f28004429 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -18,6 +18,7 @@ package com.android.systemui.classifier; import static com.android.systemui.classifier.Classifier.BACK_GESTURE; import static com.android.systemui.classifier.Classifier.GENERIC; +import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR; import static com.android.systemui.classifier.FalsingManagerProxy.FALSING_SUCCESS; import static com.android.systemui.classifier.FalsingModule.BRIGHT_LINE_GESTURE_CLASSIFERS; @@ -220,6 +221,11 @@ public class BrightLineFalsingManager implements FalsingManager { return r; }).collect(Collectors.toList()); + // check for false tap if it is a seekbar interaction + if (interactionType == MEDIA_SEEKBAR) { + localResult[0] &= isFalseTap(LOW_PENALTY); + } + logDebug("False Gesture (type: " + interactionType + "): " + localResult[0]); return localResult[0]; diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java index c2922968b58e..701df8981ca2 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java @@ -45,6 +45,7 @@ public abstract class Classifier { public static final int QS_SWIPE_SIDE = 15; public static final int BACK_GESTURE = 16; public static final int QS_SWIPE_NESTED = 17; + public static final int MEDIA_SEEKBAR = 18; @IntDef({ QUICK_SETTINGS, @@ -65,7 +66,8 @@ public abstract class Classifier { LOCK_ICON, QS_SWIPE_SIDE, QS_SWIPE_NESTED, - BACK_GESTURE + BACK_GESTURE, + MEDIA_SEEKBAR, }) @Retention(RetentionPolicy.SOURCE) public @interface InteractionType {} diff --git a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java index 5e4f149d3ca3..f8ee49add04b 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java @@ -23,6 +23,7 @@ import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHT import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_DISTANCE_VERTICAL_FLING_THRESHOLD_IN; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_DISTANCE_VERTICAL_SWIPE_THRESHOLD_IN; import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER; +import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR; import static com.android.systemui.classifier.Classifier.QS_COLLAPSE; import static com.android.systemui.classifier.Classifier.QS_SWIPE_NESTED; import static com.android.systemui.classifier.Classifier.SHADE_DRAG; @@ -153,6 +154,7 @@ class DistanceClassifier extends FalsingClassifier { @Classifier.InteractionType int interactionType, double historyBelief, double historyConfidence) { if (interactionType == BRIGHTNESS_SLIDER + || interactionType == MEDIA_SEEKBAR || interactionType == SHADE_DRAG || interactionType == QS_COLLAPSE || interactionType == Classifier.UDFPS_AUTHENTICATION diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java index 3871248eccd5..858bac30880b 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java @@ -44,9 +44,6 @@ public interface FalsingCollector { void onQsDown(); /** */ - void setQsExpanded(boolean expanded); - - /** */ boolean shouldEnforceBouncer(); /** */ diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java index 28aac051c66d..0b7d6ab5acf7 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java @@ -49,10 +49,6 @@ public class FalsingCollectorFake implements FalsingCollector { } @Override - public void setQsExpanded(boolean expanded) { - } - - @Override public boolean shouldEnforceBouncer() { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java index f5f9655ef24b..da3d293d543b 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java @@ -23,6 +23,8 @@ import android.hardware.biometrics.BiometricSourceType; import android.util.Log; import android.view.MotionEvent; +import androidx.annotation.VisibleForTesting; + import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.dagger.SysUISingleton; @@ -30,6 +32,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; @@ -133,6 +136,7 @@ class FalsingCollectorImpl implements FalsingCollector { ProximitySensor proximitySensor, StatusBarStateController statusBarStateController, KeyguardStateController keyguardStateController, + ShadeExpansionStateManager shadeExpansionStateManager, BatteryController batteryController, DockManager dockManager, @Main DelayableExecutor mainExecutor, @@ -157,6 +161,8 @@ class FalsingCollectorImpl implements FalsingCollector { mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback); + shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged); + mBatteryController.addCallback(mBatteryListener); mDockManager.addListener(mDockEventListener); } @@ -193,8 +199,8 @@ class FalsingCollectorImpl implements FalsingCollector { public void onQsDown() { } - @Override - public void setQsExpanded(boolean expanded) { + @VisibleForTesting + void onQsExpansionChanged(Boolean expanded) { if (expanded) { unregisterSensors(); } else if (mSessionStarted) { diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java index 07f94e792a93..e8c83b1e49dd 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java @@ -18,6 +18,7 @@ package com.android.systemui.classifier; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_PROXIMITY_PERCENT_COVERED_THRESHOLD; import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER; +import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR; import static com.android.systemui.classifier.Classifier.QS_COLLAPSE; import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE; import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; @@ -119,7 +120,8 @@ class ProximityClassifier extends FalsingClassifier { @Classifier.InteractionType int interactionType, double historyBelief, double historyConfidence) { if (interactionType == QUICK_SETTINGS || interactionType == BRIGHTNESS_SLIDER - || interactionType == QS_COLLAPSE || interactionType == QS_SWIPE_SIDE) { + || interactionType == QS_COLLAPSE || interactionType == QS_SWIPE_SIDE + || interactionType == MEDIA_SEEKBAR) { return Result.passed(0); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java index 776bc88ad6bf..f576a5af8907 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java @@ -20,6 +20,7 @@ package com.android.systemui.classifier; import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER; import static com.android.systemui.classifier.Classifier.LEFT_AFFORDANCE; +import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR; import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS; import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN; import static com.android.systemui.classifier.Classifier.PULSE_EXPAND; @@ -93,6 +94,10 @@ public class TypeClassifier extends FalsingClassifier { case QS_SWIPE_NESTED: wrongDirection = !vertical; break; + case MEDIA_SEEKBAR: + confidence = 0; + wrongDirection = vertical; + break; default: wrongDirection = true; break; diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java index de2bdf7ded75..840982cbcc64 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java @@ -22,6 +22,7 @@ import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHT import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_ZIGZAG_Y_SECONDARY_DEVIANCE; import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER; import static com.android.systemui.classifier.Classifier.LOCK_ICON; +import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR; import static com.android.systemui.classifier.Classifier.SHADE_DRAG; import android.graphics.Point; @@ -91,6 +92,7 @@ class ZigZagClassifier extends FalsingClassifier { @Classifier.InteractionType int interactionType, double historyBelief, double historyConfidence) { if (interactionType == BRIGHTNESS_SLIDER + || interactionType == MEDIA_SEEKBAR || interactionType == SHADE_DRAG || interactionType == LOCK_ICON) { return Result.passed(0); diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java index f526277a0a37..82e570438dab 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java @@ -31,16 +31,19 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.systemui.CoreStartable; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.util.DeviceConfigProxy; import javax.inject.Inject; +import javax.inject.Provider; /** * ClipboardListener brings up a clipboard overlay when something is copied to the clipboard. */ @SysUISingleton -public class ClipboardListener extends CoreStartable - implements ClipboardManager.OnPrimaryClipChangedListener { +public class ClipboardListener implements + CoreStartable, ClipboardManager.OnPrimaryClipChangedListener { private static final String TAG = "ClipboardListener"; @VisibleForTesting @@ -49,21 +52,32 @@ public class ClipboardListener extends CoreStartable static final String EXTRA_SUPPRESS_OVERLAY = "com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY"; + private final Context mContext; private final DeviceConfigProxy mDeviceConfig; - private final ClipboardOverlayControllerFactory mOverlayFactory; + private final Provider<ClipboardOverlayController> mOverlayProvider; + private final ClipboardOverlayControllerLegacyFactory mOverlayFactory; private final ClipboardManager mClipboardManager; private final UiEventLogger mUiEventLogger; - private ClipboardOverlayController mClipboardOverlayController; + private final FeatureFlags mFeatureFlags; + private boolean mUsingNewOverlay; + private ClipboardOverlay mClipboardOverlay; @Inject public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy, - ClipboardOverlayControllerFactory overlayFactory, ClipboardManager clipboardManager, - UiEventLogger uiEventLogger) { - super(context); + Provider<ClipboardOverlayController> clipboardOverlayControllerProvider, + ClipboardOverlayControllerLegacyFactory overlayFactory, + ClipboardManager clipboardManager, + UiEventLogger uiEventLogger, + FeatureFlags featureFlags) { + mContext = context; mDeviceConfig = deviceConfigProxy; + mOverlayProvider = clipboardOverlayControllerProvider; mOverlayFactory = overlayFactory; mClipboardManager = clipboardManager; mUiEventLogger = uiEventLogger; + mFeatureFlags = featureFlags; + + mUsingNewOverlay = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR); } @Override @@ -88,16 +102,22 @@ public class ClipboardListener extends CoreStartable return; } - if (mClipboardOverlayController == null) { - mClipboardOverlayController = mOverlayFactory.create(mContext); + boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR); + if (mClipboardOverlay == null || enabled != mUsingNewOverlay) { + mUsingNewOverlay = enabled; + if (enabled) { + mClipboardOverlay = mOverlayProvider.get(); + } else { + mClipboardOverlay = mOverlayFactory.create(mContext); + } mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource); } else { mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource); } - mClipboardOverlayController.setClipData(clipData, clipSource); - mClipboardOverlayController.setOnSessionCompleteListener(() -> { + mClipboardOverlay.setClipData(clipData, clipSource); + mClipboardOverlay.setOnSessionCompleteListener(() -> { // Session is complete, free memory until it's needed again. - mClipboardOverlayController = null; + mClipboardOverlay = null; }); } @@ -119,4 +139,10 @@ public class ClipboardListener extends CoreStartable private static boolean isEmulator() { return SystemProperties.getBoolean("ro.boot.qemu", false); } + + interface ClipboardOverlay { + void setClipData(ClipData clipData, String clipSource); + + void setOnSessionCompleteListener(Runnable runnable); + } } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index 7e499ebdf691..9f338d1c6e1d 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -17,7 +17,6 @@ package com.android.systemui.clipboardoverlay; import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS; -import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; @@ -37,11 +36,6 @@ import static java.util.Objects.requireNonNull; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.annotation.MainThread; -import android.app.ICompatCameraControlCallback; import android.app.RemoteAction; import android.content.BroadcastReceiver; import android.content.ClipData; @@ -52,14 +46,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Insets; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.Region; -import android.graphics.drawable.Icon; import android.hardware.display.DisplayManager; import android.hardware.input.InputManager; import android.net.Uri; @@ -67,57 +54,37 @@ import android.os.AsyncTask; import android.os.Looper; import android.provider.DeviceConfig; import android.text.TextUtils; -import android.util.DisplayMetrics; import android.util.Log; -import android.util.MathUtils; import android.util.Size; -import android.util.TypedValue; import android.view.Display; -import android.view.DisplayCutout; -import android.view.Gravity; import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.InputMonitor; -import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.View; -import android.view.ViewRootImpl; -import android.view.ViewTreeObserver; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityManager; -import android.view.animation.LinearInterpolator; -import android.view.animation.PathInterpolator; import android.view.textclassifier.TextClassification; import android.view.textclassifier.TextClassificationManager; import android.view.textclassifier.TextClassifier; import android.view.textclassifier.TextLinks; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.core.view.ViewCompat; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.android.internal.logging.UiEventLogger; -import com.android.internal.policy.PhoneWindow; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastSender; -import com.android.systemui.screenshot.DraggableConstraintLayout; -import com.android.systemui.screenshot.FloatingWindowUtil; -import com.android.systemui.screenshot.OverlayActionChip; +import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext; import com.android.systemui.screenshot.TimeoutHandler; import java.io.IOException; import java.util.ArrayList; +import java.util.Optional; + +import javax.inject.Inject; /** * Controls state and UI for the overlay that appears when something is added to the clipboard */ -public class ClipboardOverlayController { +public class ClipboardOverlayController implements ClipboardListener.ClipboardOverlay { private static final String TAG = "ClipboardOverlayCtrlr"; /** Constants for screenshot/copy deconflicting */ @@ -126,36 +93,22 @@ public class ClipboardOverlayController { public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY"; private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000; - private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe - private static final int FONT_SEARCH_STEP_PX = 4; private final Context mContext; private final ClipboardLogger mClipboardLogger; private final BroadcastDispatcher mBroadcastDispatcher; private final DisplayManager mDisplayManager; - private final DisplayMetrics mDisplayMetrics; - private final WindowManager mWindowManager; - private final WindowManager.LayoutParams mWindowLayoutParams; - private final PhoneWindow mWindow; + private final ClipboardOverlayWindow mWindow; private final TimeoutHandler mTimeoutHandler; - private final AccessibilityManager mAccessibilityManager; private final TextClassifier mTextClassifier; - private final DraggableConstraintLayout mView; - private final View mClipboardPreview; - private final ImageView mImagePreview; - private final TextView mTextPreview; - private final TextView mHiddenPreview; - private final View mPreviewBorder; - private final OverlayActionChip mEditChip; - private final OverlayActionChip mShareChip; - private final OverlayActionChip mRemoteCopyChip; - private final View mActionContainerBackground; - private final View mDismissButton; - private final LinearLayout mActionContainer; - private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>(); + private final ClipboardOverlayView mView; private Runnable mOnSessionCompleteListener; + private Runnable mOnRemoteCopyTapped; + private Runnable mOnShareTapped; + private Runnable mOnEditTapped; + private Runnable mOnPreviewTapped; private InputMonitor mInputMonitor; private InputEventReceiver mInputEventReceiver; @@ -163,14 +116,66 @@ public class ClipboardOverlayController { private BroadcastReceiver mCloseDialogsReceiver; private BroadcastReceiver mScreenshotReceiver; - private boolean mBlockAttach = false; private Animator mExitAnimator; private Animator mEnterAnimator; - private final int mOrientation; - private boolean mKeyboardVisible; + private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks = + new ClipboardOverlayView.ClipboardOverlayCallbacks() { + @Override + public void onInteraction() { + mTimeoutHandler.resetTimeout(); + } + + @Override + public void onSwipeDismissInitiated(Animator animator) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); + mExitAnimator = animator; + } + + @Override + public void onDismissComplete() { + hideImmediate(); + } + + @Override + public void onPreviewTapped() { + if (mOnPreviewTapped != null) { + mOnPreviewTapped.run(); + } + } + + @Override + public void onShareButtonTapped() { + if (mOnShareTapped != null) { + mOnShareTapped.run(); + } + } + + @Override + public void onEditButtonTapped() { + if (mOnEditTapped != null) { + mOnEditTapped.run(); + } + } + + @Override + public void onRemoteCopyButtonTapped() { + if (mOnRemoteCopyTapped != null) { + mOnRemoteCopyTapped.run(); + } + } + + @Override + public void onDismissButtonTapped() { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + animateOut(); + } + }; - public ClipboardOverlayController(Context context, + @Inject + public ClipboardOverlayController(@OverlayWindowContext Context context, + ClipboardOverlayView clipboardOverlayView, + ClipboardOverlayWindow clipboardOverlayWindow, BroadcastDispatcher broadcastDispatcher, BroadcastSender broadcastSender, TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) { @@ -181,121 +186,26 @@ public class ClipboardOverlayController { mClipboardLogger = new ClipboardLogger(uiEventLogger); - mAccessibilityManager = AccessibilityManager.getInstance(mContext); + mView = clipboardOverlayView; + mWindow = clipboardOverlayWindow; + mWindow.init(mView::setInsets, () -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER); + hideImmediate(); + }); + mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class)) .getTextClassifier(); - mWindowManager = mContext.getSystemService(WindowManager.class); - - mDisplayMetrics = new DisplayMetrics(); - mContext.getDisplay().getRealMetrics(mDisplayMetrics); - mTimeoutHandler = timeoutHandler; mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS); - // Setup the window that we are going to use - mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); - mWindowLayoutParams.setTitle("ClipboardOverlay"); - - mWindow = FloatingWindowUtil.getFloatingWindow(mContext); - mWindow.setWindowManager(mWindowManager, null, null); - - setWindowFocusable(false); - - mView = (DraggableConstraintLayout) - LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay, null); - mActionContainerBackground = - requireNonNull(mView.findViewById(R.id.actions_container_background)); - mActionContainer = requireNonNull(mView.findViewById(R.id.actions)); - mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview)); - mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview)); - mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview)); - mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview)); - mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border)); - mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip)); - mShareChip = requireNonNull(mView.findViewById(R.id.share_chip)); - mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip)); - mEditChip.setAlpha(1); - mShareChip.setAlpha(1); - mRemoteCopyChip.setAlpha(1); - mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button)); - - mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share)); - mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() { - @Override - public void onInteraction() { - mTimeoutHandler.resetTimeout(); - } - - @Override - public void onSwipeDismissInitiated(Animator animator) { - mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); - mExitAnimator = animator; - } + mView.setCallbacks(mClipboardCallbacks); - @Override - public void onDismissComplete() { - hideImmediate(); - } - }); - mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> { - int availableHeight = mTextPreview.getHeight() - - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom()); - mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight()); - return true; - }); - - mDismissButton.setOnClickListener(view -> { - mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED); - animateOut(); - }); - - mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); - mRemoteCopyChip.setIcon( - Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true); - mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); - mOrientation = mContext.getResources().getConfiguration().orientation; - - attachWindow(); - withWindowAttached(() -> { + mWindow.withWindowAttached(() -> { mWindow.setContentView(mView); - WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); - mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime()); - updateInsets(insets); - mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - WindowInsets insets = - mWindowManager.getCurrentWindowMetrics().getWindowInsets(); - boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime()); - if (keyboardVisible != mKeyboardVisible) { - mKeyboardVisible = keyboardVisible; - updateInsets(insets); - } - } - }); - mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( - new ViewRootImpl.ActivityConfigCallback() { - @Override - public void onConfigurationChanged(Configuration overrideConfig, - int newDisplayId) { - if (mContext.getResources().getConfiguration().orientation - != mOrientation) { - mClipboardLogger.logSessionComplete( - CLIPBOARD_OVERLAY_DISMISSED_OTHER); - hideImmediate(); - } - } - - @Override - public void requestCompatCameraControl( - boolean showControl, boolean transformationApplied, - ICompatCameraControlCallback callback) { - Log.w(TAG, "unexpected requestCompatCameraControl call"); - } - }); + mView.setInsets(mWindow.getWindowInsets(), + mContext.getResources().getConfiguration().orientation); }); mTimeoutHandler.setOnTimeoutRunnable(() -> { @@ -336,21 +246,19 @@ public class ClipboardOverlayController { broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION); } - void setClipData(ClipData clipData, String clipSource) { + @Override // ClipboardListener.ClipboardOverlay + public void setClipData(ClipData clipData, String clipSource) { if (mExitAnimator != null && mExitAnimator.isRunning()) { mExitAnimator.cancel(); } reset(); - String accessibilityAnnouncement; + String accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null && clipData.getDescription().getExtras() .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE); if (clipData == null || clipData.getItemCount() == 0) { - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); - accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + mView.showDefaultTextPreview(); } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) { ClipData.Item item = clipData.getItemAt(0); if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, @@ -360,53 +268,47 @@ public class ClipboardOverlayController { } } if (isSensitive) { - showEditableText( - mContext.getResources().getString(R.string.clipboard_asterisks), true); + showEditableText(mContext.getString(R.string.clipboard_asterisks), true); } else { showEditableText(item.getText(), false); } - showShareChip(clipData); + mOnShareTapped = () -> shareContent(clipData); + mView.showShareChip(); accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied); } else if (clipData.getItemAt(0).getUri() != null) { if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) { - showShareChip(clipData); + mOnShareTapped = () -> shareContent(clipData); + mView.showShareChip(); accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied); - } else { - accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); } } else { - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); - accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + mView.showDefaultTextPreview(); } + maybeShowRemoteCopy(clipData); + animateIn(); + mView.announceForAccessibility(accessibilityAnnouncement); + mTimeoutHandler.resetTimeout(); + } + + private void maybeShowRemoteCopy(ClipData clipData) { Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext); // Only show remote copy if it's available. PackageManager packageManager = mContext.getPackageManager(); if (packageManager.resolveActivity( remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) { - mRemoteCopyChip.setContentDescription( - mContext.getString(R.string.clipboard_send_nearby_description)); - mRemoteCopyChip.setVisibility(View.VISIBLE); - mRemoteCopyChip.setOnClickListener((v) -> { + mView.setRemoteCopyVisibility(true); + mOnRemoteCopyTapped = () -> { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED); mContext.startActivity(remoteCopyIntent); animateOut(); - }); - mActionContainerBackground.setVisibility(View.VISIBLE); + }; } else { - mRemoteCopyChip.setVisibility(View.GONE); + mView.setRemoteCopyVisibility(false); } - withWindowAttached(() -> { - if (mEnterAnimator == null || !mEnterAnimator.isRunning()) { - mView.post(this::animateIn); - } - mView.announceForAccessibility(accessibilityAnnouncement); - }); - mTimeoutHandler.resetTimeout(); } - void setOnSessionCompleteListener(Runnable runnable) { + @Override // ClipboardListener.ClipboardOverlay + public void setOnSessionCompleteListener(Runnable runnable) { mOnSessionCompleteListener = runnable; } @@ -418,72 +320,29 @@ public class ClipboardOverlayController { actions.addAll(classification.getActions()); } mView.post(() -> { - resetActionChips(); - if (actions.size() > 0) { - mActionContainerBackground.setVisibility(View.VISIBLE); - for (RemoteAction action : actions) { - Intent targetIntent = action.getActionIntent().getIntent(); - ComponentName component = targetIntent.getComponent(); - if (component != null && !TextUtils.equals(source, - component.getPackageName())) { - OverlayActionChip chip = constructActionChip(action); - mActionContainer.addView(chip); - mActionChips.add(chip); - break; // only show at most one action chip - } - } - } - }); - } - - private void showShareChip(ClipData clip) { - mShareChip.setVisibility(View.VISIBLE); - mActionContainerBackground.setVisibility(View.VISIBLE); - mShareChip.setOnClickListener((v) -> shareContent(clip)); - } - - private OverlayActionChip constructActionChip(RemoteAction action) { - OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate( - R.layout.overlay_action_chip, mActionContainer, false); - chip.setText(action.getTitle()); - chip.setContentDescription(action.getTitle()); - chip.setIcon(action.getIcon(), false); - chip.setPendingIntent(action.getActionIntent(), () -> { - mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED); - animateOut(); + Optional<RemoteAction> action = actions.stream().filter(remoteAction -> { + ComponentName component = remoteAction.getActionIntent().getIntent().getComponent(); + return component != null && !TextUtils.equals(source, component.getPackageName()); + }).findFirst(); + mView.resetActionChips(); + action.ifPresent(remoteAction -> mView.setActionChip(remoteAction, () -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED); + animateOut(); + })); }); - chip.setAlpha(1); - return chip; } private void monitorOutsideTouches() { InputManager inputManager = mContext.getSystemService(InputManager.class); mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0); - mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(), - Looper.getMainLooper()) { + mInputEventReceiver = new InputEventReceiver( + mInputMonitor.getInputChannel(), Looper.getMainLooper()) { @Override public void onInputEvent(InputEvent event) { if (event instanceof MotionEvent) { MotionEvent motionEvent = (MotionEvent) event; if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - Region touchRegion = new Region(); - - final Rect tmpRect = new Rect(); - mPreviewBorder.getBoundsOnScreen(tmpRect); - tmpRect.inset( - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, - -SWIPE_PADDING_DP)); - touchRegion.op(tmpRect, Region.Op.UNION); - mActionContainerBackground.getBoundsOnScreen(tmpRect); - tmpRect.inset( - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, - -SWIPE_PADDING_DP)); - touchRegion.op(tmpRect, Region.Op.UNION); - mDismissButton.getBoundsOnScreen(tmpRect); - touchRegion.op(tmpRect, Region.Op.UNION); - if (!touchRegion.contains( + if (!mView.isInTouchRegion( (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE); animateOut(); @@ -513,95 +372,27 @@ public class ClipboardOverlayController { animateOut(); } - private void showSinglePreview(View v) { - mTextPreview.setVisibility(View.GONE); - mImagePreview.setVisibility(View.GONE); - mHiddenPreview.setVisibility(View.GONE); - v.setVisibility(View.VISIBLE); - } - - private void showTextPreview(CharSequence text, TextView textView) { - showSinglePreview(textView); - final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length())); - textView.setText(truncatedText); - updateTextSize(truncatedText, textView); - - textView.addOnLayoutChangeListener( - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - if (right - left != oldRight - oldLeft) { - updateTextSize(truncatedText, textView); - } - }); - mEditChip.setVisibility(View.GONE); - } - - private void updateTextSize(CharSequence text, TextView textView) { - Paint paint = new Paint(textView.getPaint()); - Resources res = textView.getResources(); - float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font); - float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font); - if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) { - // If the text is a single word and would fit within the TextView at the min font size, - // find the biggest font size that will fit. - float fontSizePx = minFontSize; - while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize - && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) { - fontSizePx += FONT_SEARCH_STEP_PX; - } - // Need to turn off autosizing, otherwise setTextSize is a no-op. - textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE); - // It's possible to hit the max font size and not fill the width, so centering - // horizontally looks better in this case. - textView.setGravity(Gravity.CENTER); - textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx); - } else { - // Otherwise just stick with autosize. - textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize, - (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX); - textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); - } - } - - private static boolean fitsInView(CharSequence text, TextView textView, Paint paint, - float fontSizePx) { - paint.setTextSize(fontSizePx); - float size = paint.measureText(text.toString()); - float availableWidth = textView.getWidth() - textView.getPaddingLeft() - - textView.getPaddingRight(); - return size < availableWidth; - } - - private static boolean isOneWord(CharSequence text) { - return text.toString().split("\\s+", 2).length == 1; - } - private void showEditableText(CharSequence text, boolean hidden) { - TextView textView = hidden ? mHiddenPreview : mTextPreview; - showTextPreview(text, textView); - View.OnClickListener listener = v -> editText(); - setAccessibilityActionToEdit(textView); + mView.showTextPreview(text, hidden); + mView.setEditAccessibilityAction(true); + mOnPreviewTapped = this::editText; if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { - mEditChip.setVisibility(View.VISIBLE); - mActionContainerBackground.setVisibility(View.VISIBLE); - mEditChip.setContentDescription( - mContext.getString(R.string.clipboard_edit_text_description)); - mEditChip.setOnClickListener(listener); + mOnEditTapped = this::editText; + mView.showEditChip(mContext.getString(R.string.clipboard_edit_text_description)); } - textView.setOnClickListener(listener); } private boolean tryShowEditableImage(Uri uri, boolean isSensitive) { - View.OnClickListener listener = v -> editImage(uri); + Runnable listener = () -> editImage(uri); ContentResolver resolver = mContext.getContentResolver(); String mimeType = resolver.getType(uri); boolean isEditableImage = mimeType != null && mimeType.startsWith("image"); if (isSensitive) { - mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden)); - showSinglePreview(mHiddenPreview); + mView.showImagePreview(null); if (isEditableImage) { - mHiddenPreview.setOnClickListener(listener); - setAccessibilityActionToEdit(mHiddenPreview); + mOnPreviewTapped = listener; + mView.setEditAccessibilityAction(true); } } else if (isEditableImage) { // if the MIMEtype is image, try to load try { @@ -609,44 +400,36 @@ public class ClipboardOverlayController { // The width of the view is capped, height maintains aspect ratio, so allow it to be // taller if needed. Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null); - showSinglePreview(mImagePreview); - mImagePreview.setImageBitmap(thumbnail); - mImagePreview.setOnClickListener(listener); - setAccessibilityActionToEdit(mImagePreview); + mView.showImagePreview(thumbnail); + mView.setEditAccessibilityAction(true); + mOnPreviewTapped = listener; } catch (IOException e) { Log.e(TAG, "Thumbnail loading failed", e); - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); + mView.showDefaultTextPreview(); isEditableImage = false; } } else { - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); + mView.showDefaultTextPreview(); } if (isEditableImage && DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { - mEditChip.setVisibility(View.VISIBLE); - mActionContainerBackground.setVisibility(View.VISIBLE); - mEditChip.setOnClickListener(listener); - mEditChip.setContentDescription( - mContext.getString(R.string.clipboard_edit_image_description)); + mView.showEditChip(mContext.getString(R.string.clipboard_edit_image_description)); } return isEditableImage; } - private void setAccessibilityActionToEdit(View view) { - ViewCompat.replaceAccessibilityAction(view, - AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, - mContext.getString(R.string.clipboard_edit), null); - } - private void animateIn() { - if (mAccessibilityManager.isEnabled()) { - mDismissButton.setVisibility(View.VISIBLE); + if (mEnterAnimator != null && mEnterAnimator.isRunning()) { + return; } - mEnterAnimator = getEnterAnimation(); + mEnterAnimator = mView.getEnterAnimation(); + mEnterAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mTimeoutHandler.resetTimeout(); + } + }); mEnterAnimator.start(); } @@ -654,7 +437,7 @@ public class ClipboardOverlayController { if (mExitAnimator != null && mExitAnimator.isRunning()) { return; } - Animator anim = getExitAnimation(); + Animator anim = mView.getExitAnimation(); anim.addListener(new AnimatorListenerAdapter() { private boolean mCancelled; @@ -676,122 +459,11 @@ public class ClipboardOverlayController { anim.start(); } - private Animator getEnterAnimation() { - TimeInterpolator linearInterpolator = new LinearInterpolator(); - TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f); - AnimatorSet enterAnim = new AnimatorSet(); - - ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); - rootAnim.setInterpolator(linearInterpolator); - rootAnim.setDuration(66); - rootAnim.addUpdateListener(animation -> { - mView.setAlpha(animation.getAnimatedFraction()); - }); - - ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); - scaleAnim.setInterpolator(scaleInterpolator); - scaleAnim.setDuration(333); - scaleAnim.addUpdateListener(animation -> { - float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); - mClipboardPreview.setScaleX(previewScale); - mClipboardPreview.setScaleY(previewScale); - mPreviewBorder.setScaleX(previewScale); - mPreviewBorder.setScaleY(previewScale); - - float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); - mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); - mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); - float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction()); - float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); - mActionContainer.setScaleX(actionsScaleX); - mActionContainer.setScaleY(actionsScaleY); - mActionContainerBackground.setScaleX(actionsScaleX); - mActionContainerBackground.setScaleY(actionsScaleY); - }); - - ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); - alphaAnim.setInterpolator(linearInterpolator); - alphaAnim.setDuration(283); - alphaAnim.addUpdateListener(animation -> { - float alpha = animation.getAnimatedFraction(); - mClipboardPreview.setAlpha(alpha); - mPreviewBorder.setAlpha(alpha); - mDismissButton.setAlpha(alpha); - mActionContainer.setAlpha(alpha); - }); - - mActionContainer.setAlpha(0); - mPreviewBorder.setAlpha(0); - mClipboardPreview.setAlpha(0); - enterAnim.play(rootAnim).with(scaleAnim); - enterAnim.play(alphaAnim).after(50).after(rootAnim); - - enterAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - mView.setAlpha(1); - mTimeoutHandler.resetTimeout(); - } - }); - return enterAnim; - } - - private Animator getExitAnimation() { - TimeInterpolator linearInterpolator = new LinearInterpolator(); - TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f); - AnimatorSet exitAnim = new AnimatorSet(); - - ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); - rootAnim.setInterpolator(linearInterpolator); - rootAnim.setDuration(100); - rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction())); - - ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); - scaleAnim.setInterpolator(scaleInterpolator); - scaleAnim.setDuration(250); - scaleAnim.addUpdateListener(animation -> { - float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); - mClipboardPreview.setScaleX(previewScale); - mClipboardPreview.setScaleY(previewScale); - mPreviewBorder.setScaleX(previewScale); - mPreviewBorder.setScaleY(previewScale); - - float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); - mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); - mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); - float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction()); - float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); - mActionContainer.setScaleX(actionScaleX); - mActionContainer.setScaleY(actionScaleY); - mActionContainerBackground.setScaleX(actionScaleX); - mActionContainerBackground.setScaleY(actionScaleY); - }); - - ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); - alphaAnim.setInterpolator(linearInterpolator); - alphaAnim.setDuration(166); - alphaAnim.addUpdateListener(animation -> { - float alpha = 1 - animation.getAnimatedFraction(); - mClipboardPreview.setAlpha(alpha); - mPreviewBorder.setAlpha(alpha); - mDismissButton.setAlpha(alpha); - mActionContainer.setAlpha(alpha); - }); - - exitAnim.play(alphaAnim).with(scaleAnim); - exitAnim.play(rootAnim).after(150).after(alphaAnim); - return exitAnim; - } - - private void hideImmediate() { + void hideImmediate() { // Note this may be called multiple times if multiple dismissal events happen at the same // time. mTimeoutHandler.cancelTimeout(); - final View decorView = mWindow.peekDecorView(); - if (decorView != null && decorView.isAttachedToWindow()) { - mWindowManager.removeViewImmediate(decorView); - } + mWindow.remove(); if (mCloseDialogsReceiver != null) { mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver); mCloseDialogsReceiver = null; @@ -813,129 +485,20 @@ public class ClipboardOverlayController { } } - private void resetActionChips() { - for (OverlayActionChip chip : mActionChips) { - mActionContainer.removeView(chip); - } - mActionChips.clear(); - } - private void reset() { - mView.setTranslationX(0); - mView.setAlpha(0); - mActionContainerBackground.setVisibility(View.GONE); - mShareChip.setVisibility(View.GONE); - mEditChip.setVisibility(View.GONE); - mRemoteCopyChip.setVisibility(View.GONE); - resetActionChips(); + mOnRemoteCopyTapped = null; + mOnShareTapped = null; + mOnEditTapped = null; + mOnPreviewTapped = null; + mView.reset(); mTimeoutHandler.cancelTimeout(); mClipboardLogger.reset(); } - @MainThread - private void attachWindow() { - View decorView = mWindow.getDecorView(); - if (decorView.isAttachedToWindow() || mBlockAttach) { - return; - } - mBlockAttach = true; - mWindowManager.addView(decorView, mWindowLayoutParams); - decorView.requestApplyInsets(); - mView.requestApplyInsets(); - decorView.getViewTreeObserver().addOnWindowAttachListener( - new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - mBlockAttach = false; - } - - @Override - public void onWindowDetached() { - } - } - ); - } - - private void withWindowAttached(Runnable action) { - View decorView = mWindow.getDecorView(); - if (decorView.isAttachedToWindow()) { - action.run(); - } else { - decorView.getViewTreeObserver().addOnWindowAttachListener( - new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - mBlockAttach = false; - decorView.getViewTreeObserver().removeOnWindowAttachListener(this); - action.run(); - } - - @Override - public void onWindowDetached() { - } - }); - } - } - - private void updateInsets(WindowInsets insets) { - int orientation = mContext.getResources().getConfiguration().orientation; - FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams(); - if (p == null) { - return; - } - DisplayCutout cutout = insets.getDisplayCutout(); - Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); - Insets imeInsets = insets.getInsets(WindowInsets.Type.ime()); - if (cutout == null) { - p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom)); - } else { - Insets waterfall = cutout.getWaterfallInsets(); - if (orientation == ORIENTATION_PORTRAIT) { - p.setMargins( - waterfall.left, - Math.max(cutout.getSafeInsetTop(), waterfall.top), - waterfall.right, - Math.max(imeInsets.bottom, - Math.max(cutout.getSafeInsetBottom(), - Math.max(navBarInsets.bottom, waterfall.bottom)))); - } else { - p.setMargins( - waterfall.left, - waterfall.top, - waterfall.right, - Math.max(imeInsets.bottom, - Math.max(navBarInsets.bottom, waterfall.bottom))); - } - } - mView.setLayoutParams(p); - mView.requestLayout(); - } - private Display getDefaultDisplay() { return mDisplayManager.getDisplay(DEFAULT_DISPLAY); } - /** - * Updates the window focusability. If the window is already showing, then it updates the - * window immediately, otherwise the layout params will be applied when the window is next - * shown. - */ - private void setWindowFocusable(boolean focusable) { - int flags = mWindowLayoutParams.flags; - if (focusable) { - mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } else { - mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } - if (mWindowLayoutParams.flags == flags) { - return; - } - final View decorView = mWindow.peekDecorView(); - if (decorView != null && decorView.isAttachedToWindow()) { - mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); - } - } - static class ClipboardLogger { private final UiEventLogger mUiEventLogger; private boolean mGuarded = false; diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java new file mode 100644 index 000000000000..3a040829ba0c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java @@ -0,0 +1,963 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.clipboardoverlay; + +import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; + +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS; +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EDIT_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT; + +import static java.util.Objects.requireNonNull; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.MainThread; +import android.app.ICompatCameraControlCallback; +import android.app.RemoteAction; +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Insets; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Icon; +import android.hardware.display.DisplayManager; +import android.hardware.input.InputManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Looper; +import android.provider.DeviceConfig; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.MathUtils; +import android.util.Size; +import android.util.TypedValue; +import android.view.Display; +import android.view.DisplayCutout; +import android.view.Gravity; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.LinearInterpolator; +import android.view.animation.PathInterpolator; +import android.view.textclassifier.TextClassification; +import android.view.textclassifier.TextClassificationManager; +import android.view.textclassifier.TextClassifier; +import android.view.textclassifier.TextLinks; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.android.internal.logging.UiEventLogger; +import com.android.internal.policy.PhoneWindow; +import com.android.systemui.R; +import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.screenshot.DraggableConstraintLayout; +import com.android.systemui.screenshot.FloatingWindowUtil; +import com.android.systemui.screenshot.OverlayActionChip; +import com.android.systemui.screenshot.TimeoutHandler; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * Controls state and UI for the overlay that appears when something is added to the clipboard + */ +public class ClipboardOverlayControllerLegacy implements ClipboardListener.ClipboardOverlay { + private static final String TAG = "ClipboardOverlayCtrlr"; + private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY"; + + /** Constants for screenshot/copy deconflicting */ + public static final String SCREENSHOT_ACTION = "com.android.systemui.SCREENSHOT"; + public static final String SELF_PERMISSION = "com.android.systemui.permission.SELF"; + public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY"; + + private static final String EXTRA_EDIT_SOURCE_CLIPBOARD = "edit_source_clipboard"; + + private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000; + private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe + private static final int FONT_SEARCH_STEP_PX = 4; + + private final Context mContext; + private final ClipboardLogger mClipboardLogger; + private final BroadcastDispatcher mBroadcastDispatcher; + private final DisplayManager mDisplayManager; + private final DisplayMetrics mDisplayMetrics; + private final WindowManager mWindowManager; + private final WindowManager.LayoutParams mWindowLayoutParams; + private final PhoneWindow mWindow; + private final TimeoutHandler mTimeoutHandler; + private final AccessibilityManager mAccessibilityManager; + private final TextClassifier mTextClassifier; + + private final DraggableConstraintLayout mView; + private final View mClipboardPreview; + private final ImageView mImagePreview; + private final TextView mTextPreview; + private final TextView mHiddenPreview; + private final View mPreviewBorder; + private final OverlayActionChip mEditChip; + private final OverlayActionChip mShareChip; + private final OverlayActionChip mRemoteCopyChip; + private final View mActionContainerBackground; + private final View mDismissButton; + private final LinearLayout mActionContainer; + private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>(); + + private Runnable mOnSessionCompleteListener; + + private InputMonitor mInputMonitor; + private InputEventReceiver mInputEventReceiver; + + private BroadcastReceiver mCloseDialogsReceiver; + private BroadcastReceiver mScreenshotReceiver; + + private boolean mBlockAttach = false; + private Animator mExitAnimator; + private Animator mEnterAnimator; + private final int mOrientation; + private boolean mKeyboardVisible; + + + public ClipboardOverlayControllerLegacy(Context context, + BroadcastDispatcher broadcastDispatcher, + BroadcastSender broadcastSender, + TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) { + mBroadcastDispatcher = broadcastDispatcher; + mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class)); + final Context displayContext = context.createDisplayContext(getDefaultDisplay()); + mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null); + + mClipboardLogger = new ClipboardLogger(uiEventLogger); + + mAccessibilityManager = AccessibilityManager.getInstance(mContext); + mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class)) + .getTextClassifier(); + + mWindowManager = mContext.getSystemService(WindowManager.class); + + mDisplayMetrics = new DisplayMetrics(); + mContext.getDisplay().getRealMetrics(mDisplayMetrics); + + mTimeoutHandler = timeoutHandler; + mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS); + + // Setup the window that we are going to use + mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); + mWindowLayoutParams.setTitle("ClipboardOverlay"); + + mWindow = FloatingWindowUtil.getFloatingWindow(mContext); + mWindow.setWindowManager(mWindowManager, null, null); + + setWindowFocusable(false); + + mView = (DraggableConstraintLayout) + LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay_legacy, null); + mActionContainerBackground = + requireNonNull(mView.findViewById(R.id.actions_container_background)); + mActionContainer = requireNonNull(mView.findViewById(R.id.actions)); + mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview)); + mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview)); + mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview)); + mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview)); + mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border)); + mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip)); + mShareChip = requireNonNull(mView.findViewById(R.id.share_chip)); + mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip)); + mEditChip.setAlpha(1); + mShareChip.setAlpha(1); + mRemoteCopyChip.setAlpha(1); + mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button)); + + mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share)); + mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() { + @Override + public void onInteraction() { + mTimeoutHandler.resetTimeout(); + } + + @Override + public void onSwipeDismissInitiated(Animator animator) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); + mExitAnimator = animator; + } + + @Override + public void onDismissComplete() { + hideImmediate(); + } + }); + + mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> { + int availableHeight = mTextPreview.getHeight() + - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom()); + mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight()); + return true; + }); + + mDismissButton.setOnClickListener(view -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + animateOut(); + }); + + mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); + mRemoteCopyChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true); + mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); + mOrientation = mContext.getResources().getConfiguration().orientation; + + attachWindow(); + withWindowAttached(() -> { + mWindow.setContentView(mView); + WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime()); + updateInsets(insets); + mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + WindowInsets insets = + mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime()); + if (keyboardVisible != mKeyboardVisible) { + mKeyboardVisible = keyboardVisible; + updateInsets(insets); + } + } + }); + mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( + new ViewRootImpl.ActivityConfigCallback() { + @Override + public void onConfigurationChanged(Configuration overrideConfig, + int newDisplayId) { + if (mContext.getResources().getConfiguration().orientation + != mOrientation) { + mClipboardLogger.logSessionComplete( + CLIPBOARD_OVERLAY_DISMISSED_OTHER); + hideImmediate(); + } + } + + @Override + public void requestCompatCameraControl( + boolean showControl, boolean transformationApplied, + ICompatCameraControlCallback callback) { + Log.w(TAG, "unexpected requestCompatCameraControl call"); + } + }); + }); + + mTimeoutHandler.setOnTimeoutRunnable(() -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT); + animateOut(); + }); + + mCloseDialogsReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER); + animateOut(); + } + } + }; + + mBroadcastDispatcher.registerReceiver(mCloseDialogsReceiver, + new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS)); + mScreenshotReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (SCREENSHOT_ACTION.equals(intent.getAction())) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER); + animateOut(); + } + } + }; + + mBroadcastDispatcher.registerReceiver(mScreenshotReceiver, + new IntentFilter(SCREENSHOT_ACTION), null, null, Context.RECEIVER_EXPORTED, + SELF_PERMISSION); + monitorOutsideTouches(); + + Intent copyIntent = new Intent(COPY_OVERLAY_ACTION); + // Set package name so the system knows it's safe + copyIntent.setPackage(mContext.getPackageName()); + broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION); + } + + @Override // ClipboardListener.ClipboardOverlay + public void setClipData(ClipData clipData, String clipSource) { + if (mExitAnimator != null && mExitAnimator.isRunning()) { + mExitAnimator.cancel(); + } + reset(); + String accessibilityAnnouncement; + + boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null + && clipData.getDescription().getExtras() + .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE); + if (clipData == null || clipData.getItemCount() == 0) { + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) { + ClipData.Item item = clipData.getItemAt(0); + if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) { + if (item.getTextLinks() != null) { + AsyncTask.execute(() -> classifyText(clipData.getItemAt(0), clipSource)); + } + } + if (isSensitive) { + showEditableText( + mContext.getResources().getString(R.string.clipboard_asterisks), true); + } else { + showEditableText(item.getText(), false); + } + showShareChip(clipData); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied); + } else if (clipData.getItemAt(0).getUri() != null) { + if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) { + showShareChip(clipData); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied); + } else { + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + } + } else { + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + } + Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext); + // Only show remote copy if it's available. + PackageManager packageManager = mContext.getPackageManager(); + if (packageManager.resolveActivity( + remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) { + mRemoteCopyChip.setContentDescription( + mContext.getString(R.string.clipboard_send_nearby_description)); + mRemoteCopyChip.setVisibility(View.VISIBLE); + mRemoteCopyChip.setOnClickListener((v) -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED); + mContext.startActivity(remoteCopyIntent); + animateOut(); + }); + mActionContainerBackground.setVisibility(View.VISIBLE); + } else { + mRemoteCopyChip.setVisibility(View.GONE); + } + withWindowAttached(() -> { + if (mEnterAnimator == null || !mEnterAnimator.isRunning()) { + mView.post(this::animateIn); + } + mView.announceForAccessibility(accessibilityAnnouncement); + }); + mTimeoutHandler.resetTimeout(); + } + + @Override // ClipboardListener.ClipboardOverlay + public void setOnSessionCompleteListener(Runnable runnable) { + mOnSessionCompleteListener = runnable; + } + + private void classifyText(ClipData.Item item, String source) { + ArrayList<RemoteAction> actions = new ArrayList<>(); + for (TextLinks.TextLink link : item.getTextLinks().getLinks()) { + TextClassification classification = mTextClassifier.classifyText( + item.getText(), link.getStart(), link.getEnd(), null); + actions.addAll(classification.getActions()); + } + mView.post(() -> { + resetActionChips(); + if (actions.size() > 0) { + mActionContainerBackground.setVisibility(View.VISIBLE); + for (RemoteAction action : actions) { + Intent targetIntent = action.getActionIntent().getIntent(); + ComponentName component = targetIntent.getComponent(); + if (component != null && !TextUtils.equals(source, + component.getPackageName())) { + OverlayActionChip chip = constructActionChip(action); + mActionContainer.addView(chip); + mActionChips.add(chip); + break; // only show at most one action chip + } + } + } + }); + } + + private void showShareChip(ClipData clip) { + mShareChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mShareChip.setOnClickListener((v) -> shareContent(clip)); + } + + private OverlayActionChip constructActionChip(RemoteAction action) { + OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate( + R.layout.overlay_action_chip, mActionContainer, false); + chip.setText(action.getTitle()); + chip.setContentDescription(action.getTitle()); + chip.setIcon(action.getIcon(), false); + chip.setPendingIntent(action.getActionIntent(), () -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED); + animateOut(); + }); + chip.setAlpha(1); + return chip; + } + + private void monitorOutsideTouches() { + InputManager inputManager = mContext.getSystemService(InputManager.class); + mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0); + mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(), + Looper.getMainLooper()) { + @Override + public void onInputEvent(InputEvent event) { + if (event instanceof MotionEvent) { + MotionEvent motionEvent = (MotionEvent) event; + if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + Region touchRegion = new Region(); + + final Rect tmpRect = new Rect(); + mPreviewBorder.getBoundsOnScreen(tmpRect); + tmpRect.inset( + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, + -SWIPE_PADDING_DP)); + touchRegion.op(tmpRect, Region.Op.UNION); + mActionContainerBackground.getBoundsOnScreen(tmpRect); + tmpRect.inset( + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, + -SWIPE_PADDING_DP)); + touchRegion.op(tmpRect, Region.Op.UNION); + mDismissButton.getBoundsOnScreen(tmpRect); + touchRegion.op(tmpRect, Region.Op.UNION); + if (!touchRegion.contains( + (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE); + animateOut(); + } + } + } + finishInputEvent(event, true /* handled */); + } + }; + } + + private void editImage(Uri uri) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED); + mContext.startActivity(IntentCreator.getImageEditIntent(uri, mContext)); + animateOut(); + } + + private void editText() { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED); + mContext.startActivity(IntentCreator.getTextEditorIntent(mContext)); + animateOut(); + } + + private void shareContent(ClipData clip) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SHARE_TAPPED); + mContext.startActivity(IntentCreator.getShareIntent(clip, mContext)); + animateOut(); + } + + private void showSinglePreview(View v) { + mTextPreview.setVisibility(View.GONE); + mImagePreview.setVisibility(View.GONE); + mHiddenPreview.setVisibility(View.GONE); + v.setVisibility(View.VISIBLE); + } + + private void showTextPreview(CharSequence text, TextView textView) { + showSinglePreview(textView); + final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length())); + textView.setText(truncatedText); + updateTextSize(truncatedText, textView); + + textView.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (right - left != oldRight - oldLeft) { + updateTextSize(truncatedText, textView); + } + }); + mEditChip.setVisibility(View.GONE); + } + + private void updateTextSize(CharSequence text, TextView textView) { + Paint paint = new Paint(textView.getPaint()); + Resources res = textView.getResources(); + float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font); + float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font); + if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) { + // If the text is a single word and would fit within the TextView at the min font size, + // find the biggest font size that will fit. + float fontSizePx = minFontSize; + while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize + && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) { + fontSizePx += FONT_SEARCH_STEP_PX; + } + // Need to turn off autosizing, otherwise setTextSize is a no-op. + textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE); + // It's possible to hit the max font size and not fill the width, so centering + // horizontally looks better in this case. + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx); + } else { + // Otherwise just stick with autosize. + textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize, + (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX); + textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); + } + } + + private static boolean fitsInView(CharSequence text, TextView textView, Paint paint, + float fontSizePx) { + paint.setTextSize(fontSizePx); + float size = paint.measureText(text.toString()); + float availableWidth = textView.getWidth() - textView.getPaddingLeft() + - textView.getPaddingRight(); + return size < availableWidth; + } + + private static boolean isOneWord(CharSequence text) { + return text.toString().split("\\s+", 2).length == 1; + } + + private void showEditableText(CharSequence text, boolean hidden) { + TextView textView = hidden ? mHiddenPreview : mTextPreview; + showTextPreview(text, textView); + View.OnClickListener listener = v -> editText(); + setAccessibilityActionToEdit(textView); + if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { + mEditChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mEditChip.setContentDescription( + mContext.getString(R.string.clipboard_edit_text_description)); + mEditChip.setOnClickListener(listener); + } + textView.setOnClickListener(listener); + } + + private boolean tryShowEditableImage(Uri uri, boolean isSensitive) { + View.OnClickListener listener = v -> editImage(uri); + ContentResolver resolver = mContext.getContentResolver(); + String mimeType = resolver.getType(uri); + boolean isEditableImage = mimeType != null && mimeType.startsWith("image"); + if (isSensitive) { + mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden)); + showSinglePreview(mHiddenPreview); + if (isEditableImage) { + mHiddenPreview.setOnClickListener(listener); + setAccessibilityActionToEdit(mHiddenPreview); + } + } else if (isEditableImage) { // if the MIMEtype is image, try to load + try { + int size = mContext.getResources().getDimensionPixelSize(R.dimen.overlay_x_scale); + // The width of the view is capped, height maintains aspect ratio, so allow it to be + // taller if needed. + Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null); + showSinglePreview(mImagePreview); + mImagePreview.setImageBitmap(thumbnail); + mImagePreview.setOnClickListener(listener); + setAccessibilityActionToEdit(mImagePreview); + } catch (IOException e) { + Log.e(TAG, "Thumbnail loading failed", e); + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + isEditableImage = false; + } + } else { + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + } + if (isEditableImage && DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { + mEditChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mEditChip.setOnClickListener(listener); + mEditChip.setContentDescription( + mContext.getString(R.string.clipboard_edit_image_description)); + } + return isEditableImage; + } + + private void setAccessibilityActionToEdit(View view) { + ViewCompat.replaceAccessibilityAction(view, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + mContext.getString(R.string.clipboard_edit), null); + } + + private void animateIn() { + if (mAccessibilityManager.isEnabled()) { + mDismissButton.setVisibility(View.VISIBLE); + } + mEnterAnimator = getEnterAnimation(); + mEnterAnimator.start(); + } + + private void animateOut() { + if (mExitAnimator != null && mExitAnimator.isRunning()) { + return; + } + Animator anim = getExitAnimation(); + anim.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (!mCancelled) { + hideImmediate(); + } + } + }); + mExitAnimator = anim; + anim.start(); + } + + private Animator getEnterAnimation() { + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f); + AnimatorSet enterAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(66); + rootAnim.addUpdateListener(animation -> { + mView.setAlpha(animation.getAnimatedFraction()); + }); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(333); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction()); + float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionsScaleX); + mActionContainer.setScaleY(actionsScaleY); + mActionContainerBackground.setScaleX(actionsScaleX); + mActionContainerBackground.setScaleY(actionsScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(283); + alphaAnim.addUpdateListener(animation -> { + float alpha = animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + mActionContainer.setAlpha(0); + mPreviewBorder.setAlpha(0); + mClipboardPreview.setAlpha(0); + enterAnim.play(rootAnim).with(scaleAnim); + enterAnim.play(alphaAnim).after(50).after(rootAnim); + + enterAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mView.setAlpha(1); + mTimeoutHandler.resetTimeout(); + } + }); + return enterAnim; + } + + private Animator getExitAnimation() { + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f); + AnimatorSet exitAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(100); + rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction())); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(250); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction()); + float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionScaleX); + mActionContainer.setScaleY(actionScaleY); + mActionContainerBackground.setScaleX(actionScaleX); + mActionContainerBackground.setScaleY(actionScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(166); + alphaAnim.addUpdateListener(animation -> { + float alpha = 1 - animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + exitAnim.play(alphaAnim).with(scaleAnim); + exitAnim.play(rootAnim).after(150).after(alphaAnim); + return exitAnim; + } + + private void hideImmediate() { + // Note this may be called multiple times if multiple dismissal events happen at the same + // time. + mTimeoutHandler.cancelTimeout(); + final View decorView = mWindow.peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.removeViewImmediate(decorView); + } + if (mCloseDialogsReceiver != null) { + mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver); + mCloseDialogsReceiver = null; + } + if (mScreenshotReceiver != null) { + mBroadcastDispatcher.unregisterReceiver(mScreenshotReceiver); + mScreenshotReceiver = null; + } + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + } + if (mInputMonitor != null) { + mInputMonitor.dispose(); + mInputMonitor = null; + } + if (mOnSessionCompleteListener != null) { + mOnSessionCompleteListener.run(); + } + } + + private void resetActionChips() { + for (OverlayActionChip chip : mActionChips) { + mActionContainer.removeView(chip); + } + mActionChips.clear(); + } + + private void reset() { + mView.setTranslationX(0); + mView.setAlpha(0); + mActionContainerBackground.setVisibility(View.GONE); + mShareChip.setVisibility(View.GONE); + mEditChip.setVisibility(View.GONE); + mRemoteCopyChip.setVisibility(View.GONE); + resetActionChips(); + mTimeoutHandler.cancelTimeout(); + mClipboardLogger.reset(); + } + + @MainThread + private void attachWindow() { + View decorView = mWindow.getDecorView(); + if (decorView.isAttachedToWindow() || mBlockAttach) { + return; + } + mBlockAttach = true; + mWindowManager.addView(decorView, mWindowLayoutParams); + decorView.requestApplyInsets(); + mView.requestApplyInsets(); + decorView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + mBlockAttach = false; + } + + @Override + public void onWindowDetached() { + } + } + ); + } + + private void withWindowAttached(Runnable action) { + View decorView = mWindow.getDecorView(); + if (decorView.isAttachedToWindow()) { + action.run(); + } else { + decorView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + mBlockAttach = false; + decorView.getViewTreeObserver().removeOnWindowAttachListener(this); + action.run(); + } + + @Override + public void onWindowDetached() { + } + }); + } + } + + private void updateInsets(WindowInsets insets) { + int orientation = mContext.getResources().getConfiguration().orientation; + FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams(); + if (p == null) { + return; + } + DisplayCutout cutout = insets.getDisplayCutout(); + Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); + Insets imeInsets = insets.getInsets(WindowInsets.Type.ime()); + if (cutout == null) { + p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom)); + } else { + Insets waterfall = cutout.getWaterfallInsets(); + if (orientation == ORIENTATION_PORTRAIT) { + p.setMargins( + waterfall.left, + Math.max(cutout.getSafeInsetTop(), waterfall.top), + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(cutout.getSafeInsetBottom(), + Math.max(navBarInsets.bottom, waterfall.bottom)))); + } else { + p.setMargins( + waterfall.left, + waterfall.top, + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(navBarInsets.bottom, waterfall.bottom))); + } + } + mView.setLayoutParams(p); + mView.requestLayout(); + } + + private Display getDefaultDisplay() { + return mDisplayManager.getDisplay(DEFAULT_DISPLAY); + } + + /** + * Updates the window focusability. If the window is already showing, then it updates the + * window immediately, otherwise the layout params will be applied when the window is next + * shown. + */ + private void setWindowFocusable(boolean focusable) { + int flags = mWindowLayoutParams.flags; + if (focusable) { + mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } + if (mWindowLayoutParams.flags == flags) { + return; + } + final View decorView = mWindow.peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); + } + } + + static class ClipboardLogger { + private final UiEventLogger mUiEventLogger; + private boolean mGuarded = false; + + ClipboardLogger(UiEventLogger uiEventLogger) { + mUiEventLogger = uiEventLogger; + } + + void logSessionComplete(@NonNull UiEventLogger.UiEventEnum event) { + if (!mGuarded) { + mGuarded = true; + mUiEventLogger.log(event); + } + } + + void reset() { + mGuarded = false; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java index 8b0b2a59dd92..0d989a78947d 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java @@ -27,17 +27,17 @@ import com.android.systemui.screenshot.TimeoutHandler; import javax.inject.Inject; /** - * A factory that churns out ClipboardOverlayControllers on demand. + * A factory that churns out ClipboardOverlayControllerLegacys on demand. */ @SysUISingleton -public class ClipboardOverlayControllerFactory { +public class ClipboardOverlayControllerLegacyFactory { private final UiEventLogger mUiEventLogger; private final BroadcastDispatcher mBroadcastDispatcher; private final BroadcastSender mBroadcastSender; @Inject - public ClipboardOverlayControllerFactory(BroadcastDispatcher broadcastDispatcher, + public ClipboardOverlayControllerLegacyFactory(BroadcastDispatcher broadcastDispatcher, BroadcastSender broadcastSender, UiEventLogger uiEventLogger) { this.mBroadcastDispatcher = broadcastDispatcher; this.mBroadcastSender = broadcastSender; @@ -45,10 +45,10 @@ public class ClipboardOverlayControllerFactory { } /** - * One new ClipboardOverlayController, coming right up! + * One new ClipboardOverlayControllerLegacy, coming right up! */ - public ClipboardOverlayController create(Context context) { - return new ClipboardOverlayController(context, mBroadcastDispatcher, mBroadcastSender, + public ClipboardOverlayControllerLegacy create(Context context) { + return new ClipboardOverlayControllerLegacy(context, mBroadcastDispatcher, mBroadcastSender, new TimeoutHandler(context), mUiEventLogger); } } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java new file mode 100644 index 000000000000..2d3315759371 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.clipboardoverlay; + +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + +import static java.util.Objects.requireNonNull; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.app.RemoteAction; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Insets; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Icon; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.MathUtils; +import android.util.TypedValue; +import android.view.DisplayCutout; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowInsets; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.LinearInterpolator; +import android.view.animation.PathInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.android.systemui.R; +import com.android.systemui.screenshot.DraggableConstraintLayout; +import com.android.systemui.screenshot.FloatingWindowUtil; +import com.android.systemui.screenshot.OverlayActionChip; + +import java.util.ArrayList; + +/** + * Handles the visual elements and animations for the clipboard overlay. + */ +public class ClipboardOverlayView extends DraggableConstraintLayout { + + interface ClipboardOverlayCallbacks extends SwipeDismissCallbacks { + void onDismissButtonTapped(); + + void onRemoteCopyButtonTapped(); + + void onEditButtonTapped(); + + void onShareButtonTapped(); + + void onPreviewTapped(); + } + + private static final String TAG = "ClipboardView"; + + private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe + private static final int FONT_SEARCH_STEP_PX = 4; + + private final DisplayMetrics mDisplayMetrics; + private final AccessibilityManager mAccessibilityManager; + private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>(); + + private View mClipboardPreview; + private ImageView mImagePreview; + private TextView mTextPreview; + private TextView mHiddenPreview; + private View mPreviewBorder; + private OverlayActionChip mEditChip; + private OverlayActionChip mShareChip; + private OverlayActionChip mRemoteCopyChip; + private View mActionContainerBackground; + private View mDismissButton; + private LinearLayout mActionContainer; + + public ClipboardOverlayView(Context context) { + this(context, null); + } + + public ClipboardOverlayView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ClipboardOverlayView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mDisplayMetrics = new DisplayMetrics(); + mContext.getDisplay().getRealMetrics(mDisplayMetrics); + mAccessibilityManager = AccessibilityManager.getInstance(mContext); + } + + @Override + protected void onFinishInflate() { + mActionContainerBackground = + requireNonNull(findViewById(R.id.actions_container_background)); + mActionContainer = requireNonNull(findViewById(R.id.actions)); + mClipboardPreview = requireNonNull(findViewById(R.id.clipboard_preview)); + mImagePreview = requireNonNull(findViewById(R.id.image_preview)); + mTextPreview = requireNonNull(findViewById(R.id.text_preview)); + mHiddenPreview = requireNonNull(findViewById(R.id.hidden_preview)); + mPreviewBorder = requireNonNull(findViewById(R.id.preview_border)); + mEditChip = requireNonNull(findViewById(R.id.edit_chip)); + mShareChip = requireNonNull(findViewById(R.id.share_chip)); + mRemoteCopyChip = requireNonNull(findViewById(R.id.remote_copy_chip)); + mDismissButton = requireNonNull(findViewById(R.id.dismiss_button)); + + mEditChip.setAlpha(1); + mShareChip.setAlpha(1); + mRemoteCopyChip.setAlpha(1); + mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share)); + + mEditChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); + mRemoteCopyChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true); + mShareChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); + + mRemoteCopyChip.setContentDescription( + mContext.getString(R.string.clipboard_send_nearby_description)); + + mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> { + int availableHeight = mTextPreview.getHeight() + - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom()); + mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight()); + return true; + }); + super.onFinishInflate(); + } + + @Override + public void setCallbacks(SwipeDismissCallbacks callbacks) { + super.setCallbacks(callbacks); + ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks; + mEditChip.setOnClickListener(v -> clipboardCallbacks.onEditButtonTapped()); + mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped()); + mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped()); + mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped()); + mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped()); + } + + void setEditAccessibilityAction(boolean editable) { + if (editable) { + ViewCompat.replaceAccessibilityAction(mClipboardPreview, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + mContext.getString(R.string.clipboard_edit), null); + } else { + ViewCompat.replaceAccessibilityAction(mClipboardPreview, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + null, null); + } + } + + void setInsets(WindowInsets insets, int orientation) { + FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) getLayoutParams(); + if (p == null) { + return; + } + Rect margins = computeMargins(insets, orientation); + p.setMargins(margins.left, margins.top, margins.right, margins.bottom); + setLayoutParams(p); + requestLayout(); + } + + boolean isInTouchRegion(int x, int y) { + Region touchRegion = new Region(); + final Rect tmpRect = new Rect(); + + mPreviewBorder.getBoundsOnScreen(tmpRect); + tmpRect.inset( + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP)); + touchRegion.op(tmpRect, Region.Op.UNION); + + mActionContainerBackground.getBoundsOnScreen(tmpRect); + tmpRect.inset( + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP)); + touchRegion.op(tmpRect, Region.Op.UNION); + + mDismissButton.getBoundsOnScreen(tmpRect); + touchRegion.op(tmpRect, Region.Op.UNION); + + return touchRegion.contains(x, y); + } + + void setRemoteCopyVisibility(boolean visible) { + if (visible) { + mRemoteCopyChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + } else { + mRemoteCopyChip.setVisibility(View.GONE); + } + } + + void showDefaultTextPreview() { + String copied = mContext.getString(R.string.clipboard_overlay_text_copied); + showTextPreview(copied, false); + } + + void showTextPreview(CharSequence text, boolean hidden) { + TextView textView = hidden ? mHiddenPreview : mTextPreview; + showSinglePreview(textView); + textView.setText(text.subSequence(0, Math.min(500, text.length()))); + updateTextSize(text, textView); + textView.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (right - left != oldRight - oldLeft) { + updateTextSize(text, textView); + } + }); + mEditChip.setVisibility(View.GONE); + } + + void showImagePreview(@Nullable Bitmap thumbnail) { + if (thumbnail == null) { + mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden)); + showSinglePreview(mHiddenPreview); + } else { + mImagePreview.setImageBitmap(thumbnail); + showSinglePreview(mImagePreview); + } + } + + void showEditChip(String contentDescription) { + mEditChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mEditChip.setContentDescription(contentDescription); + } + + void showShareChip() { + mShareChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + } + + void reset() { + setTranslationX(0); + setAlpha(0); + mActionContainerBackground.setVisibility(View.GONE); + mDismissButton.setVisibility(View.GONE); + mShareChip.setVisibility(View.GONE); + mEditChip.setVisibility(View.GONE); + mRemoteCopyChip.setVisibility(View.GONE); + setEditAccessibilityAction(false); + resetActionChips(); + } + + void resetActionChips() { + for (OverlayActionChip chip : mActionChips) { + mActionContainer.removeView(chip); + } + mActionChips.clear(); + } + + Animator getEnterAnimation() { + if (mAccessibilityManager.isEnabled()) { + mDismissButton.setVisibility(View.VISIBLE); + } + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f); + AnimatorSet enterAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(66); + rootAnim.addUpdateListener(animation -> { + setAlpha(animation.getAnimatedFraction()); + }); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(333); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction()); + float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionsScaleX); + mActionContainer.setScaleY(actionsScaleY); + mActionContainerBackground.setScaleX(actionsScaleX); + mActionContainerBackground.setScaleY(actionsScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(283); + alphaAnim.addUpdateListener(animation -> { + float alpha = animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + mActionContainer.setAlpha(0); + mPreviewBorder.setAlpha(0); + mClipboardPreview.setAlpha(0); + enterAnim.play(rootAnim).with(scaleAnim); + enterAnim.play(alphaAnim).after(50).after(rootAnim); + + enterAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + setAlpha(1); + } + }); + return enterAnim; + } + + Animator getExitAnimation() { + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f); + AnimatorSet exitAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(100); + rootAnim.addUpdateListener(anim -> setAlpha(1 - anim.getAnimatedFraction())); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(250); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction()); + float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionScaleX); + mActionContainer.setScaleY(actionScaleY); + mActionContainerBackground.setScaleX(actionScaleX); + mActionContainerBackground.setScaleY(actionScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(166); + alphaAnim.addUpdateListener(animation -> { + float alpha = 1 - animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + exitAnim.play(alphaAnim).with(scaleAnim); + exitAnim.play(rootAnim).after(150).after(alphaAnim); + return exitAnim; + } + + void setActionChip(RemoteAction action, Runnable onFinish) { + mActionContainerBackground.setVisibility(View.VISIBLE); + OverlayActionChip chip = constructActionChip(action, onFinish); + mActionContainer.addView(chip); + mActionChips.add(chip); + } + + private void showSinglePreview(View v) { + mTextPreview.setVisibility(View.GONE); + mImagePreview.setVisibility(View.GONE); + mHiddenPreview.setVisibility(View.GONE); + v.setVisibility(View.VISIBLE); + } + + private OverlayActionChip constructActionChip(RemoteAction action, Runnable onFinish) { + OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate( + R.layout.overlay_action_chip, mActionContainer, false); + chip.setText(action.getTitle()); + chip.setContentDescription(action.getTitle()); + chip.setIcon(action.getIcon(), false); + chip.setPendingIntent(action.getActionIntent(), onFinish); + chip.setAlpha(1); + return chip; + } + + private static void updateTextSize(CharSequence text, TextView textView) { + Paint paint = new Paint(textView.getPaint()); + Resources res = textView.getResources(); + float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font); + float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font); + if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) { + // If the text is a single word and would fit within the TextView at the min font size, + // find the biggest font size that will fit. + float fontSizePx = minFontSize; + while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize + && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) { + fontSizePx += FONT_SEARCH_STEP_PX; + } + // Need to turn off autosizing, otherwise setTextSize is a no-op. + textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE); + // It's possible to hit the max font size and not fill the width, so centering + // horizontally looks better in this case. + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx); + } else { + // Otherwise just stick with autosize. + textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize, + (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX); + textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); + } + } + + private static boolean fitsInView(CharSequence text, TextView textView, Paint paint, + float fontSizePx) { + paint.setTextSize(fontSizePx); + float size = paint.measureText(text.toString()); + float availableWidth = textView.getWidth() - textView.getPaddingLeft() + - textView.getPaddingRight(); + return size < availableWidth; + } + + private static boolean isOneWord(CharSequence text) { + return text.toString().split("\\s+", 2).length == 1; + } + + private static Rect computeMargins(WindowInsets insets, int orientation) { + DisplayCutout cutout = insets.getDisplayCutout(); + Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); + Insets imeInsets = insets.getInsets(WindowInsets.Type.ime()); + if (cutout == null) { + return new Rect(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom)); + } else { + Insets waterfall = cutout.getWaterfallInsets(); + if (orientation == ORIENTATION_PORTRAIT) { + return new Rect( + waterfall.left, + Math.max(cutout.getSafeInsetTop(), waterfall.top), + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(cutout.getSafeInsetBottom(), + Math.max(navBarInsets.bottom, waterfall.bottom)))); + } else { + return new Rect( + waterfall.left, + waterfall.top, + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(navBarInsets.bottom, waterfall.bottom))); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java new file mode 100644 index 000000000000..9dac9b393d60 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.clipboardoverlay; + +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.app.ICompatCameraControlCallback; +import android.content.Context; +import android.content.res.Configuration; +import android.util.Log; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; + +import com.android.internal.policy.PhoneWindow; +import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext; +import com.android.systemui.screenshot.FloatingWindowUtil; + +import java.util.function.BiConsumer; + +import javax.inject.Inject; + +/** + * Handles attaching the window and the window insets for the clipboard overlay. + */ +public class ClipboardOverlayWindow extends PhoneWindow + implements ViewRootImpl.ActivityConfigCallback { + private static final String TAG = "ClipboardOverlayWindow"; + + private final Context mContext; + private final WindowManager mWindowManager; + private final WindowManager.LayoutParams mWindowLayoutParams; + + private boolean mKeyboardVisible; + private final int mOrientation; + private BiConsumer<WindowInsets, Integer> mOnKeyboardChangeListener; + private Runnable mOnOrientationChangeListener; + + @Inject + ClipboardOverlayWindow(@OverlayWindowContext Context context) { + super(context); + mContext = context; + mOrientation = mContext.getResources().getConfiguration().orientation; + + // Setup the window that we are going to use + requestFeature(Window.FEATURE_NO_TITLE); + requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS); + setBackgroundDrawableResource(android.R.color.transparent); + mWindowManager = mContext.getSystemService(WindowManager.class); + mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); + mWindowLayoutParams.setTitle("ClipboardOverlay"); + setWindowManager(mWindowManager, null, null); + setWindowFocusable(false); + } + + /** + * Set callbacks for keyboard state change and orientation change and attach the window + * + * @param onKeyboardChangeListener callback for IME visibility changes + * @param onOrientationChangeListener callback for device orientation changes + */ + public void init(@NonNull BiConsumer<WindowInsets, Integer> onKeyboardChangeListener, + @NonNull Runnable onOrientationChangeListener) { + mOnKeyboardChangeListener = onKeyboardChangeListener; + mOnOrientationChangeListener = onOrientationChangeListener; + + attach(); + withWindowAttached(() -> { + WindowInsets currentInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + mKeyboardVisible = currentInsets.isVisible(WindowInsets.Type.ime()); + peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(() -> { + WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime()); + if (keyboardVisible != mKeyboardVisible) { + mKeyboardVisible = keyboardVisible; + mOnKeyboardChangeListener.accept(insets, mOrientation); + } + }); + peekDecorView().getViewRootImpl().setActivityConfigCallback(this); + }); + } + + @Override // ViewRootImpl.ActivityConfigCallback + public void onConfigurationChanged(Configuration overrideConfig, int newDisplayId) { + if (mContext.getResources().getConfiguration().orientation != mOrientation) { + mOnOrientationChangeListener.run(); + } + } + + @Override // ViewRootImpl.ActivityConfigCallback + public void requestCompatCameraControl(boolean showControl, boolean transformationApplied, + ICompatCameraControlCallback callback) { + Log.w(TAG, "unexpected requestCompatCameraControl call"); + } + + void remove() { + final View decorView = peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.removeViewImmediate(decorView); + } + } + + WindowInsets getWindowInsets() { + return mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + } + + void withWindowAttached(Runnable action) { + View decorView = getDecorView(); + if (decorView.isAttachedToWindow()) { + action.run(); + } else { + decorView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + decorView.getViewTreeObserver().removeOnWindowAttachListener(this); + action.run(); + } + + @Override + public void onWindowDetached() { + } + }); + } + } + + @MainThread + private void attach() { + View decorView = getDecorView(); + if (decorView.isAttachedToWindow()) { + return; + } + mWindowManager.addView(decorView, mWindowLayoutParams); + decorView.requestApplyInsets(); + } + + /** + * Updates the window focusability. If the window is already showing, then it updates the + * window immediately, otherwise the layout params will be applied when the window is next + * shown. + */ + private void setWindowFocusable(boolean focusable) { + int flags = mWindowLayoutParams.flags; + if (focusable) { + mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } + if (mWindowLayoutParams.flags == flags) { + return; + } + final View decorView = peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java new file mode 100644 index 000000000000..22448130f7e5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.clipboardoverlay.dagger; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.view.Display; +import android.view.LayoutInflater; + +import com.android.systemui.R; +import com.android.systemui.clipboardoverlay.ClipboardOverlayView; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +import dagger.Module; +import dagger.Provides; + +/** Module for {@link com.android.systemui.clipboardoverlay}. */ +@Module +public interface ClipboardOverlayModule { + + /** + * + */ + @Provides + @OverlayWindowContext + static Context provideWindowContext(DisplayManager displayManager, Context context) { + Display display = displayManager.getDisplay(DEFAULT_DISPLAY); + return context.createWindowContext(display, TYPE_SCREENSHOT, null); + } + + /** + * + */ + @Provides + static ClipboardOverlayView provideClipboardOverlayView(@OverlayWindowContext Context context) { + return (ClipboardOverlayView) LayoutInflater.from(context).inflate( + R.layout.clipboard_overlay, null); + } + + @Qualifier + @Documented + @Retention(RUNTIME) + @interface OverlayWindowContext { + } +} diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt index bebade0cc484..08e8293cbe9c 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt @@ -17,6 +17,7 @@ package com.android.systemui.common.shared.model import android.annotation.StringRes +import android.content.Context /** * Models a content description, that can either be already [loaded][ContentDescription.Loaded] or @@ -30,4 +31,20 @@ sealed class ContentDescription { data class Resource( @StringRes val res: Int, ) : ContentDescription() + + companion object { + /** + * Returns the loaded content description string, or null if we don't have one. + * + * Prefer [com.android.systemui.common.ui.binder.ContentDescriptionViewBinder.bind] over + * this method. This should only be used for testing or concatenation purposes. + */ + fun ContentDescription?.loadContentDescription(context: Context): String? { + return when (this) { + null -> null + is Loaded -> this.description + is Resource -> context.getString(this.res) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt index 5d0e08ffc307..4a5693202dba 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt @@ -18,6 +18,7 @@ package com.android.systemui.common.shared.model import android.annotation.StringRes +import android.content.Context /** * Models a text, that can either be already [loaded][Text.Loaded] or be a [reference] @@ -31,4 +32,20 @@ sealed class Text { data class Resource( @StringRes val res: Int, ) : Text() + + companion object { + /** + * Returns the loaded test string, or null if we don't have one. + * + * Prefer [com.android.systemui.common.ui.binder.TextViewBinder.bind] over this method. This + * should only be used for testing or concatenation purposes. + */ + fun Text?.loadText(context: Context): String? { + return when (this) { + null -> null + is Loaded -> this.text + is Resource -> context.getString(this.res) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt new file mode 100644 index 000000000000..f95a8ee89a2c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LaunchableImageView.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.common.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import com.android.systemui.animation.LaunchableView +import com.android.systemui.animation.LaunchableViewDelegate + +class LaunchableImageView : ImageView, LaunchableView { + private val delegate = + LaunchableViewDelegate( + this, + superSetVisibility = { super.setVisibility(it) }, + superSetTransitionVisibility = { super.setTransitionVisibility(it) }, + ) + + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun setShouldBlockVisibilityChanges(block: Boolean) { + delegate.setShouldBlockVisibilityChanges(block) + } + + override fun setVisibility(visibility: Int) { + delegate.setVisibility(visibility) + } + + override fun setTransitionVisibility(visibility: Int) { + delegate.setTransitionVisibility(visibility) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt b/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt deleted file mode 100644 index d6a059da3afa..000000000000 --- a/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.containeddrawable - -import android.graphics.drawable.Drawable -import androidx.annotation.DrawableRes - -/** Convenience container for [Drawable] or a way to load it later. */ -sealed class ContainedDrawable { - data class WithDrawable(val drawable: Drawable) : ContainedDrawable() - data class WithResource(@DrawableRes val resourceId: Int) : ContainedDrawable() -} diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt index 77b65233c112..d3b5d0edd222 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt @@ -21,6 +21,8 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Bundle +import android.os.RemoteException +import android.service.dreams.IDreamManager import android.view.View import android.view.ViewGroup import android.view.WindowInsets @@ -40,11 +42,13 @@ import javax.inject.Inject */ class ControlsActivity @Inject constructor( private val uiController: ControlsUiController, - private val broadcastDispatcher: BroadcastDispatcher + private val broadcastDispatcher: BroadcastDispatcher, + private val dreamManager: IDreamManager, ) : ComponentActivity() { private lateinit var parent: ViewGroup private lateinit var broadcastReceiver: BroadcastReceiver + private var mExitToDream: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -81,17 +85,36 @@ class ControlsActivity @Inject constructor( parent = requireViewById<ViewGroup>(R.id.global_actions_controls) parent.alpha = 0f - uiController.show(parent, { finish() }, this) + uiController.show(parent, { finishOrReturnToDream() }, this) ControlsAnimations.enterAnimation(parent).start() } - override fun onBackPressed() { + override fun onResume() { + super.onResume() + mExitToDream = intent.getBooleanExtra(ControlsUiController.EXIT_TO_DREAM, false) + } + + fun finishOrReturnToDream() { + if (mExitToDream) { + try { + mExitToDream = false + dreamManager.dream() + return + } catch (e: RemoteException) { + // Fall through + } + } finish() } + override fun onBackPressed() { + finishOrReturnToDream() + } + override fun onStop() { super.onStop() + mExitToDream = false uiController.hide() } @@ -106,7 +129,8 @@ class ControlsActivity @Inject constructor( broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action = intent.getAction() - if (Intent.ACTION_SCREEN_OFF.equals(action)) { + if (action == Intent.ACTION_SCREEN_OFF || + action == Intent.ACTION_DREAMING_STARTED) { finish() } } @@ -114,6 +138,7 @@ class ControlsActivity @Inject constructor( val filter = IntentFilter() filter.addAction(Intent.ACTION_SCREEN_OFF) + filter.addAction(Intent.ACTION_DREAMING_STARTED) broadcastDispatcher.registerReceiver(broadcastReceiver, filter) } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index 822f8f2e6191..c1cfbcb0c211 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -27,6 +27,7 @@ interface ControlsUiController { companion object { public const val TAG = "ControlsUiController" public const val EXTRA_ANIMATE = "extra_animate" + public const val EXIT_TO_DREAM = "extra_exit_to_dream" } fun show(parent: ViewGroup, onDismiss: Runnable, activityContext: Context) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 4096ed4283e5..139a8b769583 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -46,6 +46,7 @@ import android.content.res.AssetManager; import android.content.res.Resources; import android.hardware.SensorManager; import android.hardware.SensorPrivacyManager; +import android.hardware.biometrics.BiometricManager; import android.hardware.camera2.CameraManager; import android.hardware.devicestate.DeviceStateManager; import android.hardware.display.AmbientDisplayConfiguration; @@ -237,22 +238,39 @@ public class FrameworkServicesModule { @Singleton static IDreamManager provideIDreamManager() { return IDreamManager.Stub.asInterface( - ServiceManager.checkService(DreamService.DREAM_SERVICE)); + ServiceManager.getService(DreamService.DREAM_SERVICE)); } @Provides @Singleton @Nullable static FaceManager provideFaceManager(Context context) { - return context.getSystemService(FaceManager.class); - + if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)) { + return context.getSystemService(FaceManager.class); + } + return null; } @Provides @Singleton @Nullable static FingerprintManager providesFingerprintManager(Context context) { - return context.getSystemService(FingerprintManager.class); + if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { + return context.getSystemService(FingerprintManager.class); + } + return null; + } + + /** + * @return null if both faceManager and fingerprintManager are null. + */ + @Provides + @Singleton + @Nullable + static BiometricManager providesBiometricManager(Context context, + @Nullable FaceManager faceManager, @Nullable FingerprintManager fingerprintManager) { + return faceManager == null && fingerprintManager == null ? null : + context.getSystemService(BiometricManager.class); } @Provides diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index a99669970a34..48bef97c30fb 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -22,25 +22,19 @@ import static com.android.systemui.Dependency.LEAK_REPORT_EMAIL_NAME; import android.content.Context; import android.hardware.SensorPrivacyManager; import android.os.Handler; -import android.os.PowerManager; import androidx.annotation.Nullable; import com.android.internal.logging.UiEventLogger; import com.android.keyguard.KeyguardViewController; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; -import com.android.systemui.dump.DumpManager; import com.android.systemui.media.dagger.MediaModule; import com.android.systemui.navigationbar.gestural.GestureModule; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.power.EnhancedEstimates; import com.android.systemui.power.dagger.PowerModule; import com.android.systemui.qs.dagger.QSModule; import com.android.systemui.qs.tileimpl.QSFactoryImpl; @@ -62,8 +56,7 @@ import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; -import com.android.systemui.statusbar.policy.BatteryController; -import com.android.systemui.statusbar.policy.BatteryControllerImpl; +import com.android.systemui.statusbar.policy.AospPolicyModule; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedControllerImpl; @@ -97,6 +90,7 @@ import dagger.Provides; * SystemUI code that variants of SystemUI _must_ include to function correctly. */ @Module(includes = { + AospPolicyModule.class, GestureModule.class, MediaModule.class, PowerModule.class, @@ -121,30 +115,6 @@ public abstract class ReferenceSystemUIModule { @Provides @SysUISingleton - static BatteryController provideBatteryController( - Context context, - EnhancedEstimates enhancedEstimates, - PowerManager powerManager, - BroadcastDispatcher broadcastDispatcher, - DemoModeController demoModeController, - DumpManager dumpManager, - @Main Handler mainHandler, - @Background Handler bgHandler) { - BatteryController bC = new BatteryControllerImpl( - context, - enhancedEstimates, - powerManager, - broadcastDispatcher, - demoModeController, - dumpManager, - mainHandler, - bgHandler); - bC.init(); - return bC; - } - - @Provides - @SysUISingleton static SensorPrivacyController provideSensorPrivacyController( SensorPrivacyManager sensorPrivacyManager) { SensorPrivacyController spC = new SensorPrivacyControllerImpl(sensorPrivacyManager); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 0d06c513d248..d05bd5120872 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -27,10 +27,8 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardSliceProvider; import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; -import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper; -import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver; -import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender; import com.android.systemui.people.PeopleProvider; +import com.android.systemui.statusbar.QsFrameTranslateModule; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.unfold.FoldStateLogger; import com.android.systemui.unfold.FoldStateLoggingProvider; @@ -66,6 +64,7 @@ import dagger.Subcomponent; @Subcomponent(modules = { DefaultComponentBinder.class, DependencyProvider.class, + QsFrameTranslateModule.class, SystemUIBinder.class, SystemUIModule.class, SystemUICoreStartableModule.class, @@ -133,9 +132,6 @@ public interface SysUIComponent { }); getNaturalRotationUnfoldProgressProvider().ifPresent(o -> o.init()); // No init method needed, just needs to be gotten so that it's created. - getMediaTttChipControllerSender(); - getMediaTttChipControllerReceiver(); - getMediaTttCommandLineHelper(); getMediaMuteAwaitConnectionCli(); getNearbyMediaDevicesManager(); getUnfoldLatencyTracker().init(); @@ -206,15 +202,6 @@ public interface SysUIComponent { Optional<NaturalRotationUnfoldProgressProvider> getNaturalRotationUnfoldProgressProvider(); /** */ - Optional<MediaTttChipControllerSender> getMediaTttChipControllerSender(); - - /** */ - Optional<MediaTttChipControllerReceiver> getMediaTttChipControllerReceiver(); - - /** */ - Optional<MediaTttCommandLineHelper> getMediaTttCommandLineHelper(); - - /** */ Optional<MediaMuteAwaitConnectionCli> getMediaMuteAwaitConnectionCli(); /** */ diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 8bb27a7bc217..721c0ba4f865 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -32,12 +32,16 @@ import com.android.systemui.keyboard.KeyboardUI import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.log.SessionTracker import com.android.systemui.media.RingtonePlayer +import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper +import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver +import com.android.systemui.media.taptotransfer.sender.MediaTttSenderCoordinator import com.android.systemui.power.PowerUI import com.android.systemui.recents.Recents import com.android.systemui.settings.dagger.MultiUserUtilsModule import com.android.systemui.shortcut.ShortcutKeyDispatcher import com.android.systemui.statusbar.notification.InstantAppNotifier import com.android.systemui.statusbar.phone.KeyguardLiftController +import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.theme.ThemeOverlayController import com.android.systemui.toast.ToastUI import com.android.systemui.usb.StorageNotification @@ -213,4 +217,30 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(KeyguardLiftController::class) abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable + + /** Inject into MediaTttSenderCoordinator. */ + @Binds + @IntoMap + @ClassKey(MediaTttSenderCoordinator::class) + abstract fun bindMediaTttSenderCoordinator(sysui: MediaTttSenderCoordinator): CoreStartable + + /** Inject into MediaTttChipControllerReceiver. */ + @Binds + @IntoMap + @ClassKey(MediaTttChipControllerReceiver::class) + abstract fun bindMediaTttChipControllerReceiver( + sysui: MediaTttChipControllerReceiver + ): CoreStartable + + /** Inject into MediaTttCommandLineHelper. */ + @Binds + @IntoMap + @ClassKey(MediaTttCommandLineHelper::class) + abstract fun bindMediaTttCommandLineHelper(sysui: MediaTttCommandLineHelper): CoreStartable + + /** Inject into ChipbarCoordinator. */ + @Binds + @IntoMap + @ClassKey(ChipbarCoordinator::class) + abstract fun bindChipbarController(sysui: ChipbarCoordinator): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 443d2774f0e0..7e31626983e7 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -23,7 +23,8 @@ import android.service.dreams.IDreamManager; import androidx.annotation.Nullable; import com.android.internal.statusbar.IStatusBarService; -import com.android.keyguard.clock.ClockModule; +import com.android.keyguard.clock.ClockInfoModule; +import com.android.keyguard.dagger.ClockRegistryModule; import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; @@ -33,6 +34,7 @@ import com.android.systemui.biometrics.AlternateUdfpsTouchProvider; import com.android.systemui.biometrics.UdfpsDisplayModeProvider; import com.android.systemui.biometrics.dagger.BiometricsModule; import com.android.systemui.classifier.FalsingModule; +import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule; import com.android.systemui.controls.dagger.ControlsModule; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.dagger.DemoModeModule; @@ -43,7 +45,7 @@ import com.android.systemui.flags.FlagsModule; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.data.BouncerViewModule; import com.android.systemui.log.dagger.LogModule; -import com.android.systemui.media.dagger.MediaProjectionModule; +import com.android.systemui.mediaprojection.appselector.MediaProjectionModule; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationBarComponent; import com.android.systemui.people.PeopleModule; @@ -61,7 +63,6 @@ import com.android.systemui.smartspace.dagger.SmartspaceModule; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationShadeWindowController; -import com.android.systemui.statusbar.QsFrameTranslateModule; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; @@ -81,6 +82,7 @@ import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule; import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule; import com.android.systemui.statusbar.window.StatusBarWindowModule; +import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule; import com.android.systemui.tuner.dagger.TunerModule; import com.android.systemui.unfold.SysUIUnfoldModule; import com.android.systemui.user.UserModule; @@ -118,7 +120,9 @@ import dagger.Provides; AssistModule.class, BiometricsModule.class, BouncerViewModule.class, - ClockModule.class, + ClipboardOverlayModule.class, + ClockInfoModule.class, + ClockRegistryModule.class, CoroutinesModule.class, DreamModule.class, ControlsModule.class, @@ -132,7 +136,6 @@ import dagger.Provides; PeopleModule.class, PluginModule.class, PrivacyModule.class, - QsFrameTranslateModule.class, ScreenshotModule.class, SensorModule.class, MultiUserUtilsModule.class, @@ -145,6 +148,7 @@ import dagger.Provides; StatusBarWindowModule.class, SysUIConcurrencyModule.class, SysUIUnfoldModule.class, + TelephonyRepositoryModule.class, TunerModule.class, UserModule.class, UtilModule.class, @@ -165,12 +169,16 @@ public abstract class SystemUIModule { @Binds abstract BootCompleteCache bindBootCompleteCache(BootCompleteCacheImpl bootCompleteCache); - /** */ + /** + * + */ @Binds public abstract ContextComponentHelper bindComponentHelper( ContextComponentResolver componentHelper); - /** */ + /** + * + */ @Binds public abstract NotificationRowBinder bindNotificationRowBinder( NotificationRowBinderImpl notificationRowBinder); @@ -209,6 +217,7 @@ public abstract class SystemUIModule { abstract SystemClock bindSystemClock(SystemClockImpl systemClock); // TODO: This should provided by the WM component + /** Provides Optional of BubbleManager */ @SysUISingleton @Provides diff --git a/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/BroadcastRunning.java b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/BroadcastRunning.java new file mode 100644 index 000000000000..5f8e54097e53 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/BroadcastRunning.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dagger.qualifiers; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface BroadcastRunning { +} diff --git a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt index 991b54e8035e..ded0fb79cd6f 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt @@ -59,7 +59,7 @@ class CutoutDecorProviderImpl( (view as? DisplayCutoutView)?.let { cutoutView -> cutoutView.setColor(tintColor) cutoutView.updateRotation(rotation) - cutoutView.onDisplayChanged(displayUniqueId) + cutoutView.updateConfiguration(displayUniqueId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt index ec0013bb5000..976afd457f79 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt @@ -34,8 +34,6 @@ import com.android.systemui.FaceScanningOverlay import com.android.systemui.biometrics.AuthController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.plugins.statusbar.StatusBarStateController import java.util.concurrent.Executor import javax.inject.Inject @@ -47,15 +45,13 @@ class FaceScanningProviderFactory @Inject constructor( private val statusBarStateController: StatusBarStateController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, @Main private val mainExecutor: Executor, - private val featureFlags: FeatureFlags ) : DecorProviderFactory() { private val display = context.display private val displayInfo = DisplayInfo() override val hasProviders: Boolean get() { - if (!featureFlags.isEnabled(Flags.FACE_SCANNING_ANIM) || - authController.faceSensorLocation == null) { + if (authController.faceSensorLocation == null) { return false } @@ -99,7 +95,7 @@ class FaceScanningProviderFactory @Inject constructor( } fun shouldShowFaceScanningAnim(): Boolean { - return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceScanning + return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceDetectionRunning } } @@ -124,7 +120,7 @@ class FaceScanningOverlayProviderImpl( view.layoutParams = it (view as? FaceScanningOverlay)?.let { overlay -> overlay.setColor(tintColor) - overlay.onDisplayChanged(displayUniqueId) + overlay.updateConfiguration(displayUniqueId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt index a25286438387..8b4aeefb6ed4 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt @@ -78,23 +78,18 @@ class RoundedCornerResDelegate( reloadMeasures() } - private fun reloadAll(newReloadToken: Int) { - if (reloadToken == newReloadToken) { - return - } - reloadToken = newReloadToken - reloadRes() - reloadMeasures() - } - fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?) { if (displayUniqueId != newDisplayUniqueId) { displayUniqueId = newDisplayUniqueId newReloadToken ?.let { reloadToken = it } reloadRes() reloadMeasures() - } else { - newReloadToken?.let { reloadAll(it) } + } else if (newReloadToken != null) { + if (reloadToken == newReloadToken) { + return + } + reloadToken = newReloadToken + reloadMeasures() } } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java index b59855426a3c..4c4aa5ce1911 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java @@ -33,6 +33,18 @@ public interface DozeHost { boolean isProvisioned(); /** + * Whether there's a pulse that's been requested but hasn't started transitioning to pulsing + * states yet. + */ + boolean isPulsePending(); + + /** + * @param isPulsePending whether a pulse has been requested but hasn't started transitioning + * to the pulse state yet + */ + void setPulsePending(boolean isPulsePending); + + /** * Makes a current pulse last for twice as long. * @param reason why we're extending it. */ diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index 4161cf6d2657..b69afeb37371 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -280,15 +280,22 @@ public class DozeLog implements Dumpable { /** * Appends pulse dropped event to logs */ - public void tracePulseDropped(boolean pulsePending, DozeMachine.State state, boolean blocked) { - mLogger.logPulseDropped(pulsePending, state, blocked); + public void tracePulseDropped(String from, DozeMachine.State state) { + mLogger.logPulseDropped(from, state); } /** * Appends sensor event dropped event to logs */ - public void traceSensorEventDropped(int sensorEvent, String reason) { - mLogger.logSensorEventDropped(sensorEvent, reason); + public void traceSensorEventDropped(@Reason int pulseReason, String reason) { + mLogger.logSensorEventDropped(pulseReason, reason); + } + + /** + * Appends pulsing event to logs. + */ + public void tracePulseEvent(String pulseEvent, boolean dozing, int pulseReason) { + mLogger.logPulseEvent(pulseEvent, dozing, DozeLog.reasonToString(pulseReason)); } /** @@ -379,6 +386,47 @@ public class DozeLog implements Dumpable { mLogger.logSetAodDimmingScrim((long) scrimOpacity); } + /** + * Appends sensor attempted to register and whether it was a successful registration. + */ + public void traceSensorRegisterAttempt(String sensorName, boolean successfulRegistration) { + mLogger.logSensorRegisterAttempt(sensorName, successfulRegistration); + } + + /** + * Appends sensor attempted to unregister and whether it was successfully unregistered. + */ + public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered) { + mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered); + } + + /** + * Appends sensor attempted to unregister and whether it was successfully unregistered + * with a reason the sensor is being unregistered. + */ + public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered, + String reason) { + mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered, reason); + } + + /** + * Appends the event of skipping a sensor registration since it's already registered. + */ + public void traceSkipRegisterSensor(String sensorInfo) { + mLogger.logSkipSensorRegistration(sensorInfo); + } + + /** + * Appends a plugin sensor was registered or unregistered event. + */ + public void tracePluginSensorUpdate(boolean registered) { + if (registered) { + mLogger.log("register plugin sensor"); + } else { + mLogger.log("unregister plugin sensor"); + } + } + private class SummaryStats { private int mCount; @@ -424,8 +472,8 @@ public class DozeLog implements Dumpable { } @Override - public void onKeyguardVisibilityChanged(boolean showing) { - traceKeyguard(showing); + public void onKeyguardVisibilityChanged(boolean visible) { + traceKeyguard(visible); } }; diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt index 4b279ec8f008..18c8e01cbf76 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt @@ -19,12 +19,13 @@ package com.android.systemui.doze import android.view.Display import com.android.systemui.doze.DozeLog.Reason import com.android.systemui.doze.DozeLog.reasonToString -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.log.dagger.DozeLog import com.android.systemui.statusbar.policy.DevicePostureController +import com.google.errorprone.annotations.CompileTimeConstant import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -155,11 +156,11 @@ class DozeLogger @Inject constructor( }) } - fun logKeyguardVisibilityChange(isShowing: Boolean) { + fun logKeyguardVisibilityChange(isVisible: Boolean) { buffer.log(TAG, INFO, { - bool1 = isShowing + bool1 = isVisible }, { - "Keyguard visibility change, isShowing=$bool1" + "Keyguard visibility change, isVisible=$bool1" }) } @@ -224,13 +225,16 @@ class DozeLogger @Inject constructor( }) } - fun logPulseDropped(pulsePending: Boolean, state: DozeMachine.State, blocked: Boolean) { + /** + * Log why a pulse was dropped and the current doze machine state. The state can be null + * if the DozeMachine is the middle of transitioning between states. + */ + fun logPulseDropped(from: String, state: DozeMachine.State?) { buffer.log(TAG, INFO, { - bool1 = pulsePending - str1 = state.name - bool2 = blocked + str1 = from + str2 = state?.name }, { - "Pulse dropped, pulsePending=$bool1 state=$str1 blocked=$bool2" + "Pulse dropped, cannot pulse from=$str1 state=$str2" }) } @@ -243,6 +247,16 @@ class DozeLogger @Inject constructor( }) } + fun logPulseEvent(pulseEvent: String, dozing: Boolean, pulseReason: String) { + buffer.log(TAG, DEBUG, { + str1 = pulseEvent + bool1 = dozing + str2 = pulseReason + }, { + "Pulse-$str1 dozing=$bool1 pulseReason=$str2" + }) + } + fun logPulseDropped(reason: String) { buffer.log(TAG, INFO, { str1 = reason @@ -311,6 +325,50 @@ class DozeLogger @Inject constructor( "Doze car mode started" }) } + + fun logSensorRegisterAttempt(sensorInfo: String, successfulRegistration: Boolean) { + buffer.log(TAG, INFO, { + str1 = sensorInfo + bool1 = successfulRegistration + }, { + "Register sensor. Success=$bool1 sensor=$str1" + }) + } + + fun logSensorUnregisterAttempt(sensorInfo: String, successfulUnregister: Boolean) { + buffer.log(TAG, INFO, { + str1 = sensorInfo + bool1 = successfulUnregister + }, { + "Unregister sensor. Success=$bool1 sensor=$str1" + }) + } + + fun logSensorUnregisterAttempt( + sensorInfo: String, + successfulUnregister: Boolean, + reason: String + ) { + buffer.log(TAG, INFO, { + str1 = sensorInfo + bool1 = successfulUnregister + str2 = reason + }, { + "Unregister sensor. reason=$str2. Success=$bool1 sensor=$str1" + }) + } + + fun logSkipSensorRegistration(sensor: String) { + buffer.log(TAG, DEBUG, { + str1 = sensor + }, { + "Skipping sensor registration because its already registered. sensor=$str1" + }) + } + + fun log(@CompileTimeConstant msg: String) { + buffer.log(TAG, DEBUG, msg) + } } private const val TAG = "DozeLog" diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java index ae412152b10e..96c35d43052e 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java @@ -20,7 +20,6 @@ import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWA import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING; import android.annotation.MainThread; -import android.app.UiModeManager; import android.content.res.Configuration; import android.hardware.display.AmbientDisplayConfiguration; import android.os.Trace; @@ -145,10 +144,9 @@ public class DozeMachine { private final Service mDozeService; private final WakeLock mWakeLock; - private final AmbientDisplayConfiguration mConfig; + private final AmbientDisplayConfiguration mAmbientDisplayConfig; private final WakefulnessLifecycle mWakefulnessLifecycle; private final DozeHost mDozeHost; - private final UiModeManager mUiModeManager; private final DockManager mDockManager; private final Part[] mParts; @@ -156,18 +154,18 @@ public class DozeMachine { private State mState = State.UNINITIALIZED; private int mPulseReason; private boolean mWakeLockHeldForCurrentState = false; + private int mUiModeType = Configuration.UI_MODE_TYPE_NORMAL; @Inject - public DozeMachine(@WrappedService Service service, AmbientDisplayConfiguration config, + public DozeMachine(@WrappedService Service service, + AmbientDisplayConfiguration ambientDisplayConfig, WakeLock wakeLock, WakefulnessLifecycle wakefulnessLifecycle, - UiModeManager uiModeManager, DozeLog dozeLog, DockManager dockManager, DozeHost dozeHost, Part[] parts) { mDozeService = service; - mConfig = config; + mAmbientDisplayConfig = ambientDisplayConfig; mWakefulnessLifecycle = wakefulnessLifecycle; mWakeLock = wakeLock; - mUiModeManager = uiModeManager; mDozeLog = dozeLog; mDockManager = dockManager; mDozeHost = dozeHost; @@ -187,6 +185,18 @@ public class DozeMachine { } /** + * Notifies the {@link DozeMachine} that {@link Configuration} has changed. + */ + public void onConfigurationChanged(Configuration newConfiguration) { + int newUiModeType = newConfiguration.uiMode & Configuration.UI_MODE_TYPE_MASK; + if (mUiModeType == newUiModeType) return; + mUiModeType = newUiModeType; + for (Part part : mParts) { + part.onUiModeTypeChanged(mUiModeType); + } + } + + /** * Requests transitioning to {@code requestedState}. * * This can be called during a state transition, in which case it will be queued until all @@ -211,6 +221,14 @@ public class DozeMachine { requestState(State.DOZE_REQUEST_PULSE, pulseReason); } + /** + * @return true if {@link DozeMachine} is currently in either {@link State#UNINITIALIZED} + * or {@link State#FINISH} + */ + public boolean isUninitializedOrFinished() { + return mState == State.UNINITIALIZED || mState == State.FINISH; + } + void onScreenState(int state) { mDozeLog.traceDisplayState(state); for (Part part : mParts) { @@ -360,7 +378,7 @@ public class DozeMachine { if (mState == State.FINISH) { return State.FINISH; } - if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR + if (mUiModeType == Configuration.UI_MODE_TYPE_CAR && (requestedState.canPulse() || requestedState.staysAwake())) { Log.i(TAG, "Doze is suppressed with all triggers disabled as car mode is active"); mDozeLog.traceCarModeStarted(); @@ -411,7 +429,7 @@ public class DozeMachine { nextState = State.FINISH; } else if (mDockManager.isDocked()) { nextState = mDockManager.isHidden() ? State.DOZE : State.DOZE_AOD_DOCKED; - } else if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) { + } else if (mAmbientDisplayConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) { nextState = State.DOZE_AOD; } else { nextState = State.DOZE; @@ -427,6 +445,7 @@ public class DozeMachine { /** Dumps the current state */ public void dump(PrintWriter pw) { pw.print(" state="); pw.println(mState); + pw.print(" mUiModeType="); pw.println(mUiModeType); pw.print(" wakeLockHeldForCurrentState="); pw.println(mWakeLockHeldForCurrentState); pw.print(" wakeLock="); pw.println(mWakeLock); pw.println("Parts:"); @@ -459,6 +478,19 @@ public class DozeMachine { /** Sets the {@link DozeMachine} when this Part is associated with one. */ default void setDozeMachine(DozeMachine dozeMachine) {} + + /** + * Notifies the Part about a change in {@link Configuration#uiMode}. + * + * @param newUiModeType {@link Configuration#UI_MODE_TYPE_NORMAL}, + * {@link Configuration#UI_MODE_TYPE_DESK}, + * {@link Configuration#UI_MODE_TYPE_CAR}, + * {@link Configuration#UI_MODE_TYPE_TELEVISION}, + * {@link Configuration#UI_MODE_TYPE_APPLIANCE}, + * {@link Configuration#UI_MODE_TYPE_WATCH}, + * or {@link Configuration#UI_MODE_TYPE_VR_HEADSET} + */ + default void onUiModeTypeChanged(int newUiModeType) {} } /** A wrapper interface for {@link android.service.dreams.DreamService} */ diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index da6c163b1eea..9a091e725818 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -23,7 +23,6 @@ import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_ import android.annotation.AnyThread; import android.app.ActivityManager; -import android.content.Context; import android.database.ContentObserver; import android.hardware.Sensor; import android.hardware.SensorManager; @@ -37,7 +36,6 @@ import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.IndentingPrintWriter; -import android.util.Log; import android.view.Display; import androidx.annotation.NonNull; @@ -88,12 +86,9 @@ import java.util.function.Consumer; * trigger callbacks on the provided {@link mProxCallback}. */ public class DozeSensors { - - private static final boolean DEBUG = DozeService.DEBUG; private static final String TAG = "DozeSensors"; private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); - private final Context mContext; private final AsyncSensorManager mSensorManager; private final AmbientDisplayConfiguration mConfig; private final WakeLock mWakeLock; @@ -144,7 +139,6 @@ public class DozeSensors { } DozeSensors( - Context context, AsyncSensorManager sensorManager, DozeParameters dozeParameters, AmbientDisplayConfiguration config, @@ -157,7 +151,6 @@ public class DozeSensors { AuthController authController, DevicePostureController devicePostureController ) { - mContext = context; mSensorManager = sensorManager; mConfig = config; mWakeLock = wakeLock; @@ -605,10 +598,7 @@ public class DozeSensors { // cancel the previous sensor: if (mRegistered) { final boolean rt = mSensorManager.cancelTriggerSensor(this, oldSensor); - if (DEBUG) { - Log.d(TAG, "posture changed, cancelTriggerSensor[" + oldSensor + "] " - + rt); - } + mDozeLog.traceSensorUnregisterAttempt(oldSensor.toString(), rt, "posture changed"); mRegistered = false; } @@ -654,19 +644,13 @@ public class DozeSensors { if (mRequested && !mDisabled && (enabledBySetting() || mIgnoresSetting)) { if (!mRegistered) { mRegistered = mSensorManager.requestTriggerSensor(this, sensor); - if (DEBUG) { - Log.d(TAG, "requestTriggerSensor[" + sensor + "] " + mRegistered); - } + mDozeLog.traceSensorRegisterAttempt(sensor.toString(), mRegistered); } else { - if (DEBUG) { - Log.d(TAG, "requestTriggerSensor[" + sensor + "] already registered"); - } + mDozeLog.traceSkipRegisterSensor(sensor.toString()); } } else if (mRegistered) { final boolean rt = mSensorManager.cancelTriggerSensor(this, sensor); - if (DEBUG) { - Log.d(TAG, "cancelTriggerSensor[" + sensor + "] " + rt); - } + mDozeLog.traceSensorUnregisterAttempt(sensor.toString(), rt); mRegistered = false; } } @@ -704,7 +688,6 @@ public class DozeSensors { final Sensor sensor = mSensors[mPosture]; mDozeLog.traceSensor(mPulseReason); mHandler.post(mWakeLock.wrap(() -> { - if (DEBUG) Log.d(TAG, "onTrigger: " + triggerEventToString(event)); if (sensor != null && sensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) { UI_EVENT_LOGGER.log(DozeSensorsUiEvent.ACTION_AMBIENT_GESTURE_PICKUP); } @@ -776,11 +759,11 @@ public class DozeSensors { && !mRegistered) { asyncSensorManager.registerPluginListener(mPluginSensor, this); mRegistered = true; - if (DEBUG) Log.d(TAG, "registerPluginListener"); + mDozeLog.tracePluginSensorUpdate(true /* registered */); } else if (mRegistered) { asyncSensorManager.unregisterPluginListener(mPluginSensor, this); mRegistered = false; - if (DEBUG) Log.d(TAG, "unregisterPluginListener"); + mDozeLog.tracePluginSensorUpdate(false /* registered */); } } @@ -813,10 +796,9 @@ public class DozeSensors { mHandler.post(mWakeLock.wrap(() -> { final long now = SystemClock.uptimeMillis(); if (now < mDebounceFrom + mDebounce) { - Log.d(TAG, "onSensorEvent dropped: " + triggerEventToString(event)); + mDozeLog.traceSensorEventDropped(mPulseReason, "debounce"); return; } - if (DEBUG) Log.d(TAG, "onSensorEvent: " + triggerEventToString(event)); mSensorCallback.onSensorPulse(mPulseReason, -1, -1, event.getValues()); })); } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java index a2eb4e3bb640..e8d7e4642e3e 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java @@ -17,6 +17,7 @@ package com.android.systemui.doze; import android.content.Context; +import android.content.res.Configuration; import android.os.PowerManager; import android.os.SystemClock; import android.service.dreams.DreamService; @@ -59,6 +60,7 @@ public class DozeService extends DreamService mPluginManager.addPluginListener(this, DozeServicePlugin.class, false /* allowMultiple */); DozeComponent dozeComponent = mDozeComponentBuilder.build(this); mDozeMachine = dozeComponent.getDozeMachine(); + mDozeMachine.onConfigurationChanged(getResources().getConfiguration()); } @Override @@ -127,6 +129,12 @@ public class DozeService extends DreamService } @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDozeMachine.onConfigurationChanged(newConfig); + } + + @Override public void onRequestHideDoze() { if (mDozeMachine != null) { mDozeMachine.requestState(DozeMachine.State.DOZE); diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java index 7ed4b35e1ee7..e6d98655b119 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java @@ -16,21 +16,13 @@ package com.android.systemui.doze; -import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE; -import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE; - -import android.app.UiModeManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; +import static android.content.res.Configuration.UI_MODE_TYPE_CAR; + import android.hardware.display.AmbientDisplayConfiguration; import android.os.PowerManager; import android.os.UserHandle; import android.text.TextUtils; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.doze.dagger.DozeScope; import com.android.systemui.statusbar.phone.BiometricUnlockController; @@ -43,7 +35,9 @@ import dagger.Lazy; /** * Handles suppressing doze on: * 1. INITIALIZED, don't allow dozing at all when: - * - in CAR_MODE + * - in CAR_MODE, in this scenario the device is asleep and won't listen for any triggers + * to wake up. In this state, no UI shows. Unlike other conditions, this suppression is only + * temporary and stops when the device exits CAR_MODE * - device is NOT provisioned * - there's a pending authentication * 2. PowerSaveMode active @@ -57,35 +51,47 @@ import dagger.Lazy; */ @DozeScope public class DozeSuppressor implements DozeMachine.Part { - private static final String TAG = "DozeSuppressor"; private DozeMachine mMachine; private final DozeHost mDozeHost; private final AmbientDisplayConfiguration mConfig; private final DozeLog mDozeLog; - private final BroadcastDispatcher mBroadcastDispatcher; - private final UiModeManager mUiModeManager; private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy; - private boolean mBroadcastReceiverRegistered; + private boolean mIsCarModeEnabled = false; @Inject public DozeSuppressor( DozeHost dozeHost, AmbientDisplayConfiguration config, DozeLog dozeLog, - BroadcastDispatcher broadcastDispatcher, - UiModeManager uiModeManager, Lazy<BiometricUnlockController> biometricUnlockControllerLazy) { mDozeHost = dozeHost; mConfig = config; mDozeLog = dozeLog; - mBroadcastDispatcher = broadcastDispatcher; - mUiModeManager = uiModeManager; mBiometricUnlockControllerLazy = biometricUnlockControllerLazy; } @Override + public void onUiModeTypeChanged(int newUiModeType) { + boolean isCarModeEnabled = newUiModeType == UI_MODE_TYPE_CAR; + if (mIsCarModeEnabled == isCarModeEnabled) { + return; + } + mIsCarModeEnabled = isCarModeEnabled; + // Do not handle the event if doze machine is not initialized yet. + // It will be handled upon initialization. + if (mMachine.isUninitializedOrFinished()) { + return; + } + if (mIsCarModeEnabled) { + handleCarModeStarted(); + } else { + handleCarModeExited(); + } + } + + @Override public void setDozeMachine(DozeMachine dozeMachine) { mMachine = dozeMachine; } @@ -94,7 +100,6 @@ public class DozeSuppressor implements DozeMachine.Part { public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) { switch (newState) { case INITIALIZED: - registerBroadcastReceiver(); mDozeHost.addCallback(mHostCallback); checkShouldImmediatelyEndDoze(); checkShouldImmediatelySuspendDoze(); @@ -108,14 +113,12 @@ public class DozeSuppressor implements DozeMachine.Part { @Override public void destroy() { - unregisterBroadcastReceiver(); mDozeHost.removeCallback(mHostCallback); } private void checkShouldImmediatelySuspendDoze() { - if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) { - mDozeLog.traceCarModeStarted(); - mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS); + if (mIsCarModeEnabled) { + handleCarModeStarted(); } } @@ -135,7 +138,7 @@ public class DozeSuppressor implements DozeMachine.Part { @Override public void dump(PrintWriter pw) { - pw.println(" uiMode=" + mUiModeManager.getCurrentModeType()); + pw.println(" isCarModeEnabled=" + mIsCarModeEnabled); pw.println(" hasPendingAuth=" + mBiometricUnlockControllerLazy.get().hasPendingAuthentication()); pw.println(" isProvisioned=" + mDozeHost.isProvisioned()); @@ -143,40 +146,18 @@ public class DozeSuppressor implements DozeMachine.Part { pw.println(" aodPowerSaveActive=" + mDozeHost.isPowerSaveActive()); } - private void registerBroadcastReceiver() { - if (mBroadcastReceiverRegistered) { - return; - } - IntentFilter filter = new IntentFilter(ACTION_ENTER_CAR_MODE); - filter.addAction(ACTION_EXIT_CAR_MODE); - mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter); - mBroadcastReceiverRegistered = true; + private void handleCarModeExited() { + mDozeLog.traceCarModeEnded(); + mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) + ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE); } - private void unregisterBroadcastReceiver() { - if (!mBroadcastReceiverRegistered) { - return; - } - mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver); - mBroadcastReceiverRegistered = false; + private void handleCarModeStarted() { + mDozeLog.traceCarModeStarted(); + mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS); } - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (ACTION_ENTER_CAR_MODE.equals(action)) { - mDozeLog.traceCarModeStarted(); - mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS); - } else if (ACTION_EXIT_CAR_MODE.equals(action)) { - mDozeLog.traceCarModeEnded(); - mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT) - ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE); - } - } - }; - - private DozeHost.Callback mHostCallback = new DozeHost.Callback() { + private final DozeHost.Callback mHostCallback = new DozeHost.Callback() { @Override public void onPowerSaveChanged(boolean active) { // handles suppression changes, while DozeMachine#transitionPolicy handles gating diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index 00ac8bc7e3fa..32cb1c01b776 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -102,7 +102,6 @@ public class DozeTriggers implements DozeMachine.Part { private final UiEventLogger mUiEventLogger; private long mNotificationPulseTime; - private boolean mPulsePending; private Runnable mAodInterruptRunnable; /** see {@link #onProximityFar} prox for callback */ @@ -199,7 +198,7 @@ public class DozeTriggers implements DozeMachine.Part { mAllowPulseTriggers = true; mSessionTracker = sessionTracker; - mDozeSensors = new DozeSensors(context, mSensorManager, dozeParameters, + mDozeSensors = new DozeSensors(mSensorManager, dozeParameters, config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor, secureSettings, authController, devicePostureController); mDockManager = dockManager; @@ -303,8 +302,8 @@ public class DozeTriggers implements DozeMachine.Part { null /* onPulseSuppressedListener */); } } else { - proximityCheckThenCall((result) -> { - if (result != null && result) { + proximityCheckThenCall((isNear) -> { + if (isNear != null && isNear) { // In pocket, drop event. mDozeLog.traceSensorEventDropped(pulseReason, "prox reporting near"); return; @@ -410,8 +409,8 @@ public class DozeTriggers implements DozeMachine.Part { sWakeDisplaySensorState = wake; if (wake) { - proximityCheckThenCall((result) -> { - if (result != null && result) { + proximityCheckThenCall((isNear) -> { + if (isNear != null && isNear) { // In pocket, drop event. return; } @@ -537,24 +536,45 @@ public class DozeTriggers implements DozeMachine.Part { return; } - if (mPulsePending || !mAllowPulseTriggers || !canPulse()) { - if (mAllowPulseTriggers) { - mDozeLog.tracePulseDropped(mPulsePending, dozeState, mDozeHost.isPulsingBlocked()); + if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse(dozeState)) { + if (!mAllowPulseTriggers) { + mDozeLog.tracePulseDropped("requestPulse - !mAllowPulseTriggers"); + } else if (mDozeHost.isPulsePending()) { + mDozeLog.tracePulseDropped("requestPulse - pulsePending"); + } else if (!canPulse(dozeState)) { + mDozeLog.tracePulseDropped("requestPulse - dozeState cannot pulse", dozeState); } runIfNotNull(onPulseSuppressedListener); return; } - mPulsePending = true; - proximityCheckThenCall((result) -> { - if (result != null && result) { + mDozeHost.setPulsePending(true); + proximityCheckThenCall((isNear) -> { + if (isNear != null && isNear) { // in pocket, abort pulse - mDozeLog.tracePulseDropped("inPocket"); - mPulsePending = false; + mDozeLog.tracePulseDropped("requestPulse - inPocket"); + mDozeHost.setPulsePending(false); runIfNotNull(onPulseSuppressedListener); } else { // not in pocket, continue pulsing - continuePulseRequest(reason); + final boolean isPulsePending = mDozeHost.isPulsePending(); + mDozeHost.setPulsePending(false); + if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse(dozeState)) { + if (!isPulsePending) { + mDozeLog.tracePulseDropped("continuePulseRequest - pulse no longer" + + " pending, pulse was cancelled before it could start" + + " transitioning to pulsing state."); + } else if (mDozeHost.isPulsingBlocked()) { + mDozeLog.tracePulseDropped("continuePulseRequest - pulsingBlocked"); + } else if (!canPulse(dozeState)) { + mDozeLog.tracePulseDropped("continuePulseRequest" + + " - doze state cannot pulse", dozeState); + } + runIfNotNull(onPulseSuppressedListener); + return; + } + + mMachine.requestPulse(reason); } }, !mDozeParameters.getProxCheckBeforePulse() || performedProxCheck, reason); @@ -563,20 +583,10 @@ public class DozeTriggers implements DozeMachine.Part { .ifPresent(uiEventEnum -> mUiEventLogger.log(uiEventEnum, getKeyguardSessionId())); } - private boolean canPulse() { - return mMachine.getState() == DozeMachine.State.DOZE - || mMachine.getState() == DozeMachine.State.DOZE_AOD - || mMachine.getState() == DozeMachine.State.DOZE_AOD_DOCKED; - } - - private void continuePulseRequest(int reason) { - mPulsePending = false; - if (mDozeHost.isPulsingBlocked() || !canPulse()) { - mDozeLog.tracePulseDropped(mPulsePending, mMachine.getState(), - mDozeHost.isPulsingBlocked()); - return; - } - mMachine.requestPulse(reason); + private boolean canPulse(DozeMachine.State dozeState) { + return dozeState == DozeMachine.State.DOZE + || dozeState == DozeMachine.State.DOZE_AOD + || dozeState == DozeMachine.State.DOZE_AOD_DOCKED; } @Nullable @@ -591,7 +601,7 @@ public class DozeTriggers implements DozeMachine.Part { pw.print(" notificationPulseTime="); pw.println(Formatter.formatShortElapsedTime(mContext, mNotificationPulseTime)); - pw.println(" pulsePending=" + mPulsePending); + pw.println(" DozeHost#isPulsePending=" + mDozeHost.isPulsePending()); pw.println("DozeSensors:"); IndentingPrintWriter idpw = new IndentingPrintWriter(pw); idpw.increaseIndent(); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java index 99ca3c76cf8d..d145f5c14917 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java @@ -40,11 +40,12 @@ import javax.inject.Inject; * {@link DreamOverlayRegistrant} is responsible for telling system server that SystemUI should be * the designated dream overlay component. */ -public class DreamOverlayRegistrant extends CoreStartable { +public class DreamOverlayRegistrant implements CoreStartable { private static final String TAG = "DreamOverlayRegistrant"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final IDreamManager mDreamManager; private final ComponentName mOverlayServiceComponent; + private final Context mContext; private final Resources mResources; private boolean mCurrentRegisteredState = false; @@ -98,7 +99,7 @@ public class DreamOverlayRegistrant extends CoreStartable { @Inject public DreamOverlayRegistrant(Context context, @Main Resources resources) { - super(context); + mContext = context; mResources = resources; mDreamManager = IDreamManager.Stub.asInterface( ServiceManager.getService(DreamService.DREAM_SERVICE)); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 696fc7254308..d1b73685a3f3 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -64,29 +64,26 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private final Executor mExecutor; // A controller for the dream overlay container view (which contains both the status bar and the // content area). - private final DreamOverlayContainerViewController mDreamOverlayContainerViewController; + private DreamOverlayContainerViewController mDreamOverlayContainerViewController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Nullable private final ComponentName mLowLightDreamComponent; private final UiEventLogger mUiEventLogger; + private final WindowManager mWindowManager; // A reference to the {@link Window} used to hold the dream overlay. private Window mWindow; + // True if a dream has bound to the service and dream overlay service has started. + private boolean mStarted = false; + // True if the service has been destroyed. - private boolean mDestroyed; + private boolean mDestroyed = false; - private final Complication.Host mHost = new Complication.Host() { - @Override - public void requestExitDream() { - mExecutor.execute(DreamOverlayService.this::requestExit); - } - }; + private final DreamOverlayComponent mDreamOverlayComponent; private final LifecycleRegistry mLifecycleRegistry; - private ViewModelStore mViewModelStore = new ViewModelStore(); - private DreamOverlayTouchMonitor mDreamOverlayTouchMonitor; private final KeyguardUpdateMonitorCallback mKeyguardCallback = @@ -103,7 +100,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ } }; - private DreamOverlayStateController mStateController; + private final DreamOverlayStateController mStateController; @VisibleForTesting public enum DreamOverlayEvent implements UiEventLogger.UiEventEnum { @@ -128,6 +125,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ public DreamOverlayService( Context context, @Main Executor executor, + WindowManager windowManager, DreamOverlayComponent.Factory dreamOverlayComponentFactory, DreamOverlayStateController stateController, KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -136,19 +134,19 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ ComponentName lowLightDreamComponent) { mContext = context; mExecutor = executor; + mWindowManager = windowManager; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLowLightDreamComponent = lowLightDreamComponent; mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback); mStateController = stateController; mUiEventLogger = uiEventLogger; - final DreamOverlayComponent component = - dreamOverlayComponentFactory.create(mViewModelStore, mHost); - mDreamOverlayContainerViewController = component.getDreamOverlayContainerViewController(); + final ViewModelStore viewModelStore = new ViewModelStore(); + final Complication.Host host = + () -> mExecutor.execute(DreamOverlayService.this::requestExit); + mDreamOverlayComponent = dreamOverlayComponentFactory.create(viewModelStore, host); + mLifecycleRegistry = mDreamOverlayComponent.getLifecycleRegistry(); setCurrentState(Lifecycle.State.CREATED); - mLifecycleRegistry = component.getLifecycleRegistry(); - mDreamOverlayTouchMonitor = component.getDreamOverlayTouchMonitor(); - mDreamOverlayTouchMonitor.init(); } private void setCurrentState(Lifecycle.State state) { @@ -159,34 +157,48 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ public void onDestroy() { mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback); setCurrentState(Lifecycle.State.DESTROYED); - final WindowManager windowManager = mContext.getSystemService(WindowManager.class); - if (mWindow != null) { - windowManager.removeView(mWindow.getDecorView()); - } - mStateController.setOverlayActive(false); - mStateController.setLowLightActive(false); + + resetCurrentDreamOverlay(); + mDestroyed = true; super.onDestroy(); } @Override public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) { - mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START); setCurrentState(Lifecycle.State.STARTED); - final ComponentName dreamComponent = getDreamComponent(); - mStateController.setLowLightActive( - dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent)); + mExecutor.execute(() -> { + mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START); + if (mDestroyed) { // The task could still be executed after the service has been destroyed. Bail if // that is the case. return; } + + if (mStarted) { + // Reset the current dream overlay before starting a new one. This can happen + // when two dreams overlap (briefly, for a smoother dream transition) and both + // dreams are bound to the dream overlay service. + resetCurrentDreamOverlay(); + } + + mDreamOverlayContainerViewController = + mDreamOverlayComponent.getDreamOverlayContainerViewController(); + mDreamOverlayTouchMonitor = mDreamOverlayComponent.getDreamOverlayTouchMonitor(); + mDreamOverlayTouchMonitor.init(); + mStateController.setShouldShowComplications(shouldShowComplications()); addOverlayWindowLocked(layoutParams); setCurrentState(Lifecycle.State.RESUMED); mStateController.setOverlayActive(true); + final ComponentName dreamComponent = getDreamComponent(); + mStateController.setLowLightActive( + dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent)); mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START); + + mStarted = true; }); } @@ -222,8 +234,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ removeContainerViewFromParent(); mWindow.setContentView(mDreamOverlayContainerViewController.getContainerView()); - final WindowManager windowManager = mContext.getSystemService(WindowManager.class); - windowManager.addView(mWindow.getDecorView(), mWindow.getAttributes()); + mWindowManager.addView(mWindow.getDecorView(), mWindow.getAttributes()); } private void removeContainerViewFromParent() { @@ -238,4 +249,18 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ Log.w(TAG, "Removing dream overlay container view parent!"); parentView.removeView(containerView); } + + private void resetCurrentDreamOverlay() { + if (mStarted && mWindow != null) { + mWindowManager.removeView(mWindow.getDecorView()); + } + + mStateController.setOverlayActive(false); + mStateController.setLowLightActive(false); + + mDreamOverlayContainerViewController = null; + mDreamOverlayTouchMonitor = null; + + mStarted = false; + } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java index 9cd149b9bfee..5694f6da0ea5 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java @@ -18,7 +18,7 @@ package com.android.systemui.dreams.complication; import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_FADE_IN_DURATION; import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_FADE_OUT_DURATION; -import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN; +import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN_DEFAULT; import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.SCOPED_COMPLICATIONS_LAYOUT; import android.animation.Animator; @@ -67,7 +67,7 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll private final Parent mParent; @Complication.Category private final int mCategory; - private final int mMargin; + private final int mDefaultMargin; /** * Default constructor. {@link Parent} allows for the {@link ViewEntry}'s surrounding @@ -75,7 +75,7 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll */ ViewEntry(View view, ComplicationLayoutParams layoutParams, TouchInsetManager.TouchInsetSession touchSession, int category, Parent parent, - int margin) { + int defaultMargin) { mView = view; // Views that are generated programmatically do not have a unique id assigned to them // at construction. A new id is assigned here to enable ConstraintLayout relative @@ -86,7 +86,7 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll mTouchInsetSession = touchSession; mCategory = category; mParent = parent; - mMargin = margin; + mDefaultMargin = defaultMargin; touchSession.addViewToTracking(mView); } @@ -195,18 +195,19 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll } if (!isRoot) { + final int margin = mLayoutParams.getMargin(mDefaultMargin); switch(direction) { case ComplicationLayoutParams.DIRECTION_DOWN: - params.setMargins(0, mMargin, 0, 0); + params.setMargins(0, margin, 0, 0); break; case ComplicationLayoutParams.DIRECTION_UP: - params.setMargins(0, 0, 0, mMargin); + params.setMargins(0, 0, 0, margin); break; case ComplicationLayoutParams.DIRECTION_END: - params.setMarginStart(mMargin); + params.setMarginStart(margin); break; case ComplicationLayoutParams.DIRECTION_START: - params.setMarginEnd(mMargin); + params.setMarginEnd(margin); break; } } @@ -263,7 +264,7 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll private final ComplicationLayoutParams mLayoutParams; private final int mCategory; private Parent mParent; - private int mMargin; + private int mDefaultMargin; Builder(View view, TouchInsetManager.TouchInsetSession touchSession, ComplicationLayoutParams lp, @Complication.Category int category) { @@ -302,8 +303,8 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll * Sets the margin that will be applied in the direction the complication is laid out * towards. */ - Builder setMargin(int margin) { - mMargin = margin; + Builder setDefaultMargin(int margin) { + mDefaultMargin = margin; return this; } @@ -312,7 +313,7 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll */ ViewEntry build() { return new ViewEntry(mView, mLayoutParams, mTouchSession, mCategory, mParent, - mMargin); + mDefaultMargin); } } @@ -472,7 +473,7 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll } private final ConstraintLayout mLayout; - private final int mMargin; + private final int mDefaultMargin; private final HashMap<ComplicationId, ViewEntry> mEntries = new HashMap<>(); private final HashMap<Integer, PositionGroup> mPositions = new HashMap<>(); private final TouchInsetManager.TouchInsetSession mSession; @@ -483,12 +484,12 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll /** */ @Inject public ComplicationLayoutEngine(@Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout layout, - @Named(COMPLICATION_MARGIN) int margin, + @Named(COMPLICATION_MARGIN_DEFAULT) int defaultMargin, TouchInsetManager.TouchInsetSession session, @Named(COMPLICATIONS_FADE_IN_DURATION) int fadeInDuration, @Named(COMPLICATIONS_FADE_OUT_DURATION) int fadeOutDuration) { mLayout = layout; - mMargin = margin; + mDefaultMargin = defaultMargin; mSession = session; mFadeInDuration = fadeInDuration; mFadeOutDuration = fadeOutDuration; @@ -537,7 +538,7 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll } final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, mSession, lp, category) - .setMargin(mMargin); + .setDefaultMargin(mDefaultMargin); // Add position group if doesn't already exist final int position = lp.getPosition(); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java index 8e8cb72d6ad0..a21eb19bd548 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java @@ -51,6 +51,8 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { private static final int FIRST_POSITION = POSITION_TOP; private static final int LAST_POSITION = POSITION_END; + private static final int MARGIN_UNSPECIFIED = 0xFFFFFFFF; + @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, prefix = { "DIRECTION_" }, value = { DIRECTION_UP, @@ -77,6 +79,8 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { private final int mWeight; + private final int mMargin; + private final boolean mSnapToGuide; // Do not allow specifying opposite positions @@ -106,7 +110,24 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { */ public ComplicationLayoutParams(int width, int height, @Position int position, @Direction int direction, int weight) { - this(width, height, position, direction, weight, false); + this(width, height, position, direction, weight, MARGIN_UNSPECIFIED, false); + } + + /** + * Constructs a {@link ComplicationLayoutParams}. + * @param width The width {@link android.view.View.MeasureSpec} for the view. + * @param height The height {@link android.view.View.MeasureSpec} for the view. + * @param position The place within the parent container where the view should be positioned. + * @param direction The direction the view should be laid out from either the parent container + * or preceding view. + * @param weight The weight that should be considered for this view when compared to other + * views. This has an impact on the placement of the view but not the rendering of + * the view. + * @param margin The margin to apply between complications. + */ + public ComplicationLayoutParams(int width, int height, @Position int position, + @Direction int direction, int weight, int margin) { + this(width, height, position, direction, weight, margin, false); } /** @@ -127,6 +148,28 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { */ public ComplicationLayoutParams(int width, int height, @Position int position, @Direction int direction, int weight, boolean snapToGuide) { + this(width, height, position, direction, weight, MARGIN_UNSPECIFIED, snapToGuide); + } + + /** + * Constructs a {@link ComplicationLayoutParams}. + * @param width The width {@link android.view.View.MeasureSpec} for the view. + * @param height The height {@link android.view.View.MeasureSpec} for the view. + * @param position The place within the parent container where the view should be positioned. + * @param direction The direction the view should be laid out from either the parent container + * or preceding view. + * @param weight The weight that should be considered for this view when compared to other + * views. This has an impact on the placement of the view but not the rendering of + * the view. + * @param margin The margin to apply between complications. + * @param snapToGuide When set to {@code true}, the dimension perpendicular to the direction + * will be automatically set to align with a predetermined guide for that + * side. For example, if the complication is aligned to the top end and + * direction is down, then the width of the complication will be set to span + * from the end of the parent to the guide. + */ + public ComplicationLayoutParams(int width, int height, @Position int position, + @Direction int direction, int weight, int margin, boolean snapToGuide) { super(width, height); if (!validatePosition(position)) { @@ -142,6 +185,8 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { mWeight = weight; + mMargin = margin; + mSnapToGuide = snapToGuide; } @@ -153,6 +198,7 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { mPosition = source.mPosition; mDirection = source.mDirection; mWeight = source.mWeight; + mMargin = source.mMargin; mSnapToGuide = source.mSnapToGuide; } @@ -215,6 +261,14 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { } /** + * Returns the margin to apply between complications, or the given default if no margin is + * specified. + */ + public int getMargin(int defaultMargin) { + return mMargin == MARGIN_UNSPECIFIED ? defaultMargin : mMargin; + } + + /** * Returns whether the complication's dimension perpendicular to direction should be * automatically set. */ diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java index bbcab60d7ba2..ee2f1af6a99b 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationTypesUpdater.java @@ -16,7 +16,6 @@ package com.android.systemui.dreams.complication; -import android.content.Context; import android.database.ContentObserver; import android.os.UserHandle; import android.provider.Settings; @@ -37,7 +36,7 @@ import javax.inject.Inject; * user, and pushes updates to {@link DreamOverlayStateController}. */ @SysUISingleton -public class ComplicationTypesUpdater extends CoreStartable { +public class ComplicationTypesUpdater implements CoreStartable { private final DreamBackend mDreamBackend; private final Executor mExecutor; private final SecureSettings mSecureSettings; @@ -45,13 +44,11 @@ public class ComplicationTypesUpdater extends CoreStartable { private final DreamOverlayStateController mDreamOverlayStateController; @Inject - ComplicationTypesUpdater(Context context, + ComplicationTypesUpdater( DreamBackend dreamBackend, @Main Executor executor, SecureSettings secureSettings, DreamOverlayStateController dreamOverlayStateController) { - super(context); - mDreamBackend = dreamBackend; mExecutor = executor; mSecureSettings = secureSettings; diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java index 675a2f46d310..77e1fc91e6ee 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java @@ -19,7 +19,6 @@ package com.android.systemui.dreams.complication; import static com.android.systemui.dreams.complication.dagger.DreamClockTimeComplicationModule.DREAM_CLOCK_TIME_COMPLICATION_VIEW; import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS; -import android.content.Context; import android.view.View; import com.android.systemui.CoreStartable; @@ -61,7 +60,7 @@ public class DreamClockTimeComplication implements Complication { * {@link CoreStartable} responsible for registering {@link DreamClockTimeComplication} with * SystemUI. */ - public static class Registrant extends CoreStartable { + public static class Registrant implements CoreStartable { private final DreamOverlayStateController mDreamOverlayStateController; private final DreamClockTimeComplication mComplication; @@ -69,10 +68,9 @@ public class DreamClockTimeComplication implements Complication { * Default constructor to register {@link DreamClockTimeComplication}. */ @Inject - public Registrant(Context context, + public Registrant( DreamOverlayStateController dreamOverlayStateController, DreamClockTimeComplication dreamClockTimeComplication) { - super(context); mDreamOverlayStateController = dreamOverlayStateController; mComplication = dreamClockTimeComplication; } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java index 2503d3c3edad..cedd850ac2ef 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java @@ -28,6 +28,9 @@ import android.util.Log; import android.view.View; import android.widget.ImageView; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.CoreStartable; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.controls.dagger.ControlsComponent; @@ -68,7 +71,7 @@ public class DreamHomeControlsComplication implements Complication { /** * {@link CoreStartable} for registering the complication with SystemUI on startup. */ - public static class Registrant extends CoreStartable { + public static class Registrant implements CoreStartable { private final DreamHomeControlsComplication mComplication; private final DreamOverlayStateController mDreamOverlayStateController; private final ControlsComponent mControlsComponent; @@ -87,11 +90,9 @@ public class DreamHomeControlsComplication implements Complication { }; @Inject - public Registrant(Context context, DreamHomeControlsComplication complication, + public Registrant(DreamHomeControlsComplication complication, DreamOverlayStateController dreamOverlayStateController, ControlsComponent controlsComponent) { - super(context); - mComplication = complication; mControlsComponent = controlsComponent; mDreamOverlayStateController = dreamOverlayStateController; @@ -158,17 +159,38 @@ public class DreamHomeControlsComplication implements Complication { private final Context mContext; private final ControlsComponent mControlsComponent; + private final UiEventLogger mUiEventLogger; + + @VisibleForTesting + public enum DreamOverlayEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "The home controls on the screensaver has been tapped.") + DREAM_HOME_CONTROLS_TAPPED(1212); + + private final int mId; + + DreamOverlayEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } + @Inject DreamHomeControlsChipViewController( @Named(DREAM_HOME_CONTROLS_CHIP_VIEW) ImageView view, ActivityStarter activityStarter, Context context, - ControlsComponent controlsComponent) { + ControlsComponent controlsComponent, + UiEventLogger uiEventLogger) { super(view); mActivityStarter = activityStarter; mContext = context; mControlsComponent = controlsComponent; + mUiEventLogger = uiEventLogger; } @Override @@ -184,9 +206,12 @@ public class DreamHomeControlsComplication implements Complication { private void onClickHomeControls(View v) { if (DEBUG) Log.d(TAG, "home controls complication tapped"); + mUiEventLogger.log(DreamOverlayEvent.DREAM_HOME_CONTROLS_TAPPED); + final Intent intent = new Intent(mContext, ControlsActivity.class) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(ControlsUiController.EXTRA_ANIMATE, true); + .putExtra(ControlsUiController.EXTRA_ANIMATE, true) + .putExtra(ControlsUiController.EXIT_TO_DREAM, true); final ActivityLaunchAnimator.Controller controller = v != null ? ActivityLaunchAnimator.Controller.fromView(v, null /* cujType */) diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java index c07d4022df76..1166c2fc1120 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java @@ -28,7 +28,7 @@ import com.android.systemui.ActivityIntentHelper; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.media.MediaCarouselController; +import com.android.systemui.media.controls.ui.MediaCarouselController; import com.android.systemui.media.dream.MediaDreamComplication; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.NotificationLockscreenUserManager; diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java index a981f255a873..c3aaf0cbf2d7 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java @@ -61,7 +61,7 @@ public class SmartSpaceComplication implements Complication { * {@link CoreStartable} responsbile for registering {@link SmartSpaceComplication} with * SystemUI. */ - public static class Registrant extends CoreStartable { + public static class Registrant implements CoreStartable { private final DreamSmartspaceController mSmartSpaceController; private final DreamOverlayStateController mDreamOverlayStateController; private final SmartSpaceComplication mComplication; @@ -78,11 +78,10 @@ public class SmartSpaceComplication implements Complication { * Default constructor for {@link SmartSpaceComplication}. */ @Inject - public Registrant(Context context, + public Registrant( DreamOverlayStateController dreamOverlayStateController, SmartSpaceComplication smartSpaceComplication, DreamSmartspaceController smartSpaceController) { - super(context); mDreamOverlayStateController = dreamOverlayStateController; mComplication = smartSpaceComplication; mSmartSpaceController = smartSpaceController; diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java index 11d89d2dc816..c9fecc96c1b5 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java @@ -37,7 +37,7 @@ import dagger.Provides; @Module public abstract class ComplicationHostViewModule { public static final String SCOPED_COMPLICATIONS_LAYOUT = "scoped_complications_layout"; - public static final String COMPLICATION_MARGIN = "complication_margin"; + public static final String COMPLICATION_MARGIN_DEFAULT = "complication_margin_default"; public static final String COMPLICATIONS_FADE_OUT_DURATION = "complications_fade_out_duration"; public static final String COMPLICATIONS_FADE_IN_DURATION = "complications_fade_in_duration"; public static final String COMPLICATIONS_RESTORE_TIMEOUT = "complication_restore_timeout"; @@ -58,7 +58,7 @@ public abstract class ComplicationHostViewModule { } @Provides - @Named(COMPLICATION_MARGIN) + @Named(COMPLICATION_MARGIN_DEFAULT) @DreamOverlayComponent.DreamOverlayScope static int providesComplicationPadding(@Main Resources resources) { return resources.getDimensionPixelSize(R.dimen.dream_overlay_complication_margin); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java index 759d6ec3bf21..7d2ce51ffbf6 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java @@ -59,11 +59,11 @@ public interface RegisteredComplicationsModule { @Named(DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS) static ComplicationLayoutParams provideClockTimeLayoutParams() { return new ComplicationLayoutParams(0, - ViewGroup.LayoutParams.WRAP_CONTENT, - ComplicationLayoutParams.POSITION_TOP - | ComplicationLayoutParams.POSITION_START, - ComplicationLayoutParams.DIRECTION_DOWN, - DREAM_CLOCK_TIME_COMPLICATION_WEIGHT); + ViewGroup.LayoutParams.WRAP_CONTENT, + ComplicationLayoutParams.POSITION_TOP + | ComplicationLayoutParams.POSITION_START, + ComplicationLayoutParams.DIRECTION_DOWN, + DREAM_CLOCK_TIME_COMPLICATION_WEIGHT); } /** @@ -73,12 +73,12 @@ public interface RegisteredComplicationsModule { @Named(DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS) static ComplicationLayoutParams provideHomeControlsChipLayoutParams(@Main Resources res) { return new ComplicationLayoutParams( - res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width), - res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height), - ComplicationLayoutParams.POSITION_BOTTOM - | ComplicationLayoutParams.POSITION_START, - ComplicationLayoutParams.DIRECTION_END, - DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT); + res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width), + res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height), + ComplicationLayoutParams.POSITION_BOTTOM + | ComplicationLayoutParams.POSITION_START, + ComplicationLayoutParams.DIRECTION_END, + DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT); } /** @@ -103,11 +103,12 @@ public interface RegisteredComplicationsModule { @Named(DREAM_SMARTSPACE_LAYOUT_PARAMS) static ComplicationLayoutParams provideSmartspaceLayoutParams() { return new ComplicationLayoutParams(0, - ViewGroup.LayoutParams.WRAP_CONTENT, - ComplicationLayoutParams.POSITION_TOP - | ComplicationLayoutParams.POSITION_START, - ComplicationLayoutParams.DIRECTION_DOWN, - DREAM_SMARTSPACE_COMPLICATION_WEIGHT, - true /*snapToGuide*/); + ViewGroup.LayoutParams.WRAP_CONTENT, + ComplicationLayoutParams.POSITION_TOP + | ComplicationLayoutParams.POSITION_START, + ComplicationLayoutParams.DIRECTION_DOWN, + DREAM_SMARTSPACE_COMPLICATION_WEIGHT, + 0, + true /*snapToGuide*/); } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java index f769a2355409..0dba4ff84c99 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java @@ -36,11 +36,11 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; +import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.KeyguardBouncer; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent; import com.android.wm.shell.animation.FlingAnimationUtils; import java.util.Optional; @@ -154,8 +154,8 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler { private void setPanelExpansion(float expansion, float dragDownAmount) { mCurrentExpansion = expansion; - PanelExpansionChangeEvent event = - new PanelExpansionChangeEvent( + ShadeExpansionChangeEvent event = + new ShadeExpansionChangeEvent( /* fraction= */ mCurrentExpansion, /* expanded= */ false, /* tracking= */ true, diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt index 08ef8f3d025f..609bd76cf210 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt @@ -24,8 +24,13 @@ import com.android.systemui.R import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL -import com.android.systemui.log.LogBuffer +import com.android.systemui.dump.nano.SystemUIProtoDump +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager +import com.google.protobuf.nano.MessageNano +import java.io.BufferedOutputStream +import java.io.FileDescriptor +import java.io.FileOutputStream import java.io.PrintWriter import javax.inject.Inject import javax.inject.Provider @@ -100,7 +105,7 @@ class DumpHandler @Inject constructor( /** * Dump the diagnostics! Behavior can be controlled via [args]. */ - fun dump(pw: PrintWriter, args: Array<String>) { + fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { Trace.beginSection("DumpManager#dump()") val start = SystemClock.uptimeMillis() @@ -111,10 +116,12 @@ class DumpHandler @Inject constructor( return } - when (parsedArgs.dumpPriority) { - PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs) - PRIORITY_ARG_NORMAL -> dumpNormal(pw, parsedArgs) - else -> dumpParameterized(pw, parsedArgs) + when { + parsedArgs.dumpPriority == PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs) + parsedArgs.dumpPriority == PRIORITY_ARG_NORMAL && !parsedArgs.proto -> { + dumpNormal(pw, parsedArgs) + } + else -> dumpParameterized(fd, pw, parsedArgs) } pw.println() @@ -122,7 +129,7 @@ class DumpHandler @Inject constructor( Trace.endSection() } - private fun dumpParameterized(pw: PrintWriter, args: ParsedArgs) { + private fun dumpParameterized(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) { when (args.command) { "bugreport-critical" -> dumpCritical(pw, args) "bugreport-normal" -> dumpNormal(pw, args) @@ -130,7 +137,13 @@ class DumpHandler @Inject constructor( "buffers" -> dumpBuffers(pw, args) "config" -> dumpConfig(pw) "help" -> dumpHelp(pw) - else -> dumpTargets(args.nonFlagArgs, pw, args) + else -> { + if (args.proto) { + dumpProtoTargets(args.nonFlagArgs, fd, args) + } else { + dumpTargets(args.nonFlagArgs, pw, args) + } + } } } @@ -160,6 +173,26 @@ class DumpHandler @Inject constructor( } } + private fun dumpProtoTargets( + targets: List<String>, + fd: FileDescriptor, + args: ParsedArgs + ) { + val systemUIProto = SystemUIProtoDump() + if (targets.isNotEmpty()) { + for (target in targets) { + dumpManager.dumpProtoTarget(target, systemUIProto, args.rawArgs) + } + } else { + dumpManager.dumpProtoDumpables(systemUIProto, args.rawArgs) + } + val buffer = BufferedOutputStream(FileOutputStream(fd)) + buffer.use { + it.write(MessageNano.toByteArray(systemUIProto)) + it.flush() + } + } + private fun dumpTargets( targets: List<String>, pw: PrintWriter, @@ -235,6 +268,7 @@ class DumpHandler @Inject constructor( pw.println("$ <invocation> buffers") pw.println("$ <invocation> bugreport-critical") pw.println("$ <invocation> bugreport-normal") + pw.println("$ <invocation> config") pw.println() pw.println("Targets can be listed:") @@ -266,6 +300,7 @@ class DumpHandler @Inject constructor( } } } + PROTO -> pArgs.proto = true "-t", "--tail" -> { pArgs.tailLength = readArgument(iterator, arg) { it.toInt() @@ -277,6 +312,9 @@ class DumpHandler @Inject constructor( "-h", "--help" -> { pArgs.command = "help" } + // This flag is passed as part of the proto dump in Bug reports, we can ignore + // it because this is our default behavior. + "-a" -> {} else -> { throw ArgParseException("Unknown flag: $arg") } @@ -313,13 +351,21 @@ class DumpHandler @Inject constructor( const val PRIORITY_ARG_CRITICAL = "CRITICAL" const val PRIORITY_ARG_HIGH = "HIGH" const val PRIORITY_ARG_NORMAL = "NORMAL" + const val PROTO = "--proto" } } private val PRIORITY_OPTIONS = arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL) -private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables") +private val COMMANDS = arrayOf( + "bugreport-critical", + "bugreport-normal", + "buffers", + "dumpables", + "config", + "help" +) private class ParsedArgs( val rawArgs: Array<String>, @@ -329,6 +375,7 @@ private class ParsedArgs( var tailLength: Int = 0 var command: String? = null var listOnly = false + var proto = false } class ArgParseException(message: String) : Exception(message) diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt index cca04da8f426..ae780896a7e2 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt @@ -18,7 +18,9 @@ package com.android.systemui.dump import android.util.ArrayMap import com.android.systemui.Dumpable -import com.android.systemui.log.LogBuffer +import com.android.systemui.ProtoDumpable +import com.android.systemui.dump.nano.SystemUIProtoDump +import com.android.systemui.plugins.log.LogBuffer import java.io.PrintWriter import javax.inject.Inject import javax.inject.Singleton @@ -90,7 +92,7 @@ open class DumpManager @Inject constructor() { target: String, pw: PrintWriter, args: Array<String>, - tailLength: Int + tailLength: Int, ) { for (dumpable in dumpables.values) { if (dumpable.name.endsWith(target)) { @@ -107,6 +109,36 @@ open class DumpManager @Inject constructor() { } } + @Synchronized + fun dumpProtoTarget( + target: String, + protoDump: SystemUIProtoDump, + args: Array<String> + ) { + for (dumpable in dumpables.values) { + if (dumpable.dumpable is ProtoDumpable && dumpable.name.endsWith(target)) { + dumpProtoDumpable(dumpable.dumpable, protoDump, args) + return + } + } + } + + @Synchronized + fun dumpProtoDumpables( + systemUIProtoDump: SystemUIProtoDump, + args: Array<String> + ) { + for (dumpable in dumpables.values) { + if (dumpable.dumpable is ProtoDumpable) { + dumpProtoDumpable( + dumpable.dumpable, + systemUIProtoDump, + args + ) + } + } + } + /** * Dumps all registered dumpables to [pw] */ @@ -184,6 +216,14 @@ open class DumpManager @Inject constructor() { buffer.dumpable.dump(pw, tailLength) } + private fun dumpProtoDumpable( + protoDumpable: ProtoDumpable, + systemUIProtoDump: SystemUIProtoDump, + args: Array<String> + ) { + protoDumpable.dumpProto(systemUIProtoDump, args) + } + private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean { val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable return existingDumpable == null || newDumpable == existingDumpable @@ -195,4 +235,4 @@ private data class RegisteredDumpable<T>( val dumpable: T ) -private const val TAG = "DumpManager"
\ No newline at end of file +private const val TAG = "DumpManager" diff --git a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt index 0eab1afc4119..8299b13d305f 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt +++ b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt @@ -19,7 +19,7 @@ package com.android.systemui.dump import android.content.Context import android.util.Log import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.util.io.Files import com.android.systemui.util.time.SystemClock import java.io.IOException diff --git a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java index 0a41a56b5ecb..da983ab03a1d 100644 --- a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java +++ b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java @@ -51,6 +51,7 @@ public class SystemUIAuxiliaryDumpService extends Service { protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { // Simulate the NORMAL priority arg being passed to us mDumpHandler.dump( + fd, pw, new String[] { DumpHandler.PRIORITY_ARG, DumpHandler.PRIORITY_ARG_NORMAL }); } diff --git a/packages/SystemUI/src/com/android/systemui/dump/sysui.proto b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto new file mode 100644 index 000000000000..cd8c08aeb2dc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto3"; + +package com.android.systemui.dump; + +import "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto"; + +option java_multiple_files = true; + +message SystemUIProtoDump { + repeated com.android.systemui.qs.QsTileState tiles = 1; +} + diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt index dfa3bcda7d72..fb4fc928c8dc 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt @@ -16,12 +16,14 @@ package com.android.systemui.flags +import android.util.Dumpable + /** * Class to manage simple DeviceConfig-based feature flags. * * See [Flags] for instructions on defining new flags. */ -interface FeatureFlags : FlagListenable { +interface FeatureFlags : FlagListenable, Dumpable { /** Returns a boolean value for the given flag. */ fun isEnabled(flag: UnreleasedFlag): Boolean diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java index 00c1a99983df..b983e5c0a0f7 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java @@ -30,29 +30,21 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.os.Bundle; -import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.internal.statusbar.IStatusBarService; -import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.statusbar.commandline.Command; -import com.android.systemui.statusbar.commandline.CommandRegistry; import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.settings.SecureSettings; import org.jetbrains.annotations.NotNull; import java.io.PrintWriter; -import java.lang.reflect.Field; import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.TreeMap; @@ -75,10 +67,9 @@ import javax.inject.Named; * To restore a flag back to its default, leave the `--ez value <0|1>` off of the command. */ @SysUISingleton -public class FeatureFlagsDebug implements FeatureFlags, Dumpable { - private static final String TAG = "SysUIFlags"; +public class FeatureFlagsDebug implements FeatureFlags { + static final String TAG = "SysUIFlags"; static final String ALL_FLAGS = "all_flags"; - private static final String FLAG_COMMAND = "flag"; private final FlagManager mFlagManager; private final SecureSettings mSecureSettings; @@ -89,7 +80,7 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable { private final Map<Integer, Flag<?>> mAllFlags; private final Map<Integer, Boolean> mBooleanFlagCache = new TreeMap<>(); private final Map<Integer, String> mStringFlagCache = new TreeMap<>(); - private final IStatusBarService mBarService; + private final Restarter mRestarter; @Inject public FeatureFlagsDebug( @@ -98,12 +89,10 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable { SecureSettings secureSettings, SystemPropertiesHelper systemProperties, @Main Resources resources, - DumpManager dumpManager, DeviceConfigProxy deviceConfigProxy, ServerFlagReader serverFlagReader, @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags, - CommandRegistry commandRegistry, - IStatusBarService barService) { + Restarter barService) { mFlagManager = flagManager; mSecureSettings = secureSettings; mResources = resources; @@ -111,7 +100,7 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable { mDeviceConfigProxy = deviceConfigProxy; mServerFlagReader = serverFlagReader; mAllFlags = allFlags; - mBarService = barService; + mRestarter = barService; IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_SET_FLAG); @@ -120,8 +109,6 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable { flagManager.setClearCacheAction(this::removeFromCache); context.registerReceiver(mReceiver, filter, null, null, Context.RECEIVER_EXPORTED_UNAUDITED); - dumpManager.registerDumpable(TAG, this); - commandRegistry.registerCommand(FLAG_COMMAND, FlagCommand::new); } @Override @@ -266,7 +253,7 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable { mFlagManager.dispatchListenersAndMaybeRestart(id, this::restartSystemUI); } - private <T> void eraseFlag(Flag<T> flag) { + <T> void eraseFlag(Flag<T> flag) { if (flag instanceof SysPropFlag) { mSystemProperties.erase(((SysPropFlag<T>) flag).getName()); dispatchListenersAndMaybeRestart(flag.getId(), this::restartAndroid); @@ -319,13 +306,10 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable { return; } Log.i(TAG, "Restarting Android"); - try { - mBarService.restart(); - } catch (RemoteException e) { - } + mRestarter.restart(); } - private void setBooleanFlagInternal(Flag<?> flag, boolean value) { + void setBooleanFlagInternal(Flag<?> flag, boolean value) { if (flag instanceof BooleanFlag) { setFlagValue(flag.getId(), value, BooleanFlagSerializer.INSTANCE); } else if (flag instanceof ResourceBooleanFlag) { @@ -342,7 +326,7 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable { } } - private void setStringFlagInternal(Flag<?> flag, String value) { + void setStringFlagInternal(Flag<?> flag, String value) { if (flag instanceof StringFlag) { setFlagValue(flag.getId(), value, StringFlagSerializer.INSTANCE); } else if (flag instanceof ResourceStringFlag) { @@ -476,154 +460,4 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable { + ": [length=" + value.length() + "] \"" + value + "\"")); } - class FlagCommand implements Command { - private final List<String> mOnCommands = List.of("true", "on", "1", "enabled"); - private final List<String> mOffCommands = List.of("false", "off", "0", "disable"); - - @Override - public void execute(@NonNull PrintWriter pw, @NonNull List<String> args) { - if (args.size() == 0) { - pw.println("Error: no flag id supplied"); - help(pw); - pw.println(); - printKnownFlags(pw); - return; - } - - if (args.size() > 2) { - pw.println("Invalid number of arguments."); - help(pw); - return; - } - - int id = 0; - try { - id = Integer.parseInt(args.get(0)); - if (!mAllFlags.containsKey(id)) { - pw.println("Unknown flag id: " + id); - pw.println(); - printKnownFlags(pw); - return; - } - } catch (NumberFormatException e) { - id = flagNameToId(args.get(0)); - if (id == 0) { - pw.println("Invalid flag. Must an integer id or flag name: " + args.get(0)); - return; - } - } - Flag<?> flag = mAllFlags.get(id); - - String cmd = ""; - if (args.size() == 2) { - cmd = args.get(1).toLowerCase(); - } - - if ("erase".equals(cmd) || "reset".equals(cmd)) { - eraseFlag(flag); - return; - } - - boolean newValue = true; - if (args.size() == 1 || "toggle".equals(cmd)) { - boolean enabled = isBooleanFlagEnabled(flag); - - if (args.size() == 1) { - pw.println("Flag " + id + " is " + enabled); - return; - } - - newValue = !enabled; - } else { - newValue = mOnCommands.contains(cmd); - if (!newValue && !mOffCommands.contains(cmd)) { - pw.println("Invalid on/off argument supplied"); - help(pw); - return; - } - } - - pw.flush(); // Next command will restart sysui, so flush before we do so. - setBooleanFlagInternal(flag, newValue); - } - - @Override - public void help(PrintWriter pw) { - pw.println( - "Usage: adb shell cmd statusbar flag <id> " - + "[true|false|1|0|on|off|enable|disable|toggle|erase|reset]"); - pw.println("The id can either be a numeric integer or the corresponding field name"); - pw.println( - "If no argument is supplied after the id, the flags runtime value is output"); - } - - private boolean isBooleanFlagEnabled(Flag<?> flag) { - if (flag instanceof ReleasedFlag) { - return isEnabled((ReleasedFlag) flag); - } else if (flag instanceof UnreleasedFlag) { - return isEnabled((UnreleasedFlag) flag); - } else if (flag instanceof ResourceBooleanFlag) { - return isEnabled((ResourceBooleanFlag) flag); - } else if (flag instanceof SysPropFlag) { - return isEnabled((SysPropBooleanFlag) flag); - } - - return false; - } - - private int flagNameToId(String flagName) { - List<Field> fields = Flags.getFlagFields(); - for (Field field : fields) { - if (flagName.equals(field.getName())) { - return fieldToId(field); - } - } - - return 0; - } - - private int fieldToId(Field field) { - try { - Flag<?> flag = (Flag<?>) field.get(null); - return flag.getId(); - } catch (IllegalAccessException e) { - // no-op - } - - return 0; - } - - private void printKnownFlags(PrintWriter pw) { - List<Field> fields = Flags.getFlagFields(); - - int longestFieldName = 0; - for (Field field : fields) { - longestFieldName = Math.max(longestFieldName, field.getName().length()); - } - - pw.println("Known Flags:"); - pw.print("Flag Name"); - for (int i = 0; i < longestFieldName - "Flag Name".length() + 1; i++) { - pw.print(" "); - } - pw.println("ID Enabled?"); - for (int i = 0; i < longestFieldName; i++) { - pw.print("="); - } - pw.println(" ==== ========"); - for (Field field : fields) { - int id = fieldToId(field); - if (id == 0 || !mAllFlags.containsKey(id)) { - continue; - } - pw.print(field.getName()); - int fieldWidth = field.getName().length(); - for (int i = 0; i < longestFieldName - fieldWidth + 1; i++) { - pw.print(" "); - } - pw.printf("%-4d ", id); - pw.println(isBooleanFlagEnabled(mAllFlags.get(id))); - } - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt new file mode 100644 index 000000000000..560dcbd78c42 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import com.android.systemui.CoreStartable +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.commandline.CommandRegistry +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Inject + +class FeatureFlagsDebugStartable +@Inject +constructor( + dumpManager: DumpManager, + private val commandRegistry: CommandRegistry, + private val flagCommand: FlagCommand, + featureFlags: FeatureFlags +) : CoreStartable { + + init { + dumpManager.registerDumpable(FeatureFlagsDebug.TAG) { pw, args -> + featureFlags.dump(pw, args) + } + } + + override fun start() { + commandRegistry.registerCommand(FlagCommand.FLAG_COMMAND) { flagCommand } + } +} + +@Module +abstract class FeatureFlagsDebugStartableModule { + @Binds + @IntoMap + @ClassKey(FeatureFlagsDebugStartable::class) + abstract fun bind(impl: FeatureFlagsDebugStartable): CoreStartable +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java index 049b17d383a2..40a8a1a9ef01 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java @@ -24,10 +24,8 @@ import android.util.SparseBooleanArray; import androidx.annotation.NonNull; -import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dump.DumpManager; import com.android.systemui.util.DeviceConfigProxy; import org.jetbrains.annotations.NotNull; @@ -44,27 +42,26 @@ import javax.inject.Inject; * how to set flags. */ @SysUISingleton -public class FeatureFlagsRelease implements FeatureFlags, Dumpable { +public class FeatureFlagsRelease implements FeatureFlags { + static final String TAG = "SysUIFlags"; + private final Resources mResources; private final SystemPropertiesHelper mSystemProperties; private final DeviceConfigProxy mDeviceConfigProxy; private final ServerFlagReader mServerFlagReader; SparseBooleanArray mBooleanCache = new SparseBooleanArray(); SparseArray<String> mStringCache = new SparseArray<>(); - private boolean mInited; @Inject public FeatureFlagsRelease( @Main Resources resources, SystemPropertiesHelper systemProperties, DeviceConfigProxy deviceConfigProxy, - ServerFlagReader serverFlagReader, - DumpManager dumpManager) { + ServerFlagReader serverFlagReader) { mResources = resources; mSystemProperties = systemProperties; mDeviceConfigProxy = deviceConfigProxy; mServerFlagReader = serverFlagReader; - dumpManager.registerDumpable("SysUIFlags", this); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt new file mode 100644 index 000000000000..e7d8cc362c56 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseStartable.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import com.android.systemui.CoreStartable +import com.android.systemui.dump.DumpManager +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Inject + +class FeatureFlagsReleaseStartable +@Inject +constructor(dumpManager: DumpManager, featureFlags: FeatureFlags) : CoreStartable { + + init { + dumpManager.registerDumpable(FeatureFlagsRelease.TAG) { pw, args -> + featureFlags.dump(pw, args) + } + } + + override fun start() { + // no-op + } +} + +@Module +abstract class FeatureFlagsReleaseStartableModule { + @Binds + @IntoMap + @ClassKey(FeatureFlagsReleaseStartable::class) + abstract fun bind(impl: FeatureFlagsReleaseStartable): CoreStartable +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java new file mode 100644 index 000000000000..4d254313a57b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags; + +import androidx.annotation.NonNull; + +import com.android.systemui.statusbar.commandline.Command; + +import java.io.PrintWriter; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Named; + +/** + * A {@link Command} used to flip flags in SystemUI. + */ +public class FlagCommand implements Command { + public static final String FLAG_COMMAND = "flag"; + + private final List<String> mOnCommands = List.of("true", "on", "1", "enabled"); + private final List<String> mOffCommands = List.of("false", "off", "0", "disable"); + private final FeatureFlagsDebug mFeatureFlags; + private final Map<Integer, Flag<?>> mAllFlags; + + @Inject + FlagCommand( + FeatureFlagsDebug featureFlags, + @Named(FeatureFlagsDebug.ALL_FLAGS) Map<Integer, Flag<?>> allFlags + ) { + mFeatureFlags = featureFlags; + mAllFlags = allFlags; + } + + @Override + public void execute(@NonNull PrintWriter pw, @NonNull List<String> args) { + if (args.size() == 0) { + pw.println("Error: no flag id supplied"); + help(pw); + pw.println(); + printKnownFlags(pw); + return; + } + + if (args.size() > 2) { + pw.println("Invalid number of arguments."); + help(pw); + return; + } + + int id = 0; + try { + id = Integer.parseInt(args.get(0)); + if (!mAllFlags.containsKey(id)) { + pw.println("Unknown flag id: " + id); + pw.println(); + printKnownFlags(pw); + return; + } + } catch (NumberFormatException e) { + id = flagNameToId(args.get(0)); + if (id == 0) { + pw.println("Invalid flag. Must an integer id or flag name: " + args.get(0)); + return; + } + } + Flag<?> flag = mAllFlags.get(id); + + String cmd = ""; + if (args.size() == 2) { + cmd = args.get(1).toLowerCase(); + } + + if ("erase".equals(cmd) || "reset".equals(cmd)) { + mFeatureFlags.eraseFlag(flag); + return; + } + + boolean newValue = true; + if (args.size() == 1 || "toggle".equals(cmd)) { + boolean enabled = isBooleanFlagEnabled(flag); + + if (args.size() == 1) { + pw.println("Flag " + id + " is " + enabled); + return; + } + + newValue = !enabled; + } else { + newValue = mOnCommands.contains(cmd); + if (!newValue && !mOffCommands.contains(cmd)) { + pw.println("Invalid on/off argument supplied"); + help(pw); + return; + } + } + + pw.flush(); // Next command will restart sysui, so flush before we do so. + mFeatureFlags.setBooleanFlagInternal(flag, newValue); + } + + @Override + public void help(PrintWriter pw) { + pw.println( + "Usage: adb shell cmd statusbar flag <id> " + + "[true|false|1|0|on|off|enable|disable|toggle|erase|reset]"); + pw.println("The id can either be a numeric integer or the corresponding field name"); + pw.println( + "If no argument is supplied after the id, the flags runtime value is output"); + } + + private boolean isBooleanFlagEnabled(Flag<?> flag) { + if (flag instanceof ReleasedFlag) { + return mFeatureFlags.isEnabled((ReleasedFlag) flag); + } else if (flag instanceof UnreleasedFlag) { + return mFeatureFlags.isEnabled((UnreleasedFlag) flag); + } else if (flag instanceof ResourceBooleanFlag) { + return mFeatureFlags.isEnabled((ResourceBooleanFlag) flag); + } else if (flag instanceof SysPropFlag) { + return mFeatureFlags.isEnabled((SysPropBooleanFlag) flag); + } + + return false; + } + + private int flagNameToId(String flagName) { + List<Field> fields = Flags.getFlagFields(); + for (Field field : fields) { + if (flagName.equals(field.getName())) { + return fieldToId(field); + } + } + + return 0; + } + + private int fieldToId(Field field) { + try { + Flag<?> flag = (Flag<?>) field.get(null); + return flag.getId(); + } catch (IllegalAccessException e) { + // no-op + } + + return 0; + } + + private void printKnownFlags(PrintWriter pw) { + List<Field> fields = Flags.getFlagFields(); + + int longestFieldName = 0; + for (Field field : fields) { + longestFieldName = Math.max(longestFieldName, field.getName().length()); + } + + pw.println("Known Flags:"); + pw.print("Flag Name"); + for (int i = 0; i < longestFieldName - "Flag Name".length() + 1; i++) { + pw.print(" "); + } + pw.println("ID Enabled?"); + for (int i = 0; i < longestFieldName; i++) { + pw.print("="); + } + pw.println(" ==== ========"); + for (Field field : fields) { + int id = fieldToId(field); + if (id == 0 || !mAllFlags.containsKey(id)) { + continue; + } + pw.print(field.getName()); + int fieldWidth = field.getName().length(); + for (int i = 0; i < longestFieldName - fieldWidth + 1; i++) { + pw.print(" "); + } + pw.printf("%-4d ", id); + pw.println(isBooleanFlagEnabled(mAllFlags.get(id))); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java deleted file mode 100644 index 93f13ebb3892..000000000000 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.flags; - -import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER; - -import com.android.internal.annotations.Keep; -import com.android.systemui.R; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * List of {@link Flag} objects for use in SystemUI. - * - * Flag Ids are integers. - * Ids must be unique. This is enforced in a unit test. - * Ids need not be sequential. Flags can "claim" a chunk of ids for flags in related features with - * a comment. This is purely for organizational purposes. - * - * On public release builds, flags will always return their default value. There is no way to - * change their value on release builds. - * - * See {@link FeatureFlagsDebug} for instructions on flipping the flags via adb. - */ -public class Flags { - public static final UnreleasedFlag TEAMFOOD = new UnreleasedFlag(1); - - /***************************************/ - // 100 - notification - public static final UnreleasedFlag NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = - new UnreleasedFlag(103); - - public static final UnreleasedFlag NSSL_DEBUG_LINES = - new UnreleasedFlag(105); - - public static final UnreleasedFlag NSSL_DEBUG_REMOVE_ANIMATION = - new UnreleasedFlag(106); - - public static final UnreleasedFlag NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE = - new UnreleasedFlag(107); - - public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS = - new ResourceBooleanFlag(108, R.bool.config_notificationToContents); - - public static final ReleasedFlag REMOVE_UNRANKED_NOTIFICATIONS = - new ReleasedFlag(109); - - public static final UnreleasedFlag FSI_REQUIRES_KEYGUARD = - new UnreleasedFlag(110, true); - - public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true); - - // next id: 112 - - /***************************************/ - // 200 - keyguard/lockscreen - - // ** Flag retired ** - // public static final BooleanFlag KEYGUARD_LAYOUT = - // new BooleanFlag(200, true); - - public static final ReleasedFlag LOCKSCREEN_ANIMATIONS = - new ReleasedFlag(201); - - public static final ReleasedFlag NEW_UNLOCK_SWIPE_ANIMATION = - new ReleasedFlag(202); - - public static final ResourceBooleanFlag CHARGING_RIPPLE = - new ResourceBooleanFlag(203, R.bool.flag_charging_ripple); - - public static final ResourceBooleanFlag BOUNCER_USER_SWITCHER = - new ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher); - - public static final ResourceBooleanFlag FACE_SCANNING_ANIM = - new ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation); - - public static final UnreleasedFlag LOCKSCREEN_CUSTOM_CLOCKS = new UnreleasedFlag(207); - - /** - * Flag to enable the usage of the new bouncer data source. This is a refactor of and - * eventual replacement of KeyguardBouncer.java. - */ - public static final UnreleasedFlag MODERN_BOUNCER = new UnreleasedFlag(208); - - /** Whether UserSwitcherActivity should use modern architecture. */ - public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY = - new ReleasedFlag(209, true); - - /** Whether the new implementation of UserSwitcherController should be used. */ - public static final UnreleasedFlag REFACTORED_USER_SWITCHER_CONTROLLER = - new UnreleasedFlag(210, false); - - /***************************************/ - // 300 - power menu - public static final ReleasedFlag POWER_MENU_LITE = - new ReleasedFlag(300); - - /***************************************/ - // 400 - smartspace - public static final ReleasedFlag SMARTSPACE_DEDUPING = - new ReleasedFlag(400); - - public static final ReleasedFlag SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED = - new ReleasedFlag(401); - - public static final ResourceBooleanFlag SMARTSPACE = - new ResourceBooleanFlag(402, R.bool.flag_smartspace); - - /***************************************/ - // 500 - quick settings - /** - * @deprecated Not needed anymore - */ - @Deprecated - public static final ReleasedFlag NEW_USER_SWITCHER = - new ReleasedFlag(500); - - public static final UnreleasedFlag COMBINED_QS_HEADERS = - new UnreleasedFlag(501, true); - - public static final ResourceBooleanFlag PEOPLE_TILE = - new ResourceBooleanFlag(502, R.bool.flag_conversations); - - public static final ResourceBooleanFlag QS_USER_DETAIL_SHORTCUT = - new ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut); - - /** - * @deprecated Not needed anymore - */ - @Deprecated - public static final ReleasedFlag NEW_FOOTER = new ReleasedFlag(504); - - public static final UnreleasedFlag NEW_HEADER = new UnreleasedFlag(505, true); - public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER = - new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher); - - public static final UnreleasedFlag NEW_FOOTER_ACTIONS = new UnreleasedFlag(507, true); - - /***************************************/ - // 600- status bar - public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER = - new ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip); - - public static final ReleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE = - new ReleasedFlag(603, false); - - public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_BACKEND = - new UnreleasedFlag(604, false); - - public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_FRONTEND = - new UnreleasedFlag(605, false); - - /***************************************/ - // 700 - dialer/calls - public static final ReleasedFlag ONGOING_CALL_STATUS_BAR_CHIP = - new ReleasedFlag(700); - - public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE = - new ReleasedFlag(701); - - public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = - new ReleasedFlag(702); - - /***************************************/ - // 800 - general visual/theme - public static final ResourceBooleanFlag MONET = - new ResourceBooleanFlag(800, R.bool.flag_monet); - - /***************************************/ - // 801 - region sampling - public static final UnreleasedFlag REGION_SAMPLING = new UnreleasedFlag(801); - - // 802 - wallpaper rendering - public static final UnreleasedFlag USE_CANVAS_RENDERER = new UnreleasedFlag(802); - - // 803 - screen contents translation - public static final UnreleasedFlag SCREEN_CONTENTS_TRANSLATION = new UnreleasedFlag(803); - - /***************************************/ - // 900 - media - public static final UnreleasedFlag MEDIA_TAP_TO_TRANSFER = new UnreleasedFlag(900); - public static final UnreleasedFlag MEDIA_SESSION_ACTIONS = new UnreleasedFlag(901); - public static final ReleasedFlag MEDIA_NEARBY_DEVICES = new ReleasedFlag(903); - public static final ReleasedFlag MEDIA_MUTE_AWAIT = new ReleasedFlag(904); - public static final UnreleasedFlag DREAM_MEDIA_COMPLICATION = new UnreleasedFlag(905); - public static final UnreleasedFlag DREAM_MEDIA_TAP_TO_OPEN = new UnreleasedFlag(906); - - // 1000 - dock - public static final ReleasedFlag SIMULATE_DOCK_THROUGH_CHARGING = - new ReleasedFlag(1000); - public static final ReleasedFlag DOCK_SETUP_ENABLED = new ReleasedFlag(1001); - - public static final UnreleasedFlag ROUNDED_BOX_RIPPLE = new UnreleasedFlag(1002, false); - - // 1100 - windowing - @Keep - public static final SysPropBooleanFlag WM_ENABLE_SHELL_TRANSITIONS = - new SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false); - - /** - * b/170163464: animate bubbles expanded view collapse with home gesture - */ - @Keep - public static final SysPropBooleanFlag BUBBLES_HOME_GESTURE = - new SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true); - - @Keep - public static final DeviceConfigBooleanFlag WM_ENABLE_PARTIAL_SCREEN_SHARING = - new DeviceConfigBooleanFlag(1102, "record_task_content", - NAMESPACE_WINDOW_MANAGER, false, true); - - @Keep - public static final SysPropBooleanFlag HIDE_NAVBAR_WINDOW = - new SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false); - - @Keep - public static final SysPropBooleanFlag WM_DESKTOP_WINDOWING = - new SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false); - - @Keep - public static final SysPropBooleanFlag WM_CAPTION_ON_SHELL = - new SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false); - - @Keep - public static final SysPropBooleanFlag FLOATING_TASKS_ENABLED = - new SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false); - - @Keep - public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES = - new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false); - - // 1200 - predictive back - @Keep - public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag( - 1200, "persist.wm.debug.predictive_back", true); - @Keep - public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK_ANIM = new SysPropBooleanFlag( - 1201, "persist.wm.debug.predictive_back_anim", false); - @Keep - public static final SysPropBooleanFlag WM_ALWAYS_ENFORCE_PREDICTIVE_BACK = - new SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false); - - public static final UnreleasedFlag NEW_BACK_AFFORDANCE = - new UnreleasedFlag(1203, false /* teamfood */); - - // 1300 - screenshots - - public static final UnreleasedFlag SCREENSHOT_REQUEST_PROCESSOR = new UnreleasedFlag(1300); - public static final UnreleasedFlag SCREENSHOT_WORK_PROFILE_POLICY = new UnreleasedFlag(1301); - - // 1400 - columbus, b/242800729 - public static final UnreleasedFlag QUICK_TAP_IN_PCC = new UnreleasedFlag(1400); - - // 1500 - chooser - public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500); - - // Pay no attention to the reflection behind the curtain. - // ========================== Curtain ========================== - // | | - // | . . . . . . . . . . . . . . . . . . . | - private static Map<Integer, Flag<?>> sFlagMap; - static Map<Integer, Flag<?>> collectFlags() { - if (sFlagMap != null) { - return sFlagMap; - } - - Map<Integer, Flag<?>> flags = new HashMap<>(); - List<Field> flagFields = getFlagFields(); - - for (Field field : flagFields) { - try { - Flag<?> flag = (Flag<?>) field.get(null); - flags.put(flag.getId(), flag); - } catch (IllegalAccessException e) { - // no-op - } - } - - sFlagMap = flags; - - return sFlagMap; - } - - static List<Field> getFlagFields() { - Field[] fields = Flags.class.getFields(); - List<Field> result = new ArrayList<>(); - - for (Field field : fields) { - Class<?> t = field.getType(); - if (Flag.class.isAssignableFrom(t)) { - result.add(field); - } - } - - return result; - } - // | . . . . . . . . . . . . . . . . . . . | - // | | - // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/ - -} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt new file mode 100644 index 000000000000..86ea251717fc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.flags + +import android.provider.DeviceConfig +import com.android.internal.annotations.Keep +import com.android.systemui.R +import java.lang.reflect.Field + +/** + * List of [Flag] objects for use in SystemUI. + * + * Flag Ids are integers. Ids must be unique. This is enforced in a unit test. Ids need not be + * sequential. Flags can "claim" a chunk of ids for flags in related features with a comment. This + * is purely for organizational purposes. + * + * On public release builds, flags will always return their default value. There is no way to change + * their value on release builds. + * + * See [FeatureFlagsDebug] for instructions on flipping the flags via adb. + */ +object Flags { + @JvmField val TEAMFOOD = UnreleasedFlag(1) + + // 100 - notification + // TODO(b/254512751): Tracking Bug + val NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = UnreleasedFlag(103) + + // TODO(b/254512732): Tracking Bug + @JvmField val NSSL_DEBUG_LINES = UnreleasedFlag(105) + + // TODO(b/254512505): Tracking Bug + @JvmField val NSSL_DEBUG_REMOVE_ANIMATION = UnreleasedFlag(106) + + // TODO(b/254512624): Tracking Bug + @JvmField + val NOTIFICATION_DRAG_TO_CONTENTS = + ResourceBooleanFlag(108, R.bool.config_notificationToContents) + + // TODO(b/254512517): Tracking Bug + val FSI_REQUIRES_KEYGUARD = UnreleasedFlag(110, teamfood = true) + + // TODO(b/254512538): Tracking Bug + val INSTANT_VOICE_REPLY = UnreleasedFlag(111, teamfood = true) + + // TODO(b/254512425): Tracking Bug + val NOTIFICATION_MEMORY_MONITOR_ENABLED = UnreleasedFlag(112, teamfood = true) + + // TODO(b/254512731): Tracking Bug + @JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true) + val STABILITY_INDEX_FIX = UnreleasedFlag(114, teamfood = true) + val SEMI_STABLE_SORT = UnreleasedFlag(115, teamfood = true) + @JvmField val NOTIFICATION_GROUP_CORNER = UnreleasedFlag(116, true) + // next id: 117 + + // 200 - keyguard/lockscreen + // ** Flag retired ** + // public static final BooleanFlag KEYGUARD_LAYOUT = + // new BooleanFlag(200, true); + // TODO(b/254512713): Tracking Bug + @JvmField val LOCKSCREEN_ANIMATIONS = ReleasedFlag(201) + + // TODO(b/254512750): Tracking Bug + val NEW_UNLOCK_SWIPE_ANIMATION = ReleasedFlag(202) + val CHARGING_RIPPLE = ResourceBooleanFlag(203, R.bool.flag_charging_ripple) + + // TODO(b/254512281): Tracking Bug + @JvmField + val BOUNCER_USER_SWITCHER = ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher) + + // TODO(b/254512676): Tracking Bug + @JvmField val LOCKSCREEN_CUSTOM_CLOCKS = UnreleasedFlag(207, teamfood = true) + + /** + * Flag to enable the usage of the new bouncer data source. This is a refactor of and eventual + * replacement of KeyguardBouncer.java. + */ + // TODO(b/254512385): Tracking Bug + @JvmField val MODERN_BOUNCER = UnreleasedFlag(208) + + /** + * Whether the user interactor and repository should use `UserSwitcherController`. + * + * If this is `false`, the interactor and repo skip the controller and directly access the + * framework APIs. + */ + // TODO(b/254513286): Tracking Bug + val USER_INTERACTOR_AND_REPO_USE_CONTROLLER = UnreleasedFlag(210) + + /** + * Whether `UserSwitcherController` should use the user interactor. + * + * When this is `true`, the controller does not directly access framework APIs. Instead, it goes + * through the interactor. + * + * Note: do not set this to true if [.USER_INTERACTOR_AND_REPO_USE_CONTROLLER] is `true` as it + * would created a cycle between controller -> interactor -> controller. + */ + // TODO(b/254513102): Tracking Bug + val USER_CONTROLLER_USES_INTERACTOR = ReleasedFlag(211) + + /** + * Whether the clock on a wide lock screen should use the new "stepping" animation for moving + * the digits when the clock moves. + */ + @JvmField val STEP_CLOCK_ANIMATION = UnreleasedFlag(212) + + /** + * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository + * will occur in stages. This is one stage of many to come. + */ + @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213, teamfood = true) + + // 300 - power menu + // TODO(b/254512600): Tracking Bug + @JvmField val POWER_MENU_LITE = ReleasedFlag(300) + + // 400 - smartspace + + // TODO(b/254513100): Tracking Bug + val SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED = ReleasedFlag(401) + val SMARTSPACE = ResourceBooleanFlag(402, R.bool.flag_smartspace) + + // 500 - quick settings + @Deprecated("Not needed anymore") val NEW_USER_SWITCHER = ReleasedFlag(500) + + // TODO(b/254512321): Tracking Bug + @JvmField val COMBINED_QS_HEADERS = UnreleasedFlag(501, teamfood = true) + val PEOPLE_TILE = ResourceBooleanFlag(502, R.bool.flag_conversations) + @JvmField + val QS_USER_DETAIL_SHORTCUT = + ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut) + + // TODO(b/254512699): Tracking Bug + @Deprecated("Not needed anymore") val NEW_FOOTER = ReleasedFlag(504) + + // TODO(b/254512747): Tracking Bug + val NEW_HEADER = UnreleasedFlag(505, teamfood = true) + + // TODO(b/254512383): Tracking Bug + @JvmField + val FULL_SCREEN_USER_SWITCHER = + ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher) + + // TODO(b/254512678): Tracking Bug + @JvmField val NEW_FOOTER_ACTIONS = ReleasedFlag(507) + + // 600- status bar + // TODO(b/254513246): Tracking Bug + val STATUS_BAR_USER_SWITCHER = ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip) + + // TODO(b/254512623): Tracking Bug + @Deprecated("Replaced by mobile and wifi specific flags.") + val NEW_STATUS_BAR_PIPELINE_BACKEND = UnreleasedFlag(604, teamfood = false) + + // TODO(b/254512660): Tracking Bug + @Deprecated("Replaced by mobile and wifi specific flags.") + val NEW_STATUS_BAR_PIPELINE_FRONTEND = UnreleasedFlag(605, teamfood = false) + + val NEW_STATUS_BAR_MOBILE_ICONS = UnreleasedFlag(606, false) + + val NEW_STATUS_BAR_WIFI_ICON = UnreleasedFlag(607, false) + + // 700 - dialer/calls + // TODO(b/254512734): Tracking Bug + val ONGOING_CALL_STATUS_BAR_CHIP = ReleasedFlag(700) + + // TODO(b/254512681): Tracking Bug + val ONGOING_CALL_IN_IMMERSIVE = ReleasedFlag(701) + + // TODO(b/254512753): Tracking Bug + val ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = ReleasedFlag(702) + + // 800 - general visual/theme + @JvmField val MONET = ResourceBooleanFlag(800, R.bool.flag_monet) + + // 801 - region sampling + // TODO(b/254512848): Tracking Bug + val REGION_SAMPLING = UnreleasedFlag(801) + + // 802 - wallpaper rendering + // TODO(b/254512923): Tracking Bug + @JvmField val USE_CANVAS_RENDERER = ReleasedFlag(802) + + // 803 - screen contents translation + // TODO(b/254513187): Tracking Bug + val SCREEN_CONTENTS_TRANSLATION = UnreleasedFlag(803) + + // 804 - monochromatic themes + @JvmField val MONOCHROMATIC_THEMES = UnreleasedFlag(804) + + // 900 - media + // TODO(b/254512697): Tracking Bug + val MEDIA_TAP_TO_TRANSFER = ReleasedFlag(900) + + // TODO(b/254512502): Tracking Bug + val MEDIA_SESSION_ACTIONS = UnreleasedFlag(901) + + // TODO(b/254512726): Tracking Bug + val MEDIA_NEARBY_DEVICES = ReleasedFlag(903) + + // TODO(b/254512695): Tracking Bug + val MEDIA_MUTE_AWAIT = ReleasedFlag(904) + + // TODO(b/254512654): Tracking Bug + @JvmField val DREAM_MEDIA_COMPLICATION = UnreleasedFlag(905) + + // TODO(b/254512673): Tracking Bug + @JvmField val DREAM_MEDIA_TAP_TO_OPEN = UnreleasedFlag(906) + + // TODO(b/254513168): Tracking Bug + val UMO_SURFACE_RIPPLE = UnreleasedFlag(907) + + // 1000 - dock + val SIMULATE_DOCK_THROUGH_CHARGING = ReleasedFlag(1000) + + // TODO(b/254512444): Tracking Bug + @JvmField val DOCK_SETUP_ENABLED = ReleasedFlag(1001) + + // TODO(b/254512758): Tracking Bug + @JvmField val ROUNDED_BOX_RIPPLE = ReleasedFlag(1002) + + // TODO(b/254512525): Tracking Bug + @JvmField val REFACTORED_DOCK_SETUP = ReleasedFlag(1003, teamfood = true) + + // 1100 - windowing + @Keep + val WM_ENABLE_SHELL_TRANSITIONS = + SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false) + + /** b/170163464: animate bubbles expanded view collapse with home gesture */ + @Keep + val BUBBLES_HOME_GESTURE = + SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true) + + // TODO(b/254513207): Tracking Bug + @JvmField + @Keep + val WM_ENABLE_PARTIAL_SCREEN_SHARING = + DeviceConfigBooleanFlag( + 1102, + "record_task_content", + DeviceConfig.NAMESPACE_WINDOW_MANAGER, + false, + teamfood = true + ) + + // TODO(b/254512674): Tracking Bug + @JvmField + @Keep + val HIDE_NAVBAR_WINDOW = SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false) + + @Keep + val WM_DESKTOP_WINDOWING = SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false) + + @Keep + val WM_CAPTION_ON_SHELL = SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false) + + @Keep + val FLOATING_TASKS_ENABLED = SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false) + + @Keep + val SHOW_FLOATING_TASKS_AS_BUBBLES = + SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false) + + @Keep + val ENABLE_FLING_TO_DISMISS_BUBBLE = + SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true) + + @Keep + val ENABLE_FLING_TO_DISMISS_PIP = + SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true) + + @Keep + val ENABLE_PIP_KEEP_CLEAR_ALGORITHM = + SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false) + + // 1200 - predictive back + @Keep + val WM_ENABLE_PREDICTIVE_BACK = + SysPropBooleanFlag(1200, "persist.wm.debug.predictive_back", true) + + @Keep + val WM_ENABLE_PREDICTIVE_BACK_ANIM = + SysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", false) + + @Keep + val WM_ALWAYS_ENFORCE_PREDICTIVE_BACK = + SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false) + + // TODO(b/254512728): Tracking Bug + @JvmField val NEW_BACK_AFFORDANCE = UnreleasedFlag(1203, teamfood = false) + + // 1300 - screenshots + // TODO(b/254512719): Tracking Bug + @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300) + + // TODO(b/254513155): Tracking Bug + @JvmField val SCREENSHOT_WORK_PROFILE_POLICY = UnreleasedFlag(1301) + + // 1400 - columbus + // TODO(b/254512756): Tracking Bug + val QUICK_TAP_IN_PCC = ReleasedFlag(1400) + + // 1500 - chooser + // TODO(b/254512507): Tracking Bug + val CHOOSER_UNBUNDLED = UnreleasedFlag(1500) + + // 1700 - clipboard + @JvmField val CLIPBOARD_OVERLAY_REFACTOR = UnreleasedFlag(1700) + + // 1800 - shade container + @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, true) + + // Pay no attention to the reflection behind the curtain. + // ========================== Curtain ========================== + // | | + // | . . . . . . . . . . . . . . . . . . . | + @JvmStatic + fun collectFlags(): Map<Int, Flag<*>> { + return flagFields + .map { field -> + // field[null] returns the current value of the field. + // See java.lang.Field#get + val flag = field[null] as Flag<*> + flag.id to flag + } + .toMap() + } + + // | . . . . . . . . . . . . . . . . . . . | + @JvmStatic + val flagFields: List<Field> + get() { + return Flags::class.java.fields.filter { f -> + Flag::class.java.isAssignableFrom(f.type) + } + } + // | | + // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/ +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt b/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt new file mode 100644 index 000000000000..8f095a24de94 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/Restarter.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.flags + +interface Restarter { + fun restart() +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java index 74d5bd577cf4..9f321d83d292 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsComponent.java @@ -36,8 +36,7 @@ import javax.inject.Provider; * Manages power menu plugins and communicates power menu actions to the CentralSurfaces. */ @SysUISingleton -public class GlobalActionsComponent extends CoreStartable - implements Callbacks, GlobalActionsManager { +public class GlobalActionsComponent implements CoreStartable, Callbacks, GlobalActionsManager { private final CommandQueue mCommandQueue; private final ExtensionController mExtensionController; @@ -48,11 +47,10 @@ public class GlobalActionsComponent extends CoreStartable private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @Inject - public GlobalActionsComponent(Context context, CommandQueue commandQueue, + public GlobalActionsComponent(CommandQueue commandQueue, ExtensionController extensionController, Provider<GlobalActions> globalActionsProvider, StatusBarKeyguardViewManager statusBarKeyguardViewManager) { - super(context); mCommandQueue = commandQueue; mExtensionController = extensionController; mGlobalActionsProvider = globalActionsProvider; diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index da5819a7f3bc..3ef5499237f1 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -116,6 +116,7 @@ import com.android.systemui.MultiListLayout; import com.android.systemui.MultiListLayout.MultiListAdapter; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.animation.Expandable; import com.android.systemui.animation.Interpolators; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; @@ -448,10 +449,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene * * @param keyguardShowing True if keyguard is showing * @param isDeviceProvisioned True if device is provisioned - * @param view The view from which we should animate the dialog when showing it + * @param expandable The expandable from which we should animate the dialog when + * showing it */ public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned, - @Nullable View view) { + @Nullable Expandable expandable) { mKeyguardShowing = keyguardShowing; mDeviceProvisioned = isDeviceProvisioned; if (mDialog != null && mDialog.isShowing()) { @@ -463,7 +465,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mDialog.dismiss(); mDialog = null; } else { - handleShow(view); + handleShow(expandable); } } @@ -495,7 +497,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } } - protected void handleShow(@Nullable View view) { + protected void handleShow(@Nullable Expandable expandable) { awakenIfNecessary(); mDialog = createDialog(); prepareDialog(); @@ -507,10 +509,12 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports mDialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM); - if (view != null) { - mDialogLaunchAnimator.showFromView(mDialog, view, - new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG)); + DialogLaunchAnimator.Controller controller = + expandable != null ? expandable.dialogLaunchController( + new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG)) : null; + if (controller != null) { + mDialogLaunchAnimator.show(mDialog, controller); } else { mDialog.show(); } @@ -1016,8 +1020,9 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene Log.w(TAG, "Bugreport handler could not be launched"); mIActivityManager.requestInteractiveBugReport(); } - // Close shade so user sees the activity - mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade); + // Maybe close shade (depends on a flag) so user sees the activity + mCentralSurfacesOptional.ifPresent( + CentralSurfaces::collapseShadeForBugreport); } catch (RemoteException e) { } } @@ -1036,8 +1041,8 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mMetricsLogger.action(MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_FULL); mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS); mIActivityManager.requestFullBugReport(); - // Close shade so user sees the activity - mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade); + // Maybe close shade (depends on a flag) so user sees the activity + mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShadeForBugreport); } catch (RemoteException e) { } return false; diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java index 568143c8df71..4f1a2b34f07c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java +++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java @@ -27,7 +27,6 @@ import android.bluetooth.le.ScanSettings; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; -import android.content.res.Configuration; import android.hardware.input.InputManager; import android.os.Handler; import android.os.HandlerThread; @@ -66,7 +65,7 @@ import javax.inject.Provider; /** */ @SysUISingleton -public class KeyboardUI extends CoreStartable implements InputManager.OnTabletModeChangedListener { +public class KeyboardUI implements CoreStartable, InputManager.OnTabletModeChangedListener { private static final String TAG = "KeyboardUI"; private static final boolean DEBUG = false; @@ -127,13 +126,12 @@ public class KeyboardUI extends CoreStartable implements InputManager.OnTabletMo @Inject public KeyboardUI(Context context, Provider<LocalBluetoothManager> bluetoothManagerProvider) { - super(context); + mContext = context; this.mBluetoothManagerProvider = bluetoothManagerProvider; } @Override public void start() { - mContext = super.mContext; HandlerThread thread = new HandlerThread("Keyboard", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); mHandler = new KeyboardHandler(thread.getLooper()); @@ -141,10 +139,6 @@ public class KeyboardUI extends CoreStartable implements InputManager.OnTabletMo } @Override - protected void onConfigurationChanged(Configuration newConfig) { - } - - @Override public void dump(PrintWriter pw, String[] args) { pw.println("KeyboardUI:"); pw.println(" mEnabled=" + mEnabled); @@ -156,7 +150,7 @@ public class KeyboardUI extends CoreStartable implements InputManager.OnTabletMo } @Override - protected void onBootCompleted() { + public void onBootCompleted() { mHandler.sendEmptyMessage(MSG_ON_BOOT_COMPLETED); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 5f96a3b56e27..a908e943bbf5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -666,7 +666,7 @@ class KeyguardUnlockAnimationController @Inject constructor( return } - if (keyguardViewController.isShowing && !playingCannedUnlockAnimation) { + if (keyguardStateController.isShowing && !playingCannedUnlockAnimation) { showOrHideSurfaceIfDismissAmountThresholdsReached() // If the surface is visible or it's about to be, start updating its appearance to @@ -726,7 +726,7 @@ class KeyguardUnlockAnimationController @Inject constructor( private fun finishKeyguardExitRemoteAnimationIfReachThreshold() { // no-op if keyguard is not showing or animation is not enabled. if (!KeyguardService.sEnableRemoteKeyguardGoingAwayAnimation || - !keyguardViewController.isShowing) { + !keyguardStateController.isShowing) { return } @@ -849,7 +849,7 @@ class KeyguardUnlockAnimationController @Inject constructor( * animation. */ fun hideKeyguardViewAfterRemoteAnimation() { - if (keyguardViewController.isShowing) { + if (keyguardStateController.isShowing) { // Hide the keyguard, with no fade out since we animated it away during the unlock. keyguardViewController.hide( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 38b98eb45aec..d90236867b99 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -25,7 +25,6 @@ import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NAV_BA import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION; import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD; import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_UNLOCK_ANIMATION; -import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT; @@ -92,6 +91,7 @@ import android.view.animation.AnimationUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.Configuration; @@ -125,6 +125,7 @@ import com.android.systemui.keyguard.dagger.KeyguardModule; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationPanelViewController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -135,7 +136,6 @@ import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.util.DeviceConfigProxy; @@ -187,7 +187,7 @@ import dagger.Lazy; * directly to the keyguard UI is posted to a {@link android.os.Handler} to ensure it is taken on the UI * thread of the keyguard. */ -public class KeyguardViewMediator extends CoreStartable implements Dumpable, +public class KeyguardViewMediator implements CoreStartable, Dumpable, StatusBarStateController.StateListener { private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000; private static final long KEYGUARD_DONE_PENDING_TIMEOUT_MS = 3000; @@ -273,6 +273,7 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, private boolean mShuttingDown; private boolean mDozing; private boolean mAnimatingScreenOff; + private final Context mContext; private final FalsingCollector mFalsingCollector; /** High level access to the power manager for WakeLocks */ @@ -322,6 +323,12 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, // true if the keyguard is hidden by another window private boolean mOccluded = false; + /** + * Whether the {@link #mOccludeAnimationController} is currently playing the occlusion + * animation. + */ + private boolean mOccludeAnimationPlaying = false; + private boolean mWakeAndUnlocking = false; /** @@ -335,12 +342,6 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, */ private int mDelayedProfileShowingSequence; - /** - * If the user has disabled the keyguard, then requests to exit, this is - * how we'll ultimately let them know whether it was successful. We use this - * var being non-null as an indicator that there is an in progress request. - */ - private IKeyguardExitCallback mExitSecureCallback; private final DismissCallbackRegistry mDismissCallbackRegistry; // the properties of the keyguard @@ -401,6 +402,11 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, private final float mWindowCornerRadius; /** + * The duration in milliseconds of the dream open animation. + */ + private final int mDreamOpenAnimationDuration; + + /** * The animation used for hiding keyguard. This is used to fetch the animation timings if * WindowManager is not providing us with them. */ @@ -512,9 +518,9 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { @Override - public void onKeyguardVisibilityChanged(boolean showing) { + public void onKeyguardVisibilityChanged(boolean visible) { synchronized (KeyguardViewMediator.this) { - if (!showing && mPendingPinLock) { + if (!visible && mPendingPinLock) { Log.i(TAG, "PIN lock requested, starting keyguard"); // Bring the keyguard back in order to show the PIN lock @@ -794,6 +800,8 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, KeyguardUpdateMonitor.StrongAuthTracker strongAuthTracker = mUpdateMonitor.getStrongAuthTracker(); int strongAuth = strongAuthTracker.getStrongAuthForUser(currentUser); + boolean allowedNonStrongAfterIdleTimeout = + strongAuthTracker.isNonStrongBiometricAllowedAfterIdleTimeout(currentUser); if (any && !strongAuthTracker.hasUserAuthenticatedSinceBoot()) { return KeyguardSecurityView.PROMPT_REASON_RESTART; @@ -804,9 +812,6 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, } else if (trustAgentsEnabled && (strongAuth & SOME_AUTH_REQUIRED_AFTER_USER_REQUEST) != 0) { return KeyguardSecurityView.PROMPT_REASON_USER_REQUEST; - } else if (trustAgentsEnabled - && (strongAuth & SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED) != 0) { - return KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED; } else if (any && ((strongAuth & STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) != 0 || mUpdateMonitor.isFingerprintLockedOut())) { return KeyguardSecurityView.PROMPT_REASON_AFTER_LOCKOUT; @@ -815,6 +820,8 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, } else if (any && (strongAuth & STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT) != 0) { return KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT; + } else if (any && !allowedNonStrongAfterIdleTimeout) { + return KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT; } return KeyguardSecurityView.PROMPT_REASON_NONE; } @@ -830,15 +837,22 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, /** * Animation launch controller for activities that occlude the keyguard. */ - private final ActivityLaunchAnimator.Controller mOccludeAnimationController = + @VisibleForTesting + final ActivityLaunchAnimator.Controller mOccludeAnimationController = new ActivityLaunchAnimator.Controller() { @Override - public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {} + public void onLaunchAnimationStart(boolean isExpandingFullyAbove) { + mOccludeAnimationPlaying = true; + } @Override public void onLaunchAnimationCancelled(@Nullable Boolean newKeyguardOccludedState) { Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: " + mOccluded); + mOccludeAnimationPlaying = false; + + // Ensure keyguard state is set correctly if we're cancelled. + mCentralSurfaces.updateIsKeyguard(); } @Override @@ -847,6 +861,12 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, mCentralSurfaces.instantCollapseNotificationPanel(); } + mOccludeAnimationPlaying = false; + + // Hide the keyguard now that we're done launching the occluding activity over + // it. + mCentralSurfaces.updateIsKeyguard(); + mInteractionJankMonitor.end(CUJ_LOCKSCREEN_OCCLUSION); } @@ -945,8 +965,7 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, } mOccludeByDreamAnimator = ValueAnimator.ofFloat(0f, 1f); - // Use the same duration as for the UNOCCLUDE. - mOccludeByDreamAnimator.setDuration(UNOCCLUDE_ANIMATION_DURATION); + mOccludeByDreamAnimator.setDuration(mDreamOpenAnimationDuration); mOccludeByDreamAnimator.setInterpolator(Interpolators.LINEAR); mOccludeByDreamAnimator.addUpdateListener( animation -> { @@ -987,9 +1006,11 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, @Override public void onAnimationCancelled(boolean isKeyguardOccluded) { - if (mUnoccludeAnimator != null) { - mUnoccludeAnimator.cancel(); - } + mContext.getMainExecutor().execute(() -> { + if (mUnoccludeAnimator != null) { + mUnoccludeAnimator.cancel(); + } + }); setOccluded(isKeyguardOccluded /* isOccluded */, false /* animate */); Log.d(TAG, "Unocclude animation cancelled. Occluded state is now: " @@ -1130,7 +1151,7 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, DreamOverlayStateController dreamOverlayStateController, Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy, Lazy<ActivityLaunchAnimator> activityLaunchAnimator) { - super(context); + mContext = context; mFalsingCollector = falsingCollector; mLockPatternUtils = lockPatternUtils; mBroadcastDispatcher = broadcastDispatcher; @@ -1176,6 +1197,9 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, mPowerButtonY = context.getResources().getDimensionPixelSize( R.dimen.physical_power_button_center_screen_location_y); mWindowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); + + mDreamOpenAnimationDuration = context.getResources().getInteger( + com.android.internal.R.integer.config_dreamOpenAnimationDuration); } public void userActivity() { @@ -1311,18 +1335,7 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, || !mLockPatternUtils.isSecure(currentUser); long timeout = getLockTimeout(KeyguardUpdateMonitor.getCurrentUser()); mLockLater = false; - if (mExitSecureCallback != null) { - if (DEBUG) Log.d(TAG, "pending exit secure callback cancelled"); - try { - mExitSecureCallback.onKeyguardExitResult(false); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); - } - mExitSecureCallback = null; - if (!mExternallyEnabled) { - hideLocked(); - } - } else if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) { + if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) { // If we are going to sleep but the keyguard is showing (and will continue to be // showing, not in the process of going away) then reset its state. Otherwise, let // this fall through and explicitly re-lock the keyguard. @@ -1641,13 +1654,6 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, mExternallyEnabled = enabled; if (!enabled && mShowing) { - if (mExitSecureCallback != null) { - if (DEBUG) Log.d(TAG, "in process of verifyUnlock request, ignoring"); - // we're in the process of handling a request to verify the user - // can get past the keyguard. ignore extraneous requests to disable / re-enable - return; - } - // hiding keyguard that is showing, remember to reshow later if (DEBUG) Log.d(TAG, "remembering to reshow, hiding keyguard, " + "disabling status bar expansion"); @@ -1661,33 +1667,23 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, mNeedToReshowWhenReenabled = false; updateInputRestrictedLocked(); - if (mExitSecureCallback != null) { - if (DEBUG) Log.d(TAG, "onKeyguardExitResult(false), resetting"); + showLocked(null); + + // block until we know the keyguard is done drawing (and post a message + // to unblock us after a timeout, so we don't risk blocking too long + // and causing an ANR). + mWaitingUntilKeyguardVisible = true; + mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING, + KEYGUARD_DONE_DRAWING_TIMEOUT_MS); + if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false"); + while (mWaitingUntilKeyguardVisible) { try { - mExitSecureCallback.onKeyguardExitResult(false); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); - } - mExitSecureCallback = null; - resetStateLocked(); - } else { - showLocked(null); - - // block until we know the keyguard is done drawing (and post a message - // to unblock us after a timeout, so we don't risk blocking too long - // and causing an ANR). - mWaitingUntilKeyguardVisible = true; - mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING, KEYGUARD_DONE_DRAWING_TIMEOUT_MS); - if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false"); - while (mWaitingUntilKeyguardVisible) { - try { - wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } + wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible"); } + if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible"); } } } @@ -1717,13 +1713,6 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, } catch (RemoteException e) { Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); } - } else if (mExitSecureCallback != null) { - // already in progress with someone else - try { - callback.onKeyguardExitResult(false); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); - } } else if (!isSecure()) { // Keyguard is not secure, no need to do anything, and we don't need to reshow @@ -1757,6 +1746,10 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, return mShowing && !mOccluded; } + public boolean isOccludeAnimationPlaying() { + return mOccludeAnimationPlaying; + } + /** * Notify us when the keyguard is occluded by another window */ @@ -1808,7 +1801,6 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, if (mOccluded != isOccluded) { mOccluded = isOccluded; - mUpdateMonitor.setKeyguardOccluded(isOccluded); mKeyguardViewControllerLazy.get().setOccluded(isOccluded, animate && mDeviceInteractive); adjustStatusBarLocked(); @@ -1884,7 +1876,7 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, // if the keyguard is already showing, don't bother. check flags in both files // to account for the hiding animation which results in a delay and discrepancy // between flags - if (mShowing && mKeyguardViewControllerLazy.get().isShowing()) { + if (mShowing && mKeyguardStateController.isShowing()) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); resetStateLocked(); return; @@ -2261,21 +2253,6 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, return; } setPendingLock(false); // user may have authenticated during the screen off animation - if (mExitSecureCallback != null) { - try { - mExitSecureCallback.onKeyguardExitResult(true /* authenciated */); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult()", e); - } - - mExitSecureCallback = null; - - // after successfully exiting securely, no need to reshow - // the keyguard when they've released the lock - mExternallyEnabled = true; - mNeedToReshowWhenReenabled = false; - updateInputRestricted(); - } handleHide(); mUpdateMonitor.clearBiometricRecognizedWhenKeyguardDone(currentUser); @@ -2975,14 +2952,14 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, */ public KeyguardViewController registerCentralSurfaces(CentralSurfaces centralSurfaces, NotificationPanelViewController panelView, - @Nullable PanelExpansionStateManager panelExpansionStateManager, + @Nullable ShadeExpansionStateManager shadeExpansionStateManager, BiometricUnlockController biometricUnlockController, View notificationContainer, KeyguardBypassController bypassController) { mCentralSurfaces = centralSurfaces; mKeyguardViewControllerLazy.get().registerCentralSurfaces( centralSurfaces, panelView, - panelExpansionStateManager, + shadeExpansionStateManager, biometricUnlockController, notificationContainer, bypassController); @@ -3084,7 +3061,6 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, pw.print(" mInputRestricted: "); pw.println(mInputRestricted); pw.print(" mOccluded: "); pw.println(mOccluded); pw.print(" mDelayedShowingSequence: "); pw.println(mDelayedShowingSequence); - pw.print(" mExitSecureCallback: "); pw.println(mExitSecureCallback); pw.print(" mDeviceInteractive: "); pw.println(mDeviceInteractive); pw.print(" mGoingToSleep: "); pw.println(mGoingToSleep); pw.print(" mHiding: "); pw.println(mHiding); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 56f1ac46a875..56a1f1ae936e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -43,6 +43,7 @@ import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; +import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -72,6 +73,7 @@ import dagger.Provides; FalsingModule.class, KeyguardQuickAffordanceModule.class, KeyguardRepositoryModule.class, + StartKeyguardTransitionModule.class, }) public class KeyguardModule { /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 840a4b20a3f0..b186ae0ceec4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -21,6 +21,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.Position import com.android.systemui.dagger.SysUISingleton import com.android.systemui.doze.DozeHost +import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject @@ -85,6 +86,18 @@ interface KeyguardRepository { */ val dozeAmount: Flow<Float> + /** Observable for the [StatusBarState] */ + val statusBarState: Flow<StatusBarState> + + /** + * Returns `true` if the keyguard is showing; `false` otherwise. + * + * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in + * the z-order (which is not really above the system UI window, but rather - the lock-screen + * becomes invisible to reveal the "occluding activity"). + */ + fun isKeyguardShowing(): Boolean + /** Sets whether the bottom area UI should animate the transition out of doze state. */ fun setAnimateDozingTransitions(animate: Boolean) @@ -103,7 +116,7 @@ class KeyguardRepositoryImpl @Inject constructor( statusBarStateController: StatusBarStateController, - keyguardStateController: KeyguardStateController, + private val keyguardStateController: KeyguardStateController, dozeHost: DozeHost, ) : KeyguardRepository { private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) @@ -148,7 +161,11 @@ constructor( } } dozeHost.addCallback(callback) - trySendWithFailureLogging(false, TAG, "initial isDozing: false") + trySendWithFailureLogging( + statusBarStateController.isDozing, + TAG, + "initial isDozing", + ) awaitClose { dozeHost.removeCallback(callback) } } @@ -168,6 +185,28 @@ constructor( awaitClose { statusBarStateController.removeCallback(callback) } } + override fun isKeyguardShowing(): Boolean { + return keyguardStateController.isShowing + } + + override val statusBarState: Flow<StatusBarState> = conflatedCallbackFlow { + val callback = + object : StatusBarStateController.StateListener { + override fun onStateChanged(state: Int) { + trySendWithFailureLogging(statusBarStateIntToObject(state), TAG, "state") + } + } + + statusBarStateController.addCallback(callback) + trySendWithFailureLogging( + statusBarStateIntToObject(statusBarStateController.getState()), + TAG, + "initial state" + ) + + awaitClose { statusBarStateController.removeCallback(callback) } + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } @@ -180,6 +219,15 @@ constructor( _clockPosition.value = Position(x, y) } + private fun statusBarStateIntToObject(value: Int): StatusBarState { + return when (value) { + 0 -> StatusBarState.SHADE + 1 -> StatusBarState.KEYGUARD + 2 -> StatusBarState.SHADE_LOCKED + else -> throw IllegalArgumentException("Invalid StatusBarState value: $value") + } + } + companion object { private const val TAG = "KeyguardRepositoryImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt new file mode 100644 index 000000000000..ab25597b077c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.data.repository + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.annotation.FloatRange +import android.os.Trace +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter + +@SysUISingleton +class KeyguardTransitionRepository @Inject constructor() { + /* + * Each transition between [KeyguardState]s will have an associated Flow. + * In order to collect these events, clients should call [transition]. + */ + private val _transitions = MutableStateFlow(TransitionStep()) + val transitions = _transitions.asStateFlow() + + /* Information about the active transition. */ + private var currentTransitionInfo: TransitionInfo? = null + /* + * When manual control of the transition is requested, a unique [UUID] is used as the handle + * to permit calls to [updateTransition] + */ + private var updateTransitionId: UUID? = null + + /** + * Interactors that require information about changes between [KeyguardState]s will call this to + * register themselves for flowable [TransitionStep]s when that transition occurs. + */ + fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> { + return transitions.filter { step -> step.from == from && step.to == to } + } + + /** + * Begin a transition from one state to another. The [info.from] must match + * [currentTransitionInfo.to], or the request will be denied. This is enforced to avoid + * unplanned transitions. + */ + fun startTransition(info: TransitionInfo): UUID? { + if (currentTransitionInfo != null) { + // Open questions: + // * Queue of transitions? buffer of 1? + // * Are transitions cancellable if a new one is triggered? + // * What validation does this need to do? + Log.wtf(TAG, "Transition still active: $currentTransitionInfo") + return null + } + currentTransitionInfo?.animator?.cancel() + + currentTransitionInfo = info + info.animator?.let { animator -> + // An animator was provided, so use it to run the transition + animator.setFloatValues(0f, 1f) + val updateListener = + object : AnimatorUpdateListener { + override fun onAnimationUpdate(animation: ValueAnimator) { + emitTransition( + info, + (animation.getAnimatedValue() as Float), + TransitionState.RUNNING + ) + } + } + val adapter = + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + Log.i(TAG, "Starting transition: $info") + emitTransition(info, 0f, TransitionState.STARTED) + } + override fun onAnimationCancel(animation: Animator) { + Log.i(TAG, "Cancelling transition: $info") + } + override fun onAnimationEnd(animation: Animator) { + Log.i(TAG, "Ending transition: $info") + emitTransition(info, 1f, TransitionState.FINISHED) + animator.removeListener(this) + animator.removeUpdateListener(updateListener) + } + } + animator.addListener(adapter) + animator.addUpdateListener(updateListener) + animator.start() + return@startTransition null + } + ?: run { + Log.i(TAG, "Starting transition (manual): $info") + emitTransition(info, 0f, TransitionState.STARTED) + + // No animator, so it's manual. Provide a mechanism to callback + updateTransitionId = UUID.randomUUID() + return@startTransition updateTransitionId + } + } + + /** + * Allows manual control of a transition. When calling [startTransition], the consumer must pass + * in a null animator. In return, it will get a unique [UUID] that will be validated to allow + * further updates. + * + * When the transition is over, TransitionState.FINISHED must be passed into the [state] + * parameter. + */ + fun updateTransition( + transitionId: UUID, + @FloatRange(from = 0.0, to = 1.0) value: Float, + state: TransitionState + ) { + if (updateTransitionId != transitionId) { + Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId") + return + } + + if (currentTransitionInfo == null) { + Log.wtf(TAG, "Attempting to update with null 'currentTransitionInfo'") + return + } + + currentTransitionInfo?.let { info -> + if (state == TransitionState.FINISHED) { + updateTransitionId = null + Log.i(TAG, "Ending transition: $info") + } + + emitTransition(info, value, state) + } + } + + private fun emitTransition( + info: TransitionInfo, + value: Float, + transitionState: TransitionState + ) { + trace(info, transitionState) + + if (transitionState == TransitionState.FINISHED) { + currentTransitionInfo = null + } + _transitions.value = TransitionStep(info.from, info.to, value, transitionState) + } + + private fun trace(info: TransitionInfo, transitionState: TransitionState) { + if ( + transitionState != TransitionState.STARTED && + transitionState != TransitionState.FINISHED + ) { + return + } + val traceName = + "Transition: ${info.from} -> ${info.to} " + + if (info.animator == null) { + "(manual)" + } else { + "" + } + val traceCookie = traceName.hashCode() + if (transitionState == TransitionState.STARTED) { + Trace.beginAsyncSection(traceName, traceCookie) + } else if (transitionState == TransitionState.FINISHED) { + Trace.endAsyncSection(traceName, traceCookie) + } + } + + companion object { + private const val TAG = "KeyguardTransitionRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt new file mode 100644 index 000000000000..400376663f1a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class AodLockscreenTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("AOD<->LOCKSCREEN") { + + override fun start() { + scope.launch { + keyguardRepository.isDozing.collect { isDozing -> + if (isDozing) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + getAnimator(), + ) + ) + } else { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 500L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt index 7d4db37c6b0f..2af9318d92ec 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt @@ -273,8 +273,8 @@ constructor( /** Tell the bouncer to start the pre hide animation. */ fun startDisappearAnimation(runnable: Runnable) { val finishRunnable = Runnable { - repository.setStartDisappearAnimation(null) runnable.run() + repository.setStartDisappearAnimation(null) } repository.setStartDisappearAnimation(finishRunnable) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index dccc94178ed5..fc2269c6b01c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.Flow class KeyguardInteractor @Inject constructor( - repository: KeyguardRepository, + private val repository: KeyguardRepository, ) { /** * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at @@ -38,6 +38,10 @@ constructor( val dozeAmount: Flow<Float> = repository.dozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow<Boolean> = repository.isDozing - /** Whether the keyguard is showing ot not. */ + /** Whether the keyguard is showing to not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing + + fun isKeyguardShowing(): Boolean { + return repository.isKeyguardShowing() + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 95acc0b8564e..f663b0dd23cd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -19,7 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import android.content.Intent import com.android.internal.widget.LockPatternUtils -import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition @@ -67,19 +67,19 @@ constructor( * * @param configKey The configuration key corresponding to the [KeyguardQuickAffordanceModel] of * the affordance that was clicked - * @param animationController An optional controller for the activity-launch animation + * @param expandable An optional [Expandable] for the activity- or dialog-launch animation */ fun onQuickAffordanceClicked( configKey: KClass<out KeyguardQuickAffordanceConfig>, - animationController: ActivityLaunchAnimator.Controller?, + expandable: Expandable?, ) { @Suppress("UNCHECKED_CAST") val config = registry.get(configKey as KClass<Nothing>) - when (val result = config.onQuickAffordanceClicked(animationController)) { + when (val result = config.onQuickAffordanceClicked(expandable)) { is KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity -> launchQuickAffordance( intent = result.intent, canShowWhileLocked = result.canShowWhileLocked, - animationController = animationController + expandable = expandable, ) is KeyguardQuickAffordanceConfig.OnClickedResult.Handled -> Unit } @@ -104,7 +104,7 @@ constructor( KeyguardQuickAffordanceModel.Visible( configKey = configs[index]::class, icon = visibleState.icon, - contentDescriptionResourceId = visibleState.contentDescriptionResourceId, + toggle = visibleState.toggle, ) } else { KeyguardQuickAffordanceModel.Hidden @@ -115,7 +115,7 @@ constructor( private fun launchQuickAffordance( intent: Intent, canShowWhileLocked: Boolean, - animationController: ActivityLaunchAnimator.Controller?, + expandable: Expandable?, ) { @LockPatternUtils.StrongAuthTracker.StrongAuthFlags val strongAuthFlags = @@ -131,13 +131,13 @@ constructor( activityStarter.postStartActivityDismissingKeyguard( intent, 0 /* delay */, - animationController + expandable?.activityLaunchController(), ) } else { activityStarter.startActivity( intent, true /* dismissShade */, - animationController, + expandable?.activityLaunchController(), true /* showOverLockscreenWhenLocked */, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt new file mode 100644 index 000000000000..b166681433a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.domain.interactor + +import android.util.Log +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import java.util.Set +import javax.inject.Inject + +@SysUISingleton +class KeyguardTransitionCoreStartable +@Inject +constructor( + private val interactors: Set<TransitionInteractor>, +) : CoreStartable { + + override fun start() { + // By listing the interactors in a when, the compiler will help enforce all classes + // extending the sealed class [TransitionInteractor] will be initialized. + interactors.forEach { + // `when` needs to be an expression in order for the compiler to enforce it being + // exhaustive + val ret = + when (it) { + is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it") + is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") + } + it.start() + } + } + + companion object { + private const val TAG = "KeyguardTransitionCoreStartable" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt new file mode 100644 index 000000000000..7409aec57b4c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionStep +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** Encapsulates business-logic related to the keyguard transitions. */ +@SysUISingleton +class KeyguardTransitionInteractor +@Inject +constructor( + repository: KeyguardTransitionRepository, +) { + /** AOD->LOCKSCREEN transition information. */ + val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN) + + /** LOCKSCREEN->AOD transition information. */ + val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD) + + /** + * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <-> + * Lockscreen (0f). + */ + val dozeAmountTransition: Flow<TransitionStep> = + merge( + aodToLockscreenTransition.map { step -> step.copy(value = 1f - step.value) }, + lockscreenToAodTransition, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt new file mode 100644 index 000000000000..3c2a12e3836a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.util.kotlin.sample +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +@SysUISingleton +class LockscreenBouncerTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val shadeRepository: ShadeRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("LOCKSCREEN<->BOUNCER") { + + private var transitionId: UUID? = null + + override fun start() { + scope.launch { + shadeRepository.shadeModel.sample( + combine( + keyguardTransitionRepository.transitions, + keyguardRepository.statusBarState, + ) { transitions, statusBarState -> + Pair(transitions, statusBarState) + } + ) { shadeModel, pair -> + val (transitions, statusBarState) = pair + + val id = transitionId + if (id != null) { + // An existing `id` means a transition is started, and calls to + // `updateTransition` will control it until FINISHED + keyguardTransitionRepository.updateTransition( + id, + shadeModel.expansionAmount, + if (shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f) { + transitionId = null + TransitionState.FINISHED + } else { + TransitionState.RUNNING + } + ) + } else { + // TODO (b/251849525): Remove statusbarstate check when that state is integrated + // into KeyguardTransitionRepository + val isOnLockscreen = + transitions.transitionState == TransitionState.FINISHED && + transitions.to == KeyguardState.LOCKSCREEN + if ( + isOnLockscreen && + shadeModel.isUserDragging && + statusBarState != SHADE_LOCKED + ) { + transitionId = + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.BOUNCER, + animator = null, + ) + ) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt new file mode 100644 index 000000000000..74c542c0043f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet + +@Module +abstract class StartKeyguardTransitionModule { + + @Binds + @IntoMap + @ClassKey(KeyguardTransitionCoreStartable::class) + abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable + + @Binds + @IntoSet + abstract fun lockscreenBouncer( + impl: LockscreenBouncerTransitionInteractor + ): TransitionInteractor + + @Binds + @IntoSet + abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt new file mode 100644 index 000000000000..a2a46d9e3a71 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor +/** + * Each TransitionInteractor is responsible for determining under which conditions to notify + * [KeyguardTransitionRepository] to signal a transition. When (and if) the transition occurs is + * determined by [KeyguardTransitionRepository]. + * + * [name] field should be a unique identifiable string representing this state, used primarily for + * logging + * + * MUST list implementing classes in dagger module [StartKeyguardTransitionModule] and also in the + * 'when' clause of [KeyguardTransitionCoreStartable] + */ +sealed class TransitionInteractor(val name: String) { + + abstract fun start() +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt index eff146984176..e56b25967910 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt @@ -17,9 +17,9 @@ package com.android.systemui.keyguard.domain.model -import androidx.annotation.StringRes -import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.common.shared.model.Icon import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState import kotlin.reflect.KClass /** @@ -35,11 +35,8 @@ sealed class KeyguardQuickAffordanceModel { /** Identifier for the affordance this is modeling. */ val configKey: KClass<out KeyguardQuickAffordanceConfig>, /** An icon for the affordance. */ - val icon: ContainedDrawable, - /** - * Resource ID for a string to use for the accessibility content description text of the - * affordance. - */ - @StringRes val contentDescriptionResourceId: Int, + val icon: Icon, + /** The toggle state for the affordance. */ + val toggle: KeyguardQuickAffordanceToggleState, ) : KeyguardQuickAffordanceModel() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt index ac2c9b1d7ff2..83842602cbee 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt @@ -20,10 +20,11 @@ package com.android.systemui.keyguard.domain.quickaffordance import android.content.Context import android.content.Intent import androidx.annotation.DrawableRes -import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.dagger.ControlsComponent @@ -60,7 +61,7 @@ constructor( } override fun onQuickAffordanceClicked( - animationController: ActivityLaunchAnimator.Controller?, + expandable: Expandable?, ): KeyguardQuickAffordanceConfig.OnClickedResult { return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity( intent = @@ -122,8 +123,14 @@ constructor( visibility == ControlsComponent.Visibility.AVAILABLE ) { KeyguardQuickAffordanceConfig.State.Visible( - icon = ContainedDrawable.WithResource(iconResourceId), - contentDescriptionResourceId = component.getTileTitleId(), + icon = + Icon.Resource( + res = iconResourceId, + contentDescription = + ContentDescription.Resource( + res = component.getTileTitleId(), + ), + ), ) } else { KeyguardQuickAffordanceConfig.State.Hidden diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt index 8fb952c217bd..95027d00c46c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -18,9 +18,9 @@ package com.android.systemui.keyguard.domain.quickaffordance import android.content.Intent -import androidx.annotation.StringRes -import com.android.systemui.animation.ActivityLaunchAnimator -import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState import kotlinx.coroutines.flow.Flow /** Defines interface that can act as data source for a single quick affordance model. */ @@ -28,9 +28,7 @@ interface KeyguardQuickAffordanceConfig { val state: Flow<State> - fun onQuickAffordanceClicked( - animationController: ActivityLaunchAnimator.Controller? - ): OnClickedResult + fun onQuickAffordanceClicked(expandable: Expandable?): OnClickedResult /** * Encapsulates the state of a "quick affordance" in the keyguard bottom area (for example, a @@ -44,12 +42,10 @@ interface KeyguardQuickAffordanceConfig { /** An affordance is visible. */ data class Visible( /** An icon for the affordance. */ - val icon: ContainedDrawable, - /** - * Resource ID for a string to use for the accessibility content description text of the - * affordance. - */ - @StringRes val contentDescriptionResourceId: Int, + val icon: Icon, + /** The toggle state for the affordance. */ + val toggle: KeyguardQuickAffordanceToggleState = + KeyguardQuickAffordanceToggleState.NotSupported, ) : State() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt index c8e5e4aebea0..502a6070a422 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt @@ -18,10 +18,11 @@ package com.android.systemui.keyguard.domain.quickaffordance import com.android.systemui.R -import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qrcodescanner.controller.QRCodeScannerController import javax.inject.Inject @@ -65,7 +66,7 @@ constructor( } override fun onQuickAffordanceClicked( - animationController: ActivityLaunchAnimator.Controller?, + expandable: Expandable?, ): KeyguardQuickAffordanceConfig.OnClickedResult { return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity( intent = controller.intent, @@ -76,8 +77,14 @@ constructor( private fun state(): KeyguardQuickAffordanceConfig.State { return if (controller.isEnabledForLockScreenButton) { KeyguardQuickAffordanceConfig.State.Visible( - icon = ContainedDrawable.WithResource(R.drawable.ic_qr_code_scanner), - contentDescriptionResourceId = R.string.accessibility_qr_code_scanner_button, + icon = + Icon.Resource( + res = R.drawable.ic_qr_code_scanner, + contentDescription = + ContentDescription.Resource( + res = R.string.accessibility_qr_code_scanner_button, + ), + ), ) } else { KeyguardQuickAffordanceConfig.State.Hidden diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt index 885af3343533..a24a0d62465f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -23,10 +23,11 @@ import android.service.quickaccesswallet.GetWalletCardsResponse import android.service.quickaccesswallet.QuickAccessWalletClient import android.util.Log import com.android.systemui.R -import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.ActivityStarter import com.android.systemui.wallet.controller.QuickAccessWalletController @@ -83,11 +84,11 @@ constructor( } override fun onQuickAffordanceClicked( - animationController: ActivityLaunchAnimator.Controller?, + expandable: Expandable?, ): KeyguardQuickAffordanceConfig.OnClickedResult { walletController.startQuickAccessUiIntent( activityStarter, - animationController, + expandable?.activityLaunchController(), /* hasCard= */ true, ) return KeyguardQuickAffordanceConfig.OnClickedResult.Handled @@ -100,8 +101,14 @@ constructor( ): KeyguardQuickAffordanceConfig.State { return if (isFeatureEnabled && hasCard && tileIcon != null) { KeyguardQuickAffordanceConfig.State.Visible( - icon = ContainedDrawable.WithDrawable(tileIcon), - contentDescriptionResourceId = R.string.accessibility_wallet_button, + icon = + Icon.Loaded( + drawable = tileIcon, + contentDescription = + ContentDescription.Resource( + res = R.string.accessibility_wallet_button, + ), + ), ) } else { KeyguardQuickAffordanceConfig.State.Hidden diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt new file mode 100644 index 000000000000..f66d5d3650c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** List of all possible states to transition to/from */ +enum class KeyguardState { + /** For initialization only */ + NONE, + /* Always-on Display. The device is in a low-power mode with a minimal UI visible */ + AOD, + /* + * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS + * (Fingerprint Sensor) variations, for the user to verify their credentials + */ + BOUNCER, + /* + * Device is actively displaying keyguard UI and is not in low-power mode. Device may be + * unlocked if SWIPE security method is used, or if face lockscreen bypass is false. + */ + LOCKSCREEN, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt new file mode 100644 index 000000000000..bb953477583d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** See [com.android.systemui.statusbar.StatusBarState] for definitions */ +enum class StatusBarState { + SHADE, + KEYGUARD, + SHADE_LOCKED, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt new file mode 100644 index 000000000000..bfccf3fe076c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +import android.animation.ValueAnimator + +/** Tracks who is controlling the current transition, and how to run it. */ +data class TransitionInfo( + val ownerName: String, + val from: KeyguardState, + val to: KeyguardState, + val animator: ValueAnimator?, // 'null' animator signal manual control +) { + override fun toString(): String = + "TransitionInfo(ownerName=$ownerName, from=$from, to=$to, " + + (if (animator != null) { + "animated" + } else { + "manual" + }) + + ")" +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt new file mode 100644 index 000000000000..d8691c17f53d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** Possible states for a running transition between [State] */ +enum class TransitionState { + NONE, + STARTED, + RUNNING, + FINISHED +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt new file mode 100644 index 000000000000..688ec912aac8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */ +data class TransitionStep( + val from: KeyguardState = KeyguardState.NONE, + val to: KeyguardState = KeyguardState.NONE, + val value: Float = 0f, // constrained [0.0, 1.0] + val transitionState: TransitionState = TransitionState.NONE, +) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt new file mode 100644 index 000000000000..55d38a41849d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.shared.quickaffordance + +/** Enumerates all possible toggle states for a quick affordance on the lock-screen. */ +sealed class KeyguardQuickAffordanceToggleState { + /** Toggling is not supported. */ + object NotSupported : KeyguardQuickAffordanceToggleState() + /** The quick affordance is on. */ + object On : KeyguardQuickAffordanceToggleState() + /** The quick affordance is off. */ + object Off : KeyguardQuickAffordanceToggleState() +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt index c4e3d4e4c1b5..2c99ca59ba6b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt @@ -29,9 +29,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.settingslib.Utils import com.android.systemui.R -import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.animation.Interpolators -import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.lifecycle.repeatWhenAttached @@ -236,21 +236,29 @@ object KeyguardBottomAreaViewBinder { } } - when (viewModel.icon) { - is ContainedDrawable.WithDrawable -> view.setImageDrawable(viewModel.icon.drawable) - is ContainedDrawable.WithResource -> view.setImageResource(viewModel.icon.resourceId) - } + IconViewBinder.bind(viewModel.icon, view) + view.isActivated = viewModel.isActivated view.drawable.setTint( Utils.getColorAttrDefaultColor( view.context, - com.android.internal.R.attr.textColorPrimary + if (viewModel.isActivated) { + com.android.internal.R.attr.textColorPrimaryInverse + } else { + com.android.internal.R.attr.textColorPrimary + }, ) ) view.backgroundTintList = - Utils.getColorAttr(view.context, com.android.internal.R.attr.colorSurface) + Utils.getColorAttr( + view.context, + if (viewModel.isActivated) { + com.android.internal.R.attr.colorAccentPrimary + } else { + com.android.internal.R.attr.colorSurface + } + ) - view.contentDescription = view.context.getString(viewModel.contentDescriptionResourceId) view.isClickable = viewModel.isClickable if (viewModel.isClickable) { view.setOnClickListener(OnClickListener(viewModel, falsingManager)) @@ -272,7 +280,7 @@ object KeyguardBottomAreaViewBinder { viewModel.onClicked( KeyguardQuickAffordanceViewModel.OnClickedParameters( configKey = viewModel.configKey, - animationController = ActivityLaunchAnimator.Controller.fromView(view), + expandable = Expandable.fromView(view), ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt index e3ebac60febb..535ca7210244 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt @@ -23,6 +23,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -116,14 +117,14 @@ constructor( isVisible = true, animateReveal = animateReveal, icon = icon, - contentDescriptionResourceId = contentDescriptionResourceId, onClicked = { parameters -> quickAffordanceInteractor.onQuickAffordanceClicked( configKey = parameters.configKey, - animationController = parameters.animationController, + expandable = parameters.expandable, ) }, isClickable = isClickable, + isActivated = toggle is KeyguardQuickAffordanceToggleState.On, ) is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt index b1de27d262cf..bf598ba85932 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt @@ -16,9 +16,8 @@ package com.android.systemui.keyguard.ui.viewmodel -import androidx.annotation.StringRes -import com.android.systemui.animation.ActivityLaunchAnimator -import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.Icon import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig import kotlin.reflect.KClass @@ -28,13 +27,13 @@ data class KeyguardQuickAffordanceViewModel( val isVisible: Boolean = false, /** Whether to animate the transition of the quick affordance from invisible to visible. */ val animateReveal: Boolean = false, - val icon: ContainedDrawable = ContainedDrawable.WithResource(0), - @StringRes val contentDescriptionResourceId: Int = 0, + val icon: Icon = Icon.Resource(res = 0, contentDescription = null), val onClicked: (OnClickedParameters) -> Unit = {}, val isClickable: Boolean = false, + val isActivated: Boolean = false, ) { data class OnClickedParameters( val configKey: KClass<out KeyguardQuickAffordanceConfig>, - val animationController: ActivityLaunchAnimator.Controller?, + val expandable: Expandable?, ) } diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt index 5651399cb891..f9e341c8629a 100644 --- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt @@ -19,6 +19,9 @@ package com.android.systemui.log import android.app.ActivityManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogcatEchoTracker + import javax.inject.Inject @SysUISingleton @@ -26,7 +29,7 @@ class LogBufferFactory @Inject constructor( private val dumpManager: DumpManager, private val logcatEchoTracker: LogcatEchoTracker ) { - /* limit the size of maxPoolSize for low ram (Go) devices */ + /* limitiometricMessageDeferralLogger the size of maxPoolSize for low ram (Go) devices */ private fun adjustMaxSize(requestedMaxSize: Int): Int { return if (ActivityManager.isLowRamDeviceStatic()) { minOf(requestedMaxSize, 20) /* low ram max log size*/ diff --git a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java index 8f9357abbba8..c7e4c5e93090 100644 --- a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java +++ b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java @@ -21,7 +21,6 @@ import static android.app.StatusBarManager.SESSION_BIOMETRIC_PROMPT; import static android.app.StatusBarManager.SESSION_KEYGUARD; import android.annotation.Nullable; -import android.content.Context; import android.os.RemoteException; import android.util.Log; @@ -48,7 +47,7 @@ import javax.inject.Inject; * session. Can be used across processes via StatusBarManagerService#registerSessionListener */ @SysUISingleton -public class SessionTracker extends CoreStartable { +public class SessionTracker implements CoreStartable { private static final String TAG = "SessionTracker"; private static final boolean DEBUG = false; @@ -65,13 +64,11 @@ public class SessionTracker extends CoreStartable { @Inject public SessionTracker( - Context context, IStatusBarService statusBarService, AuthController authController, KeyguardUpdateMonitor keyguardUpdateMonitor, KeyguardStateController keyguardStateController ) { - super(context); mStatusBarManagerService = statusBarService; mAuthController = authController; mKeyguardUpdateMonitor = keyguardUpdateMonitor; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java index 7f1ad6d20c16..eeadf406060d 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java @@ -23,7 +23,7 @@ import java.lang.annotation.RetentionPolicy; import javax.inject.Qualifier; /** - * A {@link com.android.systemui.log.LogBuffer} for BiometricMessages processing such as + * A {@link com.android.systemui.plugins.log.LogBuffer} for BiometricMessages processing such as * {@link com.android.systemui.biometrics.FaceHelpMessageDeferral} */ @Qualifier diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java index 7d1f1c2709fa..5cca1ab2abe7 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java index 9ca0293fbd86..1d016d837b02 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java index 7c5f4025117f..c9f78bcdeef8 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt new file mode 100644 index 000000000000..0645236226bd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.dagger + +import javax.inject.Qualifier + +/** A [com.android.systemui.plugins.log.LogBuffer] for keyguard clock logs. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyguardClockLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt new file mode 100644 index 000000000000..aef3471ea8ad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt @@ -0,0 +1,10 @@ +package com.android.systemui.log.dagger + +import javax.inject.Qualifier + +/** + * A [com.android.systemui.log.LogBuffer] for keyguard-related stuff. Should be used mostly for + * adding temporary logs or logging from smaller classes when creating new separate log class might + * be an overkill. + */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class KeyguardLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java index 08d969b5eb77..76d20bea4bdf 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 29e2c1cd8900..9adb855d66ac 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -22,11 +22,11 @@ import android.os.Looper; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.log.LogBuffer; import com.android.systemui.log.LogBufferFactory; -import com.android.systemui.log.LogcatEchoTracker; -import com.android.systemui.log.LogcatEchoTrackerDebug; -import com.android.systemui.log.LogcatEchoTrackerProd; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogcatEchoTracker; +import com.android.systemui.plugins.log.LogcatEchoTrackerDebug; +import com.android.systemui.plugins.log.LogcatEchoTrackerProd; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.util.Compile; @@ -43,7 +43,7 @@ public class LogModule { @SysUISingleton @DozeLog public static LogBuffer provideDozeLogBuffer(LogBufferFactory factory) { - return factory.create("DozeLog", 100); + return factory.create("DozeLog", 150); } /** Provides a logging buffer for all logs related to the data layer of notifications. */ @@ -250,7 +250,7 @@ public class LogModule { /** * Provides a buffer for our connections and disconnections to MediaBrowserService. * - * See {@link com.android.systemui.media.ResumeMediaBrowser}. + * See {@link com.android.systemui.media.controls.resume.ResumeMediaBrowser}. */ @Provides @SysUISingleton @@ -262,7 +262,7 @@ public class LogModule { /** * Provides a buffer for updates to the media carousel. * - * See {@link com.android.systemui.media.MediaCarouselController}. + * See {@link com.android.systemui.media.controls.ui.MediaCarouselController}. */ @Provides @SysUISingleton @@ -316,13 +316,23 @@ public class LogModule { } /** + * Provides a {@link LogBuffer} for keyguard clock logs. + */ + @Provides + @SysUISingleton + @KeyguardClockLog + public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) { + return factory.create("KeyguardClockLog", 500); + } + + /** * Provides a {@link LogBuffer} for use by {@link com.android.keyguard.KeyguardUpdateMonitor}. */ @Provides @SysUISingleton @KeyguardUpdateMonitorLog public static LogBuffer provideKeyguardUpdateMonitorLogBuffer(LogBufferFactory factory) { - return factory.create("KeyguardUpdateMonitorLog", 200); + return factory.create("KeyguardUpdateMonitorLog", 400); } /** @@ -334,4 +344,24 @@ public class LogModule { public static LogBuffer providerBluetoothLogBuffer(LogBufferFactory factory) { return factory.create("BluetoothLog", 50); } + + /** + * Provides a {@link LogBuffer} for general keyguard-related logs. + */ + @Provides + @SysUISingleton + @KeyguardLog + public static LogBuffer provideKeyguardLogBuffer(LogBufferFactory factory) { + return factory.create("KeyguardLog", 250); + } + + /** + * Provides a {@link LogBuffer} for Udfps logs. + */ + @Provides + @SysUISingleton + @UdfpsLog + public static LogBuffer provideUdfpsLogBuffer(LogBufferFactory factory) { + return factory.create("UdfpsLog", 1000); + } } diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java index 1d7ba94af4ed..af433476b38c 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -26,7 +26,7 @@ import java.lang.annotation.Retention; import javax.inject.Qualifier; /** - * A {@link LogBuffer} for {@link com.android.systemui.media.ResumeMediaBrowser} + * A {@link LogBuffer} for {@link com.android.systemui.media.controls.resume.ResumeMediaBrowser} */ @Qualifier @Documented diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java index b03655a543f7..f4dac6efe371 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -26,7 +26,7 @@ import java.lang.annotation.Retention; import javax.inject.Qualifier; /** - * A {@link LogBuffer} for {@link com.android.systemui.media.MediaCarouselController} + * A {@link LogBuffer} for {@link com.android.systemui.media.controls.ui.MediaCarouselController} */ @Qualifier @Documented diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java index c67d8bebe313..73690ab6c24d 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java index 53963fc8d431..0c2cd92d1bb0 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -26,7 +26,7 @@ import java.lang.annotation.Retention; import javax.inject.Qualifier; /** - * A {@link LogBuffer} for {@link com.android.systemui.media.MediaTimeoutLogger} + * A {@link LogBuffer} for {@link com.android.systemui.media.controls.pipeline.MediaTimeoutLogger} */ @Qualifier @Documented diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java index 5c572e8ef554..1570d434bc62 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java index edab8c319f87..bf216c6991d2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java index 75a34fc22c3c..5b7f4bb103b4 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -26,7 +26,7 @@ import java.lang.annotation.Retention; import javax.inject.Qualifier; /** - * A {@link LogBuffer} for {@link com.android.systemui.media.MediaViewLogger} + * A {@link LogBuffer} for {@link com.android.systemui.media.controls.ui.MediaViewLogger} */ @Qualifier @Documented diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java index b1c6dcfcb13b..6d91f0c97c8a 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java index 20fc6ff445a6..26af4964f7b8 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java index fcc184a317b8..61daf9c8d71c 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java index 760fbf3928b6..a59afa0fed1b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java index a0b686487bec..6f8ea7ff2e9b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java index 8c8753a07339..835d3490293c 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java index 7259eebf19b6..6e2bd7b2e1b5 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java index e96e532f94bf..77b1bf5fd630 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java index 557a254e5c09..9fd166b759d2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java index dd5010cf39a8..dd168bac5654 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java index bd0d298ebdee..d24bfcb88188 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java index b237f2d74483..67cdb722055b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java index f26b3164f488..af0f7c518e64 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java index dd6837563a74..4c276e2bfaab 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java index 8671dbfdf1fe..ba8b27c23ec1 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java @@ -18,7 +18,7 @@ package com.android.systemui.log.dagger; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/UdfpsLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/UdfpsLog.java new file mode 100644 index 000000000000..14000e183c10 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/UdfpsLog.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.dagger; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface UdfpsLog { +} diff --git a/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt deleted file mode 100644 index 556560c3534c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media - -import android.animation.ArgbEvaluator -import android.animation.ValueAnimator -import android.animation.ValueAnimator.AnimatorUpdateListener -import android.content.Context -import android.content.res.ColorStateList -import android.content.res.Configuration -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import android.graphics.drawable.RippleDrawable -import com.android.internal.R -import com.android.internal.annotations.VisibleForTesting -import com.android.settingslib.Utils -import com.android.systemui.monet.ColorScheme - -/** - * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme] - * is triggered. - */ -interface ColorTransition { - fun updateColorScheme(scheme: ColorScheme?): Boolean -} - -/** - * A [ColorTransition] that animates between two specific colors. - * It uses a ValueAnimator to execute the animation and interpolate between the source color and - * the target color. - * - * Selection of the target color from the scheme, and application of the interpolated color - * are delegated to callbacks. - */ -open class AnimatingColorTransition( - private val defaultColor: Int, - private val extractColor: (ColorScheme) -> Int, - private val applyColor: (Int) -> Unit -) : AnimatorUpdateListener, ColorTransition { - - private val argbEvaluator = ArgbEvaluator() - private val valueAnimator = buildAnimator() - var sourceColor: Int = defaultColor - var currentColor: Int = defaultColor - var targetColor: Int = defaultColor - - override fun onAnimationUpdate(animation: ValueAnimator) { - currentColor = argbEvaluator.evaluate( - animation.animatedFraction, sourceColor, targetColor - ) as Int - applyColor(currentColor) - } - - override fun updateColorScheme(scheme: ColorScheme?): Boolean { - val newTargetColor = if (scheme == null) defaultColor else extractColor(scheme) - if (newTargetColor != targetColor) { - sourceColor = currentColor - targetColor = newTargetColor - valueAnimator.cancel() - valueAnimator.start() - return true - } - return false - } - - init { - applyColor(defaultColor) - } - - @VisibleForTesting - open fun buildAnimator(): ValueAnimator { - val animator = ValueAnimator.ofFloat(0f, 1f) - animator.duration = 333 - animator.addUpdateListener(this) - return animator - } -} - -typealias AnimatingColorTransitionFactory = - (Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition - -/** - * ColorSchemeTransition constructs a ColorTransition for each color in the scheme - * that needs to be transitioned when changed. It also sets up the assignment functions for sending - * the sending the interpolated colors to the appropriate views. - */ -class ColorSchemeTransition internal constructor( - private val context: Context, - private val mediaViewHolder: MediaViewHolder, - animatingColorTransitionFactory: AnimatingColorTransitionFactory -) { - constructor(context: Context, mediaViewHolder: MediaViewHolder) : - this(context, mediaViewHolder, ::AnimatingColorTransition) - - val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95) - val surfaceColor = animatingColorTransitionFactory( - bgColor, - ::surfaceFromScheme - ) { surfaceColor -> - val colorList = ColorStateList.valueOf(surfaceColor) - mediaViewHolder.seamlessIcon.imageTintList = colorList - mediaViewHolder.seamlessText.setTextColor(surfaceColor) - mediaViewHolder.albumView.backgroundTintList = colorList - mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor) - } - - val accentPrimary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), - ::accentPrimaryFromScheme - ) { accentPrimary -> - val accentColorList = ColorStateList.valueOf(accentPrimary) - mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList - mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary) - } - - val accentSecondary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), - ::accentSecondaryFromScheme - ) { accentSecondary -> - val colorList = ColorStateList.valueOf(accentSecondary) - (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let { - it.setColor(colorList) - it.effectColor = colorList - } - } - - val colorSeamless = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), - { colorScheme: ColorScheme -> - // A1-100 dark in dark theme, A1-200 in light theme - if (context.resources.configuration.uiMode and - Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES) - colorScheme.accent1[2] - else colorScheme.accent1[3] - }, { seamlessColor: Int -> - val accentColorList = ColorStateList.valueOf(seamlessColor) - mediaViewHolder.seamlessButton.backgroundTintList = accentColorList - }) - - val textPrimary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), - ::textPrimaryFromScheme - ) { textPrimary -> - mediaViewHolder.titleText.setTextColor(textPrimary) - val textColorList = ColorStateList.valueOf(textPrimary) - mediaViewHolder.seekBar.thumb.setTintList(textColorList) - mediaViewHolder.seekBar.progressTintList = textColorList - mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList) - mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList) - for (button in mediaViewHolder.getTransparentActionButtons()) { - button.imageTintList = textColorList - } - mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary) - } - - val textPrimaryInverse = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimaryInverse), - ::textPrimaryInverseFromScheme - ) { textPrimaryInverse -> - mediaViewHolder.actionPlayPause.imageTintList = ColorStateList.valueOf(textPrimaryInverse) - } - - val textSecondary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorSecondary), - ::textSecondaryFromScheme - ) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) } - - val textTertiary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorTertiary), - ::textTertiaryFromScheme - ) { textTertiary -> - mediaViewHolder.seekBar.progressBackgroundTintList = ColorStateList.valueOf(textTertiary) - } - - val colorTransitions = arrayOf( - surfaceColor, - colorSeamless, - accentPrimary, - accentSecondary, - textPrimary, - textPrimaryInverse, - textSecondary, - textTertiary, - ) - - private fun loadDefaultColor(id: Int): Int { - return Utils.getColorAttr(context, id).defaultColor - } - - fun updateColorScheme(colorScheme: ColorScheme?): Boolean { - var anyChanged = false - colorTransitions.forEach { anyChanged = it.updateColorScheme(colorScheme) || anyChanged } - colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme } - return anyChanged - } -} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt deleted file mode 100644 index 8368792b8ae3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ /dev/null @@ -1,1121 +0,0 @@ -package com.android.systemui.media - -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.res.ColorStateList -import android.content.res.Configuration -import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS -import android.util.Log -import android.util.MathUtils -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import androidx.annotation.VisibleForTesting -import com.android.internal.logging.InstanceId -import com.android.systemui.Dumpable -import com.android.systemui.R -import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager -import com.android.systemui.media.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.qs.PageIndicator -import com.android.systemui.shared.system.SysUiStatsLog -import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener -import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider -import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.util.Utils -import com.android.systemui.util.animation.UniqueObjectHostView -import com.android.systemui.util.animation.requiresRemeasuring -import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.util.time.SystemClock -import com.android.systemui.util.traceSection -import java.io.PrintWriter -import java.util.TreeMap -import javax.inject.Inject -import javax.inject.Provider - -private const val TAG = "MediaCarouselController" -private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS) -private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) - -/** - * Class that is responsible for keeping the view carousel up to date. - * This also handles changes in state and applies them to the media carousel like the expansion. - */ -@SysUISingleton -class MediaCarouselController @Inject constructor( - private val context: Context, - private val mediaControlPanelFactory: Provider<MediaControlPanel>, - private val visualStabilityProvider: VisualStabilityProvider, - private val mediaHostStatesManager: MediaHostStatesManager, - private val activityStarter: ActivityStarter, - private val systemClock: SystemClock, - @Main executor: DelayableExecutor, - private val mediaManager: MediaDataManager, - configurationController: ConfigurationController, - falsingCollector: FalsingCollector, - falsingManager: FalsingManager, - dumpManager: DumpManager, - private val logger: MediaUiEventLogger, - private val debugLogger: MediaCarouselControllerLogger -) : Dumpable { - /** - * The current width of the carousel - */ - private var currentCarouselWidth: Int = 0 - - /** - * The current height of the carousel - */ - private var currentCarouselHeight: Int = 0 - - /** - * Are we currently showing only active players - */ - private var currentlyShowingOnlyActive: Boolean = false - - /** - * Is the player currently visible (at the end of the transformation - */ - private var playersVisible: Boolean = false - /** - * The desired location where we'll be at the end of the transformation. Usually this matches - * the end location, except when we're still waiting on a state update call. - */ - @MediaLocation - private var desiredLocation: Int = -1 - - /** - * The ending location of the view where it ends when all animations and transitions have - * finished - */ - @MediaLocation - private var currentEndLocation: Int = -1 - - /** - * The ending location of the view where it ends when all animations and transitions have - * finished - */ - @MediaLocation - private var currentStartLocation: Int = -1 - - /** - * The progress of the transition or 1.0 if there is no transition happening - */ - private var currentTransitionProgress: Float = 1.0f - - /** - * The measured width of the carousel - */ - private var carouselMeasureWidth: Int = 0 - - /** - * The measured height of the carousel - */ - private var carouselMeasureHeight: Int = 0 - private var desiredHostState: MediaHostState? = null - private val mediaCarousel: MediaScrollView - val mediaCarouselScrollHandler: MediaCarouselScrollHandler - val mediaFrame: ViewGroup - @VisibleForTesting - lateinit var settingsButton: View - private set - private val mediaContent: ViewGroup - private val pageIndicator: PageIndicator - private val visualStabilityCallback: OnReorderingAllowedListener - private var needsReordering: Boolean = false - private var keysNeedRemoval = mutableSetOf<String>() - var shouldScrollToActivePlayer: Boolean = false - private var isRtl: Boolean = false - set(value) { - if (value != field) { - field = value - mediaFrame.layoutDirection = - if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR - mediaCarouselScrollHandler.scrollToStart() - } - } - private var currentlyExpanded = true - set(value) { - if (field != value) { - field = value - for (player in MediaPlayerData.players()) { - player.setListening(field) - } - } - } - private val configListener = object : ConfigurationController.ConfigurationListener { - override fun onDensityOrFontScaleChanged() { - // System font changes should only happen when UMO is offscreen or a flicker may occur - updatePlayers(recreateMedia = true) - inflateSettingsButton() - } - - override fun onThemeChanged() { - updatePlayers(recreateMedia = false) - inflateSettingsButton() - } - - override fun onConfigChanged(newConfig: Configuration?) { - if (newConfig == null) return - isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL - } - - override fun onUiModeChanged() { - updatePlayers(recreateMedia = false) - inflateSettingsButton() - } - } - - /** - * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility. - * It will be called when the container is out of view. - */ - lateinit var updateUserVisibility: () -> Unit - lateinit var updateHostVisibility: () -> Unit - - private val isReorderingAllowed: Boolean - get() = visualStabilityProvider.isReorderingAllowed - - init { - dumpManager.registerDumpable(TAG, this) - mediaFrame = inflateMediaCarousel() - mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) - pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) - mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator, - executor, this::onSwipeToDismiss, this::updatePageIndicatorLocation, - this::closeGuts, falsingCollector, falsingManager, this::logSmartspaceImpression, - logger) - isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL - inflateSettingsButton() - mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) - configurationController.addCallback(configListener) - visualStabilityCallback = OnReorderingAllowedListener { - if (needsReordering) { - needsReordering = false - reorderAllPlayers(previousVisiblePlayerKey = null) - } - - keysNeedRemoval.forEach { - removePlayer(it) - } - if (keysNeedRemoval.size > 0) { - // Carousel visibility may need to be updated after late removals - updateHostVisibility() - } - keysNeedRemoval.clear() - - // Update user visibility so that no extra impression will be logged when - // activeMediaIndex resets to 0 - if (this::updateUserVisibility.isInitialized) { - updateUserVisibility() - } - - // Let's reset our scroll position - mediaCarouselScrollHandler.scrollToStart() - } - visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback) - mediaManager.addListener(object : MediaDataManager.Listener { - override fun onMediaDataLoaded( - key: String, - oldKey: String?, - data: MediaData, - immediately: Boolean, - receivedSmartspaceCardLatency: Int, - isSsReactivated: Boolean - ) { - debugLogger.logMediaLoaded(key) - if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) { - // Log card received if a new resumable media card is added - MediaPlayerData.getMediaPlayer(key)?.let { - /* ktlint-disable max-line-length */ - logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED - it.mSmartspaceId, - it.mUid, - surfaces = intArrayOf( - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY), - rank = MediaPlayerData.getMediaPlayerIndex(key)) - /* ktlint-disable max-line-length */ - } - if (mediaCarouselScrollHandler.visibleToUser && - mediaCarouselScrollHandler.visibleMediaIndex - == MediaPlayerData.getMediaPlayerIndex(key)) { - logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) - } - } else if (receivedSmartspaceCardLatency != 0) { - // Log resume card received if resumable media card is reactivated and - // resume card is ranked first - MediaPlayerData.players().forEachIndexed { index, it -> - if (it.recommendationViewHolder == null) { - it.mSmartspaceId = SmallHash.hash(it.mUid + - systemClock.currentTimeMillis().toInt()) - it.mIsImpressed = false - /* ktlint-disable max-line-length */ - logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED - it.mSmartspaceId, - it.mUid, - surfaces = intArrayOf( - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY), - rank = index, - receivedLatencyMillis = receivedSmartspaceCardLatency) - /* ktlint-disable max-line-length */ - } - } - // If media container area already visible to the user, log impression for - // reactivated card. - if (mediaCarouselScrollHandler.visibleToUser && - !mediaCarouselScrollHandler.qsExpanded) { - logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) - } - } - - val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active - if (canRemove && !Utils.useMediaResumption(context)) { - // This view isn't playing, let's remove this! This happens e.g when - // dismissing/timing out a view. We still have the data around because - // resumption could be on, but we should save the resources and release this. - if (isReorderingAllowed) { - onMediaDataRemoved(key) - } else { - keysNeedRemoval.add(key) - } - } else { - keysNeedRemoval.remove(key) - } - } - - override fun onSmartspaceMediaDataLoaded( - key: String, - data: SmartspaceMediaData, - shouldPrioritize: Boolean - ) { - debugLogger.logRecommendationLoaded(key) - // Log the case where the hidden media carousel with the existed inactive resume - // media is shown by the Smartspace signal. - if (data.isActive) { - val hasActivatedExistedResumeMedia = - !mediaManager.hasActiveMedia() && - mediaManager.hasAnyMedia() && - shouldPrioritize - if (hasActivatedExistedResumeMedia) { - // Log resume card received if resumable media card is reactivated and - // recommendation card is valid and ranked first - MediaPlayerData.players().forEachIndexed { index, it -> - if (it.recommendationViewHolder == null) { - it.mSmartspaceId = SmallHash.hash(it.mUid + - systemClock.currentTimeMillis().toInt()) - it.mIsImpressed = false - /* ktlint-disable max-line-length */ - logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED - it.mSmartspaceId, - it.mUid, - surfaces = intArrayOf( - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY), - rank = index, - receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt()) - /* ktlint-disable max-line-length */ - } - } - } - addSmartspaceMediaRecommendations(key, data, shouldPrioritize) - MediaPlayerData.getMediaPlayer(key)?.let { - /* ktlint-disable max-line-length */ - logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED - it.mSmartspaceId, - it.mUid, - surfaces = intArrayOf( - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY), - rank = MediaPlayerData.getMediaPlayerIndex(key), - receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt()) - /* ktlint-disable max-line-length */ - } - if (mediaCarouselScrollHandler.visibleToUser && - mediaCarouselScrollHandler.visibleMediaIndex - == MediaPlayerData.getMediaPlayerIndex(key)) { - logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) - } - } else { - onSmartspaceMediaDataRemoved(data.targetId, immediately = true) - } - } - - override fun onMediaDataRemoved(key: String) { - debugLogger.logMediaRemoved(key) - removePlayer(key) - } - - override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { - debugLogger.logRecommendationRemoved(key, immediately) - if (immediately || isReorderingAllowed) { - removePlayer(key) - if (!immediately) { - // Although it wasn't requested, we were able to process the removal - // immediately since reordering is allowed. So, notify hosts to update - if (this@MediaCarouselController::updateHostVisibility.isInitialized) { - updateHostVisibility() - } - } - } else { - keysNeedRemoval.add(key) - } - } - }) - mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - // The pageIndicator is not laid out yet when we get the current state update, - // Lets make sure we have the right dimensions - updatePageIndicatorLocation() - } - mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback { - override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) { - if (location == desiredLocation) { - onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false) - } - } - }) - } - - private fun inflateSettingsButton() { - val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button, - mediaFrame, false) as View - if (this::settingsButton.isInitialized) { - mediaFrame.removeView(settingsButton) - } - settingsButton = settings - mediaFrame.addView(settingsButton) - mediaCarouselScrollHandler.onSettingsButtonUpdated(settings) - settingsButton.setOnClickListener { - logger.logCarouselSettings() - activityStarter.startActivity(settingsIntent, true /* dismissShade */) - } - } - - private fun inflateMediaCarousel(): ViewGroup { - val mediaCarousel = LayoutInflater.from(context).inflate(R.layout.media_carousel, - UniqueObjectHostView(context), false) as ViewGroup - // Because this is inflated when not attached to the true view hierarchy, it resolves some - // potential issues to force that the layout direction is defined by the locale - // (rather than inherited from the parent, which would resolve to LTR when unattached). - mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE - return mediaCarousel - } - - private fun reorderAllPlayers(previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?) { - mediaContent.removeAllViews() - for (mediaPlayer in MediaPlayerData.players()) { - mediaPlayer.mediaViewHolder?.let { - mediaContent.addView(it.player) - } ?: mediaPlayer.recommendationViewHolder?.let { - mediaContent.addView(it.recommendations) - } - } - mediaCarouselScrollHandler.onPlayersChanged() - - // Automatically scroll to the active player if needed - if (shouldScrollToActivePlayer) { - shouldScrollToActivePlayer = false - val activeMediaIndex = MediaPlayerData.firstActiveMediaIndex() - if (activeMediaIndex != -1) { - previousVisiblePlayerKey?.let { - val previousVisibleIndex = MediaPlayerData.playerKeys() - .indexOfFirst { key -> it == key } - mediaCarouselScrollHandler - .scrollToPlayer(previousVisibleIndex, activeMediaIndex) - } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = activeMediaIndex) - } - } - } - - // Returns true if new player is added - private fun addOrUpdatePlayer( - key: String, - oldKey: String?, - data: MediaData, - isSsReactivated: Boolean - ): Boolean = traceSection("MediaCarouselController#addOrUpdatePlayer") { - MediaPlayerData.moveIfExists(oldKey, key) - val existingPlayer = MediaPlayerData.getMediaPlayer(key) - val curVisibleMediaKey = MediaPlayerData.playerKeys() - .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) - val isCurVisibleMediaPlaying = MediaPlayerData.getMediaData(curVisibleMediaKey)?.isPlaying - if (existingPlayer == null) { - val newPlayer = mediaControlPanelFactory.get() - newPlayer.attachPlayer(MediaViewHolder.create( - LayoutInflater.from(context), mediaContent)) - newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions - val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT) - newPlayer.mediaViewHolder?.player?.setLayoutParams(lp) - newPlayer.bindPlayer(data, key) - newPlayer.setListening(currentlyExpanded) - MediaPlayerData.addMediaPlayer( - key, data, newPlayer, systemClock, isSsReactivated, debugLogger - ) - updatePlayerToState(newPlayer, noAnimation = true) - if (data.active) { - reorderAllPlayers(curVisibleMediaKey) - } else { - needsReordering = true - } - } else { - existingPlayer.bindPlayer(data, key) - MediaPlayerData.addMediaPlayer( - key, data, existingPlayer, systemClock, isSsReactivated, debugLogger - ) - // Check the playing status of both current visible and new media players - // To make sure we scroll to the active playing media card. - if (isReorderingAllowed || - shouldScrollToActivePlayer && - data.isPlaying == true && - isCurVisibleMediaPlaying == false - ) { - reorderAllPlayers(curVisibleMediaKey) - } else { - needsReordering = true - } - } - updatePageIndicator() - mediaCarouselScrollHandler.onPlayersChanged() - mediaFrame.requiresRemeasuring = true - // Check postcondition: mediaContent should have the same number of children as there are - // elements in mediaPlayers. - if (MediaPlayerData.players().size != mediaContent.childCount) { - Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") - } - return existingPlayer == null - } - - private fun addSmartspaceMediaRecommendations( - key: String, - data: SmartspaceMediaData, - shouldPrioritize: Boolean - ) = traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") { - if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel") - if (MediaPlayerData.getMediaPlayer(key) != null) { - Log.w(TAG, "Skip adding smartspace target in carousel") - return - } - - val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey() - existingSmartspaceMediaKey?.let { - val removedPlayer = MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey) - removedPlayer?.run { debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) } - } - - val newRecs = mediaControlPanelFactory.get() - newRecs.attachRecommendation( - RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)) - newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions - val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT) - newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp) - newRecs.bindRecommendation(data) - val curVisibleMediaKey = MediaPlayerData.playerKeys() - .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) - MediaPlayerData.addMediaRecommendation( - key, data, newRecs, shouldPrioritize, systemClock, debugLogger - ) - updatePlayerToState(newRecs, noAnimation = true) - reorderAllPlayers(curVisibleMediaKey) - updatePageIndicator() - mediaFrame.requiresRemeasuring = true - // Check postcondition: mediaContent should have the same number of children as there are - // elements in mediaPlayers. - if (MediaPlayerData.players().size != mediaContent.childCount) { - Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") - } - } - - fun removePlayer( - key: String, - dismissMediaData: Boolean = true, - dismissRecommendation: Boolean = true - ) { - if (key == MediaPlayerData.smartspaceMediaKey()) { - MediaPlayerData.smartspaceMediaData?.let { - logger.logRecommendationRemoved(it.packageName, it.instanceId) - } - } - val removed = MediaPlayerData.removeMediaPlayer(key) - removed?.apply { - mediaCarouselScrollHandler.onPrePlayerRemoved(removed) - mediaContent.removeView(removed.mediaViewHolder?.player) - mediaContent.removeView(removed.recommendationViewHolder?.recommendations) - removed.onDestroy() - mediaCarouselScrollHandler.onPlayersChanged() - updatePageIndicator() - - if (dismissMediaData) { - // Inform the media manager of a potentially late dismissal - mediaManager.dismissMediaData(key, delay = 0L) - } - if (dismissRecommendation) { - // Inform the media manager of a potentially late dismissal - mediaManager.dismissSmartspaceRecommendation(key, delay = 0L) - } - } - } - - private fun updatePlayers(recreateMedia: Boolean) { - pageIndicator.tintList = ColorStateList.valueOf( - context.getColor(R.color.media_paging_indicator) - ) - - MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) -> - if (isSsMediaRec) { - val smartspaceMediaData = MediaPlayerData.smartspaceMediaData - removePlayer(key, dismissMediaData = false, dismissRecommendation = false) - smartspaceMediaData?.let { - addSmartspaceMediaRecommendations( - it.targetId, it, MediaPlayerData.shouldPrioritizeSs) - } - } else { - val isSsReactivated = MediaPlayerData.isSsReactivated(key) - if (recreateMedia) { - removePlayer(key, dismissMediaData = false, dismissRecommendation = false) - } - addOrUpdatePlayer( - key = key, oldKey = null, data = data, isSsReactivated = isSsReactivated) - } - } - } - - private fun updatePageIndicator() { - val numPages = mediaContent.getChildCount() - pageIndicator.setNumPages(numPages) - if (numPages == 1) { - pageIndicator.setLocation(0f) - } - updatePageIndicatorAlpha() - } - - /** - * Set a new interpolated state for all players. This is a state that is usually controlled - * by a finger movement where the user drags from one state to the next. - * - * @param startLocation the start location of our state or -1 if this is directly set - * @param endLocation the ending location of our state. - * @param progress the progress of the transition between startLocation and endlocation. If - * this is not a guided transformation, this will be 1.0f - * @param immediately should this state be applied immediately, canceling all animations? - */ - fun setCurrentState( - @MediaLocation startLocation: Int, - @MediaLocation endLocation: Int, - progress: Float, - immediately: Boolean - ) { - if (startLocation != currentStartLocation || - endLocation != currentEndLocation || - progress != currentTransitionProgress || - immediately - ) { - currentStartLocation = startLocation - currentEndLocation = endLocation - currentTransitionProgress = progress - for (mediaPlayer in MediaPlayerData.players()) { - updatePlayerToState(mediaPlayer, immediately) - } - maybeResetSettingsCog() - updatePageIndicatorAlpha() - } - } - - private fun updatePageIndicatorAlpha() { - val hostStates = mediaHostStatesManager.mediaHostStates - val endIsVisible = hostStates[currentEndLocation]?.visible ?: false - val startIsVisible = hostStates[currentStartLocation]?.visible ?: false - val startAlpha = if (startIsVisible) 1.0f else 0.0f - val endAlpha = if (endIsVisible) 1.0f else 0.0f - var alpha = 1.0f - if (!endIsVisible || !startIsVisible) { - var progress = currentTransitionProgress - if (!endIsVisible) { - progress = 1.0f - progress - } - // Let's fade in quickly at the end where the view is visible - progress = MathUtils.constrain( - MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress), - 0.0f, - 1.0f) - alpha = MathUtils.lerp(startAlpha, endAlpha, progress) - } - pageIndicator.alpha = alpha - } - - private fun updatePageIndicatorLocation() { - // Update the location of the page indicator, carousel clipping - val translationX = if (isRtl) { - (pageIndicator.width - currentCarouselWidth) / 2.0f - } else { - (currentCarouselWidth - pageIndicator.width) / 2.0f - } - pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation - val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams - pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height - - layoutParams.bottomMargin).toFloat() - } - - /** - * Update the dimension of this carousel. - */ - private fun updateCarouselDimensions() { - var width = 0 - var height = 0 - for (mediaPlayer in MediaPlayerData.players()) { - val controller = mediaPlayer.mediaViewController - // When transitioning the view to gone, the view gets smaller, but the translation - // Doesn't, let's add the translation - width = Math.max(width, controller.currentWidth + controller.translationX.toInt()) - height = Math.max(height, controller.currentHeight + controller.translationY.toInt()) - } - if (width != currentCarouselWidth || height != currentCarouselHeight) { - currentCarouselWidth = width - currentCarouselHeight = height - mediaCarouselScrollHandler.setCarouselBounds( - currentCarouselWidth, currentCarouselHeight) - updatePageIndicatorLocation() - } - } - - private fun maybeResetSettingsCog() { - val hostStates = mediaHostStatesManager.mediaHostStates - val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia - ?: true - val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia - ?: endShowsActive - if (currentlyShowingOnlyActive != endShowsActive || - ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && - startShowsActive != endShowsActive)) { - // Whenever we're transitioning from between differing states or the endstate differs - // we reset the translation - currentlyShowingOnlyActive = endShowsActive - mediaCarouselScrollHandler.resetTranslation(animate = true) - } - } - - private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) { - mediaPlayer.mediaViewController.setCurrentState( - startLocation = currentStartLocation, - endLocation = currentEndLocation, - transitionProgress = currentTransitionProgress, - applyImmediately = noAnimation) - } - - /** - * The desired location of this view has changed. We should remeasure the view to match - * the new bounds and kick off bounds animations if necessary. - * If an animation is happening, an animation is kicked of externally, which sets a new - * current state until we reach the targetState. - * - * @param desiredLocation the location we're going to - * @param desiredHostState the target state we're transitioning to - * @param animate should this be animated - */ - fun onDesiredLocationChanged( - desiredLocation: Int, - desiredHostState: MediaHostState?, - animate: Boolean, - duration: Long = 200, - startDelay: Long = 0 - ) = traceSection("MediaCarouselController#onDesiredLocationChanged") { - desiredHostState?.let { - if (this.desiredLocation != desiredLocation) { - // Only log an event when location changes - logger.logCarouselPosition(desiredLocation) - } - - // This is a hosting view, let's remeasure our players - this.desiredLocation = desiredLocation - this.desiredHostState = it - currentlyExpanded = it.expansion > 0 - - val shouldCloseGuts = !currentlyExpanded && - !mediaManager.hasActiveMediaOrRecommendation() && - desiredHostState.showsOnlyActiveMedia - - for (mediaPlayer in MediaPlayerData.players()) { - if (animate) { - mediaPlayer.mediaViewController.animatePendingStateChange( - duration = duration, - delay = startDelay) - } - if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) { - mediaPlayer.closeGuts(!animate) - } - - mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation) - } - mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia - mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded - val nowVisible = it.visible - if (nowVisible != playersVisible) { - playersVisible = nowVisible - if (nowVisible) { - mediaCarouselScrollHandler.resetTranslation() - } - } - updateCarouselSize() - } - } - - fun closeGuts(immediate: Boolean = true) { - MediaPlayerData.players().forEach { - it.closeGuts(immediate) - } - } - - /** - * Update the size of the carousel, remeasuring it if necessary. - */ - private fun updateCarouselSize() { - val width = desiredHostState?.measurementInput?.width ?: 0 - val height = desiredHostState?.measurementInput?.height ?: 0 - if (width != carouselMeasureWidth && width != 0 || - height != carouselMeasureHeight && height != 0) { - carouselMeasureWidth = width - carouselMeasureHeight = height - val playerWidthPlusPadding = carouselMeasureWidth + - context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) - // Let's remeasure the carousel - val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 - val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 - mediaCarousel.measure(widthSpec, heightSpec) - mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight) - // Update the padding after layout; view widths are used in RTL to calculate scrollX - mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding - } - } - - /** - * Log the user impression for media card at visibleMediaIndex. - */ - fun logSmartspaceImpression(qsExpanded: Boolean) { - val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex - if (MediaPlayerData.players().size > visibleMediaIndex) { - val mediaControlPanel = MediaPlayerData.players().elementAt(visibleMediaIndex) - val hasActiveMediaOrRecommendationCard = - MediaPlayerData.hasActiveMediaOrRecommendationCard() - if (!hasActiveMediaOrRecommendationCard && !qsExpanded) { - // Skip logging if on LS or QQS, and there is no active media card - return - } - logSmartspaceCardReported(800, // SMARTSPACE_CARD_SEEN - mediaControlPanel.mSmartspaceId, - mediaControlPanel.mUid, - intArrayOf(mediaControlPanel.surfaceForSmartspaceLogging)) - mediaControlPanel.mIsImpressed = true - } - } - - @JvmOverloads - /** - * Log Smartspace events - * - * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN) - * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new - * instanceId - * @param uid uid for the application that media comes from - * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when - * the event happened - * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1 - * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc. - * @param interactedSubcardCardinality how many media items were shown to the user when there - * is user interaction - * @param rank the rank for media card in the media carousel, starting from 0 - * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency - * between headphone connection to sysUI displays media recommendation card - * @param isSwipeToDismiss whether is to log swipe-to-dismiss event - * - */ - fun logSmartspaceCardReported( - eventId: Int, - instanceId: Int, - uid: Int, - surfaces: IntArray, - interactedSubcardRank: Int = 0, - interactedSubcardCardinality: Int = 0, - rank: Int = mediaCarouselScrollHandler.visibleMediaIndex, - receivedLatencyMillis: Int = 0, - isSwipeToDismiss: Boolean = false - ) { - if (MediaPlayerData.players().size <= rank) { - return - } - - val mediaControlKey = MediaPlayerData.playerKeys().elementAt(rank) - // Only log media resume card when Smartspace data is available - if (!mediaControlKey.isSsMediaRec && - !mediaManager.smartspaceMediaData.isActive && - MediaPlayerData.smartspaceMediaData == null) { - return - } - - val cardinality = mediaContent.getChildCount() - surfaces.forEach { surface -> - /* ktlint-disable max-line-length */ - SysUiStatsLog.write(SysUiStatsLog.SMARTSPACE_CARD_REPORTED, - eventId, - instanceId, - // Deprecated, replaced with AiAi feature type so we don't need to create logging - // card type for each new feature. - SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD, - surface, - // Use -1 as rank value to indicate user swipe to dismiss the card - if (isSwipeToDismiss) -1 else rank, - cardinality, - if (mediaControlKey.isSsMediaRec) - 15 // MEDIA_RECOMMENDATION - else if (mediaControlKey.isSsReactivated) - 43 // MEDIA_RESUME_SS_ACTIVATED - else - 31, // MEDIA_RESUME - uid, - interactedSubcardRank, - interactedSubcardCardinality, - receivedLatencyMillis, - null // Media cards cannot have subcards. - ) - /* ktlint-disable max-line-length */ - if (DEBUG) { - Log.d(TAG, "Log Smartspace card event id: $eventId instance id: $instanceId" + - " surface: $surface rank: $rank cardinality: $cardinality " + - "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " + - "isSsReactivated: ${mediaControlKey.isSsReactivated}" + - "uid: $uid " + - "interactedSubcardRank: $interactedSubcardRank " + - "interactedSubcardCardinality: $interactedSubcardCardinality " + - "received_latency_millis: $receivedLatencyMillis") - } - } - } - - private fun onSwipeToDismiss() { - MediaPlayerData.players().forEachIndexed { - index, it -> - if (it.mIsImpressed) { - logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT, - it.mSmartspaceId, - it.mUid, - intArrayOf(it.surfaceForSmartspaceLogging), - rank = index, - isSwipeToDismiss = true) - // Reset card impressed state when swipe to dismissed - it.mIsImpressed = false - } - } - logger.logSwipeDismiss() - mediaManager.onSwipeToDismiss() - } - - fun getCurrentVisibleMediaContentIntent(): PendingIntent? { - return MediaPlayerData.playerKeys() - .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)?.data?.clickIntent - } - - override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.apply { - println("keysNeedRemoval: $keysNeedRemoval") - println("dataKeys: ${MediaPlayerData.dataKeys()}") - println("playerSortKeys: ${MediaPlayerData.playerKeys()}") - println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}") - println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}") - println("current size: $currentCarouselWidth x $currentCarouselHeight") - println("location: $desiredLocation") - println("state: ${desiredHostState?.expansion}, " + - "only active ${desiredHostState?.showsOnlyActiveMedia}") - } - } -} - -@VisibleForTesting -internal object MediaPlayerData { - private val EMPTY = MediaData( - userId = -1, - initialized = false, - app = null, - appIcon = null, - artist = null, - song = null, - artwork = null, - actions = emptyList(), - actionsToShowInCompact = emptyList(), - packageName = "INVALID", - token = null, - clickIntent = null, - device = null, - active = true, - resumeAction = null, - instanceId = InstanceId.fakeInstanceId(-1), - appUid = -1) - // Whether should prioritize Smartspace card. - internal var shouldPrioritizeSs: Boolean = false - private set - internal var smartspaceMediaData: SmartspaceMediaData? = null - private set - - data class MediaSortKey( - val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation. - val data: MediaData, - val updateTime: Long = 0, - val isSsReactivated: Boolean = false - ) - - private val comparator = compareByDescending<MediaSortKey> { - it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL } - .thenByDescending { - it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL } - .thenByDescending { it.data.active } - .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec } - .thenByDescending { !it.data.resumption } - .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE } - .thenByDescending { it.data.lastActive } - .thenByDescending { it.updateTime } - .thenByDescending { it.data.notificationKey } - - private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) - private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() - - fun addMediaPlayer( - key: String, - data: MediaData, - player: MediaControlPanel, - clock: SystemClock, - isSsReactivated: Boolean, - debugLogger: MediaCarouselControllerLogger? = null - ) { - val removedPlayer = removeMediaPlayer(key) - if (removedPlayer != null && removedPlayer != player) { - debugLogger?.logPotentialMemoryLeak(key) - } - val sortKey = MediaSortKey(isSsMediaRec = false, - data, clock.currentTimeMillis(), isSsReactivated = isSsReactivated) - mediaData.put(key, sortKey) - mediaPlayers.put(sortKey, player) - } - - fun addMediaRecommendation( - key: String, - data: SmartspaceMediaData, - player: MediaControlPanel, - shouldPrioritize: Boolean, - clock: SystemClock, - debugLogger: MediaCarouselControllerLogger? = null - ) { - shouldPrioritizeSs = shouldPrioritize - val removedPlayer = removeMediaPlayer(key) - if (removedPlayer != null && removedPlayer != player) { - debugLogger?.logPotentialMemoryLeak(key) - } - val sortKey = MediaSortKey(isSsMediaRec = true, - EMPTY.copy(isPlaying = false), clock.currentTimeMillis(), isSsReactivated = true) - mediaData.put(key, sortKey) - mediaPlayers.put(sortKey, player) - smartspaceMediaData = data - } - - fun moveIfExists( - oldKey: String?, - newKey: String, - debugLogger: MediaCarouselControllerLogger? = null - ) { - if (oldKey == null || oldKey == newKey) { - return - } - - mediaData.remove(oldKey)?.let { - val removedPlayer = removeMediaPlayer(newKey) - removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) } - mediaData.put(newKey, it) - } - } - - fun getMediaData(mediaSortKey: MediaSortKey?): MediaData? { - mediaData.forEach { (key, value) -> - if (value == mediaSortKey) { - return mediaData[key]?.data - } - } - return null - } - - fun getMediaPlayer(key: String): MediaControlPanel? { - return mediaData.get(key)?.let { mediaPlayers.get(it) } - } - - fun getMediaPlayerIndex(key: String): Int { - val sortKey = mediaData.get(key) - mediaPlayers.entries.forEachIndexed { index, e -> - if (e.key == sortKey) { - return index - } - } - return -1 - } - - fun removeMediaPlayer(key: String) = mediaData.remove(key)?.let { - if (it.isSsMediaRec) { - smartspaceMediaData = null - } - mediaPlayers.remove(it) - } - - fun mediaData() = mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) } - - fun dataKeys() = mediaData.keys - - fun players() = mediaPlayers.values - - fun playerKeys() = mediaPlayers.keys - - /** Returns the index of the first non-timeout media. */ - fun firstActiveMediaIndex(): Int { - mediaPlayers.entries.forEachIndexed { index, e -> - if (!e.key.isSsMediaRec && e.key.data.active) { - return index - } - } - return -1 - } - - /** Returns the existing Smartspace target id. */ - fun smartspaceMediaKey(): String? { - mediaData.entries.forEach { e -> - if (e.value.isSsMediaRec) { - return e.key - } - } - return null - } - - @VisibleForTesting - fun clear() { - mediaData.clear() - mediaPlayers.clear() - } - - /* Returns true if there is active media player card or recommendation card */ - fun hasActiveMediaOrRecommendationCard(): Boolean { - if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) { - return true - } - if (firstActiveMediaIndex() != -1) { - return true - } - return false - } - - fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false -} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt index c6bd777fbd7a..be357ee0ff73 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt @@ -17,6 +17,7 @@ package com.android.systemui.media import android.app.ActivityOptions import android.content.Intent +import android.content.res.Configuration import android.media.projection.IMediaProjection import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION import android.os.Binder @@ -24,85 +25,73 @@ import android.os.Bundle import android.os.IBinder import android.os.ResultReceiver import android.os.UserHandle -import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.android.internal.annotations.VisibleForTesting import com.android.internal.app.ChooserActivity import com.android.internal.app.ResolverListController import com.android.internal.app.chooser.NotSelectableTargetInfo import com.android.internal.app.chooser.TargetInfo import com.android.systemui.R +import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorComponent import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController +import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorView import com.android.systemui.mediaprojection.appselector.data.RecentTask -import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter -import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter.RecentTaskClickListener +import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController +import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.AsyncActivityLauncher -import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration import javax.inject.Inject class MediaProjectionAppSelectorActivity( + private val componentFactory: MediaProjectionAppSelectorComponent.Factory, private val activityLauncher: AsyncActivityLauncher, - private val controller: MediaProjectionAppSelectorController, - private val recentTasksAdapterFactory: RecentTasksAdapter.Factory, /** This is used to override the dependency in a screenshot test */ @VisibleForTesting private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)? -) : ChooserActivity(), MediaProjectionAppSelectorView, RecentTaskClickListener { +) : ChooserActivity(), MediaProjectionAppSelectorView, MediaProjectionAppSelectorResultHandler { @Inject constructor( + componentFactory: MediaProjectionAppSelectorComponent.Factory, activityLauncher: AsyncActivityLauncher, - controller: MediaProjectionAppSelectorController, - recentTasksAdapterFactory: RecentTasksAdapter.Factory, - ) : this(activityLauncher, controller, recentTasksAdapterFactory, null) + ) : this(componentFactory, activityLauncher, null) - private var recentsRoot: ViewGroup? = null - private var recentsProgress: View? = null - private var recentsRecycler: RecyclerView? = null + private lateinit var configurationController: ConfigurationController + private lateinit var controller: MediaProjectionAppSelectorController + private lateinit var recentsViewController: MediaProjectionRecentsViewController - override fun getLayoutResource() = - R.layout.media_projection_app_selector + override fun getLayoutResource() = R.layout.media_projection_app_selector public override fun onCreate(bundle: Bundle?) { - val queryIntent = Intent(Intent.ACTION_MAIN) - .addCategory(Intent.CATEGORY_LAUNCHER) + val component = + componentFactory.create( + activity = this, + view = this, + resultHandler = this + ) + + // Create a separate configuration controller for this activity as the configuration + // might be different from the global one + configurationController = component.configurationController + controller = component.controller + recentsViewController = component.recentsViewController + + val queryIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) } intent.putExtra(Intent.EXTRA_INTENT, queryIntent) // TODO(b/240939253): update copies val title = getString(R.string.media_projection_dialog_service_title) intent.putExtra(Intent.EXTRA_TITLE, title) super.onCreate(bundle) - controller.init(this) + controller.init() } - private fun createRecentsView(parent: ViewGroup): ViewGroup { - val recentsRoot = LayoutInflater.from(this) - .inflate(R.layout.media_projection_recent_tasks, parent, - /* attachToRoot= */ false) as ViewGroup - - recentsProgress = recentsRoot.requireViewById(R.id.media_projection_recent_tasks_loader) - recentsRecycler = recentsRoot.requireViewById(R.id.media_projection_recent_tasks_recycler) - recentsRecycler?.layoutManager = LinearLayoutManager( - this, LinearLayoutManager.HORIZONTAL, - /* reverseLayout= */false - ) - - val itemDecoration = HorizontalSpacerItemDecoration( - resources.getDimensionPixelOffset( - R.dimen.media_projection_app_selector_recents_padding - ) - ) - recentsRecycler?.addItemDecoration(itemDecoration) - - return recentsRoot + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + configurationController.onConfigurationChanged(newConfig) } - override fun appliedThemeResId(): Int = - R.style.Theme_SystemUI_MediaProjectionAppSelector + override fun appliedThemeResId(): Int = R.style.Theme_SystemUI_MediaProjectionAppSelector override fun createListController(userHandle: UserHandle): ResolverListController = listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle) @@ -124,9 +113,9 @@ class MediaProjectionAppSelectorActivity( // is typically very fast, so we don't show any loaders. // We wait for the activity to be launched to make sure that the window of the activity // is created and ready to be captured. - val activityStarted = activityLauncher - .startActivityAsUser(intent, userHandle, activityOptions.toBundle()) { - onTargetActivityLaunched(launchToken) + val activityStarted = + activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) { + returnSelectedApp(launchToken) } // Rely on the ActivityManager to pop up a dialog regarding app suspension @@ -160,44 +149,27 @@ class MediaProjectionAppSelectorActivity( } override fun bind(recentTasks: List<RecentTask>) { - val recents = recentsRoot ?: return - val progress = recentsProgress ?: return - val recycler = recentsRecycler ?: return - - if (recentTasks.isEmpty()) { - recents.visibility = View.GONE - return - } - - progress.visibility = View.GONE - recycler.visibility = View.VISIBLE - recents.visibility = View.VISIBLE - - recycler.adapter = recentTasksAdapterFactory.create(recentTasks, this) - } - - override fun onRecentClicked(task: RecentTask, view: View) { - // TODO(b/240924732) Handle clicking on a recent task + recentsViewController.bind(recentTasks) } - private fun onTargetActivityLaunched(launchToken: IBinder) { + override fun returnSelectedApp(launchCookie: IBinder) { if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) { // The client requested to return the result in the result receiver instead of // activity result, let's send the media projection to the result receiver - val resultReceiver = intent - .getParcelableExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER, - ResultReceiver::class.java) as ResultReceiver - val captureRegion = MediaProjectionCaptureTarget(launchToken) - val data = Bundle().apply { - putParcelable(KEY_CAPTURE_TARGET, captureRegion) - } + val resultReceiver = + intent.getParcelableExtra( + EXTRA_CAPTURE_REGION_RESULT_RECEIVER, + ResultReceiver::class.java + ) as ResultReceiver + val captureRegion = MediaProjectionCaptureTarget(launchCookie) + val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) } resultReceiver.send(RESULT_OK, data) } else { // Return the media projection instance as activity result val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION) val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder) - projection.launchCookie = launchToken + projection.launchCookie = launchCookie val intent = Intent() intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder()) @@ -210,19 +182,16 @@ class MediaProjectionAppSelectorActivity( override fun shouldGetOnlyDefaultActivities() = false - // TODO(b/240924732) flip the flag when the recents selector is ready - override fun shouldShowContentPreview() = false + override fun shouldShowContentPreview() = true override fun createContentPreviewView(parent: ViewGroup): ViewGroup = - recentsRoot ?: createRecentsView(parent).also { - recentsRoot = it - } + recentsViewController.createView(parent) companion object { /** - * When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra - * the activity will send the [CaptureRegion] to the result receiver - * instead of returning media projection instance through activity result. + * When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra the activity will + * send the [CaptureRegion] to the result receiver instead of returning media projection + * instance through activity result. */ const val EXTRA_CAPTURE_REGION_RESULT_RECEIVER = "capture_region_result_receiver" const val KEY_CAPTURE_TARGET = "capture_region" diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt deleted file mode 100644 index d9c58c0d0d76..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media - -import android.media.session.PlaybackState -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.dagger.MediaTimeoutListenerLog -import javax.inject.Inject - -private const val TAG = "MediaTimeout" - -/** - * A buffered log for [MediaTimeoutListener] events - */ -@SysUISingleton -class MediaTimeoutLogger @Inject constructor( - @MediaTimeoutListenerLog private val buffer: LogBuffer -) { - fun logReuseListener(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - }, - { - "reuse listener: $str1" - } - ) - - fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = oldKey - str2 = newKey - bool1 = hadListener - }, - { - "migrate from $str1 to $str2, had listener? $bool1" - } - ) - - fun logUpdateListener(key: String, wasPlaying: Boolean) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - bool1 = wasPlaying - }, - { - "updating $str1, was playing? $bool1" - } - ) - - fun logDelayedUpdate(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - }, - { - "deliver delayed playback state for $str1" - } - ) - - fun logSessionDestroyed(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - }, - { - "session destroyed $str1" - } - ) - - fun logPlaybackState(key: String, state: PlaybackState?) = buffer.log( - TAG, - LogLevel.VERBOSE, - { - str1 = key - str2 = state?.toString() - }, - { - "state update: key=$str1 state=$str2" - } - ) - - fun logStateCallback(key: String) = buffer.log( - TAG, - LogLevel.VERBOSE, - { - str1 = key - }, - { - "dispatching state update for $key" - } - ) - - fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - bool1 = playing - bool2 = resumption - }, - { - "schedule timeout $str1, playing=$bool1 resumption=$bool2" - } - ) - - fun logCancelIgnored(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - }, - { - "cancellation already exists for $str1" - } - ) - - fun logTimeout(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - }, - { - "execute timeout for $str1" - } - ) - - fun logTimeoutCancelled(key: String, reason: String) = buffer.log( - TAG, - LogLevel.VERBOSE, - { - str1 = key - str2 = reason - }, - { - "media timeout cancelled for $str1, reason: $str2" - } - ) -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt deleted file mode 100644 index ac59175d4646..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt +++ /dev/null @@ -1,547 +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 - */ - -package com.android.systemui.media - -import android.content.Context -import android.content.res.Configuration -import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.R -import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.util.animation.MeasurementOutput -import com.android.systemui.util.animation.TransitionLayout -import com.android.systemui.util.animation.TransitionLayoutController -import com.android.systemui.util.animation.TransitionViewState -import com.android.systemui.util.traceSection -import javax.inject.Inject - -/** - * A class responsible for controlling a single instance of a media player handling interactions - * with the view instance and keeping the media view states up to date. - */ -class MediaViewController @Inject constructor( - private val context: Context, - private val configurationController: ConfigurationController, - private val mediaHostStatesManager: MediaHostStatesManager, - private val logger: MediaViewLogger -) { - - /** - * Indicating that the media view controller is for a notification-based player, - * session-based player, or recommendation - */ - enum class TYPE { - PLAYER, RECOMMENDATION - } - - companion object { - @JvmField - val GUTS_ANIMATION_DURATION = 500L - } - - /** - * A listener when the current dimensions of the player change - */ - lateinit var sizeChangedListener: () -> Unit - private var firstRefresh: Boolean = true - private var transitionLayout: TransitionLayout? = null - private val layoutController = TransitionLayoutController() - private var animationDelay: Long = 0 - private var animationDuration: Long = 0 - private var animateNextStateChange: Boolean = false - private val measurement = MeasurementOutput(0, 0) - private var type: TYPE = TYPE.PLAYER - - /** - * A map containing all viewStates for all locations of this mediaState - */ - private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf() - - /** - * The ending location of the view where it ends when all animations and transitions have - * finished - */ - @MediaLocation - var currentEndLocation: Int = -1 - - /** - * The starting location of the view where it starts for all animations and transitions - */ - @MediaLocation - private var currentStartLocation: Int = -1 - - /** - * The progress of the transition or 1.0 if there is no transition happening - */ - private var currentTransitionProgress: Float = 1.0f - - /** - * A temporary state used to store intermediate measurements. - */ - private val tmpState = TransitionViewState() - - /** - * A temporary state used to store intermediate measurements. - */ - private val tmpState2 = TransitionViewState() - - /** - * A temporary state used to store intermediate measurements. - */ - private val tmpState3 = TransitionViewState() - - /** - * A temporary cache key to be used to look up cache entries - */ - private val tmpKey = CacheKey() - - /** - * The current width of the player. This might not factor in case the player is animating - * to the current state, but represents the end state - */ - var currentWidth: Int = 0 - /** - * The current height of the player. This might not factor in case the player is animating - * to the current state, but represents the end state - */ - var currentHeight: Int = 0 - - /** - * Get the translationX of the layout - */ - var translationX: Float = 0.0f - private set - get() { - return transitionLayout?.translationX ?: 0.0f - } - - /** - * Get the translationY of the layout - */ - var translationY: Float = 0.0f - private set - get() { - return transitionLayout?.translationY ?: 0.0f - } - - /** - * A callback for RTL config changes - */ - private val configurationListener = object : ConfigurationController.ConfigurationListener { - override fun onConfigChanged(newConfig: Configuration?) { - // Because the TransitionLayout is not always attached (and calculates/caches layout - // results regardless of attach state), we have to force the layoutDirection of the view - // to the correct value for the user's current locale to ensure correct recalculation - // when/after calling refreshState() - newConfig?.apply { - if (transitionLayout?.rawLayoutDirection != layoutDirection) { - transitionLayout?.layoutDirection = layoutDirection - refreshState() - } - } - } - } - - /** - * A callback for media state changes - */ - val stateCallback = object : MediaHostStatesManager.Callback { - override fun onHostStateChanged( - @MediaLocation location: Int, - mediaHostState: MediaHostState - ) { - if (location == currentEndLocation || location == currentStartLocation) { - setCurrentState(currentStartLocation, - currentEndLocation, - currentTransitionProgress, - applyImmediately = false) - } - } - } - - /** - * The expanded constraint set used to render a expanded player. If it is modified, make sure - * to call [refreshState] - */ - val collapsedLayout = ConstraintSet() - - /** - * The expanded constraint set used to render a collapsed player. If it is modified, make sure - * to call [refreshState] - */ - val expandedLayout = ConstraintSet() - - /** - * Whether the guts are visible for the associated player. - */ - var isGutsVisible = false - private set - - init { - mediaHostStatesManager.addController(this) - layoutController.sizeChangedListener = { width: Int, height: Int -> - currentWidth = width - currentHeight = height - sizeChangedListener.invoke() - } - configurationController.addCallback(configurationListener) - } - - /** - * Notify this controller that the view has been removed and all listeners should be destroyed - */ - fun onDestroy() { - mediaHostStatesManager.removeController(this) - configurationController.removeCallback(configurationListener) - } - - /** - * Show guts with an animated transition. - */ - fun openGuts() { - if (isGutsVisible) return - isGutsVisible = true - animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) - setCurrentState(currentStartLocation, - currentEndLocation, - currentTransitionProgress, - applyImmediately = false) - } - - /** - * Close the guts for the associated player. - * - * @param immediate if `false`, it will animate the transition. - */ - @JvmOverloads - fun closeGuts(immediate: Boolean = false) { - if (!isGutsVisible) return - isGutsVisible = false - if (!immediate) { - animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) - } - setCurrentState(currentStartLocation, - currentEndLocation, - currentTransitionProgress, - applyImmediately = immediate) - } - - private fun ensureAllMeasurements() { - val mediaStates = mediaHostStatesManager.mediaHostStates - for (entry in mediaStates) { - obtainViewState(entry.value) - } - } - - /** - * Get the constraintSet for a given expansion - */ - private fun constraintSetForExpansion(expansion: Float): ConstraintSet = - if (expansion > 0) expandedLayout else collapsedLayout - - /** - * Set the views to be showing/hidden based on the [isGutsVisible] for a given - * [TransitionViewState]. - */ - private fun setGutsViewState(viewState: TransitionViewState) { - val controlsIds = when (type) { - TYPE.PLAYER -> MediaViewHolder.controlsIds - TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds - } - val gutsIds = GutsViewHolder.ids - controlsIds.forEach { id -> - viewState.widgetStates.get(id)?.let { state -> - // Make sure to use the unmodified state if guts are not visible. - state.alpha = if (isGutsVisible) 0f else state.alpha - state.gone = if (isGutsVisible) true else state.gone - } - } - gutsIds.forEach { id -> - viewState.widgetStates.get(id)?.let { state -> - // Make sure to use the unmodified state if guts are visible - state.alpha = if (isGutsVisible) state.alpha else 0f - state.gone = if (isGutsVisible) state.gone else true - } - } - } - - /** - * Obtain a new viewState for a given media state. This usually returns a cached state, but if - * it's not available, it will recreate one by measuring, which may be expensive. - */ - private fun obtainViewState(state: MediaHostState?): TransitionViewState? { - if (state == null || state.measurementInput == null) { - return null - } - // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey - var cacheKey = getKey(state, isGutsVisible, tmpKey) - val viewState = viewStates[cacheKey] - if (viewState != null) { - // we already have cached this measurement, let's continue - return viewState - } - // Copy the key since this might call recursively into it and we're using tmpKey - cacheKey = cacheKey.copy() - val result: TransitionViewState? - - if (transitionLayout != null) { - // Let's create a new measurement - if (state.expansion == 0.0f || state.expansion == 1.0f) { - result = transitionLayout!!.calculateViewState( - state.measurementInput!!, - constraintSetForExpansion(state.expansion), - TransitionViewState()) - - setGutsViewState(result) - // We don't want to cache interpolated or null states as this could quickly fill up - // our cache. We only cache the start and the end states since the interpolation - // is cheap - viewStates[cacheKey] = result - } else { - // This is an interpolated state - val startState = state.copy().also { it.expansion = 0.0f } - - // Given that we have a measurement and a view, let's get (guaranteed) viewstates - // from the start and end state and interpolate them - val startViewState = obtainViewState(startState) as TransitionViewState - val endState = state.copy().also { it.expansion = 1.0f } - val endViewState = obtainViewState(endState) as TransitionViewState - result = layoutController.getInterpolatedState( - startViewState, - endViewState, - state.expansion) - } - } else { - result = null - } - return result - } - - private fun getKey( - state: MediaHostState, - guts: Boolean, - result: CacheKey - ): CacheKey { - result.apply { - heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0 - widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0 - expansion = state.expansion - gutsVisible = guts - } - return result - } - - /** - * Attach a view to this controller. This may perform measurements if it's not available yet - * and should therefore be done carefully. - */ - fun attach( - transitionLayout: TransitionLayout, - type: TYPE - ) = traceSection("MediaViewController#attach") { - updateMediaViewControllerType(type) - logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation) - this.transitionLayout = transitionLayout - layoutController.attach(transitionLayout) - if (currentEndLocation == -1) { - return - } - // Set the previously set state immediately to the view, now that it's finally attached - setCurrentState( - startLocation = currentStartLocation, - endLocation = currentEndLocation, - transitionProgress = currentTransitionProgress, - applyImmediately = true) - } - - /** - * Obtain a measurement for a given location. This makes sure that the state is up to date - * and all widgets know their location. Calling this method may create a measurement if we - * don't have a cached value available already. - */ - fun getMeasurementsForState( - hostState: MediaHostState - ): MeasurementOutput? = traceSection("MediaViewController#getMeasurementsForState") { - val viewState = obtainViewState(hostState) ?: return null - measurement.measuredWidth = viewState.width - measurement.measuredHeight = viewState.height - return measurement - } - - /** - * Set a new state for the controlled view which can be an interpolation between multiple - * locations. - */ - fun setCurrentState( - @MediaLocation startLocation: Int, - @MediaLocation endLocation: Int, - transitionProgress: Float, - applyImmediately: Boolean - ) = traceSection("MediaViewController#setCurrentState") { - currentEndLocation = endLocation - currentStartLocation = startLocation - currentTransitionProgress = transitionProgress - logger.logMediaLocation("setCurrentState", startLocation, endLocation) - - val shouldAnimate = animateNextStateChange && !applyImmediately - - val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return - val startHostState = mediaHostStatesManager.mediaHostStates[startLocation] - - // Obtain the view state that we'd want to be at the end - // The view might not be bound yet or has never been measured and in that case will be - // reset once the state is fully available - var endViewState = obtainViewState(endHostState) ?: return - endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!! - layoutController.setMeasureState(endViewState) - - // If the view isn't bound, we can drop the animation, otherwise we'll execute it - animateNextStateChange = false - if (transitionLayout == null) { - return - } - - val result: TransitionViewState - var startViewState = obtainViewState(startHostState) - startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3) - - if (!endHostState.visible) { - // Let's handle the case where the end is gone first. In this case we take the - // start viewState and will make it gone - if (startViewState == null || startHostState == null || !startHostState.visible) { - // the start isn't a valid state, let's use the endstate directly - result = endViewState - } else { - // Let's get the gone presentation from the start state - result = layoutController.getGoneState(startViewState, - startHostState.disappearParameters, - transitionProgress, - tmpState) - } - } else if (startHostState != null && !startHostState.visible) { - // We have a start state and it is gone. - // Let's get presentation from the endState - result = layoutController.getGoneState(endViewState, endHostState.disappearParameters, - 1.0f - transitionProgress, - tmpState) - } else if (transitionProgress == 1.0f || startViewState == null) { - // We're at the end. Let's use that state - result = endViewState - } else if (transitionProgress == 0.0f) { - // We're at the start. Let's use that state - result = startViewState - } else { - result = layoutController.getInterpolatedState(startViewState, endViewState, - transitionProgress, tmpState) - } - logger.logMediaSize("setCurrentState", result.width, result.height) - layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration, - animationDelay) - } - - private fun updateViewStateToCarouselSize( - viewState: TransitionViewState?, - location: Int, - outState: TransitionViewState - ): TransitionViewState? { - val result = viewState?.copy(outState) ?: return null - val overrideSize = mediaHostStatesManager.carouselSizes[location] - overrideSize?.let { - // To be safe we're using a maximum here. The override size should always be set - // properly though. - result.height = Math.max(it.measuredHeight, result.height) - result.width = Math.max(it.measuredWidth, result.width) - } - logger.logMediaSize("update to carousel", result.width, result.height) - return result - } - - private fun updateMediaViewControllerType(type: TYPE) { - this.type = type - - // These XML resources contain ConstraintSets that will apply to this player type's layout - when (type) { - TYPE.PLAYER -> { - collapsedLayout.load(context, R.xml.media_session_collapsed) - expandedLayout.load(context, R.xml.media_session_expanded) - } - TYPE.RECOMMENDATION -> { - collapsedLayout.load(context, R.xml.media_recommendation_collapsed) - expandedLayout.load(context, R.xml.media_recommendation_expanded) - } - } - refreshState() - } - - /** - * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. - * In the event of [location] not being visible, [locationWhenHidden] will be used instead. - * - * @param location Target - * @param locationWhenHidden Location that will be used when the target is not - * [MediaHost.visible] - * @return State require for executing a transition, and also the respective [MediaHost]. - */ - private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? { - val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null - return obtainViewState(mediaHostState) - } - - /** - * Notify that the location is changing right now and a [setCurrentState] change is imminent. - * This updates the width the view will me measured with. - */ - fun onLocationPreChange(@MediaLocation newLocation: Int) { - obtainViewStateForLocation(newLocation)?.let { - layoutController.setMeasureState(it) - } - } - - /** - * Request that the next state change should be animated with the given parameters. - */ - fun animatePendingStateChange(duration: Long, delay: Long) { - animateNextStateChange = true - animationDuration = duration - animationDelay = delay - } - - /** - * Clear all existing measurements and refresh the state to match the view. - */ - fun refreshState() = traceSection("MediaViewController#refreshState") { - // Let's clear all of our measurements and recreate them! - viewStates.clear() - if (firstRefresh) { - // This is the first bind, let's ensure we pre-cache all measurements. Otherwise - // We'll just load these on demand. - ensureAllMeasurements() - firstRefresh = false - } - setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress, - applyImmediately = true) - } -} - -/** - * An internal key for the cache of mediaViewStates. This is a subset of the full host state. - */ -private data class CacheKey( - var widthMeasureSpec: Int = -1, - var heightMeasureSpec: Int = -1, - var expansion: Float = 0.0f, - var gutsVisible: Boolean = false -) diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java index 0b9b32b0d7d7..2a8168b0cb36 100644 --- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java +++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java @@ -51,9 +51,10 @@ import javax.inject.Inject; * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}. */ @SysUISingleton -public class RingtonePlayer extends CoreStartable { +public class RingtonePlayer implements CoreStartable { private static final String TAG = "RingtonePlayer"; private static final boolean LOGD = false; + private final Context mContext; // TODO: support Uri switching under same IBinder @@ -64,7 +65,7 @@ public class RingtonePlayer extends CoreStartable { @Inject public RingtonePlayer(Context context) { - super(context); + mContext = context; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt index 73240b54a27a..531506771459 100644 --- a/packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models import android.content.res.ColorStateList import android.util.Log @@ -23,6 +23,9 @@ import android.view.ViewGroup import android.widget.ImageButton import android.widget.TextView import com.android.systemui.R +import com.android.systemui.media.controls.ui.accentPrimaryFromScheme +import com.android.systemui.media.controls.ui.surfaceFromScheme +import com.android.systemui.media.controls.ui.textPrimaryFromScheme import com.android.systemui.monet.ColorScheme /** @@ -95,11 +98,6 @@ class GutsViewHolder constructor(itemView: View) { } companion object { - val ids = setOf( - R.id.remove_text, - R.id.cancel, - R.id.dismiss, - R.id.settings - ) + val ids = setOf(R.id.remove_text, R.id.cancel, R.id.dismiss, R.id.settings) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt index 5b2cda038bbd..f006442906e7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.app.PendingIntent import android.graphics.drawable.Drawable @@ -27,69 +27,42 @@ import com.android.systemui.R data class MediaData( val userId: Int, val initialized: Boolean = false, - /** - * App name that will be displayed on the player. - */ + /** App name that will be displayed on the player. */ val app: String?, - /** - * App icon shown on player. - */ + /** App icon shown on player. */ val appIcon: Icon?, - /** - * Artist name. - */ + /** Artist name. */ val artist: CharSequence?, - /** - * Song name. - */ + /** Song name. */ val song: CharSequence?, - /** - * Album artwork. - */ + /** Album artwork. */ val artwork: Icon?, - /** - * List of generic action buttons for the media player, based on notification actions - */ + /** List of generic action buttons for the media player, based on notification actions */ val actions: List<MediaAction>, - /** - * Same as above, but shown on smaller versions of the player, like in QQS or keyguard. - */ + /** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */ val actionsToShowInCompact: List<Int>, /** - * Semantic actions buttons, based on the PlaybackState of the media session. - * If present, these actions will be preferred in the UI over [actions] + * Semantic actions buttons, based on the PlaybackState of the media session. If present, these + * actions will be preferred in the UI over [actions] */ val semanticActions: MediaButton? = null, - /** - * Package name of the app that's posting the media. - */ + /** Package name of the app that's posting the media. */ val packageName: String, - /** - * Unique media session identifier. - */ + /** Unique media session identifier. */ val token: MediaSession.Token?, - /** - * Action to perform when the player is tapped. - * This is unrelated to {@link #actions}. - */ + /** Action to perform when the player is tapped. This is unrelated to {@link #actions}. */ val clickIntent: PendingIntent?, - /** - * Where the media is playing: phone, headphones, ear buds, remote session. - */ + /** Where the media is playing: phone, headphones, ear buds, remote session. */ val device: MediaDeviceData?, /** - * When active, a player will be displayed on keyguard and quick-quick settings. - * This is unrelated to the stream being playing or not, a player will not be active if - * timed out, or in resumption mode. + * When active, a player will be displayed on keyguard and quick-quick settings. This is + * unrelated to the stream being playing or not, a player will not be active if timed out, or in + * resumption mode. */ var active: Boolean, - /** - * Action that should be performed to restart a non active session. - */ + /** Action that should be performed to restart a non active session. */ var resumeAction: Runnable?, - /** - * Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE - */ + /** Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE */ var playbackLocation: Int = PLAYBACK_LOCAL, /** * Indicates that this player is a resumption player (ie. It only shows a play actions which @@ -102,29 +75,19 @@ data class MediaData( val notificationKey: String? = null, var hasCheckedForResume: Boolean = false, - /** - * If apps do not report PlaybackState, set as null to imply 'undetermined' - */ + /** If apps do not report PlaybackState, set as null to imply 'undetermined' */ val isPlaying: Boolean? = null, - /** - * Set from the notification and used as fallback when PlaybackState cannot be determined - */ + /** Set from the notification and used as fallback when PlaybackState cannot be determined */ val isClearable: Boolean = true, - /** - * Timestamp when this player was last active. - */ + /** Timestamp when this player was last active. */ var lastActive: Long = 0L, - /** - * Instance ID for logging purposes - */ + /** Instance ID for logging purposes */ val instanceId: InstanceId, - /** - * The UID of the app, used for logging - */ + /** The UID of the app, used for logging */ val appUid: Int ) { companion object { @@ -141,37 +104,21 @@ data class MediaData( } } -/** - * Contains [MediaAction] objects which represent specific buttons in the UI - */ +/** Contains [MediaAction] objects which represent specific buttons in the UI */ data class MediaButton( - /** - * Play/pause button - */ + /** Play/pause button */ val playOrPause: MediaAction? = null, - /** - * Next button, or custom action - */ + /** Next button, or custom action */ val nextOrCustom: MediaAction? = null, - /** - * Previous button, or custom action - */ + /** Previous button, or custom action */ val prevOrCustom: MediaAction? = null, - /** - * First custom action space - */ + /** First custom action space */ val custom0: MediaAction? = null, - /** - * Second custom action space - */ + /** Second custom action space */ val custom1: MediaAction? = null, - /** - * Whether to reserve the empty space when the nextOrCustom is null - */ + /** Whether to reserve the empty space when the nextOrCustom is null */ val reserveNext: Boolean = false, - /** - * Whether to reserve the empty space when the prevOrCustom is null - */ + /** Whether to reserve the empty space when the prevOrCustom is null */ val reservePrev: Boolean = false ) { fun getActionById(id: Int): MediaAction? { @@ -201,7 +148,8 @@ data class MediaAction( /** State of the media device. */ data class MediaDeviceData -@JvmOverloads constructor( +@JvmOverloads +constructor( /** Whether or not to enable the chip */ val enabled: Boolean, @@ -221,8 +169,8 @@ data class MediaDeviceData val showBroadcastButton: Boolean ) { /** - * Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon - * is ignored because it can change by reference frequently depending on the device type's + * Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon is + * ignored because it can change by reference frequently depending on the device type's * implementation, but this is not usually relevant unless other info has changed */ fun equalsWithoutIcon(other: MediaDeviceData?): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt index fc9515c050e5..2511324a943e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.view.LayoutInflater import android.view.View @@ -25,13 +25,12 @@ import android.widget.SeekBar import android.widget.TextView import androidx.constraintlayout.widget.Barrier import com.android.systemui.R +import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.util.animation.TransitionLayout private const val TAG = "MediaViewHolder" -/** - * Holder class for media player view - */ +/** Holder class for media player view */ class MediaViewHolder constructor(itemView: View) { val player = itemView as TransitionLayout @@ -52,8 +51,7 @@ class MediaViewHolder constructor(itemView: View) { // These views are only shown while the user is actively scrubbing val scrubbingElapsedTimeView: TextView = itemView.requireViewById(R.id.media_scrubbing_elapsed_time) - val scrubbingTotalTimeView: TextView = - itemView.requireViewById(R.id.media_scrubbing_total_time) + val scrubbingTotalTimeView: TextView = itemView.requireViewById(R.id.media_scrubbing_total_time) val gutsViewHolder = GutsViewHolder(itemView) @@ -86,15 +84,7 @@ class MediaViewHolder constructor(itemView: View) { } fun getTransparentActionButtons(): List<ImageButton> { - return listOf( - actionNext, - actionPrev, - action0, - action1, - action2, - action3, - action4 - ) + return listOf(actionNext, actionPrev, action0, action1, action2, action3, action4) } fun marquee(start: Boolean, delay: Long) { @@ -108,10 +98,8 @@ class MediaViewHolder constructor(itemView: View) { * @param inflater LayoutInflater to use to inflate the layout. * @param parent Parent of inflated view. */ - @JvmStatic fun create( - inflater: LayoutInflater, - parent: ViewGroup - ): MediaViewHolder { + @JvmStatic + fun create(inflater: LayoutInflater, parent: ViewGroup): MediaViewHolder { val mediaView = inflater.inflate(R.layout.media_session_view, parent, false) mediaView.setLayerType(View.LAYER_TYPE_HARDWARE, null) // Because this media view (a TransitionLayout) is used to measure and layout the views @@ -124,7 +112,8 @@ class MediaViewHolder constructor(itemView: View) { } } - val controlsIds = setOf( + val controlsIds = + setOf( R.id.icon, R.id.app_name, R.id.header_title, @@ -142,27 +131,23 @@ class MediaViewHolder constructor(itemView: View) { R.id.icon, R.id.media_scrubbing_elapsed_time, R.id.media_scrubbing_total_time - ) + ) // Buttons used for notification-based actions - val genericButtonIds = setOf( - R.id.action0, - R.id.action1, - R.id.action2, - R.id.action3, - R.id.action4 - ) - - val expandedBottomActionIds = setOf( - R.id.actionPrev, - R.id.actionNext, - R.id.action0, - R.id.action1, - R.id.action2, - R.id.action3, - R.id.action4, - R.id.media_scrubbing_elapsed_time, - R.id.media_scrubbing_total_time - ) + val genericButtonIds = + setOf(R.id.action0, R.id.action1, R.id.action2, R.id.action3, R.id.action4) + + val expandedBottomActionIds = + setOf( + R.id.actionPrev, + R.id.actionNext, + R.id.action0, + R.id.action1, + R.id.action2, + R.id.action3, + R.id.action4, + R.id.media_scrubbing_elapsed_time, + R.id.media_scrubbing_total_time + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt index 121021f19f70..37d956bd09eb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.animation.Animator import android.animation.ObjectAnimator @@ -24,40 +24,56 @@ import androidx.lifecycle.Observer import com.android.internal.annotations.VisibleForTesting import com.android.systemui.R import com.android.systemui.animation.Interpolators +import com.android.systemui.media.controls.ui.SquigglyProgress /** * Observer for changes from SeekBarViewModel. * * <p>Updates the seek bar views in response to changes to the model. */ -open class SeekBarObserver( - private val holder: MediaViewHolder -) : Observer<SeekBarViewModel.Progress> { +open class SeekBarObserver(private val holder: MediaViewHolder) : + Observer<SeekBarViewModel.Progress> { companion object { @JvmStatic val RESET_ANIMATION_DURATION_MS: Int = 750 @JvmStatic val RESET_ANIMATION_THRESHOLD_MS: Int = 250 } - val seekBarEnabledMaxHeight = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_height) - val seekBarDisabledHeight = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_height) - val seekBarEnabledVerticalPadding = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_session_enabled_seekbar_vertical_padding) - val seekBarDisabledVerticalPadding = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_session_disabled_seekbar_vertical_padding) + val seekBarEnabledMaxHeight = + holder.seekBar.context.resources.getDimensionPixelSize( + R.dimen.qs_media_enabled_seekbar_height + ) + val seekBarDisabledHeight = + holder.seekBar.context.resources.getDimensionPixelSize( + R.dimen.qs_media_disabled_seekbar_height + ) + val seekBarEnabledVerticalPadding = + holder.seekBar.context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_enabled_seekbar_vertical_padding + ) + val seekBarDisabledVerticalPadding = + holder.seekBar.context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_disabled_seekbar_vertical_padding + ) var seekBarResetAnimator: Animator? = null init { - val seekBarProgressWavelength = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength).toFloat() - val seekBarProgressAmplitude = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude).toFloat() - val seekBarProgressPhase = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase).toFloat() - val seekBarProgressStrokeWidth = holder.seekBar.context.resources - .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width).toFloat() + val seekBarProgressWavelength = + holder.seekBar.context.resources + .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength) + .toFloat() + val seekBarProgressAmplitude = + holder.seekBar.context.resources + .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude) + .toFloat() + val seekBarProgressPhase = + holder.seekBar.context.resources + .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase) + .toFloat() + val seekBarProgressStrokeWidth = + holder.seekBar.context.resources + .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width) + .toFloat() val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress progressDrawable?.let { it.waveLength = seekBarProgressWavelength @@ -97,16 +113,18 @@ open class SeekBarObserver( } holder.seekBar.setMax(data.duration) - val totalTimeString = DateUtils.formatElapsedTime( - data.duration / DateUtils.SECOND_IN_MILLIS) + val totalTimeString = + DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS) if (data.scrubbing) { holder.scrubbingTotalTimeView.text = totalTimeString } data.elapsedTime?.let { if (!data.scrubbing && !(seekBarResetAnimator?.isRunning ?: false)) { - if (it <= RESET_ANIMATION_THRESHOLD_MS && - holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS) { + if ( + it <= RESET_ANIMATION_THRESHOLD_MS && + holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS + ) { // This animation resets for every additional update to zero. val animator = buildResetAnimator(it) animator.start() @@ -116,24 +134,29 @@ open class SeekBarObserver( } } - val elapsedTimeString = DateUtils.formatElapsedTime( - it / DateUtils.SECOND_IN_MILLIS) + val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS) if (data.scrubbing) { holder.scrubbingElapsedTimeView.text = elapsedTimeString } - holder.seekBar.contentDescription = holder.seekBar.context.getString( - R.string.controls_media_seekbar_description, - elapsedTimeString, - totalTimeString - ) + holder.seekBar.contentDescription = + holder.seekBar.context.getString( + R.string.controls_media_seekbar_description, + elapsedTimeString, + totalTimeString + ) } } @VisibleForTesting open fun buildResetAnimator(targetTime: Int): Animator { - val animator = ObjectAnimator.ofInt(holder.seekBar, "progress", - holder.seekBar.progress, targetTime + RESET_ANIMATION_DURATION_MS) + val animator = + ObjectAnimator.ofInt( + holder.seekBar, + "progress", + holder.seekBar.progress, + targetTime + RESET_ANIMATION_DURATION_MS + ) animator.setAutoCancel(true) animator.duration = RESET_ANIMATION_DURATION_MS.toLong() animator.interpolator = Interpolators.EMPHASIZED diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt index 0359c6325749..bba5f350dd16 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.media.MediaMetadata import android.media.session.MediaController @@ -30,7 +30,9 @@ import androidx.annotation.WorkerThread import androidx.core.view.GestureDetectorCompat import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.util.concurrency.RepeatableExecutor import javax.inject.Inject @@ -40,8 +42,8 @@ private const val MIN_FLING_VELOCITY_SCALE_FACTOR = 10 private fun PlaybackState.isInMotion(): Boolean { return this.state == PlaybackState.STATE_PLAYING || - this.state == PlaybackState.STATE_FAST_FORWARDING || - this.state == PlaybackState.STATE_REWINDING + this.state == PlaybackState.STATE_FAST_FORWARDING || + this.state == PlaybackState.STATE_REWINDING } /** @@ -57,8 +59,8 @@ private fun PlaybackState.computePosition(duration: Long): Long { val updateTime = this.getLastPositionUpdateTime() val currentTime = SystemClock.elapsedRealtime() if (updateTime > 0) { - var position = (this.playbackSpeed * (currentTime - updateTime)).toLong() + - this.getPosition() + var position = + (this.playbackSpeed * (currentTime - updateTime)).toLong() + this.getPosition() if (duration >= 0 && position > duration) { position = duration.toLong() } else if (position < 0) { @@ -71,8 +73,11 @@ private fun PlaybackState.computePosition(duration: Long): Long { } /** ViewModel for seek bar in QS media player. */ -class SeekBarViewModel @Inject constructor( - @Background private val bgExecutor: RepeatableExecutor +class SeekBarViewModel +@Inject +constructor( + @Background private val bgExecutor: RepeatableExecutor, + private val falsingManager: FalsingManager, ) { private var _data = Progress(false, false, false, false, null, 0) set(value) { @@ -83,9 +88,7 @@ class SeekBarViewModel @Inject constructor( } _progress.postValue(value) } - private val _progress = MutableLiveData<Progress>().apply { - postValue(_data) - } + private val _progress = MutableLiveData<Progress>().apply { postValue(_data) } val progress: LiveData<Progress> get() = _progress private var controller: MediaController? = null @@ -97,20 +100,21 @@ class SeekBarViewModel @Inject constructor( } } private var playbackState: PlaybackState? = null - private var callback = object : MediaController.Callback() { - override fun onPlaybackStateChanged(state: PlaybackState?) { - playbackState = state - if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) { - clearController() - } else { - checkIfPollingNeeded() + private var callback = + object : MediaController.Callback() { + override fun onPlaybackStateChanged(state: PlaybackState?) { + playbackState = state + if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) { + clearController() + } else { + checkIfPollingNeeded() + } } - } - override fun onSessionDestroyed() { - clearController() + override fun onSessionDestroyed() { + clearController() + } } - } private var cancel: Runnable? = null /** Indicates if the seek interaction is considered a false guesture. */ @@ -118,12 +122,13 @@ class SeekBarViewModel @Inject constructor( /** Listening state (QS open or closed) is used to control polling of progress. */ var listening = true - set(value) = bgExecutor.execute { - if (field != value) { - field = value - checkIfPollingNeeded() + set(value) = + bgExecutor.execute { + if (field != value) { + field = value + checkIfPollingNeeded() + } } - } private var scrubbingChangeListener: ScrubbingChangeListener? = null private var enabledChangeListener: EnabledChangeListener? = null @@ -141,14 +146,13 @@ class SeekBarViewModel @Inject constructor( lateinit var logSeek: () -> Unit - /** - * Event indicating that the user has started interacting with the seek bar. - */ + /** Event indicating that the user has started interacting with the seek bar. */ @AnyThread - fun onSeekStarting() = bgExecutor.execute { - scrubbing = true - isFalseSeek = false - } + fun onSeekStarting() = + bgExecutor.execute { + scrubbing = true + isFalseSeek = false + } /** * Event indicating that the user has moved the seek bar. @@ -156,47 +160,51 @@ class SeekBarViewModel @Inject constructor( * @param position Current location in the track. */ @AnyThread - fun onSeekProgress(position: Long) = bgExecutor.execute { - if (scrubbing) { - // The user hasn't yet finished their touch gesture, so only update the data for visual - // feedback and don't update [controller] yet. - _data = _data.copy(elapsedTime = position.toInt()) - } else { - // The seek progress came from an a11y action and we should immediately update to the - // new position. (a11y actions to change the seekbar position don't trigger - // SeekBar.OnSeekBarChangeListener.onStartTrackingTouch or onStopTrackingTouch.) - onSeek(position) + fun onSeekProgress(position: Long) = + bgExecutor.execute { + if (scrubbing) { + // The user hasn't yet finished their touch gesture, so only update the data for + // visual + // feedback and don't update [controller] yet. + _data = _data.copy(elapsedTime = position.toInt()) + } else { + // The seek progress came from an a11y action and we should immediately update to + // the + // new position. (a11y actions to change the seekbar position don't trigger + // SeekBar.OnSeekBarChangeListener.onStartTrackingTouch or onStopTrackingTouch.) + onSeek(position) + } } - } - /** - * Event indicating that the seek interaction is a false gesture and it should be ignored. - */ + /** Event indicating that the seek interaction is a false gesture and it should be ignored. */ @AnyThread - fun onSeekFalse() = bgExecutor.execute { - if (scrubbing) { - isFalseSeek = true + fun onSeekFalse() = + bgExecutor.execute { + if (scrubbing) { + isFalseSeek = true + } } - } /** * Handle request to change the current position in the media track. * @param position Place to seek to in the track. */ @AnyThread - fun onSeek(position: Long) = bgExecutor.execute { - if (isFalseSeek) { - scrubbing = false - checkPlaybackPosition() - } else { - logSeek() - controller?.transportControls?.seekTo(position) - // Invalidate the cached playbackState to avoid the thumb jumping back to the previous - // position. - playbackState = null - scrubbing = false + fun onSeek(position: Long) = + bgExecutor.execute { + if (isFalseSeek) { + scrubbing = false + checkPlaybackPosition() + } else { + logSeek() + controller?.transportControls?.seekTo(position) + // Invalidate the cached playbackState to avoid the thumb jumping back to the + // previous + // position. + playbackState = null + scrubbing = false + } } - } /** * Updates media information. @@ -213,11 +221,18 @@ class SeekBarViewModel @Inject constructor( val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L val position = playbackState?.position?.toInt() val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() ?: 0 - val playing = NotificationMediaManager - .isPlayingState(playbackState?.state ?: PlaybackState.STATE_NONE) - val enabled = if (playbackState == null || - playbackState?.getState() == PlaybackState.STATE_NONE || - (duration <= 0)) false else true + val playing = + NotificationMediaManager.isPlayingState( + playbackState?.state ?: PlaybackState.STATE_NONE + ) + val enabled = + if ( + playbackState == null || + playbackState?.getState() == PlaybackState.STATE_NONE || + (duration <= 0) + ) + false + else true _data = Progress(enabled, seekAvailable, playing, scrubbing, position, duration) checkIfPollingNeeded() } @@ -228,26 +243,26 @@ class SeekBarViewModel @Inject constructor( * This should be called when the media session behind the controller has been destroyed. */ @AnyThread - fun clearController() = bgExecutor.execute { - controller = null - playbackState = null - cancel?.run() - cancel = null - _data = _data.copy(enabled = false) - } + fun clearController() = + bgExecutor.execute { + controller = null + playbackState = null + cancel?.run() + cancel = null + _data = _data.copy(enabled = false) + } - /** - * Call to clean up any resources. - */ + /** Call to clean up any resources. */ @AnyThread - fun onDestroy() = bgExecutor.execute { - controller = null - playbackState = null - cancel?.run() - cancel = null - scrubbingChangeListener = null - enabledChangeListener = null - } + fun onDestroy() = + bgExecutor.execute { + controller = null + playbackState = null + cancel?.run() + cancel = null + scrubbingChangeListener = null + enabledChangeListener = null + } @WorkerThread private fun checkPlaybackPosition() { @@ -263,8 +278,12 @@ class SeekBarViewModel @Inject constructor( val needed = listening && !scrubbing && playbackState?.isInMotion() ?: false if (needed) { if (cancel == null) { - cancel = bgExecutor.executeRepeatedly(this::checkPlaybackPosition, 0L, - POSITION_UPDATE_INTERVAL_MILLIS) + cancel = + bgExecutor.executeRepeatedly( + this::checkPlaybackPosition, + 0L, + POSITION_UPDATE_INTERVAL_MILLIS + ) } } else { cancel?.run() @@ -275,7 +294,7 @@ class SeekBarViewModel @Inject constructor( /** Gets a listener to attach to the seek bar to handle seeking. */ val seekBarListener: SeekBar.OnSeekBarChangeListener get() { - return SeekBarChangeListener(this) + return SeekBarChangeListener(this, falsingManager) } /** Attach touch handlers to the seek bar view. */ @@ -315,7 +334,8 @@ class SeekBarViewModel @Inject constructor( } private class SeekBarChangeListener( - val viewModel: SeekBarViewModel + val viewModel: SeekBarViewModel, + val falsingManager: FalsingManager, ) : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) { if (fromUser) { @@ -328,6 +348,9 @@ class SeekBarViewModel @Inject constructor( } override fun onStopTrackingTouch(bar: SeekBar) { + if (falsingManager.isFalseTouch(MEDIA_SEEKBAR)) { + viewModel.onSeekFalse() + } viewModel.onSeek(bar.progress.toLong()) } } @@ -340,15 +363,16 @@ class SeekBarViewModel @Inject constructor( */ private class SeekBarTouchListener( private val viewModel: SeekBarViewModel, - private val bar: SeekBar + private val bar: SeekBar, ) : View.OnTouchListener, GestureDetector.OnGestureListener { // Gesture detector helps decide which touch events to intercept. private val detector = GestureDetectorCompat(bar.context, this) // Velocity threshold used to decide when a fling is considered a false gesture. - private val flingVelocity: Int = ViewConfiguration.get(bar.context).run { - getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_SCALE_FACTOR - } + private val flingVelocity: Int = + ViewConfiguration.get(bar.context).run { + getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_SCALE_FACTOR + } // Indicates if the gesture should go to the seek bar or if it should be intercepted. private var shouldGoToSeekBar = false @@ -378,9 +402,9 @@ class SeekBarViewModel @Inject constructor( /** * Handle down events that press down on the thumb. * - * On the down action, determine a target box around the thumb to know when a scroll - * gesture starts by clicking on the thumb. The target box will be used by subsequent - * onScroll events. + * On the down action, determine a target box around the thumb to know when a scroll gesture + * starts by clicking on the thumb. The target box will be used by subsequent onScroll + * events. * * Returns true when the down event hits within the target box of the thumb. */ @@ -391,17 +415,19 @@ class SeekBarViewModel @Inject constructor( // TODO: account for thumb offset val progress = bar.getProgress() val range = bar.max - bar.min - val widthFraction = if (range > 0) { - (progress - bar.min).toDouble() / range - } else { - 0.0 - } + val widthFraction = + if (range > 0) { + (progress - bar.min).toDouble() / range + } else { + 0.0 + } val availableWidth = bar.width - padL - padR - val thumbX = if (bar.isLayoutRtl()) { - padL + availableWidth * (1 - widthFraction) - } else { - padL + availableWidth * widthFraction - } + val thumbX = + if (bar.isLayoutRtl()) { + padL + availableWidth * (1 - widthFraction) + } else { + padL + availableWidth * widthFraction + } // Set the min, max boundaries of the thumb box. // I'm cheating by using the height of the seek bar as the width of the box. val halfHeight: Int = bar.height / 2 diff --git a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt index 52ac4e0682a3..1a10b18a5a69 100644 --- a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.recommendation import android.view.LayoutInflater import android.view.View @@ -22,6 +22,8 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import com.android.systemui.R +import com.android.systemui.media.controls.models.GutsViewHolder +import com.android.systemui.media.controls.ui.IlluminationDrawable import com.android.systemui.util.animation.TransitionLayout private const val TAG = "RecommendationViewHolder" @@ -33,26 +35,30 @@ class RecommendationViewHolder private constructor(itemView: View) { // Recommendation screen val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon) - val mediaCoverItems = listOf<ImageView>( - itemView.requireViewById(R.id.media_cover1), - itemView.requireViewById(R.id.media_cover2), - itemView.requireViewById(R.id.media_cover3) - ) - val mediaCoverContainers = listOf<ViewGroup>( - itemView.requireViewById(R.id.media_cover1_container), - itemView.requireViewById(R.id.media_cover2_container), - itemView.requireViewById(R.id.media_cover3_container) - ) - val mediaTitles: List<TextView> = listOf( - itemView.requireViewById(R.id.media_title1), - itemView.requireViewById(R.id.media_title2), - itemView.requireViewById(R.id.media_title3) - ) - val mediaSubtitles: List<TextView> = listOf( - itemView.requireViewById(R.id.media_subtitle1), - itemView.requireViewById(R.id.media_subtitle2), - itemView.requireViewById(R.id.media_subtitle3) - ) + val mediaCoverItems = + listOf<ImageView>( + itemView.requireViewById(R.id.media_cover1), + itemView.requireViewById(R.id.media_cover2), + itemView.requireViewById(R.id.media_cover3) + ) + val mediaCoverContainers = + listOf<ViewGroup>( + itemView.requireViewById(R.id.media_cover1_container), + itemView.requireViewById(R.id.media_cover2_container), + itemView.requireViewById(R.id.media_cover3_container) + ) + val mediaTitles: List<TextView> = + listOf( + itemView.requireViewById(R.id.media_title1), + itemView.requireViewById(R.id.media_title2), + itemView.requireViewById(R.id.media_title3) + ) + val mediaSubtitles: List<TextView> = + listOf( + itemView.requireViewById(R.id.media_subtitle1), + itemView.requireViewById(R.id.media_subtitle2), + itemView.requireViewById(R.id.media_subtitle3) + ) val gutsViewHolder = GutsViewHolder(itemView) @@ -76,13 +82,14 @@ class RecommendationViewHolder private constructor(itemView: View) { * @param inflater LayoutInflater to use to inflate the layout. * @param parent Parent of inflated view. */ - @JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup): - RecommendationViewHolder { + @JvmStatic + fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder { val itemView = inflater.inflate( R.layout.media_smartspace_recommendations, parent, - false /* attachToRoot */) + false /* attachToRoot */ + ) // Because this media view (a TransitionLayout) is used to measure and layout the views // in various states before being attached to its parent, we can't depend on the default // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction. @@ -91,20 +98,38 @@ class RecommendationViewHolder private constructor(itemView: View) { } // Res Ids for the control components on the recommendation view. - val controlsIds = setOf( - R.id.recommendation_card_icon, - R.id.media_cover1, - R.id.media_cover2, - R.id.media_cover3, - R.id.media_cover1_container, - R.id.media_cover2_container, - R.id.media_cover3_container, - R.id.media_title1, - R.id.media_title2, - R.id.media_title3, - R.id.media_subtitle1, - R.id.media_subtitle2, - R.id.media_subtitle3 - ) + val controlsIds = + setOf( + R.id.recommendation_card_icon, + R.id.media_cover1, + R.id.media_cover2, + R.id.media_cover3, + R.id.media_cover1_container, + R.id.media_cover2_container, + R.id.media_cover3_container, + R.id.media_title1, + R.id.media_title2, + R.id.media_title3, + R.id.media_subtitle1, + R.id.media_subtitle2, + R.id.media_subtitle3 + ) + + val mediaTitlesAndSubtitlesIds = + setOf( + R.id.media_title1, + R.id.media_title2, + R.id.media_title3, + R.id.media_subtitle1, + R.id.media_subtitle2, + R.id.media_subtitle3 + ) + + val mediaContainersIds = + setOf( + R.id.media_cover1_container, + R.id.media_cover2_container, + R.id.media_cover3_container + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt index c8f17d93bcc8..1df42c641df6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.recommendation import android.app.smartspace.SmartspaceAction import android.content.Context @@ -22,55 +22,41 @@ import android.content.Intent import android.content.pm.PackageManager import android.text.TextUtils import android.util.Log +import androidx.annotation.VisibleForTesting import com.android.internal.logging.InstanceId -import com.android.systemui.media.MediaControlPanel.KEY_SMARTSPACE_APP_NAME + +@VisibleForTesting const val KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME" /** State of a Smartspace media recommendations view. */ data class SmartspaceMediaData( - /** - * Unique id of a Smartspace media target. - */ + /** Unique id of a Smartspace media target. */ val targetId: String, - /** - * Indicates if the status is active. - */ + /** Indicates if the status is active. */ val isActive: Boolean, - /** - * Package name of the media recommendations' provider-app. - */ + /** Package name of the media recommendations' provider-app. */ val packageName: String, - /** - * Action to perform when the card is tapped. Also contains the target's extra info. - */ + /** Action to perform when the card is tapped. Also contains the target's extra info. */ val cardAction: SmartspaceAction?, - /** - * List of media recommendations. - */ + /** List of media recommendations. */ val recommendations: List<SmartspaceAction>, - /** - * Intent for the user's initiated dismissal. - */ + /** Intent for the user's initiated dismissal. */ val dismissIntent: Intent?, - /** - * The timestamp in milliseconds that headphone is connected. - */ + /** The timestamp in milliseconds that headphone is connected. */ val headphoneConnectionTimeMillis: Long, - /** - * Instance ID for [MediaUiEventLogger] - */ + /** Instance ID for [MediaUiEventLogger] */ val instanceId: InstanceId ) { /** * Indicates if all the data is valid. * * TODO(b/230333302): Make MediaControlPanel more flexible so that we can display fewer than + * ``` * [NUM_REQUIRED_RECOMMENDATIONS]. + * ``` */ fun isValid() = getValidRecommendations().size >= NUM_REQUIRED_RECOMMENDATIONS - /** - * Returns the list of [recommendations] that have valid data. - */ + /** Returns the list of [recommendations] that have valid data. */ fun getValidRecommendations() = recommendations.filter { it.icon != null } /** Returns the upstream app name if available. */ @@ -89,9 +75,10 @@ data class SmartspaceMediaData( Log.w( TAG, "Package $packageName does not have a main launcher activity. " + - "Fallback to full app name") + "Fallback to full app name" + ) return try { - val applicationInfo = packageManager.getApplicationInfo(packageName, /* flags= */ 0) + val applicationInfo = packageManager.getApplicationInfo(packageName, /* flags= */ 0) packageManager.getApplicationLabel(applicationInfo) } catch (e: PackageManager.NameNotFoundException) { null diff --git a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataProvider.kt index 140a1fef93f7..a7ed69a9ab73 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataProvider.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.models.recommendation import android.app.smartspace.SmartspaceTarget import android.util.Log @@ -23,7 +39,7 @@ class SmartspaceMediaDataProvider @Inject constructor() : BcSmartspaceDataPlugin smartspaceMediaTargetListeners.remove(smartspaceTargetListener) } - /** Updates Smartspace data and propagates it to any listeners. */ + /** Updates Smartspace data and propagates it to any listeners. */ override fun onTargetsAvailable(targets: List<SmartspaceTarget>) { // Filter out non-media targets. val mediaTargets = mutableListOf<SmartspaceTarget>() diff --git a/packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt index 94a0835f22f8..ff763d81f950 100644 --- a/packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt @@ -14,20 +14,18 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.content.Context - import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.media.InfoMediaManager import com.android.settingslib.media.LocalMediaManager - import javax.inject.Inject -/** - * Factory to create [LocalMediaManager] objects. - */ -class LocalMediaManagerFactory @Inject constructor( +/** Factory to create [LocalMediaManager] objects. */ +class LocalMediaManagerFactory +@Inject +constructor( private val context: Context, private val localBluetoothManager: LocalBluetoothManager? ) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatest.kt index 311973ad5af0..789ef407ea9d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatest.kt @@ -14,15 +14,16 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import javax.inject.Inject -/** - * Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events. - */ -class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, - MediaDeviceManager.Listener { +/** Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events. */ +class MediaDataCombineLatest @Inject constructor() : + MediaDataManager.Listener, MediaDeviceManager.Listener { private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf() @@ -60,11 +61,7 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } } - override fun onMediaDeviceChanged( - key: String, - oldKey: String?, - data: MediaDeviceData? - ) { + override fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) { if (oldKey != null && oldKey != key && entries.contains(oldKey)) { entries[key] = entries.remove(oldKey)?.first to data update(key, oldKey) @@ -83,9 +80,7 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, */ fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) - /** - * Remove a listener registered with addListener. - */ + /** Remove a listener registered with addListener. */ fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) private fun update(key: String, oldKey: String?) { @@ -93,18 +88,14 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, if (entry != null && device != null) { val data = entry.copy(device = device) val listenersCopy = listeners.toSet() - listenersCopy.forEach { - it.onMediaDataLoaded(key, oldKey, data) - } + listenersCopy.forEach { it.onMediaDataLoaded(key, oldKey, data) } } } private fun remove(key: String) { entries.remove(key)?.let { val listenersCopy = listeners.toSet() - listenersCopy.forEach { - it.onMediaDataRemoved(key) - } + listenersCopy.forEach { it.onMediaDataRemoved(key) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt index e0c8d66cb6fd..45b319b274b2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.content.Context import android.os.SystemProperties @@ -23,6 +23,9 @@ import com.android.internal.annotations.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.settings.CurrentUserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.util.time.SystemClock @@ -34,7 +37,8 @@ import kotlin.collections.LinkedHashMap private const val TAG = "MediaDataFilter" private const val DEBUG = true -private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = ("com.google" + +private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = + ("com.google" + ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity") private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds" @@ -43,8 +47,8 @@ private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age * available within this time window, smartspace recommendations will be shown instead. */ @VisibleForTesting -internal val SMARTSPACE_MAX_AGE = SystemProperties - .getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30)) +internal val SMARTSPACE_MAX_AGE = + SystemProperties.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30)) /** * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user @@ -54,7 +58,9 @@ internal val SMARTSPACE_MAX_AGE = SystemProperties * This is added at the end of the pipeline since we may still need to handle callbacks from * background users (e.g. timeouts). */ -class MediaDataFilter @Inject constructor( +class MediaDataFilter +@Inject +constructor( private val context: Context, private val broadcastDispatcher: BroadcastDispatcher, private val broadcastSender: BroadcastSender, @@ -76,12 +82,13 @@ class MediaDataFilter @Inject constructor( private var reactivatedKey: String? = null init { - userTracker = object : CurrentUserTracker(broadcastDispatcher) { - override fun onUserSwitched(newUserId: Int) { - // Post this so we can be sure lockscreenUserManager already got the broadcast - executor.execute { handleUserSwitched(newUserId) } + userTracker = + object : CurrentUserTracker(broadcastDispatcher) { + override fun onUserSwitched(newUserId: Int) { + // Post this so we can be sure lockscreenUserManager already got the broadcast + executor.execute { handleUserSwitched(newUserId) } + } } - } userTracker.startTracking() } @@ -108,9 +115,7 @@ class MediaDataFilter @Inject constructor( userEntries.put(key, data) // Notify listeners - listeners.forEach { - it.onMediaDataLoaded(key, oldKey, data) - } + listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) } } override fun onSmartspaceMediaDataLoaded( @@ -128,14 +133,11 @@ class MediaDataFilter @Inject constructor( smartspaceMediaData = data // Before forwarding the smartspace target, first check if we have recently inactive media - val sorted = userEntries.toSortedMap(compareBy { - userEntries.get(it)?.lastActive ?: -1 - }) + val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 }) val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted) var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE data.cardAction?.let { - val smartspaceMaxAgeSeconds = - it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0) + val smartspaceMaxAgeSeconds = it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0) if (smartspaceMaxAgeSeconds > 0) { smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds) } @@ -152,13 +154,21 @@ class MediaDataFilter @Inject constructor( Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") reactivatedKey = lastActiveKey val mediaData = sorted.get(lastActiveKey)!!.copy(active = true) - logger.logRecommendationActivated(mediaData.appUid, mediaData.packageName, - mediaData.instanceId) + logger.logRecommendationActivated( + mediaData.appUid, + mediaData.packageName, + mediaData.instanceId + ) listeners.forEach { - it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, - receivedSmartspaceCardLatency = + it.onMediaDataLoaded( + lastActiveKey, + lastActiveKey, + mediaData, + receivedSmartspaceCardLatency = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis) - .toInt(), isSsReactivated = true) + .toInt(), + isSsReactivated = true + ) } } } else { @@ -170,8 +180,10 @@ class MediaDataFilter @Inject constructor( Log.d(TAG, "Invalid recommendation data. Skip showing the rec card") return } - logger.logRecommendationAdded(smartspaceMediaData.packageName, - smartspaceMediaData.instanceId) + logger.logRecommendationAdded( + smartspaceMediaData.packageName, + smartspaceMediaData.instanceId + ) listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } } @@ -179,9 +191,7 @@ class MediaDataFilter @Inject constructor( allEntries.remove(key) userEntries.remove(key)?.let { // Only notify listeners if something actually changed - listeners.forEach { - it.onMediaDataRemoved(key) - } + listeners.forEach { it.onMediaDataRemoved(key) } } } @@ -194,16 +204,17 @@ class MediaDataFilter @Inject constructor( // Notify listeners to update with actual active value userEntries.get(lastActiveKey)?.let { mediaData -> listeners.forEach { - it.onMediaDataLoaded( - lastActiveKey, lastActiveKey, mediaData, immediately) + it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately) } } } if (smartspaceMediaData.isActive) { - smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId) + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) } listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } } @@ -218,25 +229,19 @@ class MediaDataFilter @Inject constructor( userEntries.clear() keyCopy.forEach { if (DEBUG) Log.d(TAG, "Removing $it after user change") - listenersCopy.forEach { listener -> - listener.onMediaDataRemoved(it) - } + listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) } } allEntries.forEach { (key, data) -> if (lockscreenUserManager.isCurrentProfile(data.userId)) { if (DEBUG) Log.d(TAG, "Re-adding $key after user change") userEntries.put(key, data) - listenersCopy.forEach { listener -> - listener.onMediaDataLoaded(key, null, data) - } + listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) } } } } - /** - * Invoked when the user has dismissed the media carousel - */ + /** Invoked when the user has dismissed the media carousel */ fun onSwipeToDismiss() { if (DEBUG) Log.d(TAG, "Media carousel swiped away") val mediaKeys = userEntries.keys.toSet() @@ -247,55 +252,52 @@ class MediaDataFilter @Inject constructor( if (smartspaceMediaData.isActive) { val dismissIntent = smartspaceMediaData.dismissIntent if (dismissIntent == null) { - Log.w(TAG, "Cannot create dismiss action click action: " + - "extras missing dismiss_intent.") - } else if (dismissIntent.getComponent() != null && - dismissIntent.getComponent().getClassName() - == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME) { + Log.w( + TAG, + "Cannot create dismiss action click action: " + "extras missing dismiss_intent." + ) + } else if ( + dismissIntent.getComponent() != null && + dismissIntent.getComponent().getClassName() == + EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME + ) { // Dismiss the card Smartspace data through Smartspace trampoline activity. context.startActivity(dismissIntent) } else { broadcastSender.sendBroadcast(dismissIntent) } - smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId) - mediaDataManager.dismissSmartspaceRecommendation(smartspaceMediaData.targetId, - delay = 0L) + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) + mediaDataManager.dismissSmartspaceRecommendation( + smartspaceMediaData.targetId, + delay = 0L + ) } } - /** - * Are there any active media entries, including the recommendation? - */ - fun hasActiveMediaOrRecommendation() = userEntries.any { it.value.active } || + /** Are there any active media entries, including the recommendation? */ + fun hasActiveMediaOrRecommendation() = + userEntries.any { it.value.active } || (smartspaceMediaData.isActive && (smartspaceMediaData.isValid() || reactivatedKey != null)) - /** - * Are there any media entries we should display? - */ - fun hasAnyMediaOrRecommendation() = userEntries.isNotEmpty() || - (smartspaceMediaData.isActive && smartspaceMediaData.isValid()) + /** Are there any media entries we should display? */ + fun hasAnyMediaOrRecommendation() = + userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid()) - /** - * Are there any media notifications active (excluding the recommendation)? - */ + /** Are there any media notifications active (excluding the recommendation)? */ fun hasActiveMedia() = userEntries.any { it.value.active } - /** - * Are there any media entries we should display (excluding the recommendation)? - */ + /** Are there any media entries we should display (excluding the recommendation)? */ fun hasAnyMedia() = userEntries.isNotEmpty() - /** - * Add a listener for filtered [MediaData] changes - */ + /** Add a listener for filtered [MediaData] changes */ fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener) - /** - * Remove a listener that was registered with addListener - */ + /** Remove a listener that was registered with addListener */ fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) /** @@ -315,8 +317,6 @@ class MediaDataFilter @Inject constructor( val now = systemClock.elapsedRealtime() val lastActiveKey = sortedEntries.lastKey() // most recently active - return sortedEntries.get(lastActiveKey)?.let { - now - it.lastActive - } ?: Long.MAX_VALUE + return sortedEntries.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index 896fb4765c57..14dd99023b92 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.app.Notification import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME @@ -57,6 +57,17 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.models.player.MediaAction +import com.android.systemui.media.controls.models.player.MediaButton +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.models.player.MediaViewHolder +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.resume.MediaResumeListener +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState @@ -75,17 +86,19 @@ import java.util.concurrent.Executors import javax.inject.Inject // URI fields to try loading album art from -private val ART_URIS = arrayOf( +private val ART_URIS = + arrayOf( MediaMetadata.METADATA_KEY_ALBUM_ART_URI, MediaMetadata.METADATA_KEY_ART_URI, MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI -) + ) private const val TAG = "MediaDataManager" private const val DEBUG = true private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent" -private val LOADING = MediaData( +private val LOADING = + MediaData( userId = -1, initialized = false, app = null, @@ -102,37 +115,41 @@ private val LOADING = MediaData( active = true, resumeAction = null, instanceId = InstanceId.fakeInstanceId(-1), - appUid = Process.INVALID_UID) + appUid = Process.INVALID_UID + ) @VisibleForTesting -internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData( - targetId = "INVALID", - isActive = false, - packageName = "INVALID", - cardAction = null, - recommendations = emptyList(), - dismissIntent = null, - headphoneConnectionTimeMillis = 0, - instanceId = InstanceId.fakeInstanceId(-1)) +internal val EMPTY_SMARTSPACE_MEDIA_DATA = + SmartspaceMediaData( + targetId = "INVALID", + isActive = false, + packageName = "INVALID", + cardAction = null, + recommendations = emptyList(), + dismissIntent = null, + headphoneConnectionTimeMillis = 0, + instanceId = InstanceId.fakeInstanceId(-1) + ) fun isMediaNotification(sbn: StatusBarNotification): Boolean { return sbn.notification.isMediaNotification() } /** - * Allow recommendations from smartspace to show in media controls. - * Requires [Utils.useQsMediaPlayer] to be enabled. - * On by default, but can be disabled by setting to 0 + * Allow recommendations from smartspace to show in media controls. Requires + * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0 */ private fun allowMediaRecommendations(context: Context): Boolean { - val flag = Settings.Secure.getInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1) + val flag = + Settings.Secure.getInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 + ) return Utils.useQsMediaPlayer(context) && flag > 0 } -/** - * A class that facilitates management and loading of Media Data, ready for binding. - */ +/** A class that facilitates management and loading of Media Data, ready for binding. */ @SysUISingleton class MediaDataManager( private val context: Context, @@ -159,24 +176,24 @@ class MediaDataManager( companion object { // UI surface label for subscribing Smartspace updates. - @JvmField - val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" + @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" // Smartspace package name's extra key. - @JvmField - val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" + @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" // Maximum number of actions allowed in compact view - @JvmField - val MAX_COMPACT_ACTIONS = 3 + @JvmField val MAX_COMPACT_ACTIONS = 3 // Maximum number of actions allowed in expanded view - @JvmField - val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size + @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size } - private val themeText = com.android.settingslib.Utils.getColorAttr(context, - com.android.internal.R.attr.textColorPrimary).defaultColor + private val themeText = + com.android.settingslib.Utils.getColorAttr( + context, + com.android.internal.R.attr.textColorPrimary + ) + .defaultColor // Internal listeners are part of the internal pipeline. External listeners (those registered // with [MediaDeviceManager.addListener]) receive events after they have propagated through @@ -192,9 +209,7 @@ class MediaDataManager( private var smartspaceSession: SmartspaceSession? = null private var allowMediaRecommendations = allowMediaRecommendations(context) - /** - * Check whether this notification is an RCN - */ + /** Check whether this notification is an RCN */ private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean { return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) } @@ -219,29 +234,44 @@ class MediaDataManager( tunerService: TunerService, mediaFlags: MediaFlags, logger: MediaUiEventLogger - ) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory, - broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener, - mediaSessionBasedFilter, mediaDeviceManager, mediaDataCombineLatest, mediaDataFilter, - activityStarter, smartspaceMediaDataProvider, Utils.useMediaResumption(context), - Utils.useQsMediaPlayer(context), clock, tunerService, mediaFlags, logger) - - private val appChangeReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - when (intent.action) { - Intent.ACTION_PACKAGES_SUSPENDED -> { - val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) - packages?.forEach { - removeAllForPackage(it) + ) : this( + context, + backgroundExecutor, + foregroundExecutor, + mediaControllerFactory, + broadcastDispatcher, + dumpManager, + mediaTimeoutListener, + mediaResumeListener, + mediaSessionBasedFilter, + mediaDeviceManager, + mediaDataCombineLatest, + mediaDataFilter, + activityStarter, + smartspaceMediaDataProvider, + Utils.useMediaResumption(context), + Utils.useQsMediaPlayer(context), + clock, + tunerService, + mediaFlags, + logger + ) + + private val appChangeReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + Intent.ACTION_PACKAGES_SUSPENDED -> { + val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) + packages?.forEach { removeAllForPackage(it) } } - } - Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_RESTARTED -> { - intent.data?.encodedSchemeSpecificPart?.let { - removeAllForPackage(it) + Intent.ACTION_PACKAGE_REMOVED, + Intent.ACTION_PACKAGE_RESTARTED -> { + intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) } } } } } - } init { dumpManager.registerDumpable(TAG, this) @@ -262,20 +292,23 @@ class MediaDataManager( // Set up links back into the pipeline for listeners that need to send events upstream. mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> - setTimedOut(key, timedOut) } + setTimedOut(key, timedOut) + } mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> - updateState(key, state) } + updateState(key, state) + } mediaResumeListener.setManager(this) mediaDataFilter.mediaDataManager = this val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) - val uninstallFilter = IntentFilter().apply { - addAction(Intent.ACTION_PACKAGE_REMOVED) - addAction(Intent.ACTION_PACKAGE_RESTARTED) - addDataScheme("package") - } + val uninstallFilter = + IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_REMOVED) + addAction(Intent.ACTION_PACKAGE_RESTARTED) + addDataScheme("package") + } // BroadcastDispatcher does not allow filters with data schemes context.registerReceiver(appChangeReceiver, uninstallFilter) @@ -283,8 +316,10 @@ class MediaDataManager( smartspaceMediaDataProvider.registerListener(this) val smartspaceManager: SmartspaceManager = context.getSystemService(SmartspaceManager::class.java) - smartspaceSession = smartspaceManager.createSmartspaceSession( - SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()) + smartspaceSession = + smartspaceManager.createSmartspaceSession( + SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build() + ) smartspaceSession?.let { it.addOnTargetsAvailableListener( // Use a new thread listening to Smartspace updates instead of using the existing @@ -296,17 +331,24 @@ class MediaDataManager( Executors.newCachedThreadPool(), SmartspaceSession.OnTargetsAvailableListener { targets -> smartspaceMediaDataProvider.onTargetsAvailable(targets) - }) + } + ) } smartspaceSession?.let { it.requestSmartspaceUpdate() } - tunerService.addTunable(object : TunerService.Tunable { - override fun onTuningChanged(key: String?, newValue: String?) { - allowMediaRecommendations = allowMediaRecommendations(context) - if (!allowMediaRecommendations) { - dismissSmartspaceRecommendation(key = smartspaceMediaData.targetId, delay = 0L) + tunerService.addTunable( + object : TunerService.Tunable { + override fun onTuningChanged(key: String?, newValue: String?) { + allowMediaRecommendations = allowMediaRecommendations(context) + if (!allowMediaRecommendations) { + dismissSmartspaceRecommendation( + key = smartspaceMediaData.targetId, + delay = 0L + ) + } } - } - }, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION) + }, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION + ) } fun destroy() { @@ -321,10 +363,7 @@ class MediaDataManager( val oldKey = findExistingEntry(key, sbn.packageName) if (oldKey == null) { val instanceId = logger.getNewInstanceId() - val temp = LOADING.copy( - packageName = sbn.packageName, - instanceId = instanceId - ) + val temp = LOADING.copy(packageName = sbn.packageName, instanceId = instanceId) mediaEntries.put(key, temp) logEvent = true } else if (oldKey != key) { @@ -342,9 +381,7 @@ class MediaDataManager( private fun removeAllForPackage(packageName: String) { Assert.isMainThread() val toRemove = mediaEntries.filter { it.value.packageName == packageName } - toRemove.forEach { - removeEntry(it.key) - } + toRemove.forEach { removeEntry(it.key) } } fun setResumeAction(key: String, action: Runnable?) { @@ -366,32 +403,41 @@ class MediaDataManager( // Resume controls don't have a notification key, so store by package name instead if (!mediaEntries.containsKey(packageName)) { val instanceId = logger.getNewInstanceId() - val appUid = try { - context.packageManager.getApplicationInfo(packageName, 0)?.uid!! - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Could not get app UID for $packageName", e) - Process.INVALID_UID - } + val appUid = + try { + context.packageManager.getApplicationInfo(packageName, 0)?.uid!! + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Could not get app UID for $packageName", e) + Process.INVALID_UID + } - val resumeData = LOADING.copy( - packageName = packageName, - resumeAction = action, - hasCheckedForResume = true, - instanceId = instanceId, - appUid = appUid - ) + val resumeData = + LOADING.copy( + packageName = packageName, + resumeAction = action, + hasCheckedForResume = true, + instanceId = instanceId, + appUid = appUid + ) mediaEntries.put(packageName, resumeData) logger.logResumeMediaAdded(appUid, packageName, instanceId) } backgroundExecutor.execute { - loadMediaDataInBgForResumption(userId, desc, action, token, appName, appIntent, - packageName) + loadMediaDataInBgForResumption( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) } } /** - * Check if there is an existing entry that matches the key or package name. - * Returns the key that matches, or null if not found. + * Check if there is an existing entry that matches the key or package name. Returns the key + * that matches, or null if not found. */ private fun findExistingEntry(key: String, packageName: String): String? { if (mediaEntries.containsKey(key)) { @@ -410,32 +456,24 @@ class MediaDataManager( oldKey: String?, logEvent: Boolean = false ) { - backgroundExecutor.execute { - loadMediaDataInBg(key, sbn, oldKey, logEvent) - } + backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, logEvent) } } - /** - * Add a listener for changes in this class - */ + /** Add a listener for changes in this class */ fun addListener(listener: Listener) { // mediaDataFilter is the current end of the internal pipeline. Register external // listeners as listeners to it. mediaDataFilter.addListener(listener) } - /** - * Remove a listener for changes in this class - */ + /** Remove a listener for changes in this class */ fun removeListener(listener: Listener) { // Since mediaDataFilter is the current end of the internal pipelie, external listeners // have been registered to it. So, they need to be removed from it too. mediaDataFilter.removeListener(listener) } - /** - * Add a listener for internal events. - */ + /** Add a listener for internal events. */ private fun addInternalListener(listener: Listener) = internalListeners.add(listener) /** @@ -483,8 +521,8 @@ class MediaDataManager( } /** - * Called whenever the player has been paused or stopped for a while, or swiped from QQS. - * This will make the player not active anymore, hiding it from QQS and Keyguard. + * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This + * will make the player not active anymore, hiding it from QQS and Keyguard. * @see MediaData.active */ internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) { @@ -506,9 +544,7 @@ class MediaDataManager( } } - /** - * Called when the player's [PlaybackState] has been updated with new actions and/or state - */ + /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ private fun updateState(key: String, state: PlaybackState) { mediaEntries.get(key)?.let { val token = it.token @@ -516,22 +552,23 @@ class MediaDataManager( if (DEBUG) Log.d(TAG, "State updated, but token was null") return } - val actions = createActionsFromState(it.packageName, - mediaControllerFactory.create(it.token), UserHandle(it.userId)) + val actions = + createActionsFromState( + it.packageName, + mediaControllerFactory.create(it.token), + UserHandle(it.userId) + ) // Control buttons // If flag is enabled and controller has a PlaybackState, // create actions from session info // otherwise, no need to update semantic actions. - val data = if (actions != null) { - it.copy( - semanticActions = actions, - isPlaying = isPlayingState(state.state)) - } else { - it.copy( - isPlaying = isPlayingState(state.state) - ) - } + val data = + if (actions != null) { + it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) + } else { + it.copy(isPlaying = isPlayingState(state.state)) + } if (DEBUG) Log.d(TAG, "State updated outside of notification") onMediaDataLoaded(key, key, data) } @@ -544,9 +581,7 @@ class MediaDataManager( notifyMediaDataRemoved(key) } - /** - * Dismiss a media entry. Returns false if the key was not found. - */ + /** Dismiss a media entry. Returns false if the key was not found. */ fun dismissMediaData(key: String, delay: Long): Boolean { val existed = mediaEntries[key] != null backgroundExecutor.execute { @@ -564,9 +599,8 @@ class MediaDataManager( } /** - * Called whenever the recommendation has been expired, or swiped from QQS. - * This will make the recommendation view to not be shown anymore during this headphone - * connection session. + * Called whenever the recommendation has been expired, or swiped from QQS. This will make the + * recommendation view to not be shown anymore during this headphone connection session. */ fun dismissSmartspaceRecommendation(key: String, delay: Long) { if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { @@ -576,13 +610,16 @@ class MediaDataManager( if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target") if (smartspaceMediaData.isActive) { - smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId) + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) } foregroundExecutor.executeDelayed( - { notifySmartspaceMediaDataRemoved( - smartspaceMediaData.targetId, immediately = true) }, delay) + { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) }, + delay + ) } private fun loadMediaDataInBgForResumption( @@ -610,11 +647,12 @@ class MediaDataManager( if (artworkBitmap == null && desc.iconUri != null) { artworkBitmap = loadBitmapFromUri(desc.iconUri!!) } - val artworkIcon = if (artworkBitmap != null) { - Icon.createWithBitmap(artworkBitmap) - } else { - null - } + val artworkIcon = + if (artworkBitmap != null) { + Icon.createWithBitmap(artworkBitmap) + } else { + null + } val currentEntry = mediaEntries.get(packageName) val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() @@ -623,13 +661,34 @@ class MediaDataManager( val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() foregroundExecutor.execute { - onMediaDataLoaded(packageName, null, MediaData(userId, true, appName, - null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0), - MediaButton(playOrPause = mediaAction), packageName, token, appIntent, - device = null, active = false, - resumeAction = resumeAction, resumption = true, notificationKey = packageName, - hasCheckedForResume = true, lastActive = lastActive, instanceId = instanceId, - appUid = appUid)) + onMediaDataLoaded( + packageName, + null, + MediaData( + userId, + true, + appName, + null, + desc.subtitle, + desc.title, + artworkIcon, + listOf(mediaAction), + listOf(0), + MediaButton(playOrPause = mediaAction), + packageName, + token, + appIntent, + device = null, + active = false, + resumeAction = resumeAction, + resumption = true, + notificationKey = packageName, + hasCheckedForResume = true, + lastActive = lastActive, + instanceId = instanceId, + appUid = appUid + ) + ) } } @@ -639,8 +698,11 @@ class MediaDataManager( oldKey: String?, logEvent: Boolean = false ) { - val token = sbn.notification.extras.getParcelable( - Notification.EXTRA_MEDIA_SESSION, MediaSession.Token::class.java) + val token = + sbn.notification.extras.getParcelable( + Notification.EXTRA_MEDIA_SESSION, + MediaSession.Token::class.java + ) if (token == null) { return } @@ -648,10 +710,12 @@ class MediaDataManager( val metadata = mediaController.metadata val notif: Notification = sbn.notification - val appInfo = notif.extras.getParcelable( - Notification.EXTRA_BUILDER_APPLICATION_INFO, - ApplicationInfo::class.java - ) ?: getAppInfoFromPackage(sbn.packageName) + val appInfo = + notif.extras.getParcelable( + Notification.EXTRA_BUILDER_APPLICATION_INFO, + ApplicationInfo::class.java + ) + ?: getAppInfoFromPackage(sbn.packageName) // Album art var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } @@ -661,11 +725,12 @@ class MediaDataManager( if (artworkBitmap == null) { artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) } - val artWorkIcon = if (artworkBitmap == null) { - notif.getLargeIcon() - } else { - Icon.createWithBitmap(artworkBitmap) - } + val artWorkIcon = + if (artworkBitmap == null) { + notif.getLargeIcon() + } else { + Icon.createWithBitmap(artworkBitmap) + } // App name val appName = getAppName(sbn, appInfo) @@ -694,17 +759,27 @@ class MediaDataManager( val extras = sbn.notification.extras val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null) val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1) - val deviceIntent = extras.getParcelable( - Notification.EXTRA_MEDIA_REMOTE_INTENT, PendingIntent::class.java) + val deviceIntent = + extras.getParcelable( + Notification.EXTRA_MEDIA_REMOTE_INTENT, + PendingIntent::class.java + ) Log.d(TAG, "$key is RCN for $deviceName") if (deviceName != null && deviceIcon > -1) { // Name and icon must be present, but intent may be null val enabled = deviceIntent != null && deviceIntent.isActivity - val deviceDrawable = Icon.createWithResource(sbn.packageName, deviceIcon) + val deviceDrawable = + Icon.createWithResource(sbn.packageName, deviceIcon) .loadDrawable(sbn.getPackageContext(context)) - device = MediaDeviceData(enabled, deviceDrawable, deviceName, deviceIntent, - showBroadcastButton = false) + device = + MediaDeviceData( + enabled, + deviceDrawable, + deviceName, + deviceIntent, + showBroadcastButton = false + ) } } @@ -721,10 +796,13 @@ class MediaDataManager( } val playbackLocation = - if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE - else if (mediaController.playbackInfo?.playbackType == - MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) MediaData.PLAYBACK_LOCAL - else MediaData.PLAYBACK_CAST_LOCAL + if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE + else if ( + mediaController.playbackInfo?.playbackType == + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL + ) + MediaData.PLAYBACK_LOCAL + else MediaData.PLAYBACK_CAST_LOCAL val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null val currentEntry = mediaEntries.get(key) @@ -742,13 +820,36 @@ class MediaDataManager( val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true val active = mediaEntries[key]?.active ?: true - onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, appName, - smallIcon, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed, - semanticActions, sbn.packageName, token, notif.contentIntent, device, - active, resumeAction = resumeAction, playbackLocation = playbackLocation, - notificationKey = key, hasCheckedForResume = hasCheckedForResume, - isPlaying = isPlaying, isClearable = sbn.isClearable(), - lastActive = lastActive, instanceId = instanceId, appUid = appUid)) + onMediaDataLoaded( + key, + oldKey, + MediaData( + sbn.normalizedUserId, + true, + appName, + smallIcon, + artist, + song, + artWorkIcon, + actionIcons, + actionsToShowCollapsed, + semanticActions, + sbn.packageName, + token, + notif.contentIntent, + device, + active, + resumeAction = resumeAction, + playbackLocation = playbackLocation, + notificationKey = key, + hasCheckedForResume = hasCheckedForResume, + isPlaying = isPlaying, + isClearable = sbn.isClearable(), + lastActive = lastActive, + instanceId = instanceId, + appUid = appUid + ) + ) } } @@ -774,27 +875,33 @@ class MediaDataManager( } } - /** - * Generate action buttons based on notification actions - */ - private fun createActionsFromNotification(sbn: StatusBarNotification): - Pair<List<MediaAction>, List<Int>> { + /** Generate action buttons based on notification actions */ + private fun createActionsFromNotification( + sbn: StatusBarNotification + ): Pair<List<MediaAction>, List<Int>> { val notif = sbn.notification val actionIcons: MutableList<MediaAction> = ArrayList() val actions = notif.actions - var actionsToShowCollapsed = notif.extras.getIntArray( - Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() ?: mutableListOf() + var actionsToShowCollapsed = + notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() + ?: mutableListOf() if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { - Log.e(TAG, "Too many compact actions for ${sbn.key}," + - "limiting to first $MAX_COMPACT_ACTIONS") + Log.e( + TAG, + "Too many compact actions for ${sbn.key}," + + "limiting to first $MAX_COMPACT_ACTIONS" + ) actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) } if (actions != null) { for ((index, action) in actions.withIndex()) { if (index == MAX_NOTIFICATION_ACTIONS) { - Log.w(TAG, "Too many notification actions for ${sbn.key}," + - " limiting to first $MAX_NOTIFICATION_ACTIONS") + Log.w( + TAG, + "Too many notification actions for ${sbn.key}," + + " limiting to first $MAX_NOTIFICATION_ACTIONS" + ) break } if (action.getIcon() == null) { @@ -802,33 +909,38 @@ class MediaDataManager( actionsToShowCollapsed.remove(index) continue } - val runnable = if (action.actionIntent != null) { - Runnable { - if (action.actionIntent.isActivity) { - activityStarter.startPendingIntentDismissingKeyguard( - action.actionIntent) - } else if (action.isAuthenticationRequired()) { - activityStarter.dismissKeyguardThenExecute({ - var result = sendPendingIntent(action.actionIntent) - result - }, {}, true) - } else { - sendPendingIntent(action.actionIntent) + val runnable = + if (action.actionIntent != null) { + Runnable { + if (action.actionIntent.isActivity) { + activityStarter.startPendingIntentDismissingKeyguard( + action.actionIntent + ) + } else if (action.isAuthenticationRequired()) { + activityStarter.dismissKeyguardThenExecute( + { + var result = sendPendingIntent(action.actionIntent) + result + }, + {}, + true + ) + } else { + sendPendingIntent(action.actionIntent) + } } + } else { + null } - } else { - null - } - val mediaActionIcon = if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { - Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) - } else { - action.getIcon() - }.setTint(themeText).loadDrawable(context) - val mediaAction = MediaAction( - mediaActionIcon, - runnable, - action.title, - null) + val mediaActionIcon = + if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { + Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) + } else { + action.getIcon() + } + .setTint(themeText) + .loadDrawable(context) + val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) actionIcons.add(mediaAction) } } @@ -841,7 +953,9 @@ class MediaDataManager( * @param packageName Package name for the media app * @param controller MediaController for the current session * @return a Pair consisting of a list of media actions, and a list of ints representing which + * ``` * of those actions should be shown in the compact player + * ``` */ private fun createActionsFromState( packageName: String, @@ -854,59 +968,69 @@ class MediaDataManager( } // First, check for standard actions - val playOrPause = if (isConnectingState(state.state)) { - // Spinner needs to be animating to render anything. Start it here. - val drawable = context.getDrawable( - com.android.internal.R.drawable.progress_small_material) - (drawable as Animatable).start() - MediaAction( - drawable, - null, // no action to perform when clicked - context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), - // Specify a rebind id to prevent the spinner from restarting on later binds. - com.android.internal.R.drawable.progress_small_material - ) - } else if (isPlayingState(state.state)) { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) - } else { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) - } - val prevButton = getStandardAction(controller, state.actions, - PlaybackState.ACTION_SKIP_TO_PREVIOUS) - val nextButton = getStandardAction(controller, state.actions, - PlaybackState.ACTION_SKIP_TO_NEXT) + val playOrPause = + if (isConnectingState(state.state)) { + // Spinner needs to be animating to render anything. Start it here. + val drawable = + context.getDrawable(com.android.internal.R.drawable.progress_small_material) + (drawable as Animatable).start() + MediaAction( + drawable, + null, // no action to perform when clicked + context.getString(R.string.controls_media_button_connecting), + context.getDrawable(R.drawable.ic_media_connecting_container), + // Specify a rebind id to prevent the spinner from restarting on later binds. + com.android.internal.R.drawable.progress_small_material + ) + } else if (isPlayingState(state.state)) { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) + } else { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) + } + val prevButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) + val nextButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) // Then, create a way to build any custom actions that will be needed - val customActions = state.customActions.asSequence().filterNotNull().map { - getCustomAction(state, packageName, controller, it) - }.iterator() + val customActions = + state.customActions + .asSequence() + .filterNotNull() + .map { getCustomAction(state, packageName, controller, it) } + .iterator() fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null // Finally, assign the remaining button slots: play/pause A B C D // A = previous, else custom action (if not reserved) // B = next, else custom action (if not reserved) // C and D are always custom actions - val reservePrev = controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV) == true - val reserveNext = controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT) == true - - val prevOrCustom = if (prevButton != null) { - prevButton - } else if (!reservePrev) { - nextCustomAction() - } else { - null - } + val reservePrev = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV + ) == true + val reserveNext = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT + ) == true + + val prevOrCustom = + if (prevButton != null) { + prevButton + } else if (!reservePrev) { + nextCustomAction() + } else { + null + } - val nextOrCustom = if (nextButton != null) { - nextButton - } else if (!reserveNext) { - nextCustomAction() - } else { - null - } + val nextOrCustom = + if (nextButton != null) { + nextButton + } else if (!reserveNext) { + nextCustomAction() + } else { + null + } return MediaButton( playOrPause, @@ -925,11 +1049,14 @@ class MediaDataManager( * @param controller MediaController for the session * @param stateActions The actions included with the session's [PlaybackState] * @param action A [PlaybackState.Actions] value representing what action to generate. One of: + * ``` * [PlaybackState.ACTION_PLAY] * [PlaybackState.ACTION_PAUSE] * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] * [PlaybackState.ACTION_SKIP_TO_NEXT] - * @return A [MediaAction] with correct values set, or null if the state doesn't support it + * @return + * ``` + * A [MediaAction] with correct values set, or null if the state doesn't support it */ private fun getStandardAction( controller: MediaController, @@ -977,20 +1104,18 @@ class MediaDataManager( } } - /** - * Check whether the actions from a [PlaybackState] include a specific action - */ + /** Check whether the actions from a [PlaybackState] include a specific action */ private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { - if ((action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && - (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)) { + if ( + (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && + (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) + ) { return true } return (stateActions and action != 0L) } - /** - * Get a [MediaAction] representing a [PlaybackState.CustomAction] - */ + /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ private fun getCustomAction( state: PlaybackState, packageName: String, @@ -1005,9 +1130,7 @@ class MediaDataManager( ) } - /** - * Load a bitmap from the various Art metadata URIs - */ + /** Load a bitmap from the various Art metadata URIs */ private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { for (uri in ART_URIS) { val uriString = metadata.getString(uri) @@ -1042,16 +1165,18 @@ class MediaDataManager( return null } - if (!uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && + if ( + !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) && - !uri.scheme.equals(ContentResolver.SCHEME_FILE)) { + !uri.scheme.equals(ContentResolver.SCHEME_FILE) + ) { return null } val source = ImageDecoder.createSource(context.getContentResolver(), uri) return try { - ImageDecoder.decodeBitmap(source) { - decoder, info, source -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE + ImageDecoder.decodeBitmap(source) { decoder, _, _ -> + decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE } } catch (e: IOException) { Log.e(TAG, "Unable to load bitmap", e) @@ -1065,25 +1190,23 @@ class MediaDataManager( private fun getResumeMediaAction(action: Runnable): MediaAction { return MediaAction( Icon.createWithResource(context, R.drawable.ic_media_play) - .setTint(themeText).loadDrawable(context), + .setTint(themeText) + .loadDrawable(context), action, context.getString(R.string.controls_media_resume), context.getDrawable(R.drawable.ic_media_play_container) ) } - fun onMediaDataLoaded( - key: String, - oldKey: String?, - data: MediaData - ) = traceSection("MediaDataManager#onMediaDataLoaded") { - Assert.isMainThread() - if (mediaEntries.containsKey(key)) { - // Otherwise this was removed already - mediaEntries.put(key, data) - notifyMediaDataLoaded(key, oldKey, data) + fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = + traceSection("MediaDataManager#onMediaDataLoaded") { + Assert.isMainThread() + if (mediaEntries.containsKey(key)) { + // Otherwise this was removed already + mediaEntries.put(key, data) + notifyMediaDataLoaded(key, oldKey, data) + } } - } override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) { if (!allowMediaRecommendations) { @@ -1100,9 +1223,11 @@ class MediaDataManager( if (DEBUG) { Log.d(TAG, "Set Smartspace media to be inactive for the data update") } - smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId) + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false) } 1 -> { @@ -1113,15 +1238,16 @@ class MediaDataManager( } if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true) - notifySmartspaceMediaDataLoaded( - smartspaceMediaData.targetId, smartspaceMediaData) + notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) } else -> { // There should NOT be more than 1 Smartspace media update. When it happens, it // indicates a bad state or an error. Reset the status accordingly. Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") notifySmartspaceMediaDataRemoved( - smartspaceMediaData.targetId, false /* immediately */) + smartspaceMediaData.targetId, + false /* immediately */ + ) smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA } } @@ -1134,10 +1260,17 @@ class MediaDataManager( Log.d(TAG, "Not removing $key because resumable") // Move to resume key (aka package name) if that key doesn't already exist. val resumeAction = getResumeMediaAction(removed.resumeAction!!) - val updated = removed.copy(token = null, actions = listOf(resumeAction), + val updated = + removed.copy( + token = null, + actions = listOf(resumeAction), semanticActions = MediaButton(playOrPause = resumeAction), - actionsToShowInCompact = listOf(0), active = false, resumption = true, - isPlaying = false, isClearable = true) + actionsToShowInCompact = listOf(0), + active = false, + resumption = true, + isPlaying = false, + isClearable = true + ) val pkg = removed.packageName val migrate = mediaEntries.put(pkg, updated) == null // Notify listeners of "new" controls when migrating or removed and update when not @@ -1179,33 +1312,27 @@ class MediaDataManager( } } - /** - * Invoked when the user has dismissed the media carousel - */ + /** Invoked when the user has dismissed the media carousel */ fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss() - /** - * Are there any media notifications active, including the recommendations? - */ + /** Are there any media notifications active, including the recommendations? */ fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation() /** * Are there any media entries we should display, including the recommendations? - * If resumption is enabled, this will include inactive players - * If resumption is disabled, we only want to show active players + * - If resumption is enabled, this will include inactive players + * - If resumption is disabled, we only want to show active players */ fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation() - /** - * Are there any resume media notifications active, excluding the recommendations? - */ + /** Are there any resume media notifications active, excluding the recommendations? */ fun hasActiveMedia() = mediaDataFilter.hasActiveMedia() /** - * Are there any resume media notifications active, excluding the recommendations? - * If resumption is enabled, this will include inactive players - * If resumption is disabled, we only want to show active players - */ + * Are there any resume media notifications active, excluding the recommendations? + * - If resumption is enabled, this will include inactive players + * - If resumption is disabled, we only want to show active players + */ fun hasAnyMedia() = mediaDataFilter.hasAnyMedia() interface Listener { @@ -1275,10 +1402,9 @@ class MediaDataManager( ): SmartspaceMediaData { var dismissIntent: Intent? = null if (target.baseAction != null && target.baseAction.extras != null) { - dismissIntent = target - .baseAction - .extras - .getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? + dismissIntent = + target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) + as Intent? } packageName(target)?.let { return SmartspaceMediaData( @@ -1289,14 +1415,16 @@ class MediaDataManager( recommendations = target.iconGrid, dismissIntent = dismissIntent, headphoneConnectionTimeMillis = target.creationTimeMillis, - instanceId = logger.getNewInstanceId()) + instanceId = logger.getNewInstanceId() + ) } - return EMPTY_SMARTSPACE_MEDIA_DATA - .copy(targetId = target.smartspaceTargetId, - isActive = isActive, - dismissIntent = dismissIntent, - headphoneConnectionTimeMillis = target.creationTimeMillis, - instanceId = logger.getNewInstanceId()) + return EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = target.smartspaceTargetId, + isActive = isActive, + dismissIntent = dismissIntent, + headphoneConnectionTimeMillis = target.creationTimeMillis, + instanceId = logger.getNewInstanceId() + ) } private fun packageName(target: SmartspaceTarget): String? { @@ -1308,8 +1436,9 @@ class MediaDataManager( for (recommendation in recommendationList) { val extras = recommendation.extras extras?.let { - it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { - packageName -> return packageName } + it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName -> + return packageName + } } } Log.w(TAG, "No valid package name is provided.") diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt index b3a4ddf8ec1f..6a512be091e1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.bluetooth.BluetoothLeBroadcast import android.bluetooth.BluetoothLeBroadcastMetadata @@ -36,6 +36,10 @@ import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaDataUtils import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory import com.android.systemui.statusbar.policy.ConfigurationController @@ -47,10 +51,10 @@ private const val PLAYBACK_TYPE_UNKNOWN = 0 private const val TAG = "MediaDeviceManager" private const val DEBUG = true -/** - * Provides information about the route (ie. device) where playback is occurring. - */ -class MediaDeviceManager @Inject constructor( +/** Provides information about the route (ie. device) where playback is occurring. */ +class MediaDeviceManager +@Inject +constructor( private val context: Context, private val controllerFactory: MediaControllerFactory, private val localMediaManagerFactory: LocalMediaManagerFactory, @@ -70,14 +74,10 @@ class MediaDeviceManager @Inject constructor( dumpManager.registerDumpable(javaClass.name, this) } - /** - * Add a listener for changes to the media route (ie. device). - */ + /** Add a listener for changes to the media route (ie. device). */ fun addListener(listener: Listener) = listeners.add(listener) - /** - * Remove a listener that has been registered with addListener. - */ + /** Remove a listener that has been registered with addListener. */ fun removeListener(listener: Listener) = listeners.remove(listener) override fun onMediaDataLoaded( @@ -101,19 +101,11 @@ class MediaDeviceManager @Inject constructor( processDevice(key, oldKey, data.device) return } - val controller = data.token?.let { - controllerFactory.create(it) - } + val controller = data.token?.let { controllerFactory.create(it) } val localMediaManager = localMediaManagerFactory.create(data.packageName) val muteAwaitConnectionManager = - muteAwaitConnectionManagerFactory.create(localMediaManager) - entry = Entry( - key, - oldKey, - controller, - localMediaManager, - muteAwaitConnectionManager - ) + muteAwaitConnectionManagerFactory.create(localMediaManager) + entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager) entries[key] = entry entry.start() } @@ -122,11 +114,7 @@ class MediaDeviceManager @Inject constructor( override fun onMediaDataRemoved(key: String) { val token = entries.remove(key) token?.stop() - token?.let { - listeners.forEach { - it.onKeyRemoved(key) - } - } + token?.let { listeners.forEach { it.onKeyRemoved(key) } } } override fun dump(pw: PrintWriter, args: Array<String>) { @@ -141,9 +129,7 @@ class MediaDeviceManager @Inject constructor( @MainThread private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { - listeners.forEach { - it.onMediaDeviceChanged(key, oldKey, device) - } + listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) } } interface Listener { @@ -159,8 +145,10 @@ class MediaDeviceManager @Inject constructor( val controller: MediaController?, val localMediaManager: LocalMediaManager, val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager? - ) : LocalMediaManager.DeviceCallback, MediaController.Callback(), - BluetoothLeBroadcast.Callback { + ) : + LocalMediaManager.DeviceCallback, + MediaController.Callback(), + BluetoothLeBroadcast.Callback { val token get() = controller?.sessionToken @@ -171,54 +159,52 @@ class MediaDeviceManager @Inject constructor( val sameWithoutIcon = value != null && value.equalsWithoutIcon(field) if (!started || !sameWithoutIcon) { field = value - fgExecutor.execute { - processDevice(key, oldKey, value) - } + fgExecutor.execute { processDevice(key, oldKey, value) } } } // A device that is not yet connected but is expected to connect imminently. Because it's // expected to connect imminently, it should be displayed as the current device. private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null private var broadcastDescription: String? = null - private val configListener = object : ConfigurationController.ConfigurationListener { - override fun onLocaleListChanged() { - updateCurrent() + private val configListener = + object : ConfigurationController.ConfigurationListener { + override fun onLocaleListChanged() { + updateCurrent() + } } - } @AnyThread - fun start() = bgExecutor.execute { - if (!started) { - localMediaManager.registerCallback(this) - localMediaManager.startScan() - muteAwaitConnectionManager?.startListening() - playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN - controller?.registerCallback(this) - updateCurrent() - started = true - configurationController.addCallback(configListener) + fun start() = + bgExecutor.execute { + if (!started) { + localMediaManager.registerCallback(this) + localMediaManager.startScan() + muteAwaitConnectionManager?.startListening() + playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN + controller?.registerCallback(this) + updateCurrent() + started = true + configurationController.addCallback(configListener) + } } - } @AnyThread - fun stop() = bgExecutor.execute { - if (started) { - started = false - controller?.unregisterCallback(this) - localMediaManager.stopScan() - localMediaManager.unregisterCallback(this) - muteAwaitConnectionManager?.stopListening() - configurationController.removeCallback(configListener) + fun stop() = + bgExecutor.execute { + if (started) { + started = false + controller?.unregisterCallback(this) + localMediaManager.stopScan() + localMediaManager.unregisterCallback(this) + muteAwaitConnectionManager?.stopListening() + configurationController.removeCallback(configListener) + } } - } fun dump(pw: PrintWriter) { - val routingSession = controller?.let { - mr2manager.getRoutingSessionForMediaController(it) - } - val selectedRoutes = routingSession?.let { - mr2manager.getSelectedRoutes(it) - } + val routingSession = + controller?.let { mr2manager.getRoutingSessionForMediaController(it) } + val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) } with(pw) { println(" current device is ${current?.name}") val type = controller?.playbackInfo?.playbackType @@ -238,14 +224,11 @@ class MediaDeviceManager @Inject constructor( updateCurrent() } - override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute { - updateCurrent() - } + override fun onDeviceListUpdate(devices: List<MediaDevice>?) = + bgExecutor.execute { updateCurrent() } override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) { - bgExecutor.execute { - updateCurrent() - } + bgExecutor.execute { updateCurrent() } } override fun onAboutToConnectDeviceAdded( @@ -253,14 +236,17 @@ class MediaDeviceManager @Inject constructor( deviceName: String, deviceIcon: Drawable? ) { - aboutToConnectDeviceOverride = AboutToConnectDevice( - fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress), - backupMediaDeviceData = MediaDeviceData( - /* enabled */ enabled = true, - /* icon */ deviceIcon, - /* name */ deviceName, - /* showBroadcastButton */ showBroadcastButton = false) - ) + aboutToConnectDeviceOverride = + AboutToConnectDevice( + fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress), + backupMediaDeviceData = + MediaDeviceData( + /* enabled */ enabled = true, + /* icon */ deviceIcon, + /* name */ deviceName, + /* showBroadcastButton */ showBroadcastButton = false + ) + ) updateCurrent() } @@ -287,8 +273,11 @@ class MediaDeviceManager @Inject constructor( metadata: BluetoothLeBroadcastMetadata ) { if (DEBUG) { - Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " + - "metadata = $metadata") + Log.d( + TAG, + "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " + + "metadata = $metadata" + ) } updateCurrent() } @@ -315,8 +304,10 @@ class MediaDeviceManager @Inject constructor( override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) { if (DEBUG) { - Log.d(TAG, "onBroadcastUpdateFailed(), reason = $reason , " + - "broadcastId = $broadcastId") + Log.d( + TAG, + "onBroadcastUpdateFailed(), reason = $reason , " + "broadcastId = $broadcastId" + ) } } @@ -327,34 +318,45 @@ class MediaDeviceManager @Inject constructor( @WorkerThread private fun updateCurrent() { if (isLeAudioBroadcastEnabled()) { - current = MediaDeviceData( + current = + MediaDeviceData( /* enabled */ true, /* icon */ context.getDrawable(R.drawable.settings_input_antenna), /* name */ broadcastDescription, /* intent */ null, - /* showBroadcastButton */ showBroadcastButton = true) + /* showBroadcastButton */ showBroadcastButton = true + ) } else { val aboutToConnect = aboutToConnectDeviceOverride - if (aboutToConnect != null && + if ( + aboutToConnect != null && aboutToConnect.fullMediaDevice == null && - aboutToConnect.backupMediaDeviceData != null) { + aboutToConnect.backupMediaDeviceData != null + ) { // Only use [backupMediaDeviceData] when we don't have [fullMediaDevice]. current = aboutToConnect.backupMediaDeviceData return } - val device = aboutToConnect?.fullMediaDevice - ?: localMediaManager.currentConnectedDevice + val device = + aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) } // If we have a controller but get a null route, then don't trust the device val enabled = device != null && (controller == null || route != null) - val name = if (controller == null || route != null) { - route?.name?.toString() ?: device?.name - } else { - null - } - current = MediaDeviceData(enabled, device?.iconWithoutBackground, name, - id = device?.id, showBroadcastButton = false) + val name = + if (controller == null || route != null) { + route?.name?.toString() ?: device?.name + } else { + null + } + current = + MediaDeviceData( + enabled, + device?.iconWithoutBackground, + name, + id = device?.id, + showBroadcastButton = false + ) } } @@ -384,13 +386,16 @@ class MediaDeviceManager @Inject constructor( // unexpected result. // Check the current media app's name is the same with current broadcast app's name // or not. - var mediaApp = MediaDataUtils.getAppLabel( - context, localMediaManager.packageName, - context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)) + var mediaApp = + MediaDataUtils.getAppLabel( + context, + localMediaManager.packageName, + context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name) + ) var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp) if (isCurrentBroadcastedApp) { - broadcastDescription = context.getString( - R.string.broadcasting_description_is_broadcasting) + broadcastDescription = + context.getString(R.string.broadcasting_description_is_broadcasting) } else { broadcastDescription = currentBroadcastedApp } @@ -403,9 +408,9 @@ class MediaDeviceManager @Inject constructor( * [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information. * * @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If - * non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData]. + * non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData]. * @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum - * information required to display the device. Only use if [fullMediaDevice] is null. + * information required to display the device. Only use if [fullMediaDevice] is null. */ private data class AboutToConnectDevice( val fullMediaDevice: MediaDevice? = null, diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt index 31792967899d..ab93b292308e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.content.ComponentName import android.content.Context @@ -25,6 +25,8 @@ import android.media.session.MediaSessionManager import android.util.Log import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins import java.util.concurrent.Executor import javax.inject.Inject @@ -38,7 +40,9 @@ private const val TAG = "MediaSessionBasedFilter" * sessions. In this situation, there should only be a media object for the remote session. To * achieve this, update events for the local session need to be filtered. */ -class MediaSessionBasedFilter @Inject constructor( +class MediaSessionBasedFilter +@Inject +constructor( context: Context, private val sessionManager: MediaSessionManager, @Main private val foregroundExecutor: Executor, @@ -50,7 +54,7 @@ class MediaSessionBasedFilter @Inject constructor( // Keep track of MediaControllers for a given package to check if an app is casting and it // filter loaded events for local sessions. private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> = - LinkedHashMap() + LinkedHashMap() // Keep track of the key used for the session tokens. This information is used to know when to // dispatch a removed event so that a media object for a local session will be removed. @@ -59,11 +63,12 @@ class MediaSessionBasedFilter @Inject constructor( // Keep track of which media session tokens have associated notifications. private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf() - private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener { - override fun onActiveSessionsChanged(controllers: List<MediaController>) { - handleControllersChanged(controllers) + private val sessionListener = + object : MediaSessionManager.OnActiveSessionsChangedListener { + override fun onActiveSessionsChanged(controllers: List<MediaController>) { + handleControllersChanged(controllers) + } } - } init { backgroundExecutor.execute { @@ -73,14 +78,10 @@ class MediaSessionBasedFilter @Inject constructor( } } - /** - * Add a listener for filtered [MediaData] changes - */ + /** Add a listener for filtered [MediaData] changes */ fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) - /** - * Remove a listener that was registered with addListener - */ + /** Remove a listener that was registered with addListener */ fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) /** @@ -100,31 +101,32 @@ class MediaSessionBasedFilter @Inject constructor( isSsReactivated: Boolean ) { backgroundExecutor.execute { - data.token?.let { - tokensWithNotifications.add(it) - } + data.token?.let { tokensWithNotifications.add(it) } val isMigration = oldKey != null && key != oldKey if (isMigration) { keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) } } if (data.token != null) { - keyedTokens.get(key)?.let { - tokens -> - tokens.add(data.token) - } ?: run { - val tokens = mutableSetOf(data.token) - keyedTokens.put(key, tokens) - } + keyedTokens.get(key)?.let { tokens -> tokens.add(data.token) } + ?: run { + val tokens = mutableSetOf(data.token) + keyedTokens.put(key, tokens) + } } // Determine if an app is casting by checking if it has a session with playback type // PLAYBACK_TYPE_REMOTE. - val remoteControllers = packageControllers.get(data.packageName)?.filter { - it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE - } + val remoteControllers = + packageControllers.get(data.packageName)?.filter { + it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE + } // Limiting search to only apps with a single remote session. val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null - if (isMigration || remote == null || remote.sessionToken == data.token || - !tokensWithNotifications.contains(remote.sessionToken)) { + if ( + isMigration || + remote == null || + remote.sessionToken == data.token || + !tokensWithNotifications.contains(remote.sessionToken) + ) { // Not filtering in this case. Passing the event along to listeners. dispatchMediaDataLoaded(key, oldKey, data, immediately) } else { @@ -146,9 +148,7 @@ class MediaSessionBasedFilter @Inject constructor( data: SmartspaceMediaData, shouldPrioritize: Boolean ) { - backgroundExecutor.execute { - dispatchSmartspaceMediaDataLoaded(key, data) - } + backgroundExecutor.execute { dispatchSmartspaceMediaDataLoaded(key, data) } } override fun onMediaDataRemoved(key: String) { @@ -160,9 +160,7 @@ class MediaSessionBasedFilter @Inject constructor( } override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { - backgroundExecutor.execute { - dispatchSmartspaceMediaDataRemoved(key, immediately) - } + backgroundExecutor.execute { dispatchSmartspaceMediaDataRemoved(key, immediately) } } private fun dispatchMediaDataLoaded( @@ -177,9 +175,7 @@ class MediaSessionBasedFilter @Inject constructor( } private fun dispatchMediaDataRemoved(key: String) { - foregroundExecutor.execute { - listeners.toSet().forEach { it.onMediaDataRemoved(key) } - } + foregroundExecutor.execute { listeners.toSet().forEach { it.onMediaDataRemoved(key) } } } private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { @@ -196,15 +192,12 @@ class MediaSessionBasedFilter @Inject constructor( private fun handleControllersChanged(controllers: List<MediaController>) { packageControllers.clear() - controllers.forEach { - controller -> - packageControllers.get(controller.packageName)?.let { - tokens -> - tokens.add(controller) - } ?: run { - val tokens = mutableListOf(controller) - packageControllers.put(controller.packageName, tokens) - } + controllers.forEach { controller -> + packageControllers.get(controller.packageName)?.let { tokens -> tokens.add(controller) } + ?: run { + val tokens = mutableListOf(controller) + packageControllers.put(controller.packageName, tokens) + } } tokensWithNotifications.retainAll(controllers.map { it.sessionToken }) } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt index 93a29ef03393..7f5c82fb5eee 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.media.session.MediaController import android.media.session.PlaybackState @@ -22,6 +22,8 @@ import android.os.SystemProperties import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.SysuiStatusBarStateController @@ -31,18 +33,18 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject @VisibleForTesting -val PAUSED_MEDIA_TIMEOUT = SystemProperties - .getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10)) +val PAUSED_MEDIA_TIMEOUT = + SystemProperties.getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10)) @VisibleForTesting -val RESUME_MEDIA_TIMEOUT = SystemProperties - .getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3)) +val RESUME_MEDIA_TIMEOUT = + SystemProperties.getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3)) -/** - * Controller responsible for keeping track of playback states and expiring inactive streams. - */ +/** Controller responsible for keeping track of playback states and expiring inactive streams. */ @SysUISingleton -class MediaTimeoutListener @Inject constructor( +class MediaTimeoutListener +@Inject +constructor( private val mediaControllerFactory: MediaControllerFactory, @Main private val mainExecutor: DelayableExecutor, private val logger: MediaTimeoutLogger, @@ -56,7 +58,9 @@ class MediaTimeoutListener @Inject constructor( * Callback representing that a media object is now expired: * @param key Media control unique identifier * @param timedOut True when expired for {@code PAUSED_MEDIA_TIMEOUT} for active media, + * ``` * or {@code RESUME_MEDIA_TIMEOUT} for resume media + * ``` */ lateinit var timeoutCallback: (String, Boolean) -> Unit @@ -68,21 +72,25 @@ class MediaTimeoutListener @Inject constructor( lateinit var stateCallback: (String, PlaybackState) -> Unit init { - statusBarStateController.addCallback(object : StatusBarStateController.StateListener { - override fun onDozingChanged(isDozing: Boolean) { - if (!isDozing) { - // Check whether any timeouts should have expired - mediaListeners.forEach { (key, listener) -> - if (listener.cancellation != null && - listener.expiration <= systemClock.elapsedRealtime()) { - // We dozed too long - timeout now, and cancel the pending one - listener.expireMediaTimeout(key, "timeout happened while dozing") - listener.doTimeout() + statusBarStateController.addCallback( + object : StatusBarStateController.StateListener { + override fun onDozingChanged(isDozing: Boolean) { + if (!isDozing) { + // Check whether any timeouts should have expired + mediaListeners.forEach { (key, listener) -> + if ( + listener.cancellation != null && + listener.expiration <= systemClock.elapsedRealtime() + ) { + // We dozed too long - timeout now, and cancel the pending one + listener.expireMediaTimeout(key, "timeout happened while dozing") + listener.doTimeout() + } } } } } - }) + ) } override fun onMediaDataLoaded( @@ -145,10 +153,8 @@ class MediaTimeoutListener @Inject constructor( return mediaListeners[key]?.timedOut ?: false } - private inner class PlaybackStateListener( - var key: String, - data: MediaData - ) : MediaController.Callback() { + private inner class PlaybackStateListener(var key: String, data: MediaData) : + MediaController.Callback() { var timedOut = false var lastState: PlaybackState? = null @@ -162,11 +168,12 @@ class MediaTimeoutListener @Inject constructor( mediaController?.unregisterCallback(this) field = value val token = field.token - mediaController = if (token != null) { - mediaControllerFactory.create(token) - } else { - null - } + mediaController = + if (token != null) { + mediaControllerFactory.create(token) + } else { + null + } mediaController?.registerCallback(this) // Let's register the cancellations, but not dispatch events now. // Timeouts didn't happen yet and reentrant events are troublesome. @@ -212,7 +219,8 @@ class MediaTimeoutListener @Inject constructor( logger.logPlaybackState(key, state) val playingStateSame = (state?.state?.isPlaying() == isPlaying()) - val actionsSame = (lastState?.actions == state?.actions) && + val actionsSame = + (lastState?.actions == state?.actions) && areCustomActionListsEqual(lastState?.customActions, state?.customActions) val resumptionChanged = resumption != mediaData.resumption @@ -237,15 +245,14 @@ class MediaTimeoutListener @Inject constructor( return } expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state, $resumption") - val timeout = if (mediaData.resumption) { - RESUME_MEDIA_TIMEOUT - } else { - PAUSED_MEDIA_TIMEOUT - } + val timeout = + if (mediaData.resumption) { + RESUME_MEDIA_TIMEOUT + } else { + PAUSED_MEDIA_TIMEOUT + } expiration = systemClock.elapsedRealtime() + timeout - cancellation = mainExecutor.executeDelayed({ - doTimeout() - }, timeout) + cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout) } else { expireMediaTimeout(key, "playback started - $state, $key") timedOut = false @@ -301,9 +308,11 @@ class MediaTimeoutListener @Inject constructor( firstAction: PlaybackState.CustomAction, secondAction: PlaybackState.CustomAction ): Boolean { - if (firstAction.action != secondAction.action || + if ( + firstAction.action != secondAction.action || firstAction.name != secondAction.name || - firstAction.icon != secondAction.icon) { + firstAction.icon != secondAction.icon + ) { return false } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt new file mode 100644 index 000000000000..8f3f0548230f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.pipeline + +import android.media.session.PlaybackState +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.dagger.MediaTimeoutListenerLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import javax.inject.Inject + +private const val TAG = "MediaTimeout" + +/** A buffered log for [MediaTimeoutListener] events */ +@SysUISingleton +class MediaTimeoutLogger +@Inject +constructor(@MediaTimeoutListenerLog private val buffer: LogBuffer) { + fun logReuseListener(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "reuse listener: $str1" }) + + fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = oldKey + str2 = newKey + bool1 = hadListener + }, + { "migrate from $str1 to $str2, had listener? $bool1" } + ) + + fun logUpdateListener(key: String, wasPlaying: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = key + bool1 = wasPlaying + }, + { "updating $str1, was playing? $bool1" } + ) + + fun logDelayedUpdate(key: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "deliver delayed playback state for $str1" } + ) + + fun logSessionDestroyed(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "session destroyed $str1" }) + + fun logPlaybackState(key: String, state: PlaybackState?) = + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = key + str2 = state?.toString() + }, + { "state update: key=$str1 state=$str2" } + ) + + fun logStateCallback(key: String) = + buffer.log(TAG, LogLevel.VERBOSE, { str1 = key }, { "dispatching state update for $key" }) + + fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = key + bool1 = playing + bool2 = resumption + }, + { "schedule timeout $str1, playing=$bool1 resumption=$bool2" } + ) + + fun logCancelIgnored(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "cancellation already exists for $str1" }) + + fun logTimeout(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "execute timeout for $str1" }) + + fun logTimeoutCancelled(key: String, reason: String) = + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = key + str2 = reason + }, + { "media timeout cancelled for $str1, reason: $str2" } + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaBrowserFactory.java index aca033e99623..00620b5b2575 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaBrowserFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.resume; import android.content.ComponentName; import android.content.Context; diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt index cc06b6c67879..4891297dbcf9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.resume import android.content.BroadcastReceiver import android.content.ComponentName @@ -33,6 +33,9 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT import com.android.systemui.tuner.TunerService import com.android.systemui.util.Utils import com.android.systemui.util.time.SystemClock @@ -47,7 +50,9 @@ private const val MEDIA_PREFERENCES = "media_control_prefs" private const val MEDIA_PREFERENCE_KEY = "browser_components_" @SysUISingleton -class MediaResumeListener @Inject constructor( +class MediaResumeListener +@Inject +constructor( private val context: Context, private val broadcastDispatcher: BroadcastDispatcher, @Background private val backgroundExecutor: Executor, @@ -59,7 +64,7 @@ class MediaResumeListener @Inject constructor( private var useMediaResumption: Boolean = Utils.useMediaResumption(context) private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> = - ConcurrentLinkedQueue() + ConcurrentLinkedQueue() private lateinit var mediaDataManager: MediaDataManager @@ -72,40 +77,49 @@ class MediaResumeListener @Inject constructor( private var currentUserId: Int = context.userId @VisibleForTesting - val userChangeReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (Intent.ACTION_USER_UNLOCKED == intent.action) { - loadMediaResumptionControls() - } else if (Intent.ACTION_USER_SWITCHED == intent.action) { - currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) - loadSavedComponents() + val userChangeReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (Intent.ACTION_USER_UNLOCKED == intent.action) { + loadMediaResumptionControls() + } else if (Intent.ACTION_USER_SWITCHED == intent.action) { + currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) + loadSavedComponents() + } } } - } - private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() { - override fun addTrack( - desc: MediaDescription, - component: ComponentName, - browser: ResumeMediaBrowser - ) { - val token = browser.token - val appIntent = browser.appIntent - val pm = context.getPackageManager() - var appName: CharSequence = component.packageName - val resumeAction = getResumeAction(component) - try { - appName = pm.getApplicationLabel( - pm.getApplicationInfo(component.packageName, 0)) - } catch (e: PackageManager.NameNotFoundException) { - Log.e(TAG, "Error getting package information", e) - } + private val mediaBrowserCallback = + object : ResumeMediaBrowser.Callback() { + override fun addTrack( + desc: MediaDescription, + component: ComponentName, + browser: ResumeMediaBrowser + ) { + val token = browser.token + val appIntent = browser.appIntent + val pm = context.getPackageManager() + var appName: CharSequence = component.packageName + val resumeAction = getResumeAction(component) + try { + appName = + pm.getApplicationLabel(pm.getApplicationInfo(component.packageName, 0)) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(TAG, "Error getting package information", e) + } - Log.d(TAG, "Adding resume controls $desc") - mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token, - appName.toString(), appIntent, component.packageName) + Log.d(TAG, "Adding resume controls $desc") + mediaDataManager.addResumptionControls( + currentUserId, + desc, + resumeAction, + token, + appName.toString(), + appIntent, + component.packageName + ) + } } - } init { if (useMediaResumption) { @@ -113,8 +127,12 @@ class MediaResumeListener @Inject constructor( val unlockFilter = IntentFilter() unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED) unlockFilter.addAction(Intent.ACTION_USER_SWITCHED) - broadcastDispatcher.registerReceiver(userChangeReceiver, unlockFilter, null, - UserHandle.ALL) + broadcastDispatcher.registerReceiver( + userChangeReceiver, + unlockFilter, + null, + UserHandle.ALL + ) loadSavedComponents() } } @@ -123,12 +141,15 @@ class MediaResumeListener @Inject constructor( mediaDataManager = manager // Add listener for resumption setting changes - tunerService.addTunable(object : TunerService.Tunable { - override fun onTuningChanged(key: String?, newValue: String?) { - useMediaResumption = Utils.useMediaResumption(context) - mediaDataManager.setMediaResumptionEnabled(useMediaResumption) - } - }, Settings.Secure.MEDIA_CONTROLS_RESUME) + tunerService.addTunable( + object : TunerService.Tunable { + override fun onTuningChanged(key: String?, newValue: String?) { + useMediaResumption = Utils.useMediaResumption(context) + mediaDataManager.setMediaResumptionEnabled(useMediaResumption) + } + }, + Settings.Secure.MEDIA_CONTROLS_RESUME + ) } private fun loadSavedComponents() { @@ -136,8 +157,10 @@ class MediaResumeListener @Inject constructor( resumeComponents.clear() val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE) val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null) - val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex()) - ?.dropLastWhile { it.isEmpty() } + val components = + listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())?.dropLastWhile { + it.isEmpty() + } var needsUpdate = false components?.forEach { val info = it.split("/") @@ -145,17 +168,18 @@ class MediaResumeListener @Inject constructor( val className = info[1] val component = ComponentName(packageName, className) - val lastPlayed = if (info.size == 3) { - try { - info[2].toLong() - } catch (e: NumberFormatException) { + val lastPlayed = + if (info.size == 3) { + try { + info[2].toLong() + } catch (e: NumberFormatException) { + needsUpdate = true + systemClock.currentTimeMillis() + } + } else { needsUpdate = true systemClock.currentTimeMillis() } - } else { - needsUpdate = true - systemClock.currentTimeMillis() - } resumeComponents.add(component to lastPlayed) } Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}") @@ -166,9 +190,7 @@ class MediaResumeListener @Inject constructor( } } - /** - * Load controls for resuming media, if available - */ + /** Load controls for resuming media, if available */ private fun loadMediaResumptionControls() { if (!useMediaResumption) { return @@ -204,9 +226,7 @@ class MediaResumeListener @Inject constructor( val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE) val resumeInfo = pm.queryIntentServices(serviceIntent, 0) - val inf = resumeInfo?.filter { - it.serviceInfo.packageName == data.packageName - } + val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName } if (inf != null && inf.size > 0) { backgroundExecutor.execute { tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName) @@ -227,7 +247,8 @@ class MediaResumeListener @Inject constructor( Log.d(TAG, "Testing if we can connect to $componentName") // Set null action to prevent additional attempts to connect mediaDataManager.setResumeAction(key, null) - mediaBrowser = mediaBrowserFactory.create( + mediaBrowser = + mediaBrowserFactory.create( object : ResumeMediaBrowser.Callback() { override fun onConnected() { Log.d(TAG, "Connected to $componentName") @@ -250,7 +271,8 @@ class MediaResumeListener @Inject constructor( mediaBrowser = null } }, - componentName) + componentName + ) mediaBrowser?.testConnection() } @@ -285,9 +307,7 @@ class MediaResumeListener @Inject constructor( prefs.edit().putString(MEDIA_PREFERENCE_KEY + currentUserId, sb.toString()).apply() } - /** - * Get a runnable which will resume media playback - */ + /** Get a runnable which will resume media playback */ private fun getResumeAction(componentName: ComponentName): Runnable { return Runnable { mediaBrowser = mediaBrowserFactory.create(null, componentName) @@ -296,8 +316,6 @@ class MediaResumeListener @Inject constructor( } override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.apply { - println("resumeComponents: $resumeComponents") - } + pw.apply { println("resumeComponents: $resumeComponents") } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java index 40a5653a15a0..3493b2453fd6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.resume; import android.annotation.Nullable; import android.app.PendingIntent; @@ -293,7 +293,7 @@ public class ResumeMediaBrowser { public PendingIntent getAppIntent() { PackageManager pm = mContext.getPackageManager(); Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName()); - return PendingIntent.getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED); + return PendingIntent.getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE); } /** diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java index 3d1380b6bd24..c558227df0b5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.resume; import android.content.ComponentName; import android.content.Context; diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt index 41f735486c7e..335ce1d3d694 100644 --- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt @@ -14,61 +14,60 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.resume import android.content.ComponentName import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaBrowserLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** A logger for events in [ResumeMediaBrowser]. */ @SysUISingleton -class ResumeMediaBrowserLogger @Inject constructor( - @MediaBrowserLog private val buffer: LogBuffer -) { +class ResumeMediaBrowserLogger @Inject constructor(@MediaBrowserLog private val buffer: LogBuffer) { /** Logs that we've initiated a connection to a [android.media.browse.MediaBrowser]. */ - fun logConnection(componentName: ComponentName, reason: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = componentName.toShortString() - str2 = reason - }, - { "Connecting browser for component $str1 due to $str2" } - ) + fun logConnection(componentName: ComponentName, reason: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = componentName.toShortString() + str2 = reason + }, + { "Connecting browser for component $str1 due to $str2" } + ) /** Logs that we've disconnected from a [android.media.browse.MediaBrowser]. */ - fun logDisconnect(componentName: ComponentName) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = componentName.toShortString() - }, - { "Disconnecting browser for component $str1" } - ) + fun logDisconnect(componentName: ComponentName) = + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = componentName.toShortString() }, + { "Disconnecting browser for component $str1" } + ) /** * Logs that we received a [android.media.session.MediaController.Callback.onSessionDestroyed] * event. * * @param isBrowserConnected true if there's a currently connected + * ``` * [android.media.browse.MediaBrowser] and false otherwise. - * @param componentName the component name for the [ResumeMediaBrowser] that triggered this log. + * @param componentName + * ``` + * the component name for the [ResumeMediaBrowser] that triggered this log. */ - fun logSessionDestroyed( - isBrowserConnected: Boolean, - componentName: ComponentName - ) = buffer.log( - TAG, - LogLevel.DEBUG, - { - bool1 = isBrowserConnected - str1 = componentName.toShortString() - }, - { "Session destroyed. Active browser = $bool1. Browser component = $str1." } - ) + fun logSessionDestroyed(isBrowserConnected: Boolean, componentName: ComponentName) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + bool1 = isBrowserConnected + str1 = componentName.toShortString() + }, + { "Session destroyed. Active browser = $bool1. Browser component = $str1." } + ) } private const val TAG = "MediaBrowser" diff --git a/packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/AnimationBindHandler.kt index 013683e962a4..d2793bca867b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/AnimationBindHandler.kt @@ -14,19 +14,23 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.graphics.drawable.Animatable2 import android.graphics.drawable.Drawable /** - * AnimationBindHandler is responsible for tracking the bound animation state and preventing - * jank and conflicts due to media notifications arriving at any time during an animation. It - * does this in two parts. - * - Exit animations fired as a result of user input are tracked. When these are running, any + * AnimationBindHandler is responsible for tracking the bound animation state and preventing jank + * and conflicts due to media notifications arriving at any time during an animation. It does this + * in two parts. + * - Exit animations fired as a result of user input are tracked. When these are running, any + * ``` * bind actions are delayed until the animation completes (and then fired in sequence). - * - Continuous animations are tracked using their rebind id. Later calls using the same + * ``` + * - Continuous animations are tracked using their rebind id. Later calls using the same + * ``` * rebind id will be totally ignored to prevent the continuous animation from restarting. + * ``` */ internal class AnimationBindHandler : Animatable2.AnimationCallback() { private val onAnimationsComplete = mutableListOf<() -> Unit>() @@ -37,10 +41,10 @@ internal class AnimationBindHandler : Animatable2.AnimationCallback() { get() = registrations.any { it.isRunning } /** - * This check prevents rebinding to the action button if the identifier has not changed. A - * null value is always considered to be changed. This is used to prevent the connecting - * animation from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by - * an application in a row. + * This check prevents rebinding to the action button if the identifier has not changed. A null + * value is always considered to be changed. This is used to prevent the connecting animation + * from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by an + * application in a row. */ fun updateRebindId(newRebindId: Int?): Boolean { if (rebindId == null || newRebindId == null || rebindId != newRebindId) { @@ -78,4 +82,4 @@ internal class AnimationBindHandler : Animatable2.AnimationCallback() { onAnimationsComplete.clear() } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt new file mode 100644 index 000000000000..61ef2f1838e7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui + +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.graphics.drawable.RippleDrawable +import com.android.internal.R +import com.android.internal.annotations.VisibleForTesting +import com.android.settingslib.Utils +import com.android.systemui.media.controls.models.player.MediaViewHolder +import com.android.systemui.monet.ColorScheme + +/** + * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme] + * is triggered. + */ +interface ColorTransition { + fun updateColorScheme(scheme: ColorScheme?): Boolean +} + +/** + * A [ColorTransition] that animates between two specific colors. It uses a ValueAnimator to execute + * the animation and interpolate between the source color and the target color. + * + * Selection of the target color from the scheme, and application of the interpolated color are + * delegated to callbacks. + */ +open class AnimatingColorTransition( + private val defaultColor: Int, + private val extractColor: (ColorScheme) -> Int, + private val applyColor: (Int) -> Unit +) : AnimatorUpdateListener, ColorTransition { + + private val argbEvaluator = ArgbEvaluator() + private val valueAnimator = buildAnimator() + var sourceColor: Int = defaultColor + var currentColor: Int = defaultColor + var targetColor: Int = defaultColor + + override fun onAnimationUpdate(animation: ValueAnimator) { + currentColor = + argbEvaluator.evaluate(animation.animatedFraction, sourceColor, targetColor) as Int + applyColor(currentColor) + } + + override fun updateColorScheme(scheme: ColorScheme?): Boolean { + val newTargetColor = if (scheme == null) defaultColor else extractColor(scheme) + if (newTargetColor != targetColor) { + sourceColor = currentColor + targetColor = newTargetColor + valueAnimator.cancel() + valueAnimator.start() + return true + } + return false + } + + init { + applyColor(defaultColor) + } + + @VisibleForTesting + open fun buildAnimator(): ValueAnimator { + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.duration = 333 + animator.addUpdateListener(this) + return animator + } +} + +typealias AnimatingColorTransitionFactory = + (Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition + +/** + * ColorSchemeTransition constructs a ColorTransition for each color in the scheme that needs to be + * transitioned when changed. It also sets up the assignment functions for sending the sending the + * interpolated colors to the appropriate views. + */ +class ColorSchemeTransition +internal constructor( + private val context: Context, + private val mediaViewHolder: MediaViewHolder, + animatingColorTransitionFactory: AnimatingColorTransitionFactory +) { + constructor( + context: Context, + mediaViewHolder: MediaViewHolder + ) : this(context, mediaViewHolder, ::AnimatingColorTransition) + + val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95) + val surfaceColor = + animatingColorTransitionFactory(bgColor, ::surfaceFromScheme) { surfaceColor -> + val colorList = ColorStateList.valueOf(surfaceColor) + mediaViewHolder.seamlessIcon.imageTintList = colorList + mediaViewHolder.seamlessText.setTextColor(surfaceColor) + mediaViewHolder.albumView.backgroundTintList = colorList + mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor) + } + + val accentPrimary = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorPrimary), + ::accentPrimaryFromScheme + ) { accentPrimary -> + val accentColorList = ColorStateList.valueOf(accentPrimary) + mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList + mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary) + } + + val accentSecondary = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorPrimary), + ::accentSecondaryFromScheme + ) { accentSecondary -> + val colorList = ColorStateList.valueOf(accentSecondary) + (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let { + it.setColor(colorList) + it.effectColor = colorList + } + } + + val colorSeamless = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorPrimary), + { colorScheme: ColorScheme -> + // A1-100 dark in dark theme, A1-200 in light theme + if ( + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + UI_MODE_NIGHT_YES + ) + colorScheme.accent1[2] + else colorScheme.accent1[3] + }, + { seamlessColor: Int -> + val accentColorList = ColorStateList.valueOf(seamlessColor) + mediaViewHolder.seamlessButton.backgroundTintList = accentColorList + } + ) + + val textPrimary = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorPrimary), + ::textPrimaryFromScheme + ) { textPrimary -> + mediaViewHolder.titleText.setTextColor(textPrimary) + val textColorList = ColorStateList.valueOf(textPrimary) + mediaViewHolder.seekBar.thumb.setTintList(textColorList) + mediaViewHolder.seekBar.progressTintList = textColorList + mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList) + mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList) + for (button in mediaViewHolder.getTransparentActionButtons()) { + button.imageTintList = textColorList + } + mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary) + } + + val textPrimaryInverse = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorPrimaryInverse), + ::textPrimaryInverseFromScheme + ) { textPrimaryInverse -> + mediaViewHolder.actionPlayPause.imageTintList = + ColorStateList.valueOf(textPrimaryInverse) + } + + val textSecondary = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorSecondary), + ::textSecondaryFromScheme + ) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) } + + val textTertiary = + animatingColorTransitionFactory( + loadDefaultColor(R.attr.textColorTertiary), + ::textTertiaryFromScheme + ) { textTertiary -> + mediaViewHolder.seekBar.progressBackgroundTintList = + ColorStateList.valueOf(textTertiary) + } + + val colorTransitions = + arrayOf( + surfaceColor, + colorSeamless, + accentPrimary, + accentSecondary, + textPrimary, + textPrimaryInverse, + textSecondary, + textTertiary, + ) + + private fun loadDefaultColor(id: Int): Int { + return Utils.getColorAttr(context, id).defaultColor + } + + fun updateColorScheme(colorScheme: ColorScheme?): Boolean { + var anyChanged = false + colorTransitions.forEach { anyChanged = it.updateColorScheme(colorScheme) || anyChanged } + colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme } + return anyChanged + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt index 121ddd46976d..9f86cd88788b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -42,22 +42,20 @@ import org.xmlpull.v1.XmlPullParser private const val BACKGROUND_ANIM_DURATION = 370L -/** - * Drawable that can draw an animated gradient when tapped. - */ +/** Drawable that can draw an animated gradient when tapped. */ @Keep class IlluminationDrawable : Drawable() { private var themeAttrs: IntArray? = null private var cornerRadiusOverride = -1f var cornerRadius = 0f - get() { - return if (cornerRadiusOverride >= 0) { - cornerRadiusOverride - } else { - field + get() { + return if (cornerRadiusOverride >= 0) { + cornerRadiusOverride + } else { + field + } } - } private var highlightColor = Color.TRANSPARENT private var tmpHsl = floatArrayOf(0f, 0f, 0f) private var paint = Paint() @@ -65,22 +63,27 @@ class IlluminationDrawable : Drawable() { private val lightSources = arrayListOf<LightSourceDrawable>() private var backgroundColor = Color.TRANSPARENT - set(value) { - if (value == field) { - return + set(value) { + if (value == field) { + return + } + field = value + animateBackground() } - field = value - animateBackground() - } private var backgroundAnimation: ValueAnimator? = null - /** - * Draw background and gradient. - */ + /** Draw background and gradient. */ override fun draw(canvas: Canvas) { - canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(), - cornerRadius, cornerRadius, paint) + canvas.drawRoundRect( + 0f, + 0f, + bounds.width().toFloat(), + bounds.height().toFloat(), + cornerRadius, + cornerRadius, + paint + ) } override fun getOutline(outline: Outline) { @@ -105,12 +108,11 @@ class IlluminationDrawable : Drawable() { private fun updateStateFromTypedArray(a: TypedArray) { if (a.hasValue(R.styleable.IlluminationDrawable_cornerRadius)) { - cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, - cornerRadius) + cornerRadius = + a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius) } if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) { - highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / - 100f + highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f } } @@ -163,34 +165,42 @@ class IlluminationDrawable : Drawable() { private fun animateBackground() { ColorUtils.colorToHSL(backgroundColor, tmpHsl) val L = tmpHsl[2] - tmpHsl[2] = MathUtils.constrain(if (L < 1f - highlight) { - L + highlight - } else { - L - highlight - }, 0f, 1f) + tmpHsl[2] = + MathUtils.constrain( + if (L < 1f - highlight) { + L + highlight + } else { + L - highlight + }, + 0f, + 1f + ) val initialBackground = paint.color val initialHighlight = highlightColor val finalHighlight = ColorUtils.HSLToColor(tmpHsl) backgroundAnimation?.cancel() - backgroundAnimation = ValueAnimator.ofFloat(0f, 1f).apply { - duration = BACKGROUND_ANIM_DURATION - interpolator = Interpolators.FAST_OUT_LINEAR_IN - addUpdateListener { - val progress = it.animatedValue as Float - paint.color = blendARGB(initialBackground, backgroundColor, progress) - highlightColor = blendARGB(initialHighlight, finalHighlight, progress) - lightSources.forEach { it.highlightColor = highlightColor } - invalidateSelf() - } - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - backgroundAnimation = null + backgroundAnimation = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = BACKGROUND_ANIM_DURATION + interpolator = Interpolators.FAST_OUT_LINEAR_IN + addUpdateListener { + val progress = it.animatedValue as Float + paint.color = blendARGB(initialBackground, backgroundColor, progress) + highlightColor = blendARGB(initialHighlight, finalHighlight, progress) + lightSources.forEach { it.highlightColor = highlightColor } + invalidateSelf() } - }) - start() - } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + backgroundAnimation = null + } + } + ) + start() + } } override fun setTintList(tint: ColorStateList?) { @@ -215,4 +225,4 @@ class IlluminationDrawable : Drawable() { fun setCornerRadiusOverride(cornerRadius: Float?) { cornerRadiusOverride = cornerRadius ?: -1f } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt index 32600fba61a4..899148b0014c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.content.Context import android.content.res.Configuration @@ -45,7 +45,9 @@ import javax.inject.Named * switches media player positioning between split pane container vs single pane container */ @SysUISingleton -class KeyguardMediaController @Inject constructor( +class KeyguardMediaController +@Inject +constructor( @param:Named(KEYGUARD) private val mediaHost: MediaHost, private val bypassController: KeyguardBypassController, private val statusBarStateController: SysuiStatusBarStateController, @@ -56,34 +58,40 @@ class KeyguardMediaController @Inject constructor( ) { init { - statusBarStateController.addCallback(object : StatusBarStateController.StateListener { - override fun onStateChanged(newState: Int) { - refreshMediaPosition() + statusBarStateController.addCallback( + object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + refreshMediaPosition() + } } - }) - configurationController.addCallback(object : ConfigurationController.ConfigurationListener { - override fun onConfigChanged(newConfig: Configuration?) { - updateResources() + ) + configurationController.addCallback( + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + updateResources() + } } - }) + ) - val settingsObserver: ContentObserver = object : ContentObserver(handler) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - if (uri == lockScreenMediaPlayerUri) { - allowMediaPlayerOnLockScreen = + val settingsObserver: ContentObserver = + object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + if (uri == lockScreenMediaPlayerUri) { + allowMediaPlayerOnLockScreen = secureSettings.getBoolForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - true, - UserHandle.USER_CURRENT + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + true, + UserHandle.USER_CURRENT ) - refreshMediaPosition() + refreshMediaPosition() + } } } - } secureSettings.registerContentObserverForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - settingsObserver, - UserHandle.USER_ALL) + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + settingsObserver, + UserHandle.USER_ALL + ) // First let's set the desired state that we want for this host mediaHost.expansion = MediaHostState.EXPANDED @@ -110,27 +118,21 @@ class KeyguardMediaController @Inject constructor( refreshMediaPosition() } - /** - * Is the media player visible? - */ + /** Is the media player visible? */ var visible = false private set var visibilityChangedListener: ((Boolean) -> Unit)? = null - /** - * single pane media container placed at the top of the notifications list - */ + /** single pane media container placed at the top of the notifications list */ var singlePaneContainer: MediaContainerView? = null private set private var splitShadeContainer: ViewGroup? = null - /** - * Track the media player setting status on lock screen. - */ + /** Track the media player setting status on lock screen. */ private var allowMediaPlayerOnLockScreen: Boolean = true private val lockScreenMediaPlayerUri = - secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) + secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) /** * Attaches media container in single pane mode, situated at the top of the notifications list @@ -146,9 +148,7 @@ class KeyguardMediaController @Inject constructor( onMediaHostVisibilityChanged(mediaHost.visible) } - /** - * Called whenever the media hosts visibility changes - */ + /** Called whenever the media hosts visibility changes */ private fun onMediaHostVisibilityChanged(visible: Boolean) { refreshMediaPosition() if (visible) { @@ -159,9 +159,7 @@ class KeyguardMediaController @Inject constructor( } } - /** - * Attaches media container in split shade mode, situated to the left of notifications - */ + /** Attaches media container in split shade mode, situated to the left of notifications */ fun attachSplitShadeContainer(container: ViewGroup) { splitShadeContainer = container reattachHostView() @@ -183,9 +181,7 @@ class KeyguardMediaController @Inject constructor( } if (activeContainer?.childCount == 0) { // Detach the hostView from its parent view if exists - mediaHost.hostView.parent?.let { - (it as? ViewGroup)?.removeView(mediaHost.hostView) - } + mediaHost.hostView.parent?.let { (it as? ViewGroup)?.removeView(mediaHost.hostView) } activeContainer.addView(mediaHost.hostView) } } @@ -193,7 +189,8 @@ class KeyguardMediaController @Inject constructor( fun refreshMediaPosition() { val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD) // mediaHost.visible required for proper animations handling - visible = mediaHost.visible && + visible = + mediaHost.visible && !bypassController.bypassEnabled && keyguardOrUserSwitcher && allowMediaPlayerOnLockScreen diff --git a/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt index 711cb361f437..dd5c2bf497cb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -55,9 +55,7 @@ private data class RippleData( var highlight: Float ) -/** - * Drawable that can draw an animated gradient when tapped. - */ +/** Drawable that can draw an animated gradient when tapped. */ @Keep class LightSourceDrawable : Drawable() { @@ -67,17 +65,15 @@ class LightSourceDrawable : Drawable() { private var paint = Paint() var highlightColor = Color.WHITE - set(value) { - if (field == value) { - return + set(value) { + if (field == value) { + return + } + field = value + invalidateSelf() } - field = value - invalidateSelf() - } - /** - * Draw a small highlight under the finger before expanding (or cancelling) it. - */ + /** Draw a small highlight under the finger before expanding (or cancelling) it. */ private var active: Boolean = false set(value) { if (value == field) { @@ -91,46 +87,54 @@ class LightSourceDrawable : Drawable() { rippleData.progress = RIPPLE_DOWN_PROGRESS } else { rippleAnimation?.cancel() - rippleAnimation = ValueAnimator.ofFloat(rippleData.alpha, 0f).apply { - duration = RIPPLE_CANCEL_DURATION - interpolator = Interpolators.LINEAR_OUT_SLOW_IN - addUpdateListener { - rippleData.alpha = it.animatedValue as Float - invalidateSelf() - } - addListener(object : AnimatorListenerAdapter() { - var cancelled = false - override fun onAnimationCancel(animation: Animator?) { - cancelled = true - } - - override fun onAnimationEnd(animation: Animator?) { - if (cancelled) { - return - } - rippleData.progress = 0f - rippleData.alpha = 0f - rippleAnimation = null + rippleAnimation = + ValueAnimator.ofFloat(rippleData.alpha, 0f).apply { + duration = RIPPLE_CANCEL_DURATION + interpolator = Interpolators.LINEAR_OUT_SLOW_IN + addUpdateListener { + rippleData.alpha = it.animatedValue as Float invalidateSelf() } - }) - start() - } + addListener( + object : AnimatorListenerAdapter() { + var cancelled = false + override fun onAnimationCancel(animation: Animator?) { + cancelled = true + } + + override fun onAnimationEnd(animation: Animator?) { + if (cancelled) { + return + } + rippleData.progress = 0f + rippleData.alpha = 0f + rippleAnimation = null + invalidateSelf() + } + } + ) + start() + } } invalidateSelf() } private var rippleAnimation: Animator? = null - /** - * Draw background and gradient. - */ + /** Draw background and gradient. */ override fun draw(canvas: Canvas) { val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress) val centerColor = - ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt()) - paint.shader = RadialGradient(rippleData.x, rippleData.y, radius, - intArrayOf(centerColor, Color.TRANSPARENT), GRADIENT_STOPS, Shader.TileMode.CLAMP) + ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt()) + paint.shader = + RadialGradient( + rippleData.x, + rippleData.y, + radius, + intArrayOf(centerColor, Color.TRANSPARENT), + GRADIENT_STOPS, + Shader.TileMode.CLAMP + ) canvas.drawCircle(rippleData.x, rippleData.y, radius, paint) } @@ -162,8 +166,8 @@ class LightSourceDrawable : Drawable() { rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) } if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) { - rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / - 100f + rippleData.highlight = + a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f } } @@ -193,40 +197,44 @@ class LightSourceDrawable : Drawable() { invalidateSelf() } - /** - * Draws an animated ripple that expands fading away. - */ + /** Draws an animated ripple that expands fading away. */ private fun illuminate() { rippleData.alpha = 1f invalidateSelf() rippleAnimation?.cancel() - rippleAnimation = AnimatorSet().apply { - playTogether(ValueAnimator.ofFloat(1f, 0f).apply { - startDelay = 133 - duration = RIPPLE_ANIM_DURATION - startDelay - interpolator = Interpolators.LINEAR_OUT_SLOW_IN - addUpdateListener { - rippleData.alpha = it.animatedValue as Float - invalidateSelf() - } - }, ValueAnimator.ofFloat(rippleData.progress, 1f).apply { - duration = RIPPLE_ANIM_DURATION - interpolator = Interpolators.LINEAR_OUT_SLOW_IN - addUpdateListener { - rippleData.progress = it.animatedValue as Float - invalidateSelf() - } - }) - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - rippleData.progress = 0f - rippleAnimation = null - invalidateSelf() - } - }) - start() - } + rippleAnimation = + AnimatorSet().apply { + playTogether( + ValueAnimator.ofFloat(1f, 0f).apply { + startDelay = 133 + duration = RIPPLE_ANIM_DURATION - startDelay + interpolator = Interpolators.LINEAR_OUT_SLOW_IN + addUpdateListener { + rippleData.alpha = it.animatedValue as Float + invalidateSelf() + } + }, + ValueAnimator.ofFloat(rippleData.progress, 1f).apply { + duration = RIPPLE_ANIM_DURATION + interpolator = Interpolators.LINEAR_OUT_SLOW_IN + addUpdateListener { + rippleData.progress = it.animatedValue as Float + invalidateSelf() + } + } + ) + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + rippleData.progress = 0f + rippleAnimation = null + invalidateSelf() + } + } + ) + start() + } } override fun setHotspot(x: Float, y: Float) { @@ -251,8 +259,13 @@ class LightSourceDrawable : Drawable() { override fun getDirtyBounds(): Rect { val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress) - val bounds = Rect((rippleData.x - radius).toInt(), (rippleData.y - radius).toInt(), - (rippleData.x + radius).toInt(), (rippleData.y + radius).toInt()) + val bounds = + Rect( + (rippleData.x - radius).toInt(), + (rippleData.y - radius).toInt(), + (rippleData.x + radius).toInt(), + (rippleData.y + radius).toInt() + ) bounds.union(super.getDirtyBounds()) return bounds } @@ -293,4 +306,4 @@ class LightSourceDrawable : Drawable() { return changed } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt new file mode 100644 index 000000000000..e38c1baaeae9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt @@ -0,0 +1,1327 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS +import android.util.Log +import android.util.MathUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.PathInterpolator +import android.widget.LinearLayout +import androidx.annotation.VisibleForTesting +import com.android.internal.logging.InstanceId +import com.android.systemui.Dumpable +import com.android.systemui.R +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaViewHolder +import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.ui.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.media.controls.util.SmallHash +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.PageIndicator +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener +import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.Utils +import com.android.systemui.util.animation.UniqueObjectHostView +import com.android.systemui.util.animation.requiresRemeasuring +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock +import com.android.systemui.util.traceSection +import java.io.PrintWriter +import java.util.TreeMap +import javax.inject.Inject +import javax.inject.Provider + +private const val TAG = "MediaCarouselController" +private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS) +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + +/** + * Class that is responsible for keeping the view carousel up to date. This also handles changes in + * state and applies them to the media carousel like the expansion. + */ +@SysUISingleton +class MediaCarouselController +@Inject +constructor( + private val context: Context, + private val mediaControlPanelFactory: Provider<MediaControlPanel>, + private val visualStabilityProvider: VisualStabilityProvider, + private val mediaHostStatesManager: MediaHostStatesManager, + private val activityStarter: ActivityStarter, + private val systemClock: SystemClock, + @Main executor: DelayableExecutor, + private val mediaManager: MediaDataManager, + configurationController: ConfigurationController, + falsingCollector: FalsingCollector, + falsingManager: FalsingManager, + dumpManager: DumpManager, + private val logger: MediaUiEventLogger, + private val debugLogger: MediaCarouselControllerLogger +) : Dumpable { + /** The current width of the carousel */ + private var currentCarouselWidth: Int = 0 + + /** The current height of the carousel */ + private var currentCarouselHeight: Int = 0 + + /** Are we currently showing only active players */ + private var currentlyShowingOnlyActive: Boolean = false + + /** Is the player currently visible (at the end of the transformation */ + private var playersVisible: Boolean = false + /** + * The desired location where we'll be at the end of the transformation. Usually this matches + * the end location, except when we're still waiting on a state update call. + */ + @MediaLocation private var desiredLocation: Int = -1 + + /** + * The ending location of the view where it ends when all animations and transitions have + * finished + */ + @MediaLocation @VisibleForTesting var currentEndLocation: Int = -1 + + /** + * The ending location of the view where it ends when all animations and transitions have + * finished + */ + @MediaLocation private var currentStartLocation: Int = -1 + + /** The progress of the transition or 1.0 if there is no transition happening */ + private var currentTransitionProgress: Float = 1.0f + + /** The measured width of the carousel */ + private var carouselMeasureWidth: Int = 0 + + /** The measured height of the carousel */ + private var carouselMeasureHeight: Int = 0 + private var desiredHostState: MediaHostState? = null + private val mediaCarousel: MediaScrollView + val mediaCarouselScrollHandler: MediaCarouselScrollHandler + val mediaFrame: ViewGroup + @VisibleForTesting + lateinit var settingsButton: View + private set + private val mediaContent: ViewGroup + @VisibleForTesting val pageIndicator: PageIndicator + private val visualStabilityCallback: OnReorderingAllowedListener + private var needsReordering: Boolean = false + private var keysNeedRemoval = mutableSetOf<String>() + var shouldScrollToKey: Boolean = false + private var isRtl: Boolean = false + set(value) { + if (value != field) { + field = value + mediaFrame.layoutDirection = + if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR + mediaCarouselScrollHandler.scrollToStart() + } + } + private var currentlyExpanded = true + set(value) { + if (field != value) { + field = value + for (player in MediaPlayerData.players()) { + player.setListening(field) + } + } + } + + companion object { + const val ANIMATION_BASE_DURATION = 2200f + const val DURATION = 167f + const val DETAILS_DELAY = 1067f + const val CONTROLS_DELAY = 1400f + const val PAGINATION_DELAY = 1900f + const val MEDIATITLES_DELAY = 1000f + const val MEDIACONTAINERS_DELAY = 967f + val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F) + val REVERSE_BEZIER = PathInterpolator(0F, 0.68F, 1F, 0F) + + fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float { + val transformStartFraction = delay / ANIMATION_BASE_DURATION + val transformDurationFraction = duration / ANIMATION_BASE_DURATION + val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction) + return MathUtils.constrain( + (squishinessToTime - transformStartFraction) / transformDurationFraction, + 0F, + 1F + ) + } + } + + private val configListener = + object : ConfigurationController.ConfigurationListener { + override fun onDensityOrFontScaleChanged() { + // System font changes should only happen when UMO is offscreen or a flicker may + // occur + updatePlayers(recreateMedia = true) + inflateSettingsButton() + } + + override fun onThemeChanged() { + updatePlayers(recreateMedia = false) + inflateSettingsButton() + } + + override fun onConfigChanged(newConfig: Configuration?) { + if (newConfig == null) return + isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL + } + + override fun onUiModeChanged() { + updatePlayers(recreateMedia = false) + inflateSettingsButton() + } + } + + /** + * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility. + * It will be called when the container is out of view. + */ + lateinit var updateUserVisibility: () -> Unit + lateinit var updateHostVisibility: () -> Unit + + private val isReorderingAllowed: Boolean + get() = visualStabilityProvider.isReorderingAllowed + + init { + dumpManager.registerDumpable(TAG, this) + mediaFrame = inflateMediaCarousel() + mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) + pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) + mediaCarouselScrollHandler = + MediaCarouselScrollHandler( + mediaCarousel, + pageIndicator, + executor, + this::onSwipeToDismiss, + this::updatePageIndicatorLocation, + this::closeGuts, + falsingCollector, + falsingManager, + this::logSmartspaceImpression, + logger + ) + isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL + inflateSettingsButton() + mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) + configurationController.addCallback(configListener) + visualStabilityCallback = OnReorderingAllowedListener { + if (needsReordering) { + needsReordering = false + reorderAllPlayers(previousVisiblePlayerKey = null) + } + + keysNeedRemoval.forEach { removePlayer(it) } + if (keysNeedRemoval.size > 0) { + // Carousel visibility may need to be updated after late removals + updateHostVisibility() + } + keysNeedRemoval.clear() + + // Update user visibility so that no extra impression will be logged when + // activeMediaIndex resets to 0 + if (this::updateUserVisibility.isInitialized) { + updateUserVisibility() + } + + // Let's reset our scroll position + mediaCarouselScrollHandler.scrollToStart() + } + visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback) + mediaManager.addListener( + object : MediaDataManager.Listener { + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean, + receivedSmartspaceCardLatency: Int, + isSsReactivated: Boolean + ) { + debugLogger.logMediaLoaded(key) + if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) { + // Log card received if a new resumable media card is added + MediaPlayerData.getMediaPlayer(key)?.let { + /* ktlint-disable max-line-length */ + logSmartspaceCardReported( + 759, // SMARTSPACE_CARD_RECEIVED + it.mSmartspaceId, + it.mUid, + surfaces = + intArrayOf( + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY + ), + rank = MediaPlayerData.getMediaPlayerIndex(key) + ) + /* ktlint-disable max-line-length */ + } + if ( + mediaCarouselScrollHandler.visibleToUser && + mediaCarouselScrollHandler.visibleMediaIndex == + MediaPlayerData.getMediaPlayerIndex(key) + ) { + logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) + } + } else if (receivedSmartspaceCardLatency != 0) { + // Log resume card received if resumable media card is reactivated and + // resume card is ranked first + MediaPlayerData.players().forEachIndexed { index, it -> + if (it.recommendationViewHolder == null) { + it.mSmartspaceId = + SmallHash.hash( + it.mUid + systemClock.currentTimeMillis().toInt() + ) + it.mIsImpressed = false + /* ktlint-disable max-line-length */ + logSmartspaceCardReported( + 759, // SMARTSPACE_CARD_RECEIVED + it.mSmartspaceId, + it.mUid, + surfaces = + intArrayOf( + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY + ), + rank = index, + receivedLatencyMillis = receivedSmartspaceCardLatency + ) + /* ktlint-disable max-line-length */ + } + } + // If media container area already visible to the user, log impression for + // reactivated card. + if ( + mediaCarouselScrollHandler.visibleToUser && + !mediaCarouselScrollHandler.qsExpanded + ) { + logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) + } + } + + val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active + if (canRemove && !Utils.useMediaResumption(context)) { + // This view isn't playing, let's remove this! This happens e.g. when + // dismissing/timing out a view. We still have the data around because + // resumption could be on, but we should save the resources and release + // this. + if (isReorderingAllowed) { + onMediaDataRemoved(key) + } else { + keysNeedRemoval.add(key) + } + } else { + keysNeedRemoval.remove(key) + } + } + + override fun onSmartspaceMediaDataLoaded( + key: String, + data: SmartspaceMediaData, + shouldPrioritize: Boolean + ) { + debugLogger.logRecommendationLoaded(key) + // Log the case where the hidden media carousel with the existed inactive resume + // media is shown by the Smartspace signal. + if (data.isActive) { + val hasActivatedExistedResumeMedia = + !mediaManager.hasActiveMedia() && + mediaManager.hasAnyMedia() && + shouldPrioritize + if (hasActivatedExistedResumeMedia) { + // Log resume card received if resumable media card is reactivated and + // recommendation card is valid and ranked first + MediaPlayerData.players().forEachIndexed { index, it -> + if (it.recommendationViewHolder == null) { + it.mSmartspaceId = + SmallHash.hash( + it.mUid + systemClock.currentTimeMillis().toInt() + ) + it.mIsImpressed = false + /* ktlint-disable max-line-length */ + logSmartspaceCardReported( + 759, // SMARTSPACE_CARD_RECEIVED + it.mSmartspaceId, + it.mUid, + surfaces = + intArrayOf( + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY + ), + rank = index, + receivedLatencyMillis = + (systemClock.currentTimeMillis() - + data.headphoneConnectionTimeMillis) + .toInt() + ) + /* ktlint-disable max-line-length */ + } + } + } + addSmartspaceMediaRecommendations(key, data, shouldPrioritize) + MediaPlayerData.getMediaPlayer(key)?.let { + /* ktlint-disable max-line-length */ + logSmartspaceCardReported( + 759, // SMARTSPACE_CARD_RECEIVED + it.mSmartspaceId, + it.mUid, + surfaces = + intArrayOf( + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN, + SysUiStatsLog + .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY + ), + rank = MediaPlayerData.getMediaPlayerIndex(key), + receivedLatencyMillis = + (systemClock.currentTimeMillis() - + data.headphoneConnectionTimeMillis) + .toInt() + ) + /* ktlint-disable max-line-length */ + } + if ( + mediaCarouselScrollHandler.visibleToUser && + mediaCarouselScrollHandler.visibleMediaIndex == + MediaPlayerData.getMediaPlayerIndex(key) + ) { + logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) + } + } else { + onSmartspaceMediaDataRemoved(data.targetId, immediately = true) + } + } + + override fun onMediaDataRemoved(key: String) { + debugLogger.logMediaRemoved(key) + removePlayer(key) + } + + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + debugLogger.logRecommendationRemoved(key, immediately) + if (immediately || isReorderingAllowed) { + removePlayer(key) + if (!immediately) { + // Although it wasn't requested, we were able to process the removal + // immediately since reordering is allowed. So, notify hosts to update + if (this@MediaCarouselController::updateHostVisibility.isInitialized) { + updateHostVisibility() + } + } + } else { + keysNeedRemoval.add(key) + } + } + } + ) + mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + // The pageIndicator is not laid out yet when we get the current state update, + // Lets make sure we have the right dimensions + updatePageIndicatorLocation() + } + mediaHostStatesManager.addCallback( + object : MediaHostStatesManager.Callback { + override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) { + if (location == desiredLocation) { + onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false) + } + } + } + ) + } + + private fun inflateSettingsButton() { + val settings = + LayoutInflater.from(context) + .inflate(R.layout.media_carousel_settings_button, mediaFrame, false) as View + if (this::settingsButton.isInitialized) { + mediaFrame.removeView(settingsButton) + } + settingsButton = settings + mediaFrame.addView(settingsButton) + mediaCarouselScrollHandler.onSettingsButtonUpdated(settings) + settingsButton.setOnClickListener { + logger.logCarouselSettings() + activityStarter.startActivity(settingsIntent, true /* dismissShade */) + } + } + + private fun inflateMediaCarousel(): ViewGroup { + val mediaCarousel = + LayoutInflater.from(context) + .inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup + // Because this is inflated when not attached to the true view hierarchy, it resolves some + // potential issues to force that the layout direction is defined by the locale + // (rather than inherited from the parent, which would resolve to LTR when unattached). + mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE + return mediaCarousel + } + + private fun reorderAllPlayers( + previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?, + key: String? = null + ) { + mediaContent.removeAllViews() + for (mediaPlayer in MediaPlayerData.players()) { + mediaPlayer.mediaViewHolder?.let { mediaContent.addView(it.player) } + ?: mediaPlayer.recommendationViewHolder?.let { + mediaContent.addView(it.recommendations) + } + } + mediaCarouselScrollHandler.onPlayersChanged() + MediaPlayerData.updateVisibleMediaPlayers() + // Automatically scroll to the active player if needed + if (shouldScrollToKey) { + shouldScrollToKey = false + val mediaIndex = key?.let { MediaPlayerData.getMediaPlayerIndex(it) } ?: -1 + if (mediaIndex != -1) { + previousVisiblePlayerKey?.let { + val previousVisibleIndex = + MediaPlayerData.playerKeys().indexOfFirst { key -> it == key } + mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex) + } + ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex) + } + } + } + + // Returns true if new player is added + private fun addOrUpdatePlayer( + key: String, + oldKey: String?, + data: MediaData, + isSsReactivated: Boolean + ): Boolean = + traceSection("MediaCarouselController#addOrUpdatePlayer") { + MediaPlayerData.moveIfExists(oldKey, key) + val existingPlayer = MediaPlayerData.getMediaPlayer(key) + val curVisibleMediaKey = + MediaPlayerData.visiblePlayerKeys() + .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) + if (existingPlayer == null) { + val newPlayer = mediaControlPanelFactory.get() + newPlayer.attachPlayer( + MediaViewHolder.create(LayoutInflater.from(context), mediaContent) + ) + newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions + val lp = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + newPlayer.mediaViewHolder?.player?.setLayoutParams(lp) + newPlayer.bindPlayer(data, key) + newPlayer.setListening(currentlyExpanded) + MediaPlayerData.addMediaPlayer( + key, + data, + newPlayer, + systemClock, + isSsReactivated, + debugLogger + ) + updatePlayerToState(newPlayer, noAnimation = true) + // Media data added from a recommendation card should starts playing. + if ( + (shouldScrollToKey && data.isPlaying == true) || + (!shouldScrollToKey && data.active) + ) { + reorderAllPlayers(curVisibleMediaKey, key) + } else { + needsReordering = true + } + } else { + existingPlayer.bindPlayer(data, key) + MediaPlayerData.addMediaPlayer( + key, + data, + existingPlayer, + systemClock, + isSsReactivated, + debugLogger + ) + val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String() + // In case of recommendations hits. + // Check the playing status of media player and the package name. + // To make sure we scroll to the right app's media player. + if ( + isReorderingAllowed || + shouldScrollToKey && + data.isPlaying == true && + packageName == data.packageName + ) { + reorderAllPlayers(curVisibleMediaKey, key) + } else { + needsReordering = true + } + } + updatePageIndicator() + mediaCarouselScrollHandler.onPlayersChanged() + mediaFrame.requiresRemeasuring = true + // Check postcondition: mediaContent should have the same number of children as there + // are + // elements in mediaPlayers. + if (MediaPlayerData.players().size != mediaContent.childCount) { + Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") + } + return existingPlayer == null + } + + private fun addSmartspaceMediaRecommendations( + key: String, + data: SmartspaceMediaData, + shouldPrioritize: Boolean + ) = + traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") { + if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel") + if (MediaPlayerData.getMediaPlayer(key) != null) { + Log.w(TAG, "Skip adding smartspace target in carousel") + return + } + + val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey() + existingSmartspaceMediaKey?.let { + val removedPlayer = + MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey, true) + removedPlayer?.run { + debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) + } + } + + val newRecs = mediaControlPanelFactory.get() + newRecs.attachRecommendation( + RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent) + ) + newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions + val lp = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp) + newRecs.bindRecommendation(data) + val curVisibleMediaKey = + MediaPlayerData.visiblePlayerKeys() + .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) + MediaPlayerData.addMediaRecommendation( + key, + data, + newRecs, + shouldPrioritize, + systemClock, + debugLogger + ) + updatePlayerToState(newRecs, noAnimation = true) + reorderAllPlayers(curVisibleMediaKey) + updatePageIndicator() + mediaFrame.requiresRemeasuring = true + // Check postcondition: mediaContent should have the same number of children as there + // are + // elements in mediaPlayers. + if (MediaPlayerData.players().size != mediaContent.childCount) { + Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") + } + } + + fun removePlayer( + key: String, + dismissMediaData: Boolean = true, + dismissRecommendation: Boolean = true + ) { + if (key == MediaPlayerData.smartspaceMediaKey()) { + MediaPlayerData.smartspaceMediaData?.let { + logger.logRecommendationRemoved(it.packageName, it.instanceId) + } + } + val removed = + MediaPlayerData.removeMediaPlayer(key, dismissMediaData || dismissRecommendation) + removed?.apply { + mediaCarouselScrollHandler.onPrePlayerRemoved(removed) + mediaContent.removeView(removed.mediaViewHolder?.player) + mediaContent.removeView(removed.recommendationViewHolder?.recommendations) + removed.onDestroy() + mediaCarouselScrollHandler.onPlayersChanged() + updatePageIndicator() + + if (dismissMediaData) { + // Inform the media manager of a potentially late dismissal + mediaManager.dismissMediaData(key, delay = 0L) + } + if (dismissRecommendation) { + // Inform the media manager of a potentially late dismissal + mediaManager.dismissSmartspaceRecommendation(key, delay = 0L) + } + } + } + + private fun updatePlayers(recreateMedia: Boolean) { + pageIndicator.tintList = + ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) + + MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) -> + if (isSsMediaRec) { + val smartspaceMediaData = MediaPlayerData.smartspaceMediaData + removePlayer(key, dismissMediaData = false, dismissRecommendation = false) + smartspaceMediaData?.let { + addSmartspaceMediaRecommendations( + it.targetId, + it, + MediaPlayerData.shouldPrioritizeSs + ) + } + } else { + val isSsReactivated = MediaPlayerData.isSsReactivated(key) + if (recreateMedia) { + removePlayer(key, dismissMediaData = false, dismissRecommendation = false) + } + addOrUpdatePlayer( + key = key, + oldKey = null, + data = data, + isSsReactivated = isSsReactivated + ) + } + } + } + + private fun updatePageIndicator() { + val numPages = mediaContent.getChildCount() + pageIndicator.setNumPages(numPages) + if (numPages == 1) { + pageIndicator.setLocation(0f) + } + updatePageIndicatorAlpha() + } + + /** + * Set a new interpolated state for all players. This is a state that is usually controlled by a + * finger movement where the user drags from one state to the next. + * + * @param startLocation the start location of our state or -1 if this is directly set + * @param endLocation the ending location of our state. + * @param progress the progress of the transition between startLocation and endlocation. If + * ``` + * this is not a guided transformation, this will be 1.0f + * @param immediately + * ``` + * should this state be applied immediately, canceling all animations? + */ + fun setCurrentState( + @MediaLocation startLocation: Int, + @MediaLocation endLocation: Int, + progress: Float, + immediately: Boolean + ) { + if ( + startLocation != currentStartLocation || + endLocation != currentEndLocation || + progress != currentTransitionProgress || + immediately + ) { + currentStartLocation = startLocation + currentEndLocation = endLocation + currentTransitionProgress = progress + for (mediaPlayer in MediaPlayerData.players()) { + updatePlayerToState(mediaPlayer, immediately) + } + maybeResetSettingsCog() + updatePageIndicatorAlpha() + } + } + + @VisibleForTesting + fun updatePageIndicatorAlpha() { + val hostStates = mediaHostStatesManager.mediaHostStates + val endIsVisible = hostStates[currentEndLocation]?.visible ?: false + val startIsVisible = hostStates[currentStartLocation]?.visible ?: false + val startAlpha = if (startIsVisible) 1.0f else 0.0f + // when squishing in split shade, only use endState, which keeps changing + // to provide squishFraction + val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F + val endAlpha = + (if (endIsVisible) 1.0f else 0.0f) * + calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION) + var alpha = 1.0f + if (!endIsVisible || !startIsVisible) { + var progress = currentTransitionProgress + if (!endIsVisible) { + progress = 1.0f - progress + } + // Let's fade in quickly at the end where the view is visible + progress = + MathUtils.constrain(MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress), 0.0f, 1.0f) + alpha = MathUtils.lerp(startAlpha, endAlpha, progress) + } + pageIndicator.alpha = alpha + } + + private fun updatePageIndicatorLocation() { + // Update the location of the page indicator, carousel clipping + val translationX = + if (isRtl) { + (pageIndicator.width - currentCarouselWidth) / 2.0f + } else { + (currentCarouselWidth - pageIndicator.width) / 2.0f + } + pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation + val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams + pageIndicator.translationY = + (currentCarouselHeight - pageIndicator.height - layoutParams.bottomMargin).toFloat() + } + + /** Update the dimension of this carousel. */ + private fun updateCarouselDimensions() { + var width = 0 + var height = 0 + for (mediaPlayer in MediaPlayerData.players()) { + val controller = mediaPlayer.mediaViewController + // When transitioning the view to gone, the view gets smaller, but the translation + // Doesn't, let's add the translation + width = Math.max(width, controller.currentWidth + controller.translationX.toInt()) + height = Math.max(height, controller.currentHeight + controller.translationY.toInt()) + } + if (width != currentCarouselWidth || height != currentCarouselHeight) { + currentCarouselWidth = width + currentCarouselHeight = height + mediaCarouselScrollHandler.setCarouselBounds( + currentCarouselWidth, + currentCarouselHeight + ) + updatePageIndicatorLocation() + updatePageIndicatorAlpha() + } + } + + private fun maybeResetSettingsCog() { + val hostStates = mediaHostStatesManager.mediaHostStates + val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia ?: true + val startShowsActive = + hostStates[currentStartLocation]?.showsOnlyActiveMedia ?: endShowsActive + if ( + currentlyShowingOnlyActive != endShowsActive || + ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && + startShowsActive != endShowsActive) + ) { + // Whenever we're transitioning from between differing states or the endstate differs + // we reset the translation + currentlyShowingOnlyActive = endShowsActive + mediaCarouselScrollHandler.resetTranslation(animate = true) + } + } + + private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) { + mediaPlayer.mediaViewController.setCurrentState( + startLocation = currentStartLocation, + endLocation = currentEndLocation, + transitionProgress = currentTransitionProgress, + applyImmediately = noAnimation + ) + } + + /** + * The desired location of this view has changed. We should remeasure the view to match the new + * bounds and kick off bounds animations if necessary. If an animation is happening, an + * animation is kicked of externally, which sets a new current state until we reach the + * targetState. + * + * @param desiredLocation the location we're going to + * @param desiredHostState the target state we're transitioning to + * @param animate should this be animated + */ + fun onDesiredLocationChanged( + desiredLocation: Int, + desiredHostState: MediaHostState?, + animate: Boolean, + duration: Long = 200, + startDelay: Long = 0 + ) = + traceSection("MediaCarouselController#onDesiredLocationChanged") { + desiredHostState?.let { + if (this.desiredLocation != desiredLocation) { + // Only log an event when location changes + logger.logCarouselPosition(desiredLocation) + } + + // This is a hosting view, let's remeasure our players + this.desiredLocation = desiredLocation + this.desiredHostState = it + currentlyExpanded = it.expansion > 0 + + val shouldCloseGuts = + !currentlyExpanded && + !mediaManager.hasActiveMediaOrRecommendation() && + desiredHostState.showsOnlyActiveMedia + + for (mediaPlayer in MediaPlayerData.players()) { + if (animate) { + mediaPlayer.mediaViewController.animatePendingStateChange( + duration = duration, + delay = startDelay + ) + } + if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) { + mediaPlayer.closeGuts(!animate) + } + + mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation) + } + mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia + mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded + val nowVisible = it.visible + if (nowVisible != playersVisible) { + playersVisible = nowVisible + if (nowVisible) { + mediaCarouselScrollHandler.resetTranslation() + } + } + updateCarouselSize() + } + } + + fun closeGuts(immediate: Boolean = true) { + MediaPlayerData.players().forEach { it.closeGuts(immediate) } + } + + /** Update the size of the carousel, remeasuring it if necessary. */ + private fun updateCarouselSize() { + val width = desiredHostState?.measurementInput?.width ?: 0 + val height = desiredHostState?.measurementInput?.height ?: 0 + if ( + width != carouselMeasureWidth && width != 0 || + height != carouselMeasureHeight && height != 0 + ) { + carouselMeasureWidth = width + carouselMeasureHeight = height + val playerWidthPlusPadding = + carouselMeasureWidth + + context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) + // Let's remeasure the carousel + val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 + val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 + mediaCarousel.measure(widthSpec, heightSpec) + mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight) + // Update the padding after layout; view widths are used in RTL to calculate scrollX + mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding + } + } + + /** Log the user impression for media card at visibleMediaIndex. */ + fun logSmartspaceImpression(qsExpanded: Boolean) { + val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex + if (MediaPlayerData.players().size > visibleMediaIndex) { + val mediaControlPanel = MediaPlayerData.getMediaControlPanel(visibleMediaIndex) + val hasActiveMediaOrRecommendationCard = + MediaPlayerData.hasActiveMediaOrRecommendationCard() + if (!hasActiveMediaOrRecommendationCard && !qsExpanded) { + // Skip logging if on LS or QQS, and there is no active media card + return + } + mediaControlPanel?.let { + logSmartspaceCardReported( + 800, // SMARTSPACE_CARD_SEEN + it.mSmartspaceId, + it.mUid, + intArrayOf(it.surfaceForSmartspaceLogging) + ) + it.mIsImpressed = true + } + } + } + + @JvmOverloads + /** + * Log Smartspace events + * + * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN) + * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new + * instanceId + * @param uid uid for the application that media comes from + * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when + * the event happened + * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1 + * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc. + * @param interactedSubcardCardinality how many media items were shown to the user when there is + * user interaction + * @param rank the rank for media card in the media carousel, starting from 0 + * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency + * between headphone connection to sysUI displays media recommendation card + * @param isSwipeToDismiss whether is to log swipe-to-dismiss event + */ + fun logSmartspaceCardReported( + eventId: Int, + instanceId: Int, + uid: Int, + surfaces: IntArray, + interactedSubcardRank: Int = 0, + interactedSubcardCardinality: Int = 0, + rank: Int = mediaCarouselScrollHandler.visibleMediaIndex, + receivedLatencyMillis: Int = 0, + isSwipeToDismiss: Boolean = false + ) { + if (MediaPlayerData.players().size <= rank) { + return + } + + val mediaControlKey = MediaPlayerData.visiblePlayerKeys().elementAt(rank) + // Only log media resume card when Smartspace data is available + if ( + !mediaControlKey.isSsMediaRec && + !mediaManager.smartspaceMediaData.isActive && + MediaPlayerData.smartspaceMediaData == null + ) { + return + } + + val cardinality = mediaContent.getChildCount() + surfaces.forEach { surface -> + /* ktlint-disable max-line-length */ + SysUiStatsLog.write( + SysUiStatsLog.SMARTSPACE_CARD_REPORTED, + eventId, + instanceId, + // Deprecated, replaced with AiAi feature type so we don't need to create logging + // card type for each new feature. + SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD, + surface, + // Use -1 as rank value to indicate user swipe to dismiss the card + if (isSwipeToDismiss) -1 else rank, + cardinality, + if (mediaControlKey.isSsMediaRec) 15 // MEDIA_RECOMMENDATION + else if (mediaControlKey.isSsReactivated) 43 // MEDIA_RESUME_SS_ACTIVATED + else 31, // MEDIA_RESUME + uid, + interactedSubcardRank, + interactedSubcardCardinality, + receivedLatencyMillis, + null, // Media cards cannot have subcards. + null // Media cards don't have dimensions today. + ) + /* ktlint-disable max-line-length */ + if (DEBUG) { + Log.d( + TAG, + "Log Smartspace card event id: $eventId instance id: $instanceId" + + " surface: $surface rank: $rank cardinality: $cardinality " + + "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " + + "isSsReactivated: ${mediaControlKey.isSsReactivated}" + + "uid: $uid " + + "interactedSubcardRank: $interactedSubcardRank " + + "interactedSubcardCardinality: $interactedSubcardCardinality " + + "received_latency_millis: $receivedLatencyMillis" + ) + } + } + } + + private fun onSwipeToDismiss() { + MediaPlayerData.players().forEachIndexed { index, it -> + if (it.mIsImpressed) { + logSmartspaceCardReported( + SMARTSPACE_CARD_DISMISS_EVENT, + it.mSmartspaceId, + it.mUid, + intArrayOf(it.surfaceForSmartspaceLogging), + rank = index, + isSwipeToDismiss = true + ) + // Reset card impressed state when swipe to dismissed + it.mIsImpressed = false + } + } + logger.logSwipeDismiss() + mediaManager.onSwipeToDismiss() + } + + fun getCurrentVisibleMediaContentIntent(): PendingIntent? { + return MediaPlayerData.playerKeys() + .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) + ?.data + ?.clickIntent + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.apply { + println("keysNeedRemoval: $keysNeedRemoval") + println("dataKeys: ${MediaPlayerData.dataKeys()}") + println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}") + println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}") + println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}") + println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}") + println("current size: $currentCarouselWidth x $currentCarouselHeight") + println("location: $desiredLocation") + println( + "state: ${desiredHostState?.expansion}, " + + "only active ${desiredHostState?.showsOnlyActiveMedia}" + ) + } + } +} + +@VisibleForTesting +internal object MediaPlayerData { + private val EMPTY = + MediaData( + userId = -1, + initialized = false, + app = null, + appIcon = null, + artist = null, + song = null, + artwork = null, + actions = emptyList(), + actionsToShowInCompact = emptyList(), + packageName = "INVALID", + token = null, + clickIntent = null, + device = null, + active = true, + resumeAction = null, + instanceId = InstanceId.fakeInstanceId(-1), + appUid = -1 + ) + // Whether should prioritize Smartspace card. + internal var shouldPrioritizeSs: Boolean = false + private set + internal var smartspaceMediaData: SmartspaceMediaData? = null + private set + + data class MediaSortKey( + val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation. + val data: MediaData, + val key: String, + val updateTime: Long = 0, + val isSsReactivated: Boolean = false + ) + + private val comparator = + compareByDescending<MediaSortKey> { + it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL + } + .thenByDescending { + it.data.isPlaying == true && + it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL + } + .thenByDescending { it.data.active } + .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec } + .thenByDescending { !it.data.resumption } + .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE } + .thenByDescending { it.data.lastActive } + .thenByDescending { it.updateTime } + .thenByDescending { it.data.notificationKey } + + private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) + private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() + // A map that tracks order of visible media players before they get reordered. + private val visibleMediaPlayers = LinkedHashMap<String, MediaSortKey>() + + fun addMediaPlayer( + key: String, + data: MediaData, + player: MediaControlPanel, + clock: SystemClock, + isSsReactivated: Boolean, + debugLogger: MediaCarouselControllerLogger? = null + ) { + val removedPlayer = removeMediaPlayer(key) + if (removedPlayer != null && removedPlayer != player) { + debugLogger?.logPotentialMemoryLeak(key) + } + val sortKey = + MediaSortKey( + isSsMediaRec = false, + data, + key, + clock.currentTimeMillis(), + isSsReactivated = isSsReactivated + ) + mediaData.put(key, sortKey) + mediaPlayers.put(sortKey, player) + visibleMediaPlayers.put(key, sortKey) + } + + fun addMediaRecommendation( + key: String, + data: SmartspaceMediaData, + player: MediaControlPanel, + shouldPrioritize: Boolean, + clock: SystemClock, + debugLogger: MediaCarouselControllerLogger? = null + ) { + shouldPrioritizeSs = shouldPrioritize + val removedPlayer = removeMediaPlayer(key) + if (removedPlayer != null && removedPlayer != player) { + debugLogger?.logPotentialMemoryLeak(key) + } + val sortKey = + MediaSortKey( + isSsMediaRec = true, + EMPTY.copy(isPlaying = false), + key, + clock.currentTimeMillis(), + isSsReactivated = true + ) + mediaData.put(key, sortKey) + mediaPlayers.put(sortKey, player) + visibleMediaPlayers.put(key, sortKey) + smartspaceMediaData = data + } + + fun moveIfExists( + oldKey: String?, + newKey: String, + debugLogger: MediaCarouselControllerLogger? = null + ) { + if (oldKey == null || oldKey == newKey) { + return + } + + mediaData.remove(oldKey)?.let { + // MediaPlayer should not be visible + // no need to set isDismissed flag. + val removedPlayer = removeMediaPlayer(newKey) + removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) } + mediaData.put(newKey, it) + } + } + + fun getMediaControlPanel(visibleIndex: Int): MediaControlPanel? { + return mediaPlayers.get(visiblePlayerKeys().elementAt(visibleIndex)) + } + + fun getMediaPlayer(key: String): MediaControlPanel? { + return mediaData.get(key)?.let { mediaPlayers.get(it) } + } + + fun getMediaPlayerIndex(key: String): Int { + val sortKey = mediaData.get(key) + mediaPlayers.entries.forEachIndexed { index, e -> + if (e.key == sortKey) { + return index + } + } + return -1 + } + + /** + * Removes media player given the key. + * @param isDismissed determines whether the media player is removed from the carousel. + */ + fun removeMediaPlayer(key: String, isDismissed: Boolean = false) = + mediaData.remove(key)?.let { + if (it.isSsMediaRec) { + smartspaceMediaData = null + } + if (isDismissed) { + visibleMediaPlayers.remove(key) + } + mediaPlayers.remove(it) + } + + fun mediaData() = + mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) } + + fun dataKeys() = mediaData.keys + + fun players() = mediaPlayers.values + + fun playerKeys() = mediaPlayers.keys + + fun visiblePlayerKeys() = visibleMediaPlayers.values + + /** Returns the index of the first non-timeout media. */ + fun firstActiveMediaIndex(): Int { + mediaPlayers.entries.forEachIndexed { index, e -> + if (!e.key.isSsMediaRec && e.key.data.active) { + return index + } + } + return -1 + } + + /** Returns the existing Smartspace target id. */ + fun smartspaceMediaKey(): String? { + mediaData.entries.forEach { e -> + if (e.value.isSsMediaRec) { + return e.key + } + } + return null + } + + @VisibleForTesting + fun clear() { + mediaData.clear() + mediaPlayers.clear() + visibleMediaPlayers.clear() + } + + /* Returns true if there is active media player card or recommendation card */ + fun hasActiveMediaOrRecommendationCard(): Boolean { + if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) { + return true + } + if (firstActiveMediaIndex() != -1) { + return true + } + return false + } + + fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false + + /** + * This method is called when media players are reordered. To make sure we have the new version + * of the order of media players visible to user. + */ + fun updateVisibleMediaPlayers() { + visibleMediaPlayers.clear() + playerKeys().forEach { visibleMediaPlayers.put(it.key, it) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt index b1018f9544c0..eed1bd743938 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt @@ -14,63 +14,53 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaCarouselControllerLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** A debug logger for [MediaCarouselController]. */ @SysUISingleton -class MediaCarouselControllerLogger @Inject constructor( - @MediaCarouselControllerLog private val buffer: LogBuffer -) { +class MediaCarouselControllerLogger +@Inject +constructor(@MediaCarouselControllerLog private val buffer: LogBuffer) { /** * Log that there might be a potential memory leak for the [MediaControlPanel] and/or * [MediaViewController] related to [key]. */ - fun logPotentialMemoryLeak(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { str1 = key }, - { - "Potential memory leak: " + + fun logPotentialMemoryLeak(key: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { + "Potential memory leak: " + "Removing control panel for $str1 from map without calling #onDestroy" - } - ) + } + ) - fun logMediaLoaded(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { str1 = key }, - { "add player $str1" } - ) + fun logMediaLoaded(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add player $str1" }) - fun logMediaRemoved(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { str1 = key }, - { "removing player $str1" } - ) + fun logMediaRemoved(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" }) - fun logRecommendationLoaded(key: String) = buffer.log( - TAG, - LogLevel.DEBUG, - { str1 = key }, - { "add recommendation $str1" } - ) + fun logRecommendationLoaded(key: String) = + buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add recommendation $str1" }) - fun logRecommendationRemoved(key: String, immediately: Boolean) = buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - bool1 = immediately - }, - { "removing recommendation $str1, immediate=$bool1" } - ) + fun logRecommendationRemoved(key: String, immediately: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = key + bool1 = immediately + }, + { "removing recommendation $str1, immediate=$bool1" } + ) } private const val TAG = "MediaCarouselCtlrLog" diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt index ef49fd35d703..36b2eda65fab 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.graphics.Outline import android.util.MathUtils @@ -31,6 +31,7 @@ import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PageIndicator import com.android.systemui.util.concurrency.DelayableExecutor @@ -43,16 +44,13 @@ private const val RUBBERBAND_FACTOR = 0.2f private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f /** - * Default spring configuration to use for animations where stiffness and/or damping ratio - * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. + * Default spring configuration to use for animations where stiffness and/or damping ratio were not + * provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. */ -private val translationConfig = PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_MEDIUM, - SpringForce.DAMPING_RATIO_LOW_BOUNCY) +private val translationConfig = + PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY) -/** - * A controller class for the media scrollview, responsible for touch handling - */ +/** A controller class for the media scrollview, responsible for touch handling */ class MediaCarouselScrollHandler( private val scrollView: MediaScrollView, private val pageIndicator: PageIndicator, @@ -65,57 +63,36 @@ class MediaCarouselScrollHandler( private val logSmartspaceImpression: (Boolean) -> Unit, private val logger: MediaUiEventLogger ) { - /** - * Is the view in RTL - */ - val isRtl: Boolean get() = scrollView.isLayoutRtl - /** - * Do we need falsing protection? - */ + /** Is the view in RTL */ + val isRtl: Boolean + get() = scrollView.isLayoutRtl + /** Do we need falsing protection? */ var falsingProtectionNeeded: Boolean = false - /** - * The width of the carousel - */ + /** The width of the carousel */ private var carouselWidth: Int = 0 - /** - * The height of the carousel - */ + /** The height of the carousel */ private var carouselHeight: Int = 0 - /** - * How much are we scrolled into the current media? - */ + /** How much are we scrolled into the current media? */ private var cornerRadius: Int = 0 - /** - * The content where the players are added - */ + /** The content where the players are added */ private var mediaContent: ViewGroup - /** - * The gesture detector to detect touch gestures - */ + /** The gesture detector to detect touch gestures */ private val gestureDetector: GestureDetectorCompat - /** - * The settings button view - */ + /** The settings button view */ private lateinit var settingsButton: View - /** - * What's the currently visible player index? - */ + /** What's the currently visible player index? */ var visibleMediaIndex: Int = 0 private set - /** - * How much are we scrolled into the current media? - */ + /** How much are we scrolled into the current media? */ private var scrollIntoCurrentMedia: Int = 0 - /** - * how much is the content translated in X - */ + /** how much is the content translated in X */ var contentTranslation = 0.0f private set(value) { field = value @@ -125,9 +102,7 @@ class MediaCarouselScrollHandler( updateClipToOutline() } - /** - * The width of a player including padding - */ + /** The width of a player including padding */ var playerWidthPlusPadding: Int = 0 set(value) { field = value @@ -135,82 +110,75 @@ class MediaCarouselScrollHandler( // it's still at the same place var newRelativeScroll = visibleMediaIndex * playerWidthPlusPadding if (scrollIntoCurrentMedia > playerWidthPlusPadding) { - newRelativeScroll += playerWidthPlusPadding - - (scrollIntoCurrentMedia - playerWidthPlusPadding) + newRelativeScroll += + playerWidthPlusPadding - (scrollIntoCurrentMedia - playerWidthPlusPadding) } else { newRelativeScroll += scrollIntoCurrentMedia } scrollView.relativeScrollX = newRelativeScroll } - /** - * Does the dismiss currently show the setting cog? - */ + /** Does the dismiss currently show the setting cog? */ var showsSettingsButton: Boolean = false - /** - * A utility to detect gestures, used in the touch listener - */ - private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { - override fun onFling( - eStart: MotionEvent?, - eCurrent: MotionEvent?, - vX: Float, - vY: Float - ) = onFling(vX, vY) - - override fun onScroll( - down: MotionEvent?, - lastMotion: MotionEvent?, - distanceX: Float, - distanceY: Float - ) = onScroll(down!!, lastMotion!!, distanceX) - - override fun onDown(e: MotionEvent?): Boolean { - if (falsingProtectionNeeded) { - falsingCollector.onNotificationStartDismissing() + /** A utility to detect gestures, used in the touch listener */ + private val gestureListener = + object : GestureDetector.SimpleOnGestureListener() { + override fun onFling( + eStart: MotionEvent?, + eCurrent: MotionEvent?, + vX: Float, + vY: Float + ) = onFling(vX, vY) + + override fun onScroll( + down: MotionEvent?, + lastMotion: MotionEvent?, + distanceX: Float, + distanceY: Float + ) = onScroll(down!!, lastMotion!!, distanceX) + + override fun onDown(e: MotionEvent?): Boolean { + if (falsingProtectionNeeded) { + falsingCollector.onNotificationStartDismissing() + } + return false } - return false } - } - /** - * The touch listener for the scroll view - */ - private val touchListener = object : Gefingerpoken { - override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!) - override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!) - } + /** The touch listener for the scroll view */ + private val touchListener = + object : Gefingerpoken { + override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!) + override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!) + } - /** - * A listener that is invoked when the scrolling changes to update player visibilities - */ - private val scrollChangedListener = object : View.OnScrollChangeListener { - override fun onScrollChange( - v: View?, - scrollX: Int, - scrollY: Int, - oldScrollX: Int, - oldScrollY: Int - ) { - if (playerWidthPlusPadding == 0) { - return - } + /** A listener that is invoked when the scrolling changes to update player visibilities */ + private val scrollChangedListener = + object : View.OnScrollChangeListener { + override fun onScrollChange( + v: View?, + scrollX: Int, + scrollY: Int, + oldScrollX: Int, + oldScrollY: Int + ) { + if (playerWidthPlusPadding == 0) { + return + } - val relativeScrollX = scrollView.relativeScrollX - onMediaScrollingChanged(relativeScrollX / playerWidthPlusPadding, - relativeScrollX % playerWidthPlusPadding) + val relativeScrollX = scrollView.relativeScrollX + onMediaScrollingChanged( + relativeScrollX / playerWidthPlusPadding, + relativeScrollX % playerWidthPlusPadding + ) + } } - } - /** - * Whether the media card is visible to user if any - */ + /** Whether the media card is visible to user if any */ var visibleToUser: Boolean = false - /** - * Whether the quick setting is expanded or not - */ + /** Whether the quick setting is expanded or not */ var qsExpanded: Boolean = false init { @@ -219,47 +187,61 @@ class MediaCarouselScrollHandler( scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER) mediaContent = scrollView.contentContainer scrollView.setOnScrollChangeListener(scrollChangedListener) - scrollView.outlineProvider = object : ViewOutlineProvider() { - override fun getOutline(view: View?, outline: Outline?) { - outline?.setRoundRect(0, 0, carouselWidth, carouselHeight, cornerRadius.toFloat()) + scrollView.outlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View?, outline: Outline?) { + outline?.setRoundRect( + 0, + 0, + carouselWidth, + carouselHeight, + cornerRadius.toFloat() + ) + } } - } } fun onSettingsButtonUpdated(button: View) { settingsButton = button // We don't have a context to resolve, lets use the settingsbuttons one since that is // reinflated appropriately - cornerRadius = settingsButton.resources.getDimensionPixelSize( - Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius)) + cornerRadius = + settingsButton.resources.getDimensionPixelSize( + Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius) + ) updateSettingsPresentation() scrollView.invalidateOutline() } private fun updateSettingsPresentation() { if (showsSettingsButton && settingsButton.width > 0) { - val settingsOffset = MathUtils.map( + val settingsOffset = + MathUtils.map( 0.0f, getMaxTranslation().toFloat(), 0.0f, 1.0f, - Math.abs(contentTranslation)) - val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width * + Math.abs(contentTranslation) + ) + val settingsTranslation = + (1.0f - settingsOffset) * + -settingsButton.width * SETTINGS_BUTTON_TRANSLATION_FRACTION - val newTranslationX = if (isRtl) { - // In RTL, the 0-placement is on the right side of the view, not the left... - if (contentTranslation > 0) { - -(scrollView.width - settingsTranslation - settingsButton.width) - } else { - -settingsTranslation - } - } else { - if (contentTranslation > 0) { - settingsTranslation + val newTranslationX = + if (isRtl) { + // In RTL, the 0-placement is on the right side of the view, not the left... + if (contentTranslation > 0) { + -(scrollView.width - settingsTranslation - settingsButton.width) + } else { + -settingsTranslation + } } else { - scrollView.width - settingsTranslation - settingsButton.width + if (contentTranslation > 0) { + settingsTranslation + } else { + scrollView.width - settingsTranslation - settingsButton.width + } } - } val rotation = (1.0f - settingsOffset) * 50 settingsButton.rotation = rotation * -Math.signum(contentTranslation) val alpha = MathUtils.saturate(MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset)) @@ -289,7 +271,10 @@ class MediaCarouselScrollHandler( return false } } - if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) { + if (motionEvent.action == MotionEvent.ACTION_MOVE) { + // cancel on going animation if there is any. + PhysicsAnimator.getInstance(this).cancel() + } else if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) { // It's an up and the fling didn't take it above val relativePos = scrollView.relativeScrollX % playerWidthPlusPadding val scrollXAmount: Int @@ -303,16 +288,14 @@ class MediaCarouselScrollHandler( val newScrollX = scrollView.relativeScrollX + dx // Delay the scrolling since scrollView calls springback which cancels // the animation again.. - mainExecutor.execute { - scrollView.smoothScrollTo(newScrollX, scrollView.scrollY) - } + mainExecutor.execute { scrollView.smoothScrollTo(newScrollX, scrollView.scrollY) } } val currentTranslation = scrollView.getContentTranslation() if (currentTranslation != 0.0f) { // We started a Swipe but didn't end up with a fling. Let's either go to the // dismissed position or go back. - val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 || - isFalseTouch() + val springBack = + Math.abs(currentTranslation) < getMaxTranslation() / 2 || isFalseTouch() val newTranslation: Float if (springBack) { newTranslation = 0.0f @@ -321,13 +304,17 @@ class MediaCarouselScrollHandler( if (!showsSettingsButton) { // Delay the dismiss a bit to avoid too much overlap. Waiting until the // animation has finished also feels a bit too slow here. - mainExecutor.executeDelayed({ - dismissCallback.invoke() - }, DISMISS_DELAY) + mainExecutor.executeDelayed({ dismissCallback.invoke() }, DISMISS_DELAY) } } - PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, - newTranslation, startVelocity = 0.0f, config = translationConfig).start() + PhysicsAnimator.getInstance(this) + .spring( + CONTENT_TRANSLATION, + newTranslation, + startVelocity = 0.0f, + config = translationConfig + ) + .start() scrollView.animationTargetX = newTranslation } } @@ -335,10 +322,11 @@ class MediaCarouselScrollHandler( return false } - private fun isFalseTouch() = falsingProtectionNeeded && - falsingManager.isFalseTouch(NOTIFICATION_DISMISS) + private fun isFalseTouch() = + falsingProtectionNeeded && falsingManager.isFalseTouch(NOTIFICATION_DISMISS) - private fun getMaxTranslation() = if (showsSettingsButton) { + private fun getMaxTranslation() = + if (showsSettingsButton) { settingsButton.width } else { playerWidthPlusPadding @@ -348,15 +336,10 @@ class MediaCarouselScrollHandler( return gestureDetector.onTouchEvent(motionEvent) } - fun onScroll( - down: MotionEvent, - lastMotion: MotionEvent, - distanceX: Float - ): Boolean { + fun onScroll(down: MotionEvent, lastMotion: MotionEvent, distanceX: Float): Boolean { val totalX = lastMotion.x - down.x val currentTranslation = scrollView.getContentTranslation() - if (currentTranslation != 0.0f || - !scrollView.canScrollHorizontally((-totalX).toInt())) { + if (currentTranslation != 0.0f || !scrollView.canScrollHorizontally((-totalX).toInt())) { var newTranslation = currentTranslation - distanceX val absTranslation = Math.abs(newTranslation) if (absTranslation > getMaxTranslation()) { @@ -370,14 +353,18 @@ class MediaCarouselScrollHandler( newTranslation = currentTranslation - distanceX * RUBBERBAND_FACTOR } else { // We just crossed the boundary, let's rubberband it all - newTranslation = Math.signum(newTranslation) * (getMaxTranslation() + - (absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR) + newTranslation = + Math.signum(newTranslation) * + (getMaxTranslation() + + (absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR) } } // Otherwise we don't have do do anything, and will remove the unrubberbanded // translation } - if (Math.signum(newTranslation) != Math.signum(currentTranslation) && - currentTranslation != 0.0f) { + if ( + Math.signum(newTranslation) != Math.signum(currentTranslation) && + currentTranslation != 0.0f + ) { // We crossed the 0.0 threshold of the translation. Let's see if we're allowed // to scroll into the new direction if (scrollView.canScrollHorizontally(-newTranslation.toInt())) { @@ -388,8 +375,14 @@ class MediaCarouselScrollHandler( } val physicsAnimator = PhysicsAnimator.getInstance(this) if (physicsAnimator.isRunning()) { - physicsAnimator.spring(CONTENT_TRANSLATION, - newTranslation, startVelocity = 0.0f, config = translationConfig).start() + physicsAnimator + .spring( + CONTENT_TRANSLATION, + newTranslation, + startVelocity = 0.0f, + config = translationConfig + ) + .start() } else { contentTranslation = newTranslation } @@ -399,10 +392,7 @@ class MediaCarouselScrollHandler( return false } - private fun onFling( - vX: Float, - vY: Float - ): Boolean { + private fun onFling(vX: Float, vY: Float): Boolean { if (vX * vX < 0.5 * vY * vY) { return false } @@ -421,13 +411,17 @@ class MediaCarouselScrollHandler( // Delay the dismiss a bit to avoid too much overlap. Waiting until the animation // has finished also feels a bit too slow here. if (!showsSettingsButton) { - mainExecutor.executeDelayed({ - dismissCallback.invoke() - }, DISMISS_DELAY) + mainExecutor.executeDelayed({ dismissCallback.invoke() }, DISMISS_DELAY) } } - PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, - newTranslation, startVelocity = vX, config = translationConfig).start() + PhysicsAnimator.getInstance(this) + .spring( + CONTENT_TRANSLATION, + newTranslation, + startVelocity = vX, + config = translationConfig + ) + .start() scrollView.animationTargetX = newTranslation } else { // We're flinging the player! Let's go either to the previous or to the next player @@ -440,21 +434,18 @@ class MediaCarouselScrollHandler( val view = mediaContent.getChildAt(destIndex) // We need to post this since we're dispatching a touch to the underlying view to cancel // but canceling will actually abort the animation. - mainExecutor.execute { - scrollView.smoothScrollTo(view.left, scrollView.scrollY) - } + mainExecutor.execute { scrollView.smoothScrollTo(view.left, scrollView.scrollY) } } return true } - /** - * Reset the translation of the players when swiped - */ + /** Reset the translation of the players when swiped */ fun resetTranslation(animate: Boolean = false) { if (scrollView.getContentTranslation() != 0.0f) { if (animate) { - PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, - 0.0f, config = translationConfig).start() + PhysicsAnimator.getInstance(this) + .spring(CONTENT_TRANSLATION, 0.0f, config = translationConfig) + .start() scrollView.animationTargetX = 0.0f } else { PhysicsAnimator.getInstance(this).cancel() @@ -482,21 +473,22 @@ class MediaCarouselScrollHandler( closeGuts(false) updatePlayerVisibilities() } - val relativeLocation = visibleMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) - scrollInAmount.toFloat() / playerWidthPlusPadding else 0f + val relativeLocation = + visibleMediaIndex.toFloat() + + if (playerWidthPlusPadding > 0) scrollInAmount.toFloat() / playerWidthPlusPadding + else 0f // Fix the location, because PageIndicator does not handle RTL internally - val location = if (isRtl) { - mediaContent.childCount - relativeLocation - 1 - } else { - relativeLocation - } + val location = + if (isRtl) { + mediaContent.childCount - relativeLocation - 1 + } else { + relativeLocation + } pageIndicator.setLocation(location) updateClipToOutline() } - /** - * Notified whenever the players or their order has changed - */ + /** Notified whenever the players or their order has changed */ fun onPlayersChanged() { updatePlayerVisibilities() updateMediaPaddings() @@ -526,8 +518,8 @@ class MediaCarouselScrollHandler( } /** - * Notify that a player will be removed right away. This gives us the opporunity to look - * where it was and update our scroll position. + * Notify that a player will be removed right away. This gives us the opporunity to look where + * it was and update our scroll position. */ fun onPrePlayerRemoved(removed: MediaControlPanel) { val removedIndex = mediaContent.indexOfChild(removed.mediaViewHolder?.player) @@ -547,9 +539,7 @@ class MediaCarouselScrollHandler( } } - /** - * Update the bounds of the carousel - */ + /** Update the bounds of the carousel */ fun setCarouselBounds(currentCarouselWidth: Int, currentCarouselHeight: Int) { if (currentCarouselHeight != carouselHeight || currentCarouselWidth != carouselHeight) { carouselWidth = currentCarouselWidth @@ -558,9 +548,7 @@ class MediaCarouselScrollHandler( } } - /** - * Reset the MediaScrollView to the start. - */ + /** Reset the MediaScrollView to the start. */ fun scrollToStart() { scrollView.relativeScrollX = 0 } @@ -578,21 +566,22 @@ class MediaCarouselScrollHandler( val destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex) val view = mediaContent.getChildAt(destIndex) // We need to post this to wait for the active player becomes visible. - mainExecutor.executeDelayed({ - scrollView.smoothScrollTo(view.left, scrollView.scrollY) - }, SCROLL_DELAY) + mainExecutor.executeDelayed( + { scrollView.smoothScrollTo(view.left, scrollView.scrollY) }, + SCROLL_DELAY + ) } companion object { - private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>( - "contentTranslation") { - override fun getValue(handler: MediaCarouselScrollHandler): Float { - return handler.contentTranslation - } + private val CONTENT_TRANSLATION = + object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") { + override fun getValue(handler: MediaCarouselScrollHandler): Float { + return handler.contentTranslation + } - override fun setValue(handler: MediaCarouselScrollHandler, value: Float) { - handler.contentTranslation = value + override fun setValue(handler: MediaCarouselScrollHandler, value: Float) { + handler.contentTranslation = value + } } - } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaColorSchemes.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt index 208766d7f5e6..82abf9bfcc80 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaColorSchemes.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import com.android.systemui.monet.ColorScheme diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index 759795f84963..18ecadb28cf3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.ui; import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS; -import static com.android.systemui.media.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS; +import static com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS; import android.animation.Animator; import android.animation.AnimatorInflater; @@ -76,6 +76,20 @@ import com.android.systemui.bluetooth.BroadcastDialogController; import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.media.controls.models.GutsViewHolder; +import com.android.systemui.media.controls.models.player.MediaAction; +import com.android.systemui.media.controls.models.player.MediaButton; +import com.android.systemui.media.controls.models.player.MediaData; +import com.android.systemui.media.controls.models.player.MediaDeviceData; +import com.android.systemui.media.controls.models.player.MediaViewHolder; +import com.android.systemui.media.controls.models.player.SeekBarObserver; +import com.android.systemui.media.controls.models.player.SeekBarViewModel; +import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder; +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData; +import com.android.systemui.media.controls.pipeline.MediaDataManager; +import com.android.systemui.media.controls.util.MediaDataUtils; +import com.android.systemui.media.controls.util.MediaUiEventLogger; +import com.android.systemui.media.controls.util.SmallHash; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.monet.ColorScheme; import com.android.systemui.monet.Style; @@ -111,7 +125,6 @@ public class MediaControlPanel { "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT"; private static final String KEY_SMARTSPACE_ARTIST_NAME = "artist_name"; private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND"; - protected static final String KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME"; // Event types logged by smartspace private static final int SMARTSPACE_CARD_CLICK_EVENT = 760; @@ -251,6 +264,9 @@ public class MediaControlPanel { }); } + /** + * Clean up seekbar and controller when panel is destroyed + */ public void onDestroy() { if (mSeekBarObserver != null) { mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver); @@ -651,7 +667,7 @@ public class MediaControlPanel { return; } - CharSequence contentDescription; + CharSequence contentDescription; if (mMediaViewController.isGutsVisible()) { contentDescription = mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText(); @@ -1441,7 +1457,7 @@ public class MediaControlPanel { } // Automatically scroll to the active player once the media is loaded. - mMediaCarouselController.setShouldScrollToActivePlayer(true); + mMediaCarouselController.setShouldScrollToKey(true); }); } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index e0b6d1f9de7b..6b46d8f30cad 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -59,9 +59,7 @@ import javax.inject.Inject private val TAG: String = MediaHierarchyManager::class.java.simpleName -/** - * Similarly to isShown but also excludes views that have 0 alpha - */ +/** Similarly to isShown but also excludes views that have 0 alpha */ val View.isShownNotFaded: Boolean get() { var current: View = this @@ -86,7 +84,9 @@ val View.isShownNotFaded: Boolean * and animate the positions of the views to achieve seamless transitions. */ @SysUISingleton -class MediaHierarchyManager @Inject constructor( +class MediaHierarchyManager +@Inject +constructor( private val context: Context, private val statusBarStateController: SysuiStatusBarStateController, private val keyguardStateController: KeyguardStateController, @@ -101,12 +101,10 @@ class MediaHierarchyManager @Inject constructor( @Main private val handler: Handler, ) { - /** - * Track the media player setting status on lock screen. - */ + /** Track the media player setting status on lock screen. */ private var allowMediaPlayerOnLockScreen: Boolean = true private val lockScreenMediaPlayerUri = - secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) + secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) /** * Whether we "skip" QQS during panel expansion. @@ -118,8 +116,8 @@ class MediaHierarchyManager @Inject constructor( /** * The root overlay of the hierarchy. This is where the media notification is attached to - * whenever the view is transitioning from one host to another. It also make sure that the - * view is always in its final state when it is attached to a view host. + * whenever the view is transitioning from one host to another. It also make sure that the view + * is always in its final state when it is attached to a view host. */ private var rootOverlay: ViewGroupOverlay? = null @@ -138,69 +136,68 @@ class MediaHierarchyManager @Inject constructor( */ private var animationStartCrossFadeProgress = 0.0f - /** - * The starting alpha of the animation - */ + /** The starting alpha of the animation */ private var animationStartAlpha = 0.0f - /** - * The starting location of the cross fade if an animation is running right now. - */ - @MediaLocation - private var crossFadeAnimationStartLocation = -1 + /** The starting location of the cross fade if an animation is running right now. */ + @MediaLocation private var crossFadeAnimationStartLocation = -1 - /** - * The end location of the cross fade if an animation is running right now. - */ - @MediaLocation - private var crossFadeAnimationEndLocation = -1 + /** The end location of the cross fade if an animation is running right now. */ + @MediaLocation private var crossFadeAnimationEndLocation = -1 private var targetBounds: Rect = Rect() private val mediaFrame get() = mediaCarouselController.mediaFrame private var statusbarState: Int = statusBarStateController.state - private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { - interpolator = Interpolators.FAST_OUT_SLOW_IN - addUpdateListener { - updateTargetState() - val currentAlpha: Float - var boundsProgress = animatedFraction - if (isCrossFadeAnimatorRunning) { - animationCrossFadeProgress = MathUtils.lerp(animationStartCrossFadeProgress, 1.0f, - animatedFraction) - // When crossfading, let's keep the bounds at the right location during fading - boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f - currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress) - } else { - // If we're not crossfading, let's interpolate from the start alpha to 1.0f - currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction) + private var animator = + ValueAnimator.ofFloat(0.0f, 1.0f).apply { + interpolator = Interpolators.FAST_OUT_SLOW_IN + addUpdateListener { + updateTargetState() + val currentAlpha: Float + var boundsProgress = animatedFraction + if (isCrossFadeAnimatorRunning) { + animationCrossFadeProgress = + MathUtils.lerp(animationStartCrossFadeProgress, 1.0f, animatedFraction) + // When crossfading, let's keep the bounds at the right location during fading + boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f + currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress) + } else { + // If we're not crossfading, let's interpolate from the start alpha to 1.0f + currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction) + } + interpolateBounds( + animationStartBounds, + targetBounds, + boundsProgress, + result = currentBounds + ) + resolveClipping(currentClipping) + applyState(currentBounds, currentAlpha, clipBounds = currentClipping) } - interpolateBounds(animationStartBounds, targetBounds, boundsProgress, - result = currentBounds) - resolveClipping(currentClipping) - applyState(currentBounds, currentAlpha, clipBounds = currentClipping) - } - addListener(object : AnimatorListenerAdapter() { - private var cancelled: Boolean = false + addListener( + object : AnimatorListenerAdapter() { + private var cancelled: Boolean = false + + override fun onAnimationCancel(animation: Animator?) { + cancelled = true + animationPending = false + rootView?.removeCallbacks(startAnimation) + } - override fun onAnimationCancel(animation: Animator?) { - cancelled = true - animationPending = false - rootView?.removeCallbacks(startAnimation) - } + override fun onAnimationEnd(animation: Animator?) { + isCrossFadeAnimatorRunning = false + if (!cancelled) { + applyTargetStateIfNotAnimating() + } + } - override fun onAnimationEnd(animation: Animator?) { - isCrossFadeAnimatorRunning = false - if (!cancelled) { - applyTargetStateIfNotAnimating() + override fun onAnimationStart(animation: Animator?) { + cancelled = false + animationPending = false + } } - } - - override fun onAnimationStart(animation: Animator?) { - cancelled = false - animationPending = false - } - }) - } + ) + } private fun resolveClipping(result: Rect) { if (animationStartClipping.isEmpty) result.set(targetClipping) @@ -210,42 +207,31 @@ class MediaHierarchyManager @Inject constructor( private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_DREAM_OVERLAY + 1) /** - * The last location where this view was at before going to the desired location. This is - * useful for guided transitions. + * The last location where this view was at before going to the desired location. This is useful + * for guided transitions. */ - @MediaLocation - private var previousLocation = -1 - /** - * The desired location where the view will be at the end of the transition. - */ - @MediaLocation - private var desiredLocation = -1 + @MediaLocation private var previousLocation = -1 + /** The desired location where the view will be at the end of the transition. */ + @MediaLocation private var desiredLocation = -1 /** - * The current attachment location where the view is currently attached. - * Usually this matches the desired location except for animations whenever a view moves - * to the new desired location, during which it is in [IN_OVERLAY]. + * The current attachment location where the view is currently attached. Usually this matches + * the desired location except for animations whenever a view moves to the new desired location, + * during which it is in [IN_OVERLAY]. */ - @MediaLocation - private var currentAttachmentLocation = -1 + @MediaLocation private var currentAttachmentLocation = -1 private var inSplitShade = false - /** - * Is there any active media in the carousel? - */ + /** Is there any active media in the carousel? */ private var hasActiveMedia: Boolean = false get() = mediaHosts.get(LOCATION_QQS)?.visible == true - /** - * Are we currently waiting on an animation to start? - */ + /** Are we currently waiting on an animation to start? */ private var animationPending: Boolean = false private val startAnimation: Runnable = Runnable { animator.start() } - /** - * The expansion of quick settings - */ + /** The expansion of quick settings */ var qsExpansion: Float = 0.0f set(value) { if (field != value) { @@ -258,9 +244,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Is quick setting expanded? - */ + /** Is quick setting expanded? */ var qsExpanded: Boolean = false set(value) { if (field != value) { @@ -281,9 +265,8 @@ class MediaHierarchyManager @Inject constructor( private var distanceForFullShadeTransition = 0 /** - * The amount of progress we are currently in if we're transitioning to the full shade. - * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full - * shade. + * The amount of progress we are currently in if we're transitioning to the full shade. 0.0f + * means we're not transitioning yet, while 1 means we're all the way in the full shade. */ private var fullShadeTransitionProgress = 0f set(value) { @@ -305,9 +288,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Is there currently a cross-fade animation running driven by an animator? - */ + /** Is there currently a cross-fade animation running driven by an animator? */ private var isCrossFadeAnimatorRunning = false /** @@ -316,8 +297,10 @@ class MediaHierarchyManager @Inject constructor( * the transition starts, this will no longer return true. */ private val isTransitioningToFullShade: Boolean - get() = fullShadeTransitionProgress != 0f && !bypassController.bypassEnabled && - statusbarState == StatusBarState.KEYGUARD + get() = + fullShadeTransitionProgress != 0f && + !bypassController.bypassEnabled && + statusbarState == StatusBarState.KEYGUARD /** * Set the amount of pixels we have currently dragged down if we're transitioning to the full @@ -354,17 +337,13 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Are location changes currently blocked? - */ + /** Are location changes currently blocked? */ private val blockLocationChanges: Boolean get() { return goingToSleep || dozeAnimationRunning } - /** - * Are we currently going to sleep - */ + /** Are we currently going to sleep */ private var goingToSleep: Boolean = false set(value) { if (field != value) { @@ -375,9 +354,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Are we currently fullyAwake - */ + /** Are we currently fullyAwake */ private var fullyAwake: Boolean = false set(value) { if (field != value) { @@ -388,9 +365,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Is the doze animation currently Running - */ + /** Is the doze animation currently Running */ private var dozeAnimationRunning: Boolean = false private set(value) { if (field != value) { @@ -401,9 +376,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Is the dream overlay currently active - */ + /** Is the dream overlay currently active */ private var dreamOverlayActive: Boolean = false private set(value) { if (field != value) { @@ -412,9 +385,7 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Is the dream media complication currently active - */ + /** Is the dream media complication currently active */ private var dreamMediaComplicationActive: Boolean = false private set(value) { if (field != value) { @@ -424,16 +395,13 @@ class MediaHierarchyManager @Inject constructor( } /** - * The current cross fade progress. 0.5f means it's just switching - * between the start and the end location and the content is fully faded, while 0.75f means - * that we're halfway faded in again in the target state. - * This is only valid while [isCrossFadeAnimatorRunning] is true. + * The current cross fade progress. 0.5f means it's just switching between the start and the end + * location and the content is fully faded, while 0.75f means that we're halfway faded in again + * in the target state. This is only valid while [isCrossFadeAnimatorRunning] is true. */ private var animationCrossFadeProgress = 1.0f - /** - * The current carousel Alpha. - */ + /** The current carousel Alpha. */ private var carouselAlpha: Float = 1.0f set(value) { if (field == value) { @@ -447,8 +415,8 @@ class MediaHierarchyManager @Inject constructor( * Calculate the alpha of the view when given a cross-fade progress. * * @param crossFadeProgress The current cross fade progress. 0.5f means it's just switching - * between the start and the end location and the content is fully faded, while 0.75f means - * that we're halfway faded in again in the target state. + * between the start and the end location and the content is fully faded, while 0.75f means that + * we're halfway faded in again in the target state. */ private fun calculateAlphaFromCrossFade(crossFadeProgress: Float): Float { if (crossFadeProgress <= 0.5f) { @@ -460,132 +428,152 @@ class MediaHierarchyManager @Inject constructor( init { updateConfiguration() - configurationController.addCallback(object : ConfigurationController.ConfigurationListener { - override fun onConfigChanged(newConfig: Configuration?) { - updateConfiguration() - updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = true) - } - }) - statusBarStateController.addCallback(object : StatusBarStateController.StateListener { - override fun onStatePreChange(oldState: Int, newState: Int) { - // We're updating the location before the state change happens, since we want the - // location of the previous state to still be up to date when the animation starts - statusbarState = newState - updateDesiredLocation() - } - - override fun onStateChanged(newState: Int) { - updateTargetState() - // Enters shade from lock screen - if (newState == StatusBarState.SHADE_LOCKED && isLockScreenShadeVisibleToUser()) { - mediaCarouselController.logSmartspaceImpression(qsExpanded) + configurationController.addCallback( + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + updateConfiguration() + updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = true) } - mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser() - } - - override fun onDozeAmountChanged(linear: Float, eased: Float) { - dozeAnimationRunning = linear != 0.0f && linear != 1.0f } + ) + statusBarStateController.addCallback( + object : StatusBarStateController.StateListener { + override fun onStatePreChange(oldState: Int, newState: Int) { + // We're updating the location before the state change happens, since we want + // the + // location of the previous state to still be up to date when the animation + // starts + statusbarState = newState + updateDesiredLocation() + } - override fun onDozingChanged(isDozing: Boolean) { - if (!isDozing) { - dozeAnimationRunning = false - // Enters lock screen from screen off - if (isLockScreenVisibleToUser()) { + override fun onStateChanged(newState: Int) { + updateTargetState() + // Enters shade from lock screen + if ( + newState == StatusBarState.SHADE_LOCKED && isLockScreenShadeVisibleToUser() + ) { mediaCarouselController.logSmartspaceImpression(qsExpanded) } - } else { - updateDesiredLocation() - qsExpanded = false - closeGuts() + mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = + isVisibleToUser() } - mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser() - } - override fun onExpandedChanged(isExpanded: Boolean) { - // Enters shade from home screen - if (isHomeScreenShadeVisibleToUser()) { - mediaCarouselController.logSmartspaceImpression(qsExpanded) + override fun onDozeAmountChanged(linear: Float, eased: Float) { + dozeAnimationRunning = linear != 0.0f && linear != 1.0f } - mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser() - } - }) - dreamOverlayStateController.addCallback(object : DreamOverlayStateController.Callback { - override fun onComplicationsChanged() { - dreamMediaComplicationActive = dreamOverlayStateController.complications.any { - it is MediaDreamComplication + override fun onDozingChanged(isDozing: Boolean) { + if (!isDozing) { + dozeAnimationRunning = false + // Enters lock screen from screen off + if (isLockScreenVisibleToUser()) { + mediaCarouselController.logSmartspaceImpression(qsExpanded) + } + } else { + updateDesiredLocation() + qsExpanded = false + closeGuts() + } + mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = + isVisibleToUser() } - } - override fun onStateChanged() { - dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it } + override fun onExpandedChanged(isExpanded: Boolean) { + // Enters shade from home screen + if (isHomeScreenShadeVisibleToUser()) { + mediaCarouselController.logSmartspaceImpression(qsExpanded) + } + mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = + isVisibleToUser() + } } - }) + ) + + dreamOverlayStateController.addCallback( + object : DreamOverlayStateController.Callback { + override fun onComplicationsChanged() { + dreamMediaComplicationActive = + dreamOverlayStateController.complications.any { + it is MediaDreamComplication + } + } - wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer { - override fun onFinishedGoingToSleep() { - goingToSleep = false + override fun onStateChanged() { + dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it } + } } + ) - override fun onStartedGoingToSleep() { - goingToSleep = true - fullyAwake = false - } + wakefulnessLifecycle.addObserver( + object : WakefulnessLifecycle.Observer { + override fun onFinishedGoingToSleep() { + goingToSleep = false + } - override fun onFinishedWakingUp() { - goingToSleep = false - fullyAwake = true - } + override fun onStartedGoingToSleep() { + goingToSleep = true + fullyAwake = false + } + + override fun onFinishedWakingUp() { + goingToSleep = false + fullyAwake = true + } - override fun onStartedWakingUp() { - goingToSleep = false + override fun onStartedWakingUp() { + goingToSleep = false + } } - }) + ) mediaCarouselController.updateUserVisibility = { mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser() } mediaCarouselController.updateHostVisibility = { - mediaHosts.forEach { - it?.updateViewVisibility() - } + mediaHosts.forEach { it?.updateViewVisibility() } } - panelEventsEvents.registerListener(object : NotifPanelEvents.Listener { - override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) { - skipQqsOnExpansion = isExpandImmediateEnabled - updateDesiredLocation() + panelEventsEvents.registerListener( + object : NotifPanelEvents.Listener { + override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) { + skipQqsOnExpansion = isExpandImmediateEnabled + updateDesiredLocation() + } } - }) + ) - val settingsObserver: ContentObserver = object : ContentObserver(handler) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - if (uri == lockScreenMediaPlayerUri) { - allowMediaPlayerOnLockScreen = + val settingsObserver: ContentObserver = + object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + if (uri == lockScreenMediaPlayerUri) { + allowMediaPlayerOnLockScreen = secureSettings.getBoolForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - true, - UserHandle.USER_CURRENT + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + true, + UserHandle.USER_CURRENT ) + } } } - } secureSettings.registerContentObserverForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - settingsObserver, - UserHandle.USER_ALL) + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + settingsObserver, + UserHandle.USER_ALL + ) } private fun updateConfiguration() { - distanceForFullShadeTransition = context.resources.getDimensionPixelSize( - R.dimen.lockscreen_shade_media_transition_distance) + distanceForFullShadeTransition = + context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_media_transition_distance + ) inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources) } /** - * Register a media host and create a view can be attached to a view hierarchy - * and where the players will be placed in when the host is the currently desired state. + * Register a media host and create a view can be attached to a view hierarchy and where the + * players will be placed in when the host is the currently desired state. * * @return the hostView associated with this location */ @@ -613,27 +601,26 @@ class MediaHierarchyManager @Inject constructor( return viewHost } - /** - * Close the guts in all players in [MediaCarouselController]. - */ + /** Close the guts in all players in [MediaCarouselController]. */ fun closeGuts() { mediaCarouselController.closeGuts() } private fun createUniqueObjectHost(): UniqueObjectHostView { val viewHost = UniqueObjectHostView(context) - viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(p0: View?) { - if (rootOverlay == null) { - rootView = viewHost.viewRootImpl.view - rootOverlay = (rootView!!.overlay as ViewGroupOverlay) + viewHost.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(p0: View?) { + if (rootOverlay == null) { + rootView = viewHost.viewRootImpl.view + rootOverlay = (rootView!!.overlay as ViewGroupOverlay) + } + viewHost.removeOnAttachStateChangeListener(this) } - viewHost.removeOnAttachStateChangeListener(this) - } - override fun onViewDetachedFromWindow(p0: View?) { + override fun onViewDetachedFromWindow(p0: View?) {} } - }) + ) return viewHost } @@ -643,127 +630,141 @@ class MediaHierarchyManager @Inject constructor( * * @param forceNoAnimation optional parameter telling the system not to animate * @param forceStateUpdate optional parameter telling the system to update transition state + * ``` * even if location did not change + * ``` */ private fun updateDesiredLocation( forceNoAnimation: Boolean = false, forceStateUpdate: Boolean = false - ) = traceSection("MediaHierarchyManager#updateDesiredLocation") { - val desiredLocation = calculateLocation() - if (desiredLocation != this.desiredLocation || forceStateUpdate) { - if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) { - // Only update previous location when it actually changes - previousLocation = this.desiredLocation - } else if (forceStateUpdate) { - val onLockscreen = (!bypassController.bypassEnabled && - (statusbarState == StatusBarState.KEYGUARD)) - if (desiredLocation == LOCATION_QS && previousLocation == LOCATION_LOCKSCREEN && - !onLockscreen) { - // If media active state changed and the device is now unlocked, update the - // previous location so we animate between the correct hosts - previousLocation = LOCATION_QQS + ) = + traceSection("MediaHierarchyManager#updateDesiredLocation") { + val desiredLocation = calculateLocation() + if (desiredLocation != this.desiredLocation || forceStateUpdate) { + if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) { + // Only update previous location when it actually changes + previousLocation = this.desiredLocation + } else if (forceStateUpdate) { + val onLockscreen = + (!bypassController.bypassEnabled && + (statusbarState == StatusBarState.KEYGUARD)) + if ( + desiredLocation == LOCATION_QS && + previousLocation == LOCATION_LOCKSCREEN && + !onLockscreen + ) { + // If media active state changed and the device is now unlocked, update the + // previous location so we animate between the correct hosts + previousLocation = LOCATION_QQS + } } + val isNewView = this.desiredLocation == -1 + this.desiredLocation = desiredLocation + // Let's perform a transition + val animate = + !forceNoAnimation && shouldAnimateTransition(desiredLocation, previousLocation) + val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) + val host = getHost(desiredLocation) + val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE + if (!willFade || isCurrentlyInGuidedTransformation() || !animate) { + // if we're fading, we want the desired location / measurement only to change + // once fully faded. This is happening in the host attachment + mediaCarouselController.onDesiredLocationChanged( + desiredLocation, + host, + animate, + animDuration, + delay + ) + } + performTransitionToNewLocation(isNewView, animate) } - val isNewView = this.desiredLocation == -1 - this.desiredLocation = desiredLocation - // Let's perform a transition - val animate = !forceNoAnimation && - shouldAnimateTransition(desiredLocation, previousLocation) - val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) - val host = getHost(desiredLocation) - val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE - if (!willFade || isCurrentlyInGuidedTransformation() || !animate) { - // if we're fading, we want the desired location / measurement only to change - // once fully faded. This is happening in the host attachment - mediaCarouselController.onDesiredLocationChanged(desiredLocation, host, - animate, animDuration, delay) - } - performTransitionToNewLocation(isNewView, animate) } - } - private fun performTransitionToNewLocation( - isNewView: Boolean, - animate: Boolean - ) = traceSection("MediaHierarchyManager#performTransitionToNewLocation") { - if (previousLocation < 0 || isNewView) { - cancelAnimationAndApplyDesiredState() - return - } - val currentHost = getHost(desiredLocation) - val previousHost = getHost(previousLocation) - if (currentHost == null || previousHost == null) { - cancelAnimationAndApplyDesiredState() - return - } - updateTargetState() - if (isCurrentlyInGuidedTransformation()) { - applyTargetStateIfNotAnimating() - } else if (animate) { - val wasCrossFading = isCrossFadeAnimatorRunning - val previewsCrossFadeProgress = animationCrossFadeProgress - animator.cancel() - if (currentAttachmentLocation != previousLocation || - !previousHost.hostView.isAttachedToWindow) { - // Let's animate to the new position, starting from the current position - // We also go in here in case the view was detached, since the bounds wouldn't - // be correct anymore - animationStartBounds.set(currentBounds) - animationStartClipping.set(currentClipping) - } else { - // otherwise, let's take the freshest state, since the current one could - // be outdated - animationStartBounds.set(previousHost.currentBounds) - animationStartClipping.set(previousHost.currentClipping) + private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) = + traceSection("MediaHierarchyManager#performTransitionToNewLocation") { + if (previousLocation < 0 || isNewView) { + cancelAnimationAndApplyDesiredState() + return } - val transformationType = calculateTransformationType() - var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE - var crossFadeStartProgress = 0.0f - // The alpha is only relevant when not cross fading - var newCrossFadeStartLocation = previousLocation - if (wasCrossFading) { - if (currentAttachmentLocation == crossFadeAnimationEndLocation) { - if (needsCrossFade) { - // We were previously crossFading and we've already reached - // the end view, Let's start crossfading from the same position there - crossFadeStartProgress = 1.0f - previewsCrossFadeProgress - } - // Otherwise let's fade in from the current alpha, but not cross fade + val currentHost = getHost(desiredLocation) + val previousHost = getHost(previousLocation) + if (currentHost == null || previousHost == null) { + cancelAnimationAndApplyDesiredState() + return + } + updateTargetState() + if (isCurrentlyInGuidedTransformation()) { + applyTargetStateIfNotAnimating() + } else if (animate) { + val wasCrossFading = isCrossFadeAnimatorRunning + val previewsCrossFadeProgress = animationCrossFadeProgress + animator.cancel() + if ( + currentAttachmentLocation != previousLocation || + !previousHost.hostView.isAttachedToWindow + ) { + // Let's animate to the new position, starting from the current position + // We also go in here in case the view was detached, since the bounds wouldn't + // be correct anymore + animationStartBounds.set(currentBounds) + animationStartClipping.set(currentClipping) } else { - // We haven't reached the previous location yet, let's still cross fade from - // where we were. - newCrossFadeStartLocation = crossFadeAnimationStartLocation - if (newCrossFadeStartLocation == desiredLocation) { - // we're crossFading back to where we were, let's start at the end position - crossFadeStartProgress = 1.0f - previewsCrossFadeProgress + // otherwise, let's take the freshest state, since the current one could + // be outdated + animationStartBounds.set(previousHost.currentBounds) + animationStartClipping.set(previousHost.currentClipping) + } + val transformationType = calculateTransformationType() + var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE + var crossFadeStartProgress = 0.0f + // The alpha is only relevant when not cross fading + var newCrossFadeStartLocation = previousLocation + if (wasCrossFading) { + if (currentAttachmentLocation == crossFadeAnimationEndLocation) { + if (needsCrossFade) { + // We were previously crossFading and we've already reached + // the end view, Let's start crossfading from the same position there + crossFadeStartProgress = 1.0f - previewsCrossFadeProgress + } + // Otherwise let's fade in from the current alpha, but not cross fade } else { - // Let's start from where we are right now - crossFadeStartProgress = previewsCrossFadeProgress - // We need to force cross fading as we haven't reached the end location yet - needsCrossFade = true + // We haven't reached the previous location yet, let's still cross fade from + // where we were. + newCrossFadeStartLocation = crossFadeAnimationStartLocation + if (newCrossFadeStartLocation == desiredLocation) { + // we're crossFading back to where we were, let's start at the end + // position + crossFadeStartProgress = 1.0f - previewsCrossFadeProgress + } else { + // Let's start from where we are right now + crossFadeStartProgress = previewsCrossFadeProgress + // We need to force cross fading as we haven't reached the end location + // yet + needsCrossFade = true + } } + } else if (needsCrossFade) { + // let's not flicker and start with the same alpha + crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f } - } else if (needsCrossFade) { - // let's not flicker and start with the same alpha - crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f - } - isCrossFadeAnimatorRunning = needsCrossFade - crossFadeAnimationStartLocation = newCrossFadeStartLocation - crossFadeAnimationEndLocation = desiredLocation - animationStartAlpha = carouselAlpha - animationStartCrossFadeProgress = crossFadeStartProgress - adjustAnimatorForTransition(desiredLocation, previousLocation) - if (!animationPending) { - rootView?.let { - // Let's delay the animation start until we finished laying out - animationPending = true - it.postOnAnimation(startAnimation) + isCrossFadeAnimatorRunning = needsCrossFade + crossFadeAnimationStartLocation = newCrossFadeStartLocation + crossFadeAnimationEndLocation = desiredLocation + animationStartAlpha = carouselAlpha + animationStartCrossFadeProgress = crossFadeStartProgress + adjustAnimatorForTransition(desiredLocation, previousLocation) + if (!animationPending) { + rootView?.let { + // Let's delay the animation start until we finished laying out + animationPending = true + it.postOnAnimation(startAnimation) + } } + } else { + cancelAnimationAndApplyDesiredState() } - } else { - cancelAnimationAndApplyDesiredState() } - } private fun shouldAnimateTransition( @MediaLocation currentLocation: Int, @@ -777,23 +778,29 @@ class MediaHierarchyManager @Inject constructor( } // This is an invalid transition, and can happen when using the camera gesture from the // lock screen. Disallow. - if (previousLocation == LOCATION_LOCKSCREEN && - desiredLocation == LOCATION_QQS && - statusbarState == StatusBarState.SHADE) { + if ( + previousLocation == LOCATION_LOCKSCREEN && + desiredLocation == LOCATION_QQS && + statusbarState == StatusBarState.SHADE + ) { return false } - if (currentLocation == LOCATION_QQS && + if ( + currentLocation == LOCATION_QQS && previousLocation == LOCATION_LOCKSCREEN && (statusBarStateController.leaveOpenOnKeyguardHide() || - statusbarState == StatusBarState.SHADE_LOCKED)) { + statusbarState == StatusBarState.SHADE_LOCKED) + ) { // Usually listening to the isShown is enough to determine this, but there is some // non-trivial reattaching logic happening that will make the view not-shown earlier return true } - if (statusbarState == StatusBarState.KEYGUARD && (currentLocation == LOCATION_LOCKSCREEN || - previousLocation == LOCATION_LOCKSCREEN)) { + if ( + statusbarState == StatusBarState.KEYGUARD && + (currentLocation == LOCATION_LOCKSCREEN || previousLocation == LOCATION_LOCKSCREEN) + ) { // We're always fading from lockscreen to keyguard in situations where the player // is already fully hidden return false @@ -814,8 +821,10 @@ class MediaHierarchyManager @Inject constructor( var delay = 0L if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) { // Going to the full shade, let's adjust the animation duration - if (statusbarState == StatusBarState.SHADE && - keyguardStateController.isKeyguardFadingAway) { + if ( + statusbarState == StatusBarState.SHADE && + keyguardStateController.isKeyguardFadingAway + ) { delay = keyguardStateController.keyguardFadingAwayDelay } animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong() @@ -834,14 +843,16 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Updates the bounds that the view wants to be in at the end of the animation. - */ + /** Updates the bounds that the view wants to be in at the end of the animation. */ private fun updateTargetState() { var starthost = getHost(previousLocation) var endHost = getHost(desiredLocation) - if (isCurrentlyInGuidedTransformation() && !isCurrentlyFading() && starthost != null && - endHost != null) { + if ( + isCurrentlyInGuidedTransformation() && + !isCurrentlyFading() && + starthost != null && + endHost != null + ) { val progress = getTransformationProgress() // If either of the hosts are invisible, let's keep them at the other host location to // have a nicer disappear animation. Otherwise the currentBounds of the state might @@ -868,14 +879,15 @@ class MediaHierarchyManager @Inject constructor( progress: Float, result: Rect? = null ): Rect { - val left = MathUtils.lerp(startBounds.left.toFloat(), - endBounds.left.toFloat(), progress).toInt() - val top = MathUtils.lerp(startBounds.top.toFloat(), - endBounds.top.toFloat(), progress).toInt() - val right = MathUtils.lerp(startBounds.right.toFloat(), - endBounds.right.toFloat(), progress).toInt() - val bottom = MathUtils.lerp(startBounds.bottom.toFloat(), - endBounds.bottom.toFloat(), progress).toInt() + val left = + MathUtils.lerp(startBounds.left.toFloat(), endBounds.left.toFloat(), progress).toInt() + val top = + MathUtils.lerp(startBounds.top.toFloat(), endBounds.top.toFloat(), progress).toInt() + val right = + MathUtils.lerp(startBounds.right.toFloat(), endBounds.right.toFloat(), progress).toInt() + val bottom = + MathUtils.lerp(startBounds.bottom.toFloat(), endBounds.bottom.toFloat(), progress) + .toInt() val resultBounds = result ?: Rect() resultBounds.set(left, top, right, bottom) return resultBounds @@ -884,17 +896,15 @@ class MediaHierarchyManager @Inject constructor( /** @return true if this transformation is guided by an external progress like a finger */ fun isCurrentlyInGuidedTransformation(): Boolean { return hasValidStartAndEndLocations() && - getTransformationProgress() >= 0 && - areGuidedTransitionHostsVisible() + getTransformationProgress() >= 0 && + areGuidedTransitionHostsVisible() } private fun hasValidStartAndEndLocations(): Boolean { return previousLocation != -1 && desiredLocation != -1 } - /** - * Calculate the transformation type for the current animation - */ + /** Calculate the transformation type for the current animation */ @VisibleForTesting @TransformationType fun calculateTransformationType(): Int { @@ -904,8 +914,10 @@ class MediaHierarchyManager @Inject constructor( } return TRANSFORMATION_TYPE_FADE } - if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS || - previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN) { + if ( + previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS || + previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN + ) { // animating between ls and qs should fade, as QS is clipped. return TRANSFORMATION_TYPE_FADE } @@ -918,7 +930,7 @@ class MediaHierarchyManager @Inject constructor( private fun areGuidedTransitionHostsVisible(): Boolean { return getHost(previousLocation)?.visible == true && - getHost(desiredLocation)?.visible == true + getHost(desiredLocation)?.visible == true } /** @@ -966,103 +978,115 @@ class MediaHierarchyManager @Inject constructor( } } - /** - * Apply the current state to the view, updating it's bounds and desired state - */ + /** Apply the current state to the view, updating it's bounds and desired state */ private fun applyState( bounds: Rect, alpha: Float, immediately: Boolean = false, clipBounds: Rect = EMPTY_RECT - ) = traceSection("MediaHierarchyManager#applyState") { - currentBounds.set(bounds) - currentClipping = clipBounds - carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f - val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading() - val startLocation = if (onlyUseEndState) -1 else previousLocation - val progress = if (onlyUseEndState) 1.0f else getTransformationProgress() - val endLocation = resolveLocationForFading() - mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately) - updateHostAttachment() - if (currentAttachmentLocation == IN_OVERLAY) { - // Setting the clipping on the hierarchy of `mediaFrame` does not work - if (!currentClipping.isEmpty) { - currentBounds.intersect(currentClipping) - } - mediaFrame.setLeftTopRightBottom( + ) = + traceSection("MediaHierarchyManager#applyState") { + currentBounds.set(bounds) + currentClipping = clipBounds + carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f + val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading() + val startLocation = if (onlyUseEndState) -1 else previousLocation + val progress = if (onlyUseEndState) 1.0f else getTransformationProgress() + val endLocation = resolveLocationForFading() + mediaCarouselController.setCurrentState( + startLocation, + endLocation, + progress, + immediately + ) + updateHostAttachment() + if (currentAttachmentLocation == IN_OVERLAY) { + // Setting the clipping on the hierarchy of `mediaFrame` does not work + if (!currentClipping.isEmpty) { + currentBounds.intersect(currentClipping) + } + mediaFrame.setLeftTopRightBottom( currentBounds.left, currentBounds.top, currentBounds.right, - currentBounds.bottom) + currentBounds.bottom + ) + } } - } - private fun updateHostAttachment() = traceSection( - "MediaHierarchyManager#updateHostAttachment" - ) { - var newLocation = resolveLocationForFading() - var canUseOverlay = !isCurrentlyFading() - if (isCrossFadeAnimatorRunning) { - if (getHost(newLocation)?.visible == true && - getHost(newLocation)?.hostView?.isShown == false && - newLocation != desiredLocation) { - // We're crossfading but the view is already hidden. Let's move to the overlay - // instead. This happens when animating to the full shade using a button click. - canUseOverlay = true + private fun updateHostAttachment() = + traceSection("MediaHierarchyManager#updateHostAttachment") { + var newLocation = resolveLocationForFading() + var canUseOverlay = !isCurrentlyFading() + if (isCrossFadeAnimatorRunning) { + if ( + getHost(newLocation)?.visible == true && + getHost(newLocation)?.hostView?.isShown == false && + newLocation != desiredLocation + ) { + // We're crossfading but the view is already hidden. Let's move to the overlay + // instead. This happens when animating to the full shade using a button click. + canUseOverlay = true + } } - } - val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay - newLocation = if (inOverlay) IN_OVERLAY else newLocation - if (currentAttachmentLocation != newLocation) { - currentAttachmentLocation = newLocation + val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay + newLocation = if (inOverlay) IN_OVERLAY else newLocation + if (currentAttachmentLocation != newLocation) { + currentAttachmentLocation = newLocation - // Remove the carousel from the old host - (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame) + // Remove the carousel from the old host + (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame) - // Add it to the new one - if (inOverlay) { - rootOverlay!!.add(mediaFrame) - } else { - val targetHost = getHost(newLocation)!!.hostView - // When adding back to the host, let's make sure to reset the bounds. - // Usually adding the view will trigger a layout that does this automatically, - // but we sometimes suppress this. - targetHost.addView(mediaFrame) - val left = targetHost.paddingLeft - val top = targetHost.paddingTop - mediaFrame.setLeftTopRightBottom( + // Add it to the new one + if (inOverlay) { + rootOverlay!!.add(mediaFrame) + } else { + val targetHost = getHost(newLocation)!!.hostView + // When adding back to the host, let's make sure to reset the bounds. + // Usually adding the view will trigger a layout that does this automatically, + // but we sometimes suppress this. + targetHost.addView(mediaFrame) + val left = targetHost.paddingLeft + val top = targetHost.paddingTop + mediaFrame.setLeftTopRightBottom( left, top, left + currentBounds.width(), - top + currentBounds.height()) - - if (mediaFrame.childCount > 0) { - val child = mediaFrame.getChildAt(0) - if (mediaFrame.height < child.height) { - Log.wtf(TAG, "mediaFrame height is too small for child: " + - "${mediaFrame.height} vs ${child.height}") + top + currentBounds.height() + ) + + if (mediaFrame.childCount > 0) { + val child = mediaFrame.getChildAt(0) + if (mediaFrame.height < child.height) { + Log.wtf( + TAG, + "mediaFrame height is too small for child: " + + "${mediaFrame.height} vs ${child.height}" + ) + } } } - } - if (isCrossFadeAnimatorRunning) { - // When cross-fading with an animation, we only notify the media carousel of the - // location change, once the view is reattached to the new place and not immediately - // when the desired location changes. This callback will update the measurement - // of the carousel, only once we've faded out at the old location and then reattach - // to fade it in at the new location. - mediaCarouselController.onDesiredLocationChanged( - newLocation, - getHost(newLocation), - animate = false - ) + if (isCrossFadeAnimatorRunning) { + // When cross-fading with an animation, we only notify the media carousel of the + // location change, once the view is reattached to the new place and not + // immediately + // when the desired location changes. This callback will update the measurement + // of the carousel, only once we've faded out at the old location and then + // reattach + // to fade it in at the new location. + mediaCarouselController.onDesiredLocationChanged( + newLocation, + getHost(newLocation), + animate = false + ) + } } } - } /** - * Calculate the location when cross fading between locations. While fading out, - * the content should remain in the previous location, while after the switch it should - * be at the desired location. + * Calculate the location when cross fading between locations. While fading out, the content + * should remain in the previous location, while after the switch it should be at the desired + * location. */ private fun resolveLocationForFading(): Int { if (isCrossFadeAnimatorRunning) { @@ -1079,7 +1103,8 @@ class MediaHierarchyManager @Inject constructor( private fun isTransitionRunning(): Boolean { return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f || - animator.isRunning || animationPending + animator.isRunning || + animationPending } @MediaLocation @@ -1088,31 +1113,39 @@ class MediaHierarchyManager @Inject constructor( // Keep the current location until we're allowed to again return desiredLocation } - val onLockscreen = (!bypassController.bypassEnabled && - (statusbarState == StatusBarState.KEYGUARD)) - val location = when { - dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY - (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS - qsExpansion > 0.4f && onLockscreen -> LOCATION_QS - !hasActiveMedia -> LOCATION_QS - onLockscreen && isSplitShadeExpanding() -> LOCATION_QS - onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS - onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN - else -> LOCATION_QQS - } + val onLockscreen = + (!bypassController.bypassEnabled && (statusbarState == StatusBarState.KEYGUARD)) + val location = + when { + dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY + (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS + qsExpansion > 0.4f && onLockscreen -> LOCATION_QS + !hasActiveMedia -> LOCATION_QS + onLockscreen && isSplitShadeExpanding() -> LOCATION_QS + onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS + onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN + else -> LOCATION_QQS + } // When we're on lock screen and the player is not active, we should keep it in QS. // Otherwise it will try to animate a transition that doesn't make sense. - if (location == LOCATION_LOCKSCREEN && getHost(location)?.visible != true && - !statusBarStateController.isDozing) { + if ( + location == LOCATION_LOCKSCREEN && + getHost(location)?.visible != true && + !statusBarStateController.isDozing + ) { return LOCATION_QS } - if (location == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS && - collapsingShadeFromQS) { + if ( + location == LOCATION_LOCKSCREEN && + desiredLocation == LOCATION_QS && + collapsingShadeFromQS + ) { // When collapsing on the lockscreen, we want to remain in QS return LOCATION_QS } - if (location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN && - !fullyAwake) { + if ( + location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN && !fullyAwake + ) { // When unlocking from dozing / while waking up, the media shouldn't be transitioning // in an animated way. Let's keep it in the lockscreen until we're fully awake and // reattach it without an animation @@ -1129,9 +1162,7 @@ class MediaHierarchyManager @Inject constructor( return inSplitShade && isTransitioningToFullShade } - /** - * Are we currently transforming to the full shade and already in QQS - */ + /** Are we currently transforming to the full shade and already in QQS */ private fun isTransformingToFullShadeAndInQQS(): Boolean { if (!isTransitioningToFullShade) { return false @@ -1143,9 +1174,7 @@ class MediaHierarchyManager @Inject constructor( return fullShadeTransitionProgress > 0.5f } - /** - * Is the current transformationType fading - */ + /** Is the current transformationType fading */ private fun isCurrentlyFading(): Boolean { if (isSplitShadeExpanding()) { // Split shade always uses transition instead of fade. @@ -1157,60 +1186,49 @@ class MediaHierarchyManager @Inject constructor( return isCrossFadeAnimatorRunning } - /** - * Returns true when the media card could be visible to the user if existed. - */ + /** Returns true when the media card could be visible to the user if existed. */ private fun isVisibleToUser(): Boolean { - return isLockScreenVisibleToUser() || isLockScreenShadeVisibleToUser() || - isHomeScreenShadeVisibleToUser() + return isLockScreenVisibleToUser() || + isLockScreenShadeVisibleToUser() || + isHomeScreenShadeVisibleToUser() } private fun isLockScreenVisibleToUser(): Boolean { return !statusBarStateController.isDozing && - !keyguardViewController.isBouncerShowing && - statusBarStateController.state == StatusBarState.KEYGUARD && - allowMediaPlayerOnLockScreen && - statusBarStateController.isExpanded && - !qsExpanded + !keyguardViewController.isBouncerShowing && + statusBarStateController.state == StatusBarState.KEYGUARD && + allowMediaPlayerOnLockScreen && + statusBarStateController.isExpanded && + !qsExpanded } private fun isLockScreenShadeVisibleToUser(): Boolean { return !statusBarStateController.isDozing && - !keyguardViewController.isBouncerShowing && - (statusBarStateController.state == StatusBarState.SHADE_LOCKED || - (statusBarStateController.state == StatusBarState.KEYGUARD && qsExpanded)) + !keyguardViewController.isBouncerShowing && + (statusBarStateController.state == StatusBarState.SHADE_LOCKED || + (statusBarStateController.state == StatusBarState.KEYGUARD && qsExpanded)) } private fun isHomeScreenShadeVisibleToUser(): Boolean { return !statusBarStateController.isDozing && - statusBarStateController.state == StatusBarState.SHADE && - statusBarStateController.isExpanded + statusBarStateController.state == StatusBarState.SHADE && + statusBarStateController.isExpanded } companion object { - /** - * Attached in expanded quick settings - */ + /** Attached in expanded quick settings */ const val LOCATION_QS = 0 - /** - * Attached in the collapsed QS - */ + /** Attached in the collapsed QS */ const val LOCATION_QQS = 1 - /** - * Attached on the lock screen - */ + /** Attached on the lock screen */ const val LOCATION_LOCKSCREEN = 2 - /** - * Attached on the dream overlay - */ + /** Attached on the dream overlay */ const val LOCATION_DREAM_OVERLAY = 3 - /** - * Attached at the root of the hierarchy in an overlay - */ + /** Attached at the root of the hierarchy in an overlay */ const val IN_OVERLAY = -1000 /** @@ -1226,18 +1244,29 @@ class MediaHierarchyManager @Inject constructor( const val TRANSFORMATION_TYPE_FADE = 1 } } + private val EMPTY_RECT = Rect() -@IntDef(prefix = ["TRANSFORMATION_TYPE_"], value = [ - MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION, - MediaHierarchyManager.TRANSFORMATION_TYPE_FADE]) +@IntDef( + prefix = ["TRANSFORMATION_TYPE_"], + value = + [ + MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION, + MediaHierarchyManager.TRANSFORMATION_TYPE_FADE + ] +) @Retention(AnnotationRetention.SOURCE) private annotation class TransformationType -@IntDef(prefix = ["LOCATION_"], value = [ - MediaHierarchyManager.LOCATION_QS, - MediaHierarchyManager.LOCATION_QQS, - MediaHierarchyManager.LOCATION_LOCKSCREEN, - MediaHierarchyManager.LOCATION_DREAM_OVERLAY]) +@IntDef( + prefix = ["LOCATION_"], + value = + [ + MediaHierarchyManager.LOCATION_QS, + MediaHierarchyManager.LOCATION_QQS, + MediaHierarchyManager.LOCATION_LOCKSCREEN, + MediaHierarchyManager.LOCATION_DREAM_OVERLAY + ] +) @Retention(AnnotationRetention.SOURCE) annotation class MediaLocation diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt index bffb0fdec707..455b7de3dc0c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt @@ -1,9 +1,28 @@ -package com.android.systemui.media +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui import android.graphics.Rect import android.util.ArraySet import android.view.View import android.view.View.OnAttachStateChangeListener +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.util.animation.DisappearParameters import com.android.systemui.util.animation.MeasurementInput import com.android.systemui.util.animation.MeasurementOutput @@ -11,7 +30,8 @@ import com.android.systemui.util.animation.UniqueObjectHostView import java.util.Objects import javax.inject.Inject -class MediaHost constructor( +class MediaHost +constructor( private val state: MediaHostStateHolder, private val mediaHierarchyManager: MediaHierarchyManager, private val mediaDataManager: MediaDataManager, @@ -26,14 +46,10 @@ class MediaHost constructor( private var inited: Boolean = false - /** - * Are we listening to media data changes? - */ + /** Are we listening to media data changes? */ private var listeningToMediaData = false - /** - * Get the current bounds on the screen. This makes sure the state is fresh and up to date - */ + /** Get the current bounds on the screen. This makes sure the state is fresh and up to date */ val currentBounds: Rect = Rect() get() { hostView.getLocationOnScreen(tmpLocationOnScreen) @@ -62,38 +78,39 @@ class MediaHost constructor( */ val currentClipping = Rect() - private val listener = object : MediaDataManager.Listener { - override fun onMediaDataLoaded( - key: String, - oldKey: String?, - data: MediaData, - immediately: Boolean, - receivedSmartspaceCardLatency: Int, - isSsReactivated: Boolean - ) { - if (immediately) { - updateViewVisibility() + private val listener = + object : MediaDataManager.Listener { + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean, + receivedSmartspaceCardLatency: Int, + isSsReactivated: Boolean + ) { + if (immediately) { + updateViewVisibility() + } } - } - override fun onSmartspaceMediaDataLoaded( - key: String, - data: SmartspaceMediaData, - shouldPrioritize: Boolean - ) { - updateViewVisibility() - } - - override fun onMediaDataRemoved(key: String) { - updateViewVisibility() - } + override fun onSmartspaceMediaDataLoaded( + key: String, + data: SmartspaceMediaData, + shouldPrioritize: Boolean + ) { + updateViewVisibility() + } - override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { - if (immediately) { + override fun onMediaDataRemoved(key: String) { updateViewVisibility() } + + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + if (immediately) { + updateViewVisibility() + } + } } - } fun addVisibilityChangeListener(listener: (Boolean) -> Unit) { visibleChangedListeners.add(listener) @@ -104,12 +121,14 @@ class MediaHost constructor( } /** - * Initialize this MediaObject and create a host view. - * All state should already be set on this host before calling this method in order to avoid - * unnecessary state changes which lead to remeasurings later on. + * Initialize this MediaObject and create a host view. All state should already be set on this + * host before calling this method in order to avoid unnecessary state changes which lead to + * remeasurings later on. * * @param location the location this host name has. Used to identify the host during + * ``` * transitions. + * ``` */ fun init(@MediaLocation location: Int) { if (inited) { @@ -122,36 +141,42 @@ class MediaHost constructor( // Listen by default, as the host might not be attached by our clients, until // they get a visibility change. We still want to stay up to date in that case! setListeningToMediaData(true) - hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View?) { - setListeningToMediaData(true) - updateViewVisibility() - } + hostView.addOnAttachStateChangeListener( + object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View?) { + setListeningToMediaData(true) + updateViewVisibility() + } - override fun onViewDetachedFromWindow(v: View?) { - setListeningToMediaData(false) + override fun onViewDetachedFromWindow(v: View?) { + setListeningToMediaData(false) + } } - }) + ) // Listen to measurement updates and update our state with it - hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager { - override fun onMeasure(input: MeasurementInput): MeasurementOutput { - // Modify the measurement to exactly match the dimensions - if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) { - input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec( - View.MeasureSpec.getSize(input.widthMeasureSpec), - View.MeasureSpec.EXACTLY) + hostView.measurementManager = + object : UniqueObjectHostView.MeasurementManager { + override fun onMeasure(input: MeasurementInput): MeasurementOutput { + // Modify the measurement to exactly match the dimensions + if ( + View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST + ) { + input.widthMeasureSpec = + View.MeasureSpec.makeMeasureSpec( + View.MeasureSpec.getSize(input.widthMeasureSpec), + View.MeasureSpec.EXACTLY + ) + } + // This will trigger a state change that ensures that we now have a state + // available + state.measurementInput = input + return mediaHostStatesManager.updateCarouselDimensions(location, state) } - // This will trigger a state change that ensures that we now have a state available - state.measurementInput = input - return mediaHostStatesManager.updateCarouselDimensions(location, state) } - } // Whenever the state changes, let our state manager know - state.changedListener = { - mediaHostStatesManager.updateHostState(location, state) - } + state.changedListener = { mediaHostStatesManager.updateHostState(location, state) } updateViewVisibility() } @@ -172,17 +197,16 @@ class MediaHost constructor( * the visibility has changed */ fun updateViewVisibility() { - state.visible = if (showsOnlyActiveMedia) { - mediaDataManager.hasActiveMediaOrRecommendation() - } else { - mediaDataManager.hasAnyMediaOrRecommendation() - } + state.visible = + if (showsOnlyActiveMedia) { + mediaDataManager.hasActiveMediaOrRecommendation() + } else { + mediaDataManager.hasAnyMediaOrRecommendation() + } val newVisibility = if (visible) View.VISIBLE else View.GONE if (newVisibility != hostView.visibility) { hostView.visibility = newVisibility - visibleChangedListeners.forEach { - it.invoke(visible) - } + visibleChangedListeners.forEach { it.invoke(visible) } } } @@ -203,6 +227,14 @@ class MediaHost constructor( } } + override var squishFraction: Float = 1.0f + set(value) { + if (!value.equals(field)) { + field = value + changedListener?.invoke() + } + } + override var showsOnlyActiveMedia: Boolean = false set(value) { if (!value.equals(field)) { @@ -242,17 +274,14 @@ class MediaHost constructor( private var lastDisappearHash = disappearParameters.hashCode() - /** - * A listener for all changes. This won't be copied over when invoking [copy] - */ + /** A listener for all changes. This won't be copied over when invoking [copy] */ var changedListener: (() -> Unit)? = null - /** - * Get a copy of this state. This won't copy any listeners it may have set - */ + /** Get a copy of this state. This won't copy any listeners it may have set */ override fun copy(): MediaHostState { val mediaHostState = MediaHostStateHolder() mediaHostState.expansion = expansion + mediaHostState.squishFraction = squishFraction mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia mediaHostState.measurementInput = measurementInput?.copy() mediaHostState.visible = visible @@ -271,6 +300,9 @@ class MediaHost constructor( if (expansion != other.expansion) { return false } + if (squishFraction != other.squishFraction) { + return false + } if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) { return false } @@ -289,6 +321,7 @@ class MediaHost constructor( override fun hashCode(): Int { var result = measurementInput?.hashCode() ?: 0 result = 31 * result + expansion.hashCode() + result = 31 * result + squishFraction.hashCode() result = 31 * result + falsingProtectionNeeded.hashCode() result = 31 * result + showsOnlyActiveMedia.hashCode() result = 31 * result + if (visible) 1 else 2 @@ -299,15 +332,13 @@ class MediaHost constructor( } /** - * A description of a media host state that describes the behavior whenever the media carousel - * is hosted. The HostState notifies the media players of changes to their properties, who - * in turn will create view states from it. - * When adding a new property to this, make sure to update the listener and notify them - * about the changes. - * In case you need to have a different rendering based on the state, you can add a new - * constraintState to the [MediaViewController]. Otherwise, similar host states will resolve - * to the same viewstate, a behavior that is described in [CacheKey]. Make sure to only update - * that key if the underlying view needs to have a different measurement. + * A description of a media host state that describes the behavior whenever the media carousel is + * hosted. The HostState notifies the media players of changes to their properties, who in turn will + * create view states from it. When adding a new property to this, make sure to update the listener + * and notify them about the changes. In case you need to have a different rendering based on the + * state, you can add a new constraintState to the [MediaViewController]. Otherwise, similar host + * states will resolve to the same viewstate, a behavior that is described in [CacheKey]. Make sure + * to only update that key if the underlying view needs to have a different measurement. */ interface MediaHostState { @@ -317,41 +348,36 @@ interface MediaHostState { } /** - * The last measurement input that this state was measured with. Infers width and height of - * the players. + * The last measurement input that this state was measured with. Infers width and height of the + * players. */ var measurementInput: MeasurementInput? /** - * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions), - * [EXPANDED] for fully expanded (up to 5 actions). + * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions), [EXPANDED] + * for fully expanded (up to 5 actions). */ var expansion: Float - /** - * Is this host only showing active media or is it showing all of them including resumption? - */ + /** Fraction of the height animation. */ + var squishFraction: Float + + /** Is this host only showing active media or is it showing all of them including resumption? */ var showsOnlyActiveMedia: Boolean - /** - * If the view should be VISIBLE or GONE. - */ + /** If the view should be VISIBLE or GONE. */ val visible: Boolean - /** - * Does this host need any falsing protection? - */ + /** Does this host need any falsing protection? */ var falsingProtectionNeeded: Boolean /** * The parameters how the view disappears from this location when going to a host that's not - * visible. If modified, make sure to set this value again on the host to ensure the values - * are propagated + * visible. If modified, make sure to set this value again on the host to ensure the values are + * propagated */ var disappearParameters: DisappearParameters - /** - * Get a copy of this view state, deepcopying all appropriate members - */ + /** Get a copy of this view state, deepcopying all appropriate members */ fun copy(): MediaHostState } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt index aea2934c46fe..ae3ce333d41d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import com.android.systemui.dagger.SysUISingleton import com.android.systemui.util.animation.MeasurementOutput @@ -38,85 +38,76 @@ class MediaHostStatesManager @Inject constructor() { */ val carouselSizes: MutableMap<Int, MeasurementOutput> = mutableMapOf() - /** - * A map with all media states of all locations. - */ + /** A map with all media states of all locations. */ val mediaHostStates: MutableMap<Int, MediaHostState> = mutableMapOf() /** - * Notify that a media state for a given location has changed. Should only be called from - * Media hosts themselves. + * Notify that a media state for a given location has changed. Should only be called from Media + * hosts themselves. */ - fun updateHostState( - @MediaLocation location: Int, - hostState: MediaHostState - ) = traceSection("MediaHostStatesManager#updateHostState") { - val currentState = mediaHostStates.get(location) - if (!hostState.equals(currentState)) { - val newState = hostState.copy() - mediaHostStates.put(location, newState) - updateCarouselDimensions(location, hostState) - // First update all the controllers to ensure they get the chance to measure - for (controller in controllers) { - controller.stateCallback.onHostStateChanged(location, newState) - } + fun updateHostState(@MediaLocation location: Int, hostState: MediaHostState) = + traceSection("MediaHostStatesManager#updateHostState") { + val currentState = mediaHostStates.get(location) + if (!hostState.equals(currentState)) { + val newState = hostState.copy() + mediaHostStates.put(location, newState) + updateCarouselDimensions(location, hostState) + // First update all the controllers to ensure they get the chance to measure + for (controller in controllers) { + controller.stateCallback.onHostStateChanged(location, newState) + } - // Then update all other callbacks which may depend on the controllers above - for (callback in callbacks) { - callback.onHostStateChanged(location, newState) + // Then update all other callbacks which may depend on the controllers above + for (callback in callbacks) { + callback.onHostStateChanged(location, newState) + } } } - } /** - * Get the dimensions of all players combined, which determines the overall height of the - * media carousel and the media hosts. + * Get the dimensions of all players combined, which determines the overall height of the media + * carousel and the media hosts. */ fun updateCarouselDimensions( @MediaLocation location: Int, hostState: MediaHostState - ): MeasurementOutput = traceSection("MediaHostStatesManager#updateCarouselDimensions") { - val result = MeasurementOutput(0, 0) - for (controller in controllers) { - val measurement = controller.getMeasurementsForState(hostState) - measurement?.let { - if (it.measuredHeight > result.measuredHeight) { - result.measuredHeight = it.measuredHeight - } - if (it.measuredWidth > result.measuredWidth) { - result.measuredWidth = it.measuredWidth + ): MeasurementOutput = + traceSection("MediaHostStatesManager#updateCarouselDimensions") { + val result = MeasurementOutput(0, 0) + for (controller in controllers) { + val measurement = controller.getMeasurementsForState(hostState) + measurement?.let { + if (it.measuredHeight > result.measuredHeight) { + result.measuredHeight = it.measuredHeight + } + if (it.measuredWidth > result.measuredWidth) { + result.measuredWidth = it.measuredWidth + } } } + carouselSizes[location] = result + return result } - carouselSizes[location] = result - return result - } - /** - * Add a callback to be called when a MediaState has updated - */ + /** Add a callback to be called when a MediaState has updated */ fun addCallback(callback: Callback) { callbacks.add(callback) } - /** - * Remove a callback that listens to media states - */ + /** Remove a callback that listens to media states */ fun removeCallback(callback: Callback) { callbacks.remove(callback) } /** - * Register a controller that listens to media states and is used to determine the size of - * the media carousel + * Register a controller that listens to media states and is used to determine the size of the + * media carousel */ fun addController(controller: MediaViewController) { controllers.add(controller) } - /** - * Notify the manager about the removal of a controller. - */ + /** Notify the manager about the removal of a controller. */ fun removeController(controller: MediaViewController) { controllers.remove(controller) } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt index 00273bc34552..0e0746590776 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui import android.content.Context import android.os.SystemClock @@ -11,15 +27,13 @@ import com.android.systemui.Gefingerpoken import com.android.wm.shell.animation.physicsAnimator /** - * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful - * when only measuring children but not the parent, when trying to apply a new scroll position + * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful when + * only measuring children but not the parent, when trying to apply a new scroll position */ -class MediaScrollView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) - : HorizontalScrollView(context, attrs, defStyleAttr) { +class MediaScrollView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + HorizontalScrollView(context, attrs, defStyleAttr) { lateinit var contentContainer: ViewGroup private set @@ -34,35 +48,33 @@ class MediaScrollView @JvmOverloads constructor( * Get the current content translation. This is usually the normal translationX of the content, * but when animating, it might differ */ - fun getContentTranslation() = if (contentContainer.physicsAnimator.isRunning()) { - animationTargetX - } else { - contentContainer.translationX - } + fun getContentTranslation() = + if (contentContainer.physicsAnimator.isRunning()) { + animationTargetX + } else { + contentContainer.translationX + } /** * Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media - * carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX - * is always absolute. This function is its own inverse. + * carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX is + * always absolute. This function is its own inverse. */ - private fun transformScrollX(scrollX: Int): Int = if (isLayoutRtl) { - contentContainer.width - width - scrollX - } else { - scrollX - } + private fun transformScrollX(scrollX: Int): Int = + if (isLayoutRtl) { + contentContainer.width - width - scrollX + } else { + scrollX + } - /** - * Get the layoutDirection-relative (start-to-end) scroll X position of the carousel. - */ + /** Get the layoutDirection-relative (start-to-end) scroll X position of the carousel. */ var relativeScrollX: Int get() = transformScrollX(scrollX) set(value) { scrollX = transformScrollX(value) } - /** - * Allow all scrolls to go through, use base implementation - */ + /** Allow all scrolls to go through, use base implementation */ override fun scrollTo(x: Int, y: Int) { if (mScrollX != x || mScrollY != y) { val oldX: Int = mScrollX @@ -79,17 +91,13 @@ class MediaScrollView @JvmOverloads constructor( override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { var intercept = false - touchListener?.let { - intercept = it.onInterceptTouchEvent(ev) - } + touchListener?.let { intercept = it.onInterceptTouchEvent(ev) } return super.onInterceptTouchEvent(ev) || intercept } override fun onTouchEvent(ev: MotionEvent?): Boolean { var touch = false - touchListener?.let { - touch = it.onTouchEvent(ev) - } + touchListener?.let { touch = it.onTouchEvent(ev) } return super.onTouchEvent(ev) || touch } @@ -113,19 +121,25 @@ class MediaScrollView @JvmOverloads constructor( // When we're dismissing we ignore all the scrolling return false } - return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, - scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent) + return super.overScrollBy( + deltaX, + deltaY, + scrollX, + scrollY, + scrollRangeX, + scrollRangeY, + maxOverScrollX, + maxOverScrollY, + isTouchEvent + ) } - /** - * Cancel the current touch event going on. - */ + /** Cancel the current touch event going on. */ fun cancelCurrentScroll() { val now = SystemClock.uptimeMillis() - val event = MotionEvent.obtain(now, now, - MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0) + val event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0) event.source = InputDevice.SOURCE_TOUCHSCREEN super.onTouchEvent(event) event.recycle() } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt new file mode 100644 index 000000000000..4bf3031c02b4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -0,0 +1,607 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.media.controls.ui + +import android.content.Context +import android.content.res.Configuration +import androidx.annotation.VisibleForTesting +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.R +import com.android.systemui.media.controls.models.GutsViewHolder +import com.android.systemui.media.controls.models.player.MediaViewHolder +import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.animation.MeasurementOutput +import com.android.systemui.util.animation.TransitionLayout +import com.android.systemui.util.animation.TransitionLayoutController +import com.android.systemui.util.animation.TransitionViewState +import com.android.systemui.util.traceSection +import javax.inject.Inject + +/** + * A class responsible for controlling a single instance of a media player handling interactions + * with the view instance and keeping the media view states up to date. + */ +class MediaViewController +@Inject +constructor( + private val context: Context, + private val configurationController: ConfigurationController, + private val mediaHostStatesManager: MediaHostStatesManager, + private val logger: MediaViewLogger +) { + + /** + * Indicating that the media view controller is for a notification-based player, session-based + * player, or recommendation + */ + enum class TYPE { + PLAYER, + RECOMMENDATION + } + + companion object { + @JvmField val GUTS_ANIMATION_DURATION = 500L + val controlIds = + setOf( + R.id.media_progress_bar, + R.id.actionNext, + R.id.actionPrev, + R.id.action0, + R.id.action1, + R.id.action2, + R.id.action3, + R.id.action4, + R.id.media_scrubbing_elapsed_time, + R.id.media_scrubbing_total_time + ) + + val detailIds = + setOf( + R.id.header_title, + R.id.header_artist, + R.id.actionPlayPause, + ) + } + + /** A listener when the current dimensions of the player change */ + lateinit var sizeChangedListener: () -> Unit + private var firstRefresh: Boolean = true + @VisibleForTesting private var transitionLayout: TransitionLayout? = null + private val layoutController = TransitionLayoutController() + private var animationDelay: Long = 0 + private var animationDuration: Long = 0 + private var animateNextStateChange: Boolean = false + private val measurement = MeasurementOutput(0, 0) + private var type: TYPE = TYPE.PLAYER + + /** A map containing all viewStates for all locations of this mediaState */ + private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf() + + /** + * The ending location of the view where it ends when all animations and transitions have + * finished + */ + @MediaLocation var currentEndLocation: Int = -1 + + /** The starting location of the view where it starts for all animations and transitions */ + @MediaLocation private var currentStartLocation: Int = -1 + + /** The progress of the transition or 1.0 if there is no transition happening */ + private var currentTransitionProgress: Float = 1.0f + + /** A temporary state used to store intermediate measurements. */ + private val tmpState = TransitionViewState() + + /** A temporary state used to store intermediate measurements. */ + private val tmpState2 = TransitionViewState() + + /** A temporary state used to store intermediate measurements. */ + private val tmpState3 = TransitionViewState() + + /** A temporary cache key to be used to look up cache entries */ + private val tmpKey = CacheKey() + + /** + * The current width of the player. This might not factor in case the player is animating to the + * current state, but represents the end state + */ + var currentWidth: Int = 0 + /** + * The current height of the player. This might not factor in case the player is animating to + * the current state, but represents the end state + */ + var currentHeight: Int = 0 + + /** Get the translationX of the layout */ + var translationX: Float = 0.0f + private set + get() { + return transitionLayout?.translationX ?: 0.0f + } + + /** Get the translationY of the layout */ + var translationY: Float = 0.0f + private set + get() { + return transitionLayout?.translationY ?: 0.0f + } + + /** A callback for RTL config changes */ + private val configurationListener = + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + // Because the TransitionLayout is not always attached (and calculates/caches layout + // results regardless of attach state), we have to force the layoutDirection of the + // view + // to the correct value for the user's current locale to ensure correct + // recalculation + // when/after calling refreshState() + newConfig?.apply { + if (transitionLayout?.rawLayoutDirection != layoutDirection) { + transitionLayout?.layoutDirection = layoutDirection + refreshState() + } + } + } + } + + /** A callback for media state changes */ + val stateCallback = + object : MediaHostStatesManager.Callback { + override fun onHostStateChanged( + @MediaLocation location: Int, + mediaHostState: MediaHostState + ) { + if (location == currentEndLocation || location == currentStartLocation) { + setCurrentState( + currentStartLocation, + currentEndLocation, + currentTransitionProgress, + applyImmediately = false + ) + } + } + } + + /** + * The expanded constraint set used to render a expanded player. If it is modified, make sure to + * call [refreshState] + */ + val collapsedLayout = ConstraintSet() + + /** + * The expanded constraint set used to render a collapsed player. If it is modified, make sure + * to call [refreshState] + */ + val expandedLayout = ConstraintSet() + + /** Whether the guts are visible for the associated player. */ + var isGutsVisible = false + private set + + init { + mediaHostStatesManager.addController(this) + layoutController.sizeChangedListener = { width: Int, height: Int -> + currentWidth = width + currentHeight = height + sizeChangedListener.invoke() + } + configurationController.addCallback(configurationListener) + } + + /** + * Notify this controller that the view has been removed and all listeners should be destroyed + */ + fun onDestroy() { + mediaHostStatesManager.removeController(this) + configurationController.removeCallback(configurationListener) + } + + /** Show guts with an animated transition. */ + fun openGuts() { + if (isGutsVisible) return + isGutsVisible = true + animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) + setCurrentState( + currentStartLocation, + currentEndLocation, + currentTransitionProgress, + applyImmediately = false + ) + } + + /** + * Close the guts for the associated player. + * + * @param immediate if `false`, it will animate the transition. + */ + @JvmOverloads + fun closeGuts(immediate: Boolean = false) { + if (!isGutsVisible) return + isGutsVisible = false + if (!immediate) { + animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) + } + setCurrentState( + currentStartLocation, + currentEndLocation, + currentTransitionProgress, + applyImmediately = immediate + ) + } + + private fun ensureAllMeasurements() { + val mediaStates = mediaHostStatesManager.mediaHostStates + for (entry in mediaStates) { + obtainViewState(entry.value) + } + } + + /** Get the constraintSet for a given expansion */ + private fun constraintSetForExpansion(expansion: Float): ConstraintSet = + if (expansion > 0) expandedLayout else collapsedLayout + + /** + * Set the views to be showing/hidden based on the [isGutsVisible] for a given + * [TransitionViewState]. + */ + private fun setGutsViewState(viewState: TransitionViewState) { + val controlsIds = + when (type) { + TYPE.PLAYER -> MediaViewHolder.controlsIds + TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds + } + val gutsIds = GutsViewHolder.ids + controlsIds.forEach { id -> + viewState.widgetStates.get(id)?.let { state -> + // Make sure to use the unmodified state if guts are not visible. + state.alpha = if (isGutsVisible) 0f else state.alpha + state.gone = if (isGutsVisible) true else state.gone + } + } + gutsIds.forEach { id -> + viewState.widgetStates.get(id)?.let { state -> + // Make sure to use the unmodified state if guts are visible + state.alpha = if (isGutsVisible) state.alpha else 0f + state.gone = if (isGutsVisible) state.gone else true + } + } + } + + /** Apply squishFraction to a copy of viewState such that the cached version is untouched. */ + internal fun squishViewState( + viewState: TransitionViewState, + squishFraction: Float + ): TransitionViewState { + val squishedViewState = viewState.copy() + squishedViewState.height = (squishedViewState.height * squishFraction).toInt() + controlIds.forEach { id -> + squishedViewState.widgetStates.get(id)?.let { state -> + state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION) + } + } + + detailIds.forEach { id -> + squishedViewState.widgetStates.get(id)?.let { state -> + state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION) + } + } + + RecommendationViewHolder.mediaContainersIds.forEach { id -> + squishedViewState.widgetStates.get(id)?.let { state -> + state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION) + } + } + + RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id -> + squishedViewState.widgetStates.get(id)?.let { state -> + state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION) + } + } + + return squishedViewState + } + + /** + * Obtain a new viewState for a given media state. This usually returns a cached state, but if + * it's not available, it will recreate one by measuring, which may be expensive. + */ + @VisibleForTesting + fun obtainViewState(state: MediaHostState?): TransitionViewState? { + if (state == null || state.measurementInput == null) { + return null + } + // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey + var cacheKey = getKey(state, isGutsVisible, tmpKey) + val viewState = viewStates[cacheKey] + if (viewState != null) { + // we already have cached this measurement, let's continue + if (state.squishFraction <= 1f) { + return squishViewState(viewState, state.squishFraction) + } + return viewState + } + // Copy the key since this might call recursively into it and we're using tmpKey + cacheKey = cacheKey.copy() + val result: TransitionViewState? + + if (transitionLayout == null) { + return null + } + // Let's create a new measurement + if (state.expansion == 0.0f || state.expansion == 1.0f) { + result = + transitionLayout!!.calculateViewState( + state.measurementInput!!, + constraintSetForExpansion(state.expansion), + TransitionViewState() + ) + + setGutsViewState(result) + // We don't want to cache interpolated or null states as this could quickly fill up + // our cache. We only cache the start and the end states since the interpolation + // is cheap + viewStates[cacheKey] = result + } else { + // This is an interpolated state + val startState = state.copy().also { it.expansion = 0.0f } + + // Given that we have a measurement and a view, let's get (guaranteed) viewstates + // from the start and end state and interpolate them + val startViewState = obtainViewState(startState) as TransitionViewState + val endState = state.copy().also { it.expansion = 1.0f } + val endViewState = obtainViewState(endState) as TransitionViewState + result = + layoutController.getInterpolatedState(startViewState, endViewState, state.expansion) + } + if (state.squishFraction <= 1f) { + return squishViewState(result, state.squishFraction) + } + return result + } + + private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey { + result.apply { + heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0 + widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0 + expansion = state.expansion + gutsVisible = guts + } + return result + } + + /** + * Attach a view to this controller. This may perform measurements if it's not available yet and + * should therefore be done carefully. + */ + fun attach(transitionLayout: TransitionLayout, type: TYPE) = + traceSection("MediaViewController#attach") { + updateMediaViewControllerType(type) + logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation) + this.transitionLayout = transitionLayout + layoutController.attach(transitionLayout) + if (currentEndLocation == -1) { + return + } + // Set the previously set state immediately to the view, now that it's finally attached + setCurrentState( + startLocation = currentStartLocation, + endLocation = currentEndLocation, + transitionProgress = currentTransitionProgress, + applyImmediately = true + ) + } + + /** + * Obtain a measurement for a given location. This makes sure that the state is up to date and + * all widgets know their location. Calling this method may create a measurement if we don't + * have a cached value available already. + */ + fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? = + traceSection("MediaViewController#getMeasurementsForState") { + val viewState = obtainViewState(hostState) ?: return null + measurement.measuredWidth = viewState.width + measurement.measuredHeight = viewState.height + return measurement + } + + /** + * Set a new state for the controlled view which can be an interpolation between multiple + * locations. + */ + fun setCurrentState( + @MediaLocation startLocation: Int, + @MediaLocation endLocation: Int, + transitionProgress: Float, + applyImmediately: Boolean + ) = + traceSection("MediaViewController#setCurrentState") { + currentEndLocation = endLocation + currentStartLocation = startLocation + currentTransitionProgress = transitionProgress + logger.logMediaLocation("setCurrentState", startLocation, endLocation) + + val shouldAnimate = animateNextStateChange && !applyImmediately + + val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return + val startHostState = mediaHostStatesManager.mediaHostStates[startLocation] + + // Obtain the view state that we'd want to be at the end + // The view might not be bound yet or has never been measured and in that case will be + // reset once the state is fully available + var endViewState = obtainViewState(endHostState) ?: return + endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!! + layoutController.setMeasureState(endViewState) + + // If the view isn't bound, we can drop the animation, otherwise we'll execute it + animateNextStateChange = false + if (transitionLayout == null) { + return + } + + val result: TransitionViewState + var startViewState = obtainViewState(startHostState) + startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3) + + if (!endHostState.visible) { + // Let's handle the case where the end is gone first. In this case we take the + // start viewState and will make it gone + if (startViewState == null || startHostState == null || !startHostState.visible) { + // the start isn't a valid state, let's use the endstate directly + result = endViewState + } else { + // Let's get the gone presentation from the start state + result = + layoutController.getGoneState( + startViewState, + startHostState.disappearParameters, + transitionProgress, + tmpState + ) + } + } else if (startHostState != null && !startHostState.visible) { + // We have a start state and it is gone. + // Let's get presentation from the endState + result = + layoutController.getGoneState( + endViewState, + endHostState.disappearParameters, + 1.0f - transitionProgress, + tmpState + ) + } else if (transitionProgress == 1.0f || startViewState == null) { + // We're at the end. Let's use that state + result = endViewState + } else if (transitionProgress == 0.0f) { + // We're at the start. Let's use that state + result = startViewState + } else { + result = + layoutController.getInterpolatedState( + startViewState, + endViewState, + transitionProgress, + tmpState + ) + } + logger.logMediaSize("setCurrentState", result.width, result.height) + layoutController.setState( + result, + applyImmediately, + shouldAnimate, + animationDuration, + animationDelay + ) + } + + private fun updateViewStateToCarouselSize( + viewState: TransitionViewState?, + location: Int, + outState: TransitionViewState + ): TransitionViewState? { + val result = viewState?.copy(outState) ?: return null + val overrideSize = mediaHostStatesManager.carouselSizes[location] + overrideSize?.let { + // To be safe we're using a maximum here. The override size should always be set + // properly though. + result.height = Math.max(it.measuredHeight, result.height) + result.width = Math.max(it.measuredWidth, result.width) + } + logger.logMediaSize("update to carousel", result.width, result.height) + return result + } + + private fun updateMediaViewControllerType(type: TYPE) { + this.type = type + + // These XML resources contain ConstraintSets that will apply to this player type's layout + when (type) { + TYPE.PLAYER -> { + collapsedLayout.load(context, R.xml.media_session_collapsed) + expandedLayout.load(context, R.xml.media_session_expanded) + } + TYPE.RECOMMENDATION -> { + collapsedLayout.load(context, R.xml.media_recommendation_collapsed) + expandedLayout.load(context, R.xml.media_recommendation_expanded) + } + } + refreshState() + } + + /** + * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. In the event + * of [location] not being visible, [locationWhenHidden] will be used instead. + * + * @param location Target + * @param locationWhenHidden Location that will be used when the target is not + * [MediaHost.visible] + * @return State require for executing a transition, and also the respective [MediaHost]. + */ + private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? { + val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null + return obtainViewState(mediaHostState) + } + + /** + * Notify that the location is changing right now and a [setCurrentState] change is imminent. + * This updates the width the view will me measured with. + */ + fun onLocationPreChange(@MediaLocation newLocation: Int) { + obtainViewStateForLocation(newLocation)?.let { layoutController.setMeasureState(it) } + } + + /** Request that the next state change should be animated with the given parameters. */ + fun animatePendingStateChange(duration: Long, delay: Long) { + animateNextStateChange = true + animationDuration = duration + animationDelay = delay + } + + /** Clear all existing measurements and refresh the state to match the view. */ + fun refreshState() = + traceSection("MediaViewController#refreshState") { + // Let's clear all of our measurements and recreate them! + viewStates.clear() + if (firstRefresh) { + // This is the first bind, let's ensure we pre-cache all measurements. Otherwise + // We'll just load these on demand. + ensureAllMeasurements() + firstRefresh = false + } + setCurrentState( + currentStartLocation, + currentEndLocation, + currentTransitionProgress, + applyImmediately = true + ) + } +} + +/** An internal key for the cache of mediaViewStates. This is a subset of the full host state. */ +private data class CacheKey( + var widthMeasureSpec: Int = -1, + var heightMeasureSpec: Int = -1, + var expansion: Float = 0.0f, + var gutsVisible: Boolean = false +) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt index 73868189b362..fdac33ac20b0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt @@ -14,50 +14,42 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaViewLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "MediaView" -/** - * A buffered log for media view events that are too noisy for regular logging - */ +/** A buffered log for media view events that are too noisy for regular logging */ @SysUISingleton -class MediaViewLogger @Inject constructor( - @MediaViewLog private val buffer: LogBuffer -) { +class MediaViewLogger @Inject constructor(@MediaViewLog private val buffer: LogBuffer) { fun logMediaSize(reason: String, width: Int, height: Int) { buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = reason - int1 = width - int2 = height - }, - { - "size ($str1): $int1 x $int2" - } + TAG, + LogLevel.DEBUG, + { + str1 = reason + int1 = width + int2 = height + }, + { "size ($str1): $int1 x $int2" } ) } fun logMediaLocation(reason: String, startLocation: Int, endLocation: Int) { buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = reason - int1 = startLocation - int2 = endLocation - }, - { - "location ($str1): $int1 -> $int2" - } + TAG, + LogLevel.DEBUG, + { + str1 = reason + int1 = startLocation + int2 = endLocation + }, + { "location ($str1): $int1 -> $int2" } ) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt index 48f4a16cc538..1cdcf5ed2702 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -73,4 +73,4 @@ internal open class MetadataAnimationHandler( exitAnimator.addListener(this) enterAnimator.addListener(this) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt index 6bc94cd5f525..e9b2cf2b18d1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -23,8 +39,7 @@ import kotlin.math.cos private const val TAG = "Squiggly" private const val TWO_PI = (Math.PI * 2f).toFloat() -@VisibleForTesting -internal const val DISABLED_ALPHA = 77 +@VisibleForTesting internal const val DISABLED_ALPHA = 77 class SquigglyProgress : Drawable() { @@ -86,26 +101,29 @@ class SquigglyProgress : Drawable() { lastFrameTime = SystemClock.uptimeMillis() } heightAnimator?.cancel() - heightAnimator = ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply { - if (animate) { - startDelay = 60 - duration = 800 - interpolator = Interpolators.EMPHASIZED_DECELERATE - } else { - duration = 550 - interpolator = Interpolators.STANDARD_DECELERATE - } - addUpdateListener { - heightFraction = it.animatedValue as Float - invalidateSelf() - } - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - heightAnimator = null + heightAnimator = + ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply { + if (animate) { + startDelay = 60 + duration = 800 + interpolator = Interpolators.EMPHASIZED_DECELERATE + } else { + duration = 550 + interpolator = Interpolators.STANDARD_DECELERATE } - }) - start() - } + addUpdateListener { + heightFraction = it.animatedValue as Float + invalidateSelf() + } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + heightAnimator = null + } + } + ) + start() + } } override fun draw(canvas: Canvas) { @@ -120,9 +138,15 @@ class SquigglyProgress : Drawable() { val progress = level / 10_000f val totalWidth = bounds.width().toFloat() val totalProgressPx = totalWidth * progress - val waveProgressPx = totalWidth * ( - if (!transitionEnabled || progress > matchedWaveEndpoint) progress else - lerp(minWaveEndpoint, matchedWaveEndpoint, lerpInv(0f, matchedWaveEndpoint, progress))) + val waveProgressPx = + totalWidth * + (if (!transitionEnabled || progress > matchedWaveEndpoint) progress + else + lerp( + minWaveEndpoint, + matchedWaveEndpoint, + lerpInv(0f, matchedWaveEndpoint, progress) + )) // Build Wiggly Path val waveStart = -phaseOffset - waveLength / 2f @@ -132,10 +156,8 @@ class SquigglyProgress : Drawable() { val computeAmplitude: (Float, Float) -> Float = { x, sign -> if (transitionEnabled) { val length = transitionPeriods * waveLength - val coeff = lerpInvSat( - waveProgressPx + length / 2f, - waveProgressPx - length / 2f, - x) + val coeff = + lerpInvSat(waveProgressPx + length / 2f, waveProgressPx - length / 2f, x) sign * heightFraction * lineAmplitude * coeff } else { sign * heightFraction * lineAmplitude @@ -156,10 +178,7 @@ class SquigglyProgress : Drawable() { val nextX = currentX + dist val midX = currentX + dist / 2 val nextAmp = computeAmplitude(nextX, waveSign) - path.cubicTo( - midX, currentAmp, - midX, nextAmp, - nextX, nextAmp) + path.cubicTo(midX, currentAmp, midX, nextAmp, nextX, nextAmp) currentAmp = nextAmp currentX = nextX } @@ -229,7 +248,7 @@ class SquigglyProgress : Drawable() { private fun updateColors(tintColor: Int, alpha: Int) { wavePaint.color = ColorUtils.setAlphaComponent(tintColor, alpha) - linePaint.color = ColorUtils.setAlphaComponent(tintColor, - (DISABLED_ALPHA * (alpha / 255f)).toInt()) + linePaint.color = + ColorUtils.setAlphaComponent(tintColor, (DISABLED_ALPHA * (alpha / 255f)).toInt()) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.java index ed3e10939b6a..6caf5c20b81c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.util; import android.annotation.NonNull; import android.content.Context; diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataUtils.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java index b8185b9de7e8..bcfceaa3205e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataUtils.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java @@ -14,15 +14,25 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.util; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.text.TextUtils; +/** + * Utility class with common methods for media controls + */ public class MediaDataUtils { + /** + * Get the application label for a given package + * @param context the context to use + * @param packageName Package to check + * @param unknownName Fallback string if application is not found + * @return The label or fallback string + */ public static String getAppLabel(Context context, String packageName, String unknownName) { if (TextUtils.isEmpty(packageName)) { return null; diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFeatureFlag.kt index 75eb33da64d8..91dac6f1a528 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFeatureFlag.kt @@ -14,15 +14,13 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.util import android.content.Context import com.android.systemui.util.Utils import javax.inject.Inject -/** - * Provides access to the current value of the feature flag. - */ +/** Provides access to the current value of the feature flag. */ class MediaFeatureFlag @Inject constructor(private val context: Context) { val enabled get() = Utils.useQsMediaPlayer(context) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index b85ae4820d49..8d4931a5d08c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.util import android.app.StatusBarManager import android.os.UserHandle @@ -34,9 +34,7 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { return enabled || featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS) } - /** - * Check whether we support displaying information about mute await connections. - */ + /** Check whether we support displaying information about mute await connections. */ fun areMuteAwaitConnectionsEnabled() = featureFlags.isEnabled(Flags.MEDIA_MUTE_AWAIT) /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt index 0baf01e7476f..3ad8c21e8a1e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.util import com.android.internal.logging.InstanceId import com.android.internal.logging.InstanceIdSequence @@ -22,22 +22,21 @@ import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.ui.MediaHierarchyManager +import com.android.systemui.media.controls.ui.MediaLocation import java.lang.IllegalArgumentException import javax.inject.Inject private const val INSTANCE_ID_MAX = 1 shl 20 -/** - * A helper class to log events related to the media controls - */ +/** A helper class to log events related to the media controls */ @SysUISingleton class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) { private val instanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX) - /** - * Get a new instance ID for a new media control - */ + /** Get a new instance ID for a new media control */ fun getNewInstanceId(): InstanceId { return instanceIdSequence.newInstanceId() } @@ -48,12 +47,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) instanceId: InstanceId, playbackLocation: Int ) { - val event = when (playbackLocation) { - MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED - MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED - MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED - else -> throw IllegalArgumentException("Unknown playback location") - } + val event = + when (playbackLocation) { + MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED + MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED + MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED + else -> throw IllegalArgumentException("Unknown playback location") + } logger.logWithInstanceId(event, uid, packageName, instanceId) } @@ -63,12 +63,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) instanceId: InstanceId, playbackLocation: Int ) { - val event = when (playbackLocation) { - MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL - MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST - MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE - else -> throw IllegalArgumentException("Unknown playback location") - } + val event = + when (playbackLocation) { + MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL + MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST + MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE + else -> throw IllegalArgumentException("Unknown playback location") + } logger.logWithInstanceId(event, uid, packageName, instanceId) } @@ -107,8 +108,12 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) } fun logLongPressSettings(uid: Int, packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.OPEN_SETTINGS_LONG_PRESS, uid, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.OPEN_SETTINGS_LONG_PRESS, + uid, + packageName, + instanceId + ) } fun logCarouselSettings() { @@ -117,12 +122,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) } fun logTapAction(buttonId: Int, uid: Int, packageName: String, instanceId: InstanceId) { - val event = when (buttonId) { - R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE - R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV - R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT - else -> MediaUiEvent.TAP_ACTION_OTHER - } + val event = + when (buttonId) { + R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE + R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV + R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT + else -> MediaUiEvent.TAP_ACTION_OTHER + } logger.logWithInstanceId(event, uid, packageName, instanceId) } @@ -140,148 +146,130 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) } fun logCarouselPosition(@MediaLocation location: Int) { - val event = when (location) { - MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS - MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS - MediaHierarchyManager.LOCATION_LOCKSCREEN -> - MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN - MediaHierarchyManager.LOCATION_DREAM_OVERLAY -> - MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM - else -> throw IllegalArgumentException("Unknown media carousel location $location") - } + val event = + when (location) { + MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS + MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS + MediaHierarchyManager.LOCATION_LOCKSCREEN -> + MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN + MediaHierarchyManager.LOCATION_DREAM_OVERLAY -> + MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM + else -> throw IllegalArgumentException("Unknown media carousel location $location") + } logger.log(event) } fun logRecommendationAdded(packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ADDED, 0, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.MEDIA_RECOMMENDATION_ADDED, + 0, + packageName, + instanceId + ) } fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED, 0, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED, + 0, + packageName, + instanceId + ) } fun logRecommendationActivated(uid: Int, packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED, uid, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED, + uid, + packageName, + instanceId + ) } fun logRecommendationItemTap(packageName: String, instanceId: InstanceId, position: Int) { - logger.logWithInstanceIdAndPosition(MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP, 0, - packageName, instanceId, position) + logger.logWithInstanceIdAndPosition( + MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP, + 0, + packageName, + instanceId, + position + ) } fun logRecommendationCardTap(packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP, 0, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP, + 0, + packageName, + instanceId + ) } fun logOpenBroadcastDialog(uid: Int, packageName: String, instanceId: InstanceId) { - logger.logWithInstanceId(MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG, uid, packageName, - instanceId) + logger.logWithInstanceId( + MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG, + uid, + packageName, + instanceId + ) } } enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum { @UiEvent(doc = "A new media control was added for media playing locally on the device") LOCAL_MEDIA_ADDED(1029), - @UiEvent(doc = "A new media control was added for media cast from the device") CAST_MEDIA_ADDED(1030), - @UiEvent(doc = "A new media control was added for media playing remotely") REMOTE_MEDIA_ADDED(1031), - @UiEvent(doc = "The media for an existing control was transferred to local playback") TRANSFER_TO_LOCAL(1032), - @UiEvent(doc = "The media for an existing control was transferred to a cast device") TRANSFER_TO_CAST(1033), - @UiEvent(doc = "The media for an existing control was transferred to a remote device") TRANSFER_TO_REMOTE(1034), - - @UiEvent(doc = "A new resumable media control was added") - RESUME_MEDIA_ADDED(1013), - + @UiEvent(doc = "A new resumable media control was added") RESUME_MEDIA_ADDED(1013), @UiEvent(doc = "An existing active media control was converted into resumable media") ACTIVE_TO_RESUME(1014), - - @UiEvent(doc = "A media control timed out") - MEDIA_TIMEOUT(1015), - - @UiEvent(doc = "A media control was removed from the carousel") - MEDIA_REMOVED(1016), - - @UiEvent(doc = "User swiped to another control within the media carousel") - CAROUSEL_PAGE(1017), - - @UiEvent(doc = "The user swiped away the media carousel") - DISMISS_SWIPE(1018), - - @UiEvent(doc = "The user long pressed on a media control") - OPEN_LONG_PRESS(1019), - + @UiEvent(doc = "A media control timed out") MEDIA_TIMEOUT(1015), + @UiEvent(doc = "A media control was removed from the carousel") MEDIA_REMOVED(1016), + @UiEvent(doc = "User swiped to another control within the media carousel") CAROUSEL_PAGE(1017), + @UiEvent(doc = "The user swiped away the media carousel") DISMISS_SWIPE(1018), + @UiEvent(doc = "The user long pressed on a media control") OPEN_LONG_PRESS(1019), @UiEvent(doc = "The user dismissed a media control via its long press menu") DISMISS_LONG_PRESS(1020), - @UiEvent(doc = "The user opened media settings from a media control's long press menu") OPEN_SETTINGS_LONG_PRESS(1021), - @UiEvent(doc = "The user opened media settings from the media carousel") OPEN_SETTINGS_CAROUSEL(1022), - @UiEvent(doc = "The play/pause button on a media control was tapped") TAP_ACTION_PLAY_PAUSE(1023), - - @UiEvent(doc = "The previous button on a media control was tapped") - TAP_ACTION_PREV(1024), - - @UiEvent(doc = "The next button on a media control was tapped") - TAP_ACTION_NEXT(1025), - + @UiEvent(doc = "The previous button on a media control was tapped") TAP_ACTION_PREV(1024), + @UiEvent(doc = "The next button on a media control was tapped") TAP_ACTION_NEXT(1025), @UiEvent(doc = "A custom or generic action button on a media control was tapped") TAP_ACTION_OTHER(1026), - - @UiEvent(doc = "The user seeked on a media control using the seekbar") - ACTION_SEEK(1027), - + @UiEvent(doc = "The user seeked on a media control using the seekbar") ACTION_SEEK(1027), @UiEvent(doc = "The user opened the output switcher from a media control") OPEN_OUTPUT_SWITCHER(1028), - - @UiEvent(doc = "The user tapped on a media control view") - MEDIA_TAP_CONTENT_VIEW(1036), - - @UiEvent(doc = "The media carousel moved to QQS") - MEDIA_CAROUSEL_LOCATION_QQS(1037), - - @UiEvent(doc = "THe media carousel moved to QS") - MEDIA_CAROUSEL_LOCATION_QS(1038), - + @UiEvent(doc = "The user tapped on a media control view") MEDIA_TAP_CONTENT_VIEW(1036), + @UiEvent(doc = "The media carousel moved to QQS") MEDIA_CAROUSEL_LOCATION_QQS(1037), + @UiEvent(doc = "THe media carousel moved to QS") MEDIA_CAROUSEL_LOCATION_QS(1038), @UiEvent(doc = "The media carousel moved to the lockscreen") MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039), - @UiEvent(doc = "The media carousel moved to the dream state") MEDIA_CAROUSEL_LOCATION_DREAM(1040), - @UiEvent(doc = "A media recommendation card was added to the media carousel") MEDIA_RECOMMENDATION_ADDED(1041), - @UiEvent(doc = "A media recommendation card was removed from the media carousel") MEDIA_RECOMMENDATION_REMOVED(1042), - @UiEvent(doc = "An existing media control was made active as a recommendation") MEDIA_RECOMMENDATION_ACTIVATED(1043), - @UiEvent(doc = "User tapped on an item in a media recommendation card") MEDIA_RECOMMENDATION_ITEM_TAP(1044), - @UiEvent(doc = "User tapped on a media recommendation card") MEDIA_RECOMMENDATION_CARD_TAP(1045), - @UiEvent(doc = "User opened the broadcast dialog from a media control") MEDIA_OPEN_BROADCAST_DIALOG(1079); override fun getId() = metricId -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/SmallHash.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/SmallHash.java index de7aac609955..97483a61baa4 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SmallHash.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/SmallHash.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.util; import java.util.Objects; diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index 66c036cee600..3e5d337bff9d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -17,24 +17,22 @@ package com.android.systemui.media.dagger; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.log.LogBuffer; import com.android.systemui.log.dagger.MediaTttReceiverLogBuffer; import com.android.systemui.log.dagger.MediaTttSenderLogBuffer; -import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.MediaFlags; -import com.android.systemui.media.MediaHierarchyManager; -import com.android.systemui.media.MediaHost; -import com.android.systemui.media.MediaHostStatesManager; +import com.android.systemui.media.controls.pipeline.MediaDataManager; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; +import com.android.systemui.media.controls.ui.MediaHost; +import com.android.systemui.media.controls.ui.MediaHostStatesManager; +import com.android.systemui.media.controls.util.MediaFlags; import com.android.systemui.media.dream.dagger.MediaComplicationComponent; import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper; import com.android.systemui.media.taptotransfer.MediaTttFlags; import com.android.systemui.media.taptotransfer.common.MediaTttLogger; -import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver; import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger; -import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender; import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger; +import com.android.systemui.plugins.log.LogBuffer; import java.util.Optional; @@ -94,30 +92,6 @@ public interface MediaModule { return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); } - /** */ - @Provides - @SysUISingleton - static Optional<MediaTttChipControllerSender> providesMediaTttChipControllerSender( - MediaTttFlags mediaTttFlags, - Lazy<MediaTttChipControllerSender> controllerSenderLazy) { - if (!mediaTttFlags.isMediaTttEnabled()) { - return Optional.empty(); - } - return Optional.of(controllerSenderLazy.get()); - } - - /** */ - @Provides - @SysUISingleton - static Optional<MediaTttChipControllerReceiver> providesMediaTttChipControllerReceiver( - MediaTttFlags mediaTttFlags, - Lazy<MediaTttChipControllerReceiver> controllerReceiverLazy) { - if (!mediaTttFlags.isMediaTttEnabled()) { - return Optional.empty(); - } - return Optional.of(controllerReceiverLazy.get()); - } - @Provides @SysUISingleton @MediaTttSenderLogger diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt deleted file mode 100644 index 185b4fca87d0..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.dagger - -import android.app.Activity -import android.content.ComponentName -import android.content.Context -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.media.MediaProjectionAppSelectorActivity -import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController -import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader -import com.android.systemui.mediaprojection.appselector.data.AppIconLoader -import com.android.systemui.mediaprojection.appselector.data.IconLoaderLibAppIconLoader -import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider -import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader -import com.android.systemui.mediaprojection.appselector.data.ShellRecentTaskListProvider -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class MediaProjectionAppSelector - -@Module -abstract class MediaProjectionModule { - - @Binds - @IntoMap - @ClassKey(MediaProjectionAppSelectorActivity::class) - abstract fun provideMediaProjectionAppSelectorActivity( - activity: MediaProjectionAppSelectorActivity - ): Activity - - @Binds - abstract fun bindRecentTaskThumbnailLoader( - impl: ActivityTaskManagerThumbnailLoader - ): RecentTaskThumbnailLoader - - @Binds - abstract fun bindRecentTaskListProvider( - impl: ShellRecentTaskListProvider - ): RecentTaskListProvider - - @Binds - abstract fun bindAppIconLoader(impl: IconLoaderLibAppIconLoader): AppIconLoader - - companion object { - @Provides - fun provideController( - recentTaskListProvider: RecentTaskListProvider, - context: Context, - @MediaProjectionAppSelector scope: CoroutineScope - ): MediaProjectionAppSelectorController { - val appSelectorComponentName = - ComponentName(context, MediaProjectionAppSelectorActivity::class.java) - - return MediaProjectionAppSelectorController( - recentTaskListProvider, - scope, - appSelectorComponentName - ) - } - - @MediaProjectionAppSelector - @Provides - fun provideCoroutineScope(@Application applicationScope: CoroutineScope): CoroutineScope = - CoroutineScope(applicationScope.coroutineContext + SupervisorJob()) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java index 65c5bc76f3c5..69b5698b9042 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java @@ -21,9 +21,9 @@ import static com.android.systemui.media.dream.dagger.MediaComplicationComponent import android.widget.FrameLayout; -import com.android.systemui.media.MediaHierarchyManager; -import com.android.systemui.media.MediaHost; -import com.android.systemui.media.MediaHostState; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; +import com.android.systemui.media.controls.ui.MediaHost; +import com.android.systemui.media.controls.ui.MediaHostState; import com.android.systemui.util.ViewController; import javax.inject.Inject; diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java index 53b4d434bfcb..20e8ae6719f3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java +++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java @@ -18,7 +18,6 @@ package com.android.systemui.media.dream; import static com.android.systemui.flags.Flags.DREAM_MEDIA_COMPLICATION; -import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; @@ -28,9 +27,9 @@ import com.android.systemui.CoreStartable; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dreams.complication.DreamMediaEntryComplication; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.media.MediaData; -import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.SmartspaceMediaData; +import com.android.systemui.media.controls.models.player.MediaData; +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData; +import com.android.systemui.media.controls.pipeline.MediaDataManager; import javax.inject.Inject; @@ -38,7 +37,7 @@ import javax.inject.Inject; * {@link MediaDreamSentinel} is responsible for tracking media state and registering/unregistering * the media complication as appropriate */ -public class MediaDreamSentinel extends CoreStartable { +public class MediaDreamSentinel implements CoreStartable { private static final String TAG = "MediaDreamSentinel"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -113,11 +112,10 @@ public class MediaDreamSentinel extends CoreStartable { private final FeatureFlags mFeatureFlags; @Inject - public MediaDreamSentinel(Context context, MediaDataManager mediaDataManager, + public MediaDreamSentinel(MediaDataManager mediaDataManager, DreamOverlayStateController dreamOverlayStateController, DreamMediaEntryComplication mediaEntryComplication, FeatureFlags featureFlags) { - super(context); mMediaDataManager = mediaDataManager; mDreamOverlayStateController = dreamOverlayStateController; mMediaEntryComplication = mediaEntryComplication; diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt index ffcc1f75f077..e26089450c21 100644 --- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt @@ -21,7 +21,7 @@ import com.android.settingslib.media.DeviceIconUtil import com.android.settingslib.media.LocalMediaManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.media.MediaFlags +import com.android.systemui.media.controls.util.MediaFlags import java.util.concurrent.Executor import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt index 78f4e012da03..5ace3ea8a05b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt @@ -1,9 +1,9 @@ package com.android.systemui.media.muteawait import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.MediaMuteAwaitLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Log messages for [MediaMuteAwaitConnectionManager]. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt index 46b2cc141b3c..78408fce5a36 100644 --- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt @@ -1,9 +1,9 @@ package com.android.systemui.media.nearby import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NearbyMediaDevicesLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Log messages for [NearbyMediaDevicesManager]. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java b/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java index d60172a17988..0ba5f28c351f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java +++ b/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java @@ -40,7 +40,7 @@ import javax.inject.Inject; * documented at {@link #handleTaskStackChanged} apply. */ @SysUISingleton -public class HomeSoundEffectController extends CoreStartable { +public class HomeSoundEffectController implements CoreStartable { private static final String TAG = "HomeSoundEffectController"; private final AudioManager mAudioManager; @@ -65,7 +65,6 @@ public class HomeSoundEffectController extends CoreStartable { TaskStackChangeListeners taskStackChangeListeners, ActivityManagerWrapper activityManagerWrapper, PackageManager packageManager) { - super(context); mAudioManager = audioManager; mTaskStackChangeListeners = taskStackChangeListeners; mActivityManagerWrapper = activityManagerWrapper; diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt index 00b0ff9b128d..a4a968067462 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt @@ -22,6 +22,7 @@ import android.content.Context import android.media.MediaRoute2Info import android.util.Log import androidx.annotation.VisibleForTesting +import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.taptotransfer.receiver.ChipStateReceiver @@ -39,14 +40,10 @@ import javax.inject.Inject */ @SysUISingleton class MediaTttCommandLineHelper @Inject constructor( - commandRegistry: CommandRegistry, + private val commandRegistry: CommandRegistry, private val context: Context, @Main private val mainExecutor: Executor -) { - init { - commandRegistry.registerCommand(SENDER_COMMAND) { SenderCommand() } - commandRegistry.registerCommand(RECEIVER_COMMAND) { ReceiverCommand() } - } +) : CoreStartable { /** All commands for the sender device. */ inner class SenderCommand : Command { @@ -56,7 +53,7 @@ class MediaTttCommandLineHelper @Inject constructor( val displayState: Int? try { displayState = ChipStateSender.getSenderStateIdFromName(commandName) - } catch (ex: IllegalArgumentException) { + } catch (ex: IllegalArgumentException) { pw.println("Invalid command name $commandName") return } @@ -150,6 +147,11 @@ class MediaTttCommandLineHelper @Inject constructor( "<chipState> useAppIcon=[true|false]") } } + + override fun start() { + commandRegistry.registerCommand(SENDER_COMMAND) { SenderCommand() } + commandRegistry.registerCommand(RECEIVER_COMMAND) { ReceiverCommand() } + } } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/README.md b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/README.md index 6379960b85e9..b5a0483e0c69 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/README.md +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/README.md @@ -41,3 +41,5 @@ in the `receiver` package, and code that's shared between them is in the `common ## Testing If you want to test out the tap-to-transfer chip without using the `@SystemApi`s, you can use adb commands instead. Refer to `MediaTttCommandLineHelper` for information about adb commands. + +TODO(b/245610654): Update this page once the chipbar migration is complete. diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt index b565f3c22f24..120f7d673881 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt @@ -16,8 +16,8 @@ package com.android.systemui.media.taptotransfer.common -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.temporarydisplay.TemporaryViewLogger /** @@ -43,6 +43,21 @@ class MediaTttLogger( ) } + /** + * Logs an error in trying to update to [displayState]. + * + * [displayState] is either a [android.app.StatusBarManager.MediaTransferSenderState] or + * a [android.app.StatusBarManager.MediaTransferReceiverState]. + */ + fun logStateChangeError(displayState: Int) { + buffer.log( + tag, + LogLevel.ERROR, + { int1 = displayState }, + { "Cannot display state=$int1; aborting" } + ) + } + /** Logs that we couldn't find information for [packageName]. */ fun logPackageNotFound(packageName: String) { buffer.log( diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt index 792ae7ca6049..0a6043793ef6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt @@ -19,9 +19,10 @@ package com.android.systemui.media.taptotransfer.common import android.content.Context import android.content.pm.PackageManager import android.graphics.drawable.Drawable -import com.android.internal.widget.CachingIconView import com.android.settingslib.Utils import com.android.systemui.R +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon /** Utility methods for media tap-to-transfer. */ class MediaTttUtils { @@ -32,6 +33,23 @@ class MediaTttUtils { const val WAKE_REASON = "MEDIA_TRANSFER_ACTIVATED" /** + * Returns the information needed to display the icon in [Icon] form. + * + * See [getIconInfoFromPackageName]. + */ + fun getIconFromPackageName( + context: Context, + appPackageName: String?, + logger: MediaTttLogger, + ): Icon { + val iconInfo = getIconInfoFromPackageName(context, appPackageName, logger) + return Icon.Loaded( + iconInfo.drawable, + ContentDescription.Loaded(iconInfo.contentDescription) + ) + } + + /** * Returns the information needed to display the icon. * * The information will either contain app name and icon of the app playing media, or a @@ -76,29 +94,6 @@ class MediaTttUtils { isAppIcon = false ) } - - /** - * Sets an icon to be displayed by the given view. - * - * @param iconSize the size in pixels that the icon should be. If null, the size of - * [appIconView] will not be adjusted. - */ - fun setIcon( - appIconView: CachingIconView, - icon: Drawable, - iconContentDescription: CharSequence, - iconSize: Int? = null, - ) { - iconSize?.let { size -> - val lp = appIconView.layoutParams - lp.width = size - lp.height = size - appIconView.layoutParams = lp - } - - appIconView.contentDescription = iconContentDescription - appIconView.setImageDrawable(icon) - } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index dfd9e22c14b1..dc794e66b918 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -19,21 +19,23 @@ package com.android.systemui.media.taptotransfer.receiver import android.annotation.SuppressLint import android.app.StatusBarManager import android.content.Context +import android.graphics.Rect import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.media.MediaRoute2Info import android.os.Handler import android.os.PowerManager -import android.util.Log import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager +import com.android.internal.widget.CachingIconView import com.android.settingslib.Utils import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.common.MediaTttUtils import com.android.systemui.statusbar.CommandQueue @@ -43,16 +45,19 @@ import com.android.systemui.temporarydisplay.TemporaryViewDisplayController import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.android.systemui.util.animation.AnimationUtil.Companion.frames import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.view.ViewUtil import javax.inject.Inject /** * A controller to display and hide the Media Tap-To-Transfer chip on the **receiving** device. * * This chip is shown when a user is transferring media to/from a sending device and this device. + * + * TODO(b/245610654): Re-name this to be MediaTttReceiverCoordinator. */ @SysUISingleton class MediaTttChipControllerReceiver @Inject constructor( - commandQueue: CommandQueue, + private val commandQueue: CommandQueue, context: Context, @MediaTttReceiverLogger logger: MediaTttLogger, windowManager: WindowManager, @@ -61,7 +66,9 @@ class MediaTttChipControllerReceiver @Inject constructor( configurationController: ConfigurationController, powerManager: PowerManager, @Main private val mainHandler: Handler, + private val mediaTttFlags: MediaTttFlags, private val uiEventLogger: MediaTttReceiverUiEventLogger, + private val viewUtil: ViewUtil, ) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger>( context, logger, @@ -82,7 +89,6 @@ class MediaTttChipControllerReceiver @Inject constructor( height = WindowManager.LayoutParams.MATCH_PARENT layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS fitInsetsTypes = 0 // Ignore insets from all system bars - flags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE } private val commandQueueCallbacks = object : CommandQueue.Callbacks { @@ -98,10 +104,6 @@ class MediaTttChipControllerReceiver @Inject constructor( } } - init { - commandQueue.addCallback(commandQueueCallbacks) - } - private fun updateMediaTapToTransferReceiverDisplay( @StatusBarManager.MediaTransferReceiverState displayState: Int, routeInfo: MediaRoute2Info, @@ -113,13 +115,13 @@ class MediaTttChipControllerReceiver @Inject constructor( logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName) if (chipState == null) { - Log.e(RECEIVER_TAG, "Unhandled MediaTransferReceiverState $displayState") + logger.logStateChangeError(displayState) return } uiEventLogger.logReceiverStateChange(chipState) if (chipState == ChipStateReceiver.FAR_FROM_SENDER) { - removeView(removalReason = ChipStateReceiver.FAR_FROM_SENDER::class.simpleName!!) + removeView(removalReason = ChipStateReceiver.FAR_FROM_SENDER.name) return } if (appIcon == null) { @@ -138,32 +140,33 @@ class MediaTttChipControllerReceiver @Inject constructor( ) } - override fun updateView(newInfo: ChipReceiverInfo, currentView: ViewGroup) { - super.updateView(newInfo, currentView) + override fun start() { + if (mediaTttFlags.isMediaTttEnabled()) { + commandQueue.addCallback(commandQueueCallbacks) + } + } + override fun updateView(newInfo: ChipReceiverInfo, currentView: ViewGroup) { val iconInfo = MediaTttUtils.getIconInfoFromPackageName( context, newInfo.routeInfo.clientPackageName, logger ) val iconDrawable = newInfo.appIconDrawableOverride ?: iconInfo.drawable val iconContentDescription = newInfo.appNameOverride ?: iconInfo.contentDescription - val iconSize = context.resources.getDimensionPixelSize( + val iconPadding = if (iconInfo.isAppIcon) { - R.dimen.media_ttt_icon_size_receiver + 0 } else { - R.dimen.media_ttt_generic_icon_size_receiver + context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_padding) } - ) - MediaTttUtils.setIcon( - currentView.requireViewById(R.id.app_icon), - iconDrawable, - iconContentDescription, - iconSize, - ) + val iconView = currentView.getAppIconView() + iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding) + iconView.setImageDrawable(iconDrawable) + iconView.contentDescription = iconContentDescription } override fun animateViewIn(view: ViewGroup) { - val appIconView = view.requireViewById<View>(R.id.app_icon) + val appIconView = view.getAppIconView() appIconView.animate() .translationYBy(-1 * getTranslationAmount().toFloat()) .setDuration(30.frames) @@ -177,6 +180,12 @@ class MediaTttChipControllerReceiver @Inject constructor( startRipple(view.requireViewById(R.id.ripple)) } + override fun getTouchableRegion(view: View, outRect: Rect) { + // Even though the app icon view isn't touchable, users might think it is. So, use it as the + // touchable region to ensure that touches don't get passed to the window below. + viewUtil.setRectToViewWindowLocation(view.getAppIconView(), outRect) + } + /** Returns the amount that the chip will be translated by in its intro animation. */ private fun getTranslationAmount(): Int { return context.resources.getDimensionPixelSize(R.dimen.media_ttt_receiver_vert_translation) @@ -204,16 +213,19 @@ class MediaTttChipControllerReceiver @Inject constructor( private fun layoutRipple(rippleView: ReceiverChipRippleView) { val windowBounds = windowManager.currentWindowMetrics.bounds - val height = windowBounds.height() - val width = windowBounds.width() + val height = windowBounds.height().toFloat() + val width = windowBounds.width().toFloat() - val maxDiameter = height / 2.5f - rippleView.setMaxSize(maxDiameter, maxDiameter) + rippleView.setMaxSize(width / 2f, height / 2f) // Center the ripple on the bottom of the screen in the middle. - rippleView.setCenter(width * 0.5f, height.toFloat()) + rippleView.setCenter(width * 0.5f, height) val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent) rippleView.setColor(color, 70) } + + private fun View.getAppIconView(): CachingIconView { + return this.requireViewById(R.id.app_icon) + } } data class ChipReceiverInfo( @@ -223,5 +235,3 @@ data class ChipReceiverInfo( ) : TemporaryViewInfo { override fun getTimeoutMs() = DEFAULT_TIMEOUT_MILLIS } - -private const val RECEIVER_TAG = "MediaTapToTransferRcvr" diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt index 6a505f06a495..e354a03f1725 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt @@ -18,6 +18,7 @@ package com.android.systemui.media.taptotransfer.receiver import android.content.Context import android.util.AttributeSet +import com.android.systemui.ripple.RippleShader import com.android.systemui.ripple.RippleView /** @@ -25,9 +26,9 @@ import com.android.systemui.ripple.RippleView */ class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleView(context, attrs) { init { - // TODO: use RippleShape#ELLIPSE when calling setupShader. - setupShader() + setupShader(RippleShader.RippleShape.ELLIPSE) setRippleFill(true) + setSparkleStrength(0f) duration = 3000L } } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt index 4379d25406bf..6e596ee1f473 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt @@ -18,13 +18,11 @@ package com.android.systemui.media.taptotransfer.sender import android.app.StatusBarManager import android.content.Context -import android.media.MediaRoute2Info import android.util.Log -import android.view.View import androidx.annotation.StringRes import com.android.internal.logging.UiEventLogger -import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R +import com.android.systemui.common.shared.model.Text import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS /** @@ -35,6 +33,7 @@ import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS * @property stringResId the res ID of the string that should be displayed in the chip. Null if the * state should not have the chip be displayed. * @property transferStatus the transfer status that the chip state represents. + * @property endItem the item that should be displayed in the end section of the chip. * @property timeout the amount of time this chip should display on the screen before it times out * and disappears. */ @@ -43,6 +42,7 @@ enum class ChipStateSender( val uiEvent: UiEventLogger.UiEventEnum, @StringRes val stringResId: Int?, val transferStatus: TransferStatus, + val endItem: SenderEndItem?, val timeout: Long = DEFAULT_TIMEOUT_MILLIS ) { /** @@ -55,6 +55,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST, R.string.media_move_closer_to_start_cast, transferStatus = TransferStatus.NOT_STARTED, + endItem = null, ), /** @@ -68,6 +69,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST, R.string.media_move_closer_to_end_cast, transferStatus = TransferStatus.NOT_STARTED, + endItem = null, ), /** @@ -79,6 +81,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED, R.string.media_transfer_playing_different_device, transferStatus = TransferStatus.IN_PROGRESS, + endItem = SenderEndItem.Loading, timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS ), @@ -91,6 +94,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED, R.string.media_transfer_playing_this_device, transferStatus = TransferStatus.IN_PROGRESS, + endItem = SenderEndItem.Loading, timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS ), @@ -102,33 +106,13 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED, R.string.media_transfer_playing_different_device, transferStatus = TransferStatus.SUCCEEDED, - ) { - override fun undoClickListener( - controllerSender: MediaTttChipControllerSender, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger - ): View.OnClickListener? { - if (undoCallback == null) { - return null - } - return View.OnClickListener { - uiEventLogger.logUndoClicked( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED - ) - undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToThisDeviceTriggered - // state, but that may take too long to go through the binder and the user may be - // confused ast o why the UI hasn't changed yet. So, we immediately change the UI - // here. - controllerSender.displayView( - ChipSenderInfo( - TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo, undoCallback - ) - ) - } - } - }, + endItem = SenderEndItem.UndoButton( + uiEventOnClick = + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED, + newState = + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED + ), + ), /** * A state representing that a transfer back to this device has been successfully completed. @@ -138,33 +122,13 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, R.string.media_transfer_playing_this_device, transferStatus = TransferStatus.SUCCEEDED, - ) { - override fun undoClickListener( - controllerSender: MediaTttChipControllerSender, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger - ): View.OnClickListener? { - if (undoCallback == null) { - return null - } - return View.OnClickListener { - uiEventLogger.logUndoClicked( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED - ) - undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToReceiverTriggered - // state, but that may take too long to go through the binder and the user may be - // confused as to why the UI hasn't changed yet. So, we immediately change the UI - // here. - controllerSender.displayView( - ChipSenderInfo( - TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo, undoCallback - ) - ) - } - } - }, + endItem = SenderEndItem.UndoButton( + uiEventOnClick = + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED, + newState = + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED + ), + ), /** A state representing that a transfer to the receiver device has failed. */ TRANSFER_TO_RECEIVER_FAILED( @@ -172,6 +136,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED, R.string.media_transfer_failed, transferStatus = TransferStatus.FAILED, + endItem = SenderEndItem.Error, ), /** A state representing that a transfer back to this device has failed. */ @@ -180,6 +145,7 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED, R.string.media_transfer_failed, transferStatus = TransferStatus.FAILED, + endItem = SenderEndItem.Error, ), /** A state representing that this device is far away from any receiver device. */ @@ -188,36 +154,27 @@ enum class ChipStateSender( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER, stringResId = null, transferStatus = TransferStatus.TOO_FAR, - ); + // We shouldn't be displaying the chipbar anyway + endItem = null, + ) { + override fun getChipTextString(context: Context, otherDeviceName: String): Text { + // TODO(b/245610654): Better way to handle this. + throw IllegalArgumentException("FAR_FROM_RECEIVER should never be displayed, " + + "so its string should never be fetched") + } + }; /** * Returns a fully-formed string with the text that the chip should display. * + * Throws an NPE if [stringResId] is null. + * * @param otherDeviceName the name of the other device involved in the transfer. */ - fun getChipTextString(context: Context, otherDeviceName: String): String? { - if (stringResId == null) { - return null - } - return context.getString(stringResId, otherDeviceName) + open fun getChipTextString(context: Context, otherDeviceName: String): Text { + return Text.Loaded(context.getString(stringResId!!, otherDeviceName)) } - /** - * Returns a click listener for the undo button on the chip. Returns null if this chip state - * doesn't have an undo button. - * - * @param controllerSender passed as a parameter in case we want to display a new chip state - * when undo is clicked. - * @param undoCallback if present, the callback that should be called when the user clicks the - * undo button. The undo button will only be shown if this is non-null. - */ - open fun undoClickListener( - controllerSender: MediaTttChipControllerSender, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger - ): View.OnClickListener? = null - companion object { /** * Returns the sender state enum associated with the given [displayState] from @@ -243,6 +200,26 @@ enum class ChipStateSender( } } +/** Represents the item that should be displayed in the end section of the chip. */ +sealed class SenderEndItem { + /** A loading icon should be displayed. */ + object Loading : SenderEndItem() + + /** An error icon should be displayed. */ + object Error : SenderEndItem() + + /** + * An undo button should be displayed. + * + * @property uiEventOnClick the UI event to log when this button is clicked. + * @property newState the state that should immediately be transitioned to. + */ + data class UndoButton( + val uiEventOnClick: UiEventLogger.UiEventEnum, + @StatusBarManager.MediaTransferSenderState val newState: Int, + ) : SenderEndItem() +} + // Give the Transfer*Triggered states a longer timeout since those states represent an active // process and we should keep the user informed about it as long as possible (but don't allow it to // continue indefinitely). diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt deleted file mode 100644 index e539f3fd842d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.taptotransfer.sender - -import android.app.StatusBarManager -import android.content.Context -import android.media.MediaRoute2Info -import android.os.PowerManager -import android.util.Log -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import android.view.accessibility.AccessibilityManager -import android.widget.TextView -import com.android.internal.statusbar.IUndoMediaTransferCallback -import com.android.systemui.R -import com.android.systemui.animation.Interpolators -import com.android.systemui.animation.ViewHierarchyAnimator -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.media.taptotransfer.common.MediaTttLogger -import com.android.systemui.media.taptotransfer.common.MediaTttUtils -import com.android.systemui.statusbar.CommandQueue -import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.temporarydisplay.TemporaryDisplayRemovalReason -import com.android.systemui.temporarydisplay.TemporaryViewDisplayController -import com.android.systemui.temporarydisplay.TemporaryViewInfo -import com.android.systemui.util.concurrency.DelayableExecutor -import javax.inject.Inject - -/** - * A controller to display and hide the Media Tap-To-Transfer chip on the **sending** device. This - * chip is shown when a user is transferring media to/from this device and a receiver device. - */ -@SysUISingleton -class MediaTttChipControllerSender @Inject constructor( - commandQueue: CommandQueue, - context: Context, - @MediaTttSenderLogger logger: MediaTttLogger, - windowManager: WindowManager, - @Main mainExecutor: DelayableExecutor, - accessibilityManager: AccessibilityManager, - configurationController: ConfigurationController, - powerManager: PowerManager, - private val uiEventLogger: MediaTttSenderUiEventLogger -) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>( - context, - logger, - windowManager, - mainExecutor, - accessibilityManager, - configurationController, - powerManager, - R.layout.media_ttt_chip, - MediaTttUtils.WINDOW_TITLE, - MediaTttUtils.WAKE_REASON, -) { - override val windowLayoutParams = commonWindowLayoutParams.apply { - gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) - } - - private val commandQueueCallbacks = object : CommandQueue.Callbacks { - override fun updateMediaTapToTransferSenderDisplay( - @StatusBarManager.MediaTransferSenderState displayState: Int, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback? - ) { - this@MediaTttChipControllerSender.updateMediaTapToTransferSenderDisplay( - displayState, routeInfo, undoCallback - ) - } - } - - init { - commandQueue.addCallback(commandQueueCallbacks) - } - - private fun updateMediaTapToTransferSenderDisplay( - @StatusBarManager.MediaTransferSenderState displayState: Int, - routeInfo: MediaRoute2Info, - undoCallback: IUndoMediaTransferCallback? - ) { - val chipState: ChipStateSender? = ChipStateSender.getSenderStateFromId(displayState) - val stateName = chipState?.name ?: "Invalid" - logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName) - - if (chipState == null) { - Log.e(SENDER_TAG, "Unhandled MediaTransferSenderState $displayState") - return - } - uiEventLogger.logSenderStateChange(chipState) - - if (chipState == ChipStateSender.FAR_FROM_RECEIVER) { - removeView(removalReason = ChipStateSender.FAR_FROM_RECEIVER.name) - } else { - displayView(ChipSenderInfo(chipState, routeInfo, undoCallback)) - } - } - - override fun updateView( - newInfo: ChipSenderInfo, - currentView: ViewGroup - ) { - super.updateView(newInfo, currentView) - - val chipState = newInfo.state - - // App icon - val iconInfo = MediaTttUtils.getIconInfoFromPackageName( - context, newInfo.routeInfo.clientPackageName, logger - ) - MediaTttUtils.setIcon( - currentView.requireViewById(R.id.app_icon), - iconInfo.drawable, - iconInfo.contentDescription - ) - - // Text - val otherDeviceName = newInfo.routeInfo.name.toString() - val chipText = chipState.getChipTextString(context, otherDeviceName) - currentView.requireViewById<TextView>(R.id.text).text = chipText - - // Loading - currentView.requireViewById<View>(R.id.loading).visibility = - (chipState.transferStatus == TransferStatus.IN_PROGRESS).visibleIfTrue() - - // Undo - val undoView = currentView.requireViewById<View>(R.id.undo) - val undoClickListener = chipState.undoClickListener( - this, newInfo.routeInfo, newInfo.undoCallback, uiEventLogger - ) - undoView.setOnClickListener(undoClickListener) - undoView.visibility = (undoClickListener != null).visibleIfTrue() - - // Failure - currentView.requireViewById<View>(R.id.failure_icon).visibility = - (chipState.transferStatus == TransferStatus.FAILED).visibleIfTrue() - - // For accessibility - currentView.requireViewById<ViewGroup>( - R.id.media_ttt_sender_chip_inner - ).contentDescription = "${iconInfo.contentDescription} $chipText" - } - - override fun animateViewIn(view: ViewGroup) { - val chipInnerView = view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner) - ViewHierarchyAnimator.animateAddition( - chipInnerView, - ViewHierarchyAnimator.Hotspot.TOP, - Interpolators.EMPHASIZED_DECELERATE, - duration = ANIMATION_DURATION, - includeMargins = true, - includeFadeIn = true, - // We can only request focus once the animation finishes. - onAnimationEnd = { chipInnerView.requestAccessibilityFocus() }, - ) - } - - override fun removeView(removalReason: String) { - // Don't remove the chip if we're in progress or succeeded, since the user should still be - // able to see the status of the transfer. (But do remove it if it's finally timed out.) - val transferStatus = info?.state?.transferStatus - if ( - (transferStatus == TransferStatus.IN_PROGRESS || - transferStatus == TransferStatus.SUCCEEDED) && - removalReason != TemporaryDisplayRemovalReason.REASON_TIMEOUT - ) { - logger.logRemovalBypass( - removalReason, bypassReason = "transferStatus=${transferStatus.name}" - ) - return - } - super.removeView(removalReason) - } - - private fun Boolean.visibleIfTrue(): Int { - return if (this) { - View.VISIBLE - } else { - View.GONE - } - } -} - -data class ChipSenderInfo( - val state: ChipStateSender, - val routeInfo: MediaRoute2Info, - val undoCallback: IUndoMediaTransferCallback? = null -) : TemporaryViewInfo { - override fun getTimeoutMs() = state.timeout -} - -const val SENDER_TAG = "MediaTapToTransferSender" -private const val ANIMATION_DURATION = 500L diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt new file mode 100644 index 000000000000..1fa8faeecd82 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.taptotransfer.sender + +import android.app.StatusBarManager +import android.content.Context +import android.media.MediaRoute2Info +import android.view.View +import com.android.internal.logging.UiEventLogger +import com.android.internal.statusbar.IUndoMediaTransferCallback +import com.android.systemui.CoreStartable +import com.android.systemui.R +import com.android.systemui.common.shared.model.Text +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.media.taptotransfer.MediaTttFlags +import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.media.taptotransfer.common.MediaTttUtils +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem +import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo +import javax.inject.Inject + +/** + * A coordinator for showing/hiding the Media Tap-To-Transfer UI on the **sending** device. This UI + * is shown when a user is transferring media to/from this device and a receiver device. + */ +@SysUISingleton +class MediaTttSenderCoordinator +@Inject +constructor( + private val chipbarCoordinator: ChipbarCoordinator, + private val commandQueue: CommandQueue, + private val context: Context, + @MediaTttSenderLogger private val logger: MediaTttLogger, + private val mediaTttFlags: MediaTttFlags, + private val uiEventLogger: MediaTttSenderUiEventLogger, +) : CoreStartable { + + private var displayedState: ChipStateSender? = null + + private val commandQueueCallbacks = + object : CommandQueue.Callbacks { + override fun updateMediaTapToTransferSenderDisplay( + @StatusBarManager.MediaTransferSenderState displayState: Int, + routeInfo: MediaRoute2Info, + undoCallback: IUndoMediaTransferCallback? + ) { + this@MediaTttSenderCoordinator.updateMediaTapToTransferSenderDisplay( + displayState, + routeInfo, + undoCallback + ) + } + } + + override fun start() { + if (mediaTttFlags.isMediaTttEnabled()) { + commandQueue.addCallback(commandQueueCallbacks) + } + } + + private fun updateMediaTapToTransferSenderDisplay( + @StatusBarManager.MediaTransferSenderState displayState: Int, + routeInfo: MediaRoute2Info, + undoCallback: IUndoMediaTransferCallback? + ) { + val chipState: ChipStateSender? = ChipStateSender.getSenderStateFromId(displayState) + val stateName = chipState?.name ?: "Invalid" + logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName) + + if (chipState == null) { + logger.logStateChangeError(displayState) + return + } + uiEventLogger.logSenderStateChange(chipState) + + if (chipState == ChipStateSender.FAR_FROM_RECEIVER) { + // Return early if we're not displaying a chip anyway + val currentDisplayedState = displayedState ?: return + + val removalReason = ChipStateSender.FAR_FROM_RECEIVER.name + if ( + currentDisplayedState.transferStatus == TransferStatus.IN_PROGRESS || + currentDisplayedState.transferStatus == TransferStatus.SUCCEEDED + ) { + // Don't remove the chip if we're in progress or succeeded, since the user should + // still be able to see the status of the transfer. + logger.logRemovalBypass( + removalReason, + bypassReason = "transferStatus=${currentDisplayedState.transferStatus.name}" + ) + return + } + + displayedState = null + chipbarCoordinator.removeView(removalReason) + } else { + displayedState = chipState + chipbarCoordinator.displayView( + createChipbarInfo( + chipState, + routeInfo, + undoCallback, + context, + logger, + ) + ) + } + } + + /** + * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display. + */ + private fun createChipbarInfo( + chipStateSender: ChipStateSender, + routeInfo: MediaRoute2Info, + undoCallback: IUndoMediaTransferCallback?, + context: Context, + logger: MediaTttLogger, + ): ChipbarInfo { + val packageName = routeInfo.clientPackageName + val otherDeviceName = routeInfo.name.toString() + + return ChipbarInfo( + // Display the app's icon as the start icon + startIcon = MediaTttUtils.getIconFromPackageName(context, packageName, logger), + text = chipStateSender.getChipTextString(context, otherDeviceName), + endItem = + when (chipStateSender.endItem) { + null -> null + is SenderEndItem.Loading -> ChipbarEndItem.Loading + is SenderEndItem.Error -> ChipbarEndItem.Error + is SenderEndItem.UndoButton -> { + if (undoCallback != null) { + getUndoButton( + undoCallback, + chipStateSender.endItem.uiEventOnClick, + chipStateSender.endItem.newState, + routeInfo, + ) + } else { + null + } + } + }, + vibrationEffect = chipStateSender.transferStatus.vibrationEffect, + ) + } + + /** + * Returns an undo button for the chip. + * + * When the button is clicked: [undoCallback] will be triggered, [uiEvent] will be logged, and + * this coordinator will transition to [newState]. + */ + private fun getUndoButton( + undoCallback: IUndoMediaTransferCallback, + uiEvent: UiEventLogger.UiEventEnum, + @StatusBarManager.MediaTransferSenderState newState: Int, + routeInfo: MediaRoute2Info, + ): ChipbarEndItem.Button { + val onClickListener = + View.OnClickListener { + uiEventLogger.logUndoClicked(uiEvent) + undoCallback.onUndoTriggered() + + // The external service should eventually send us a new TransferTriggered state, but + // but that may take too long to go through the binder and the user may be confused + // as to why the UI hasn't changed yet. So, we immediately change the UI here. + updateMediaTapToTransferSenderDisplay( + newState, + routeInfo, + // Since we're force-updating the UI, we don't have any [undoCallback] from the + // external service (and TransferTriggered states don't have undo callbacks + // anyway). + undoCallback = null, + ) + } + + return ChipbarEndItem.Button( + Text.Resource(R.string.media_transfer_undo), + onClickListener, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt index f15720df5245..b96380976dec 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt @@ -16,16 +16,36 @@ package com.android.systemui.media.taptotransfer.sender -/** Represents the different possible transfer states that we could be in. */ -enum class TransferStatus { +import android.os.VibrationEffect + +/** + * Represents the different possible transfer states that we could be in and the vibration effects + * that come with updating transfer states. + * + * @property vibrationEffect an optional vibration effect when the transfer status is changed. + */ +enum class TransferStatus( + val vibrationEffect: VibrationEffect? = null, +) { /** The transfer hasn't started yet. */ - NOT_STARTED, + NOT_STARTED( + vibrationEffect = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1.0f, 0) + .compose() + ), /** The transfer is currently ongoing but hasn't completed yet. */ - IN_PROGRESS, + IN_PROGRESS( + vibrationEffect = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 0) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.7f, 70) + .compose(), + ), /** The transfer has completed successfully. */ SUCCEEDED, /** The transfer has completed with a failure. */ - FAILED, + FAILED(vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)), /** The device is too far away to do a transfer. */ TOO_FAR, } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt new file mode 100644 index 000000000000..7fd100fd1398 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.mediaprojection.appselector + +import android.app.Activity +import android.content.ComponentName +import android.content.Context +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.media.MediaProjectionAppSelectorActivity +import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader +import com.android.systemui.mediaprojection.appselector.data.AppIconLoader +import com.android.systemui.mediaprojection.appselector.data.IconLoaderLibAppIconLoader +import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider +import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader +import com.android.systemui.mediaprojection.appselector.data.ShellRecentTaskListProvider +import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController +import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider +import com.android.systemui.statusbar.phone.ConfigurationControllerImpl +import com.android.systemui.statusbar.policy.ConfigurationController +import dagger.Binds +import dagger.BindsInstance +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Qualifier +import javax.inject.Scope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob + +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class MediaProjectionAppSelector + +@Retention(AnnotationRetention.RUNTIME) @Scope annotation class MediaProjectionAppSelectorScope + +@Module(subcomponents = [MediaProjectionAppSelectorComponent::class]) +interface MediaProjectionModule { + @Binds + @IntoMap + @ClassKey(MediaProjectionAppSelectorActivity::class) + fun provideMediaProjectionAppSelectorActivity( + activity: MediaProjectionAppSelectorActivity + ): Activity +} + +/** Scoped values for [MediaProjectionAppSelectorComponent]. + * We create a scope for the activity so certain dependencies like [TaskPreviewSizeProvider] + * could be reused. */ +@Module +interface MediaProjectionAppSelectorModule { + + @Binds + @MediaProjectionAppSelectorScope + fun bindRecentTaskThumbnailLoader( + impl: ActivityTaskManagerThumbnailLoader + ): RecentTaskThumbnailLoader + + @Binds + @MediaProjectionAppSelectorScope + fun bindRecentTaskListProvider(impl: ShellRecentTaskListProvider): RecentTaskListProvider + + @Binds + @MediaProjectionAppSelectorScope + fun bindAppIconLoader(impl: IconLoaderLibAppIconLoader): AppIconLoader + + companion object { + @Provides + @MediaProjectionAppSelector + @MediaProjectionAppSelectorScope + fun provideAppSelectorComponentName(context: Context): ComponentName = + ComponentName(context, MediaProjectionAppSelectorActivity::class.java) + + @Provides + @MediaProjectionAppSelector + @MediaProjectionAppSelectorScope + fun bindConfigurationController( + activity: MediaProjectionAppSelectorActivity + ): ConfigurationController = ConfigurationControllerImpl(activity) + + @Provides + @MediaProjectionAppSelector + @MediaProjectionAppSelectorScope + fun provideCoroutineScope(@Application applicationScope: CoroutineScope): CoroutineScope = + CoroutineScope(applicationScope.coroutineContext + SupervisorJob()) + } +} + +@Subcomponent(modules = [MediaProjectionAppSelectorModule::class]) +@MediaProjectionAppSelectorScope +interface MediaProjectionAppSelectorComponent { + + /** Generates [MediaProjectionAppSelectorComponent]. */ + @Subcomponent.Factory + interface Factory { + /** + * Create a factory to inject the activity into the graph + */ + fun create( + @BindsInstance activity: MediaProjectionAppSelectorActivity, + @BindsInstance view: MediaProjectionAppSelectorView, + @BindsInstance resultHandler: MediaProjectionAppSelectorResultHandler, + ): MediaProjectionAppSelectorComponent + } + + val controller: MediaProjectionAppSelectorController + val recentsViewController: MediaProjectionRecentsViewController + + @MediaProjectionAppSelector val configurationController: ConfigurationController +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt index 2b381a954e27..d744a40b60d8 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt @@ -17,20 +17,22 @@ package com.android.systemui.mediaprojection.appselector import android.content.ComponentName -import com.android.systemui.media.dagger.MediaProjectionAppSelector import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import javax.inject.Inject -class MediaProjectionAppSelectorController( +@MediaProjectionAppSelectorScope +class MediaProjectionAppSelectorController @Inject constructor( private val recentTaskListProvider: RecentTaskListProvider, + private val view: MediaProjectionAppSelectorView, @MediaProjectionAppSelector private val scope: CoroutineScope, - private val appSelectorComponentName: ComponentName + @MediaProjectionAppSelector private val appSelectorComponentName: ComponentName ) { - fun init(view: MediaProjectionAppSelectorView) { + fun init() { scope.launch { val tasks = recentTaskListProvider.loadRecentTasks().sortTasks() view.bind(tasks) diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt new file mode 100644 index 000000000000..93c3bce87ad3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt @@ -0,0 +1,15 @@ +package com.android.systemui.mediaprojection.appselector + +import android.os.IBinder + +/** + * Interface that allows to continue the media projection flow and return the selected app + * result to the original caller. + */ +interface MediaProjectionAppSelectorResultHandler { + /** + * Return selected app to the original caller of the media projection app picker. + * @param launchCookie launch cookie of the launched activity of the target app + */ + fun returnSelectedApp(launchCookie: IBinder) +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt new file mode 100644 index 000000000000..c816446d5c25 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.mediaprojection.appselector.view + +import android.app.ActivityOptions +import android.app.IActivityTaskManager +import android.graphics.Rect +import android.os.Binder +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.android.systemui.R +import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler +import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorScope +import com.android.systemui.mediaprojection.appselector.data.RecentTask +import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter.RecentTaskClickListener +import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener +import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration +import javax.inject.Inject + +/** + * Controller that handles view of the recent apps selector in the media projection activity. + * It is responsible for creating and updating recent apps view. + */ +@MediaProjectionAppSelectorScope +class MediaProjectionRecentsViewController +@Inject +constructor( + private val recentTasksAdapterFactory: RecentTasksAdapter.Factory, + private val taskViewSizeProvider: TaskPreviewSizeProvider, + private val activityTaskManager: IActivityTaskManager, + private val resultHandler: MediaProjectionAppSelectorResultHandler, +) : RecentTaskClickListener, TaskPreviewSizeListener { + + private var views: Views? = null + private var lastBoundData: List<RecentTask>? = null + + init { + taskViewSizeProvider.addCallback(this) + } + + fun createView(parent: ViewGroup): ViewGroup = + views?.root ?: createRecentViews(parent).also { + views = it + lastBoundData?.let { recents -> bind(recents) } + }.root + + fun bind(recentTasks: List<RecentTask>) { + views?.apply { + if (recentTasks.isEmpty()) { + root.visibility = View.GONE + return + } + + progress.visibility = View.GONE + recycler.visibility = View.VISIBLE + root.visibility = View.VISIBLE + + recycler.adapter = + recentTasksAdapterFactory.create( + recentTasks, + this@MediaProjectionRecentsViewController + ) + } + + lastBoundData = recentTasks + } + + private fun createRecentViews(parent: ViewGroup): Views { + val recentsRoot = + LayoutInflater.from(parent.context) + .inflate(R.layout.media_projection_recent_tasks, parent, /* attachToRoot= */ false) + as ViewGroup + + val container = recentsRoot.findViewById<View>(R.id.media_projection_recent_tasks_container) + container.setTaskHeightSize() + + val progress = recentsRoot.requireViewById<View>(R.id.media_projection_recent_tasks_loader) + val recycler = + recentsRoot.requireViewById<RecyclerView>(R.id.media_projection_recent_tasks_recycler) + recycler.layoutManager = + LinearLayoutManager( + parent.context, + LinearLayoutManager.HORIZONTAL, + /* reverseLayout= */ false + ) + + val itemDecoration = + HorizontalSpacerItemDecoration( + parent.resources.getDimensionPixelOffset( + R.dimen.media_projection_app_selector_recents_padding + ) + ) + recycler.addItemDecoration(itemDecoration) + + return Views(recentsRoot, container, progress, recycler) + } + + override fun onRecentAppClicked(task: RecentTask, view: View) { + val launchCookie = Binder() + val activityOptions = + ActivityOptions.makeScaleUpAnimation( + view, + /* startX= */ 0, + /* startY= */ 0, + view.width, + view.height + ) + activityOptions.launchCookie = launchCookie + + activityTaskManager.startActivityFromRecents(task.taskId, activityOptions.toBundle()) + resultHandler.returnSelectedApp(launchCookie) + } + + override fun onTaskSizeChanged(size: Rect) { + views?.recentsContainer?.setTaskHeightSize() + } + + private fun View.setTaskHeightSize() { + val thumbnailHeight = taskViewSizeProvider.size.height() + val itemHeight = + thumbnailHeight + + context.resources.getDimensionPixelSize( + R.dimen.media_projection_app_selector_task_icon_size + ) + + context.resources.getDimensionPixelSize( + R.dimen.media_projection_app_selector_task_icon_margin + ) * 2 + + layoutParams = layoutParams.apply { height = itemHeight } + } + + private class Views( + val root: ViewGroup, + val recentsContainer: View, + val progress: View, + val recycler: RecyclerView + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt new file mode 100644 index 000000000000..b682bd172837 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.mediaprojection.appselector.view + +import android.content.Context +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.Shader +import android.util.AttributeSet +import android.view.View +import android.view.WindowManager +import androidx.core.content.getSystemService +import androidx.core.content.res.use +import com.android.internal.R as AndroidR +import com.android.systemui.R +import com.android.systemui.mediaprojection.appselector.data.RecentTask +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.systemui.shared.recents.utilities.PreviewPositionHelper +import com.android.systemui.shared.recents.utilities.Utilities.isTablet + +/** + * Custom view that shows a thumbnail preview of one recent task based on [ThumbnailData]. + * It handles proper cropping and positioning of the thumbnail using [PreviewPositionHelper]. + */ +class MediaProjectionTaskView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + View(context, attrs, defStyleAttr) { + + private val defaultBackgroundColor: Int + + init { + val backgroundColorAttribute = intArrayOf(android.R.attr.colorBackgroundFloating) + defaultBackgroundColor = + context.obtainStyledAttributes(backgroundColorAttribute).use { + it.getColor(/* index= */ 0, /* defValue= */ Color.BLACK) + } + } + + private val windowManager: WindowManager = context.getSystemService()!! + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val backgroundPaint = + Paint(Paint.ANTI_ALIAS_FLAG).apply { color = defaultBackgroundColor } + private val cornerRadius = + context.resources.getDimensionPixelSize( + R.dimen.media_projection_app_selector_task_rounded_corners + ) + private val previewPositionHelper = PreviewPositionHelper() + private val previewRect = Rect() + + private var task: RecentTask? = null + private var thumbnailData: ThumbnailData? = null + + private var bitmapShader: BitmapShader? = null + + fun bindTask(task: RecentTask?, thumbnailData: ThumbnailData?) { + this.task = task + this.thumbnailData = thumbnailData + + // Strip alpha channel to make sure that the color is not semi-transparent + val color = (task?.colorBackground ?: Color.BLACK) or 0xFF000000.toInt() + + paint.color = color + backgroundPaint.color = color + + refresh() + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + updateThumbnailMatrix() + invalidate() + } + + override fun onDraw(canvas: Canvas) { + // Always draw the background since the snapshots might be translucent or partially empty + // (For example, tasks been reparented out of dismissing split root when drag-to-dismiss + // split screen). + canvas.drawRoundRect( + 0f, + 1f, + width.toFloat(), + (height - 1).toFloat(), + cornerRadius.toFloat(), + cornerRadius.toFloat(), + backgroundPaint + ) + + val drawBackgroundOnly = task == null || bitmapShader == null || thumbnailData == null + if (drawBackgroundOnly) { + return + } + + // Draw the task thumbnail using bitmap shader in the paint + canvas.drawRoundRect( + 0f, + 0f, + width.toFloat(), + height.toFloat(), + cornerRadius.toFloat(), + cornerRadius.toFloat(), + paint + ) + } + + private fun refresh() { + val thumbnailBitmap = thumbnailData?.thumbnail + + if (thumbnailBitmap != null) { + thumbnailBitmap.prepareToDraw() + bitmapShader = + BitmapShader(thumbnailBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + paint.shader = bitmapShader + updateThumbnailMatrix() + } else { + bitmapShader = null + paint.shader = null + } + + invalidate() + } + + private fun updateThumbnailMatrix() { + previewPositionHelper.isOrientationChanged = false + + val bitmapShader = bitmapShader ?: return + val thumbnailData = thumbnailData ?: return + val display = context.display ?: return + val windowMetrics = windowManager.maximumWindowMetrics + + previewRect.set(0, 0, thumbnailData.thumbnail.width, thumbnailData.thumbnail.height) + + val currentRotation: Int = display.rotation + val displayWidthPx = windowMetrics.bounds.width() + val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL + val isTablet = isTablet(context) + val taskbarSize = + if (isTablet) { + resources.getDimensionPixelSize(AndroidR.dimen.taskbar_frame_height) + } else { + 0 + } + + previewPositionHelper.updateThumbnailMatrix( + previewRect, + thumbnailData, + measuredWidth, + measuredHeight, + displayWidthPx, + taskbarSize, + isTablet, + currentRotation, + isRtl + ) + + bitmapShader.setLocalMatrix(previewPositionHelper.matrix) + paint.shader = bitmapShader + } +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt index ec5abc7a12f4..15cfeee5174e 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt @@ -16,15 +16,17 @@ package com.android.systemui.mediaprojection.appselector.view +import android.graphics.Rect import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView import com.android.systemui.R -import com.android.systemui.media.dagger.MediaProjectionAppSelector +import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelector import com.android.systemui.mediaprojection.appselector.data.AppIconLoader import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader +import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -32,19 +34,27 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -class RecentTaskViewHolder @AssistedInject constructor( - @Assisted root: ViewGroup, +class RecentTaskViewHolder +@AssistedInject +constructor( + @Assisted private val root: ViewGroup, private val iconLoader: AppIconLoader, private val thumbnailLoader: RecentTaskThumbnailLoader, + private val taskViewSizeProvider: TaskPreviewSizeProvider, @MediaProjectionAppSelector private val scope: CoroutineScope -) : RecyclerView.ViewHolder(root) { +) : RecyclerView.ViewHolder(root), ConfigurationListener, TaskPreviewSizeProvider.TaskPreviewSizeListener { + val thumbnailView: MediaProjectionTaskView = root.requireViewById(R.id.task_thumbnail) private val iconView: ImageView = root.requireViewById(R.id.task_icon) - private val thumbnailView: ImageView = root.requireViewById(R.id.task_thumbnail) private var job: Job? = null + init { + updateThumbnailSize() + } + fun bind(task: RecentTask, onClick: (View) -> Unit) { + taskViewSizeProvider.addCallback(this) job?.cancel() job = @@ -57,20 +67,33 @@ class RecentTaskViewHolder @AssistedInject constructor( } launch { val thumbnail = thumbnailLoader.loadThumbnail(task.taskId) - thumbnailView.setImageBitmap(thumbnail?.thumbnail) + thumbnailView.bindTask(task, thumbnail) } } - thumbnailView.setOnClickListener(onClick) + root.setOnClickListener(onClick) } fun onRecycled() { + taskViewSizeProvider.removeCallback(this) iconView.setImageDrawable(null) - thumbnailView.setImageBitmap(null) + thumbnailView.bindTask(null, null) job?.cancel() job = null } + override fun onTaskSizeChanged(size: Rect) { + updateThumbnailSize() + } + + private fun updateThumbnailSize() { + thumbnailView.layoutParams = + thumbnailView.layoutParams.apply { + width = taskViewSizeProvider.size.width() + height = taskViewSizeProvider.size.height() + } + } + @AssistedFactory fun interface Factory { fun create(root: ViewGroup): RecentTaskViewHolder diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt index ec9cfa88f34d..6af50a0eb699 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt @@ -26,7 +26,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -class RecentTasksAdapter @AssistedInject constructor( +class RecentTasksAdapter +@AssistedInject +constructor( @Assisted private val items: List<RecentTask>, @Assisted private val listener: RecentTaskClickListener, private val viewHolderFactory: RecentTaskViewHolder.Factory @@ -34,8 +36,8 @@ class RecentTasksAdapter @AssistedInject constructor( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentTaskViewHolder { val taskItem = - LayoutInflater.from(parent.context) - .inflate(R.layout.media_projection_task_item, null) as ViewGroup + LayoutInflater.from(parent.context) + .inflate(R.layout.media_projection_task_item, parent, false) as ViewGroup return viewHolderFactory.create(taskItem) } @@ -43,7 +45,7 @@ class RecentTasksAdapter @AssistedInject constructor( override fun onBindViewHolder(holder: RecentTaskViewHolder, position: Int) { val task = items[position] holder.bind(task, onClick = { - listener.onRecentClicked(task, holder.itemView) + listener.onRecentAppClicked(task, holder.itemView) }) } @@ -54,7 +56,7 @@ class RecentTasksAdapter @AssistedInject constructor( } interface RecentTaskClickListener { - fun onRecentClicked(task: RecentTask, view: View) + fun onRecentAppClicked(task: RecentTask, view: View) } @AssistedFactory diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt new file mode 100644 index 000000000000..88d5eaaff216 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.mediaprojection.appselector.view + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Rect +import android.view.WindowManager +import com.android.internal.R as AndroidR +import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorScope +import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener +import com.android.systemui.shared.recents.utilities.Utilities.isTablet +import com.android.systemui.statusbar.policy.CallbackController +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener +import javax.inject.Inject + +@MediaProjectionAppSelectorScope +class TaskPreviewSizeProvider +@Inject +constructor( + private val context: Context, + private val windowManager: WindowManager, + configurationController: ConfigurationController +) : CallbackController<TaskPreviewSizeListener>, ConfigurationListener { + + /** Returns the size of the task preview on the screen in pixels */ + val size: Rect = calculateSize() + + private val listeners = arrayListOf<TaskPreviewSizeListener>() + + init { + configurationController.addCallback(this) + } + + override fun onConfigChanged(newConfig: Configuration) { + val newSize = calculateSize() + if (newSize != size) { + size.set(newSize) + listeners.forEach { it.onTaskSizeChanged(size) } + } + } + + private fun calculateSize(): Rect { + val windowMetrics = windowManager.maximumWindowMetrics + val maximumWindowHeight = windowMetrics.bounds.height() + val width = windowMetrics.bounds.width() + var height = maximumWindowHeight + + val isTablet = isTablet(context) + if (isTablet) { + val taskbarSize = + context.resources.getDimensionPixelSize(AndroidR.dimen.taskbar_frame_height) + height -= taskbarSize + } + + val previewSize = Rect(0, 0, width, height) + val scale = (height / maximumWindowHeight.toFloat()) / SCREEN_HEIGHT_TO_TASK_HEIGHT_RATIO + previewSize.scale(scale) + + return previewSize + } + + override fun addCallback(listener: TaskPreviewSizeListener) { + listeners += listener + } + + override fun removeCallback(listener: TaskPreviewSizeListener) { + listeners -= listener + } + + interface TaskPreviewSizeListener { + fun onTaskSizeChanged(size: Rect) + } +} + +/** + * How many times smaller the task preview should be on the screen comparing to the height of the + * screen + */ +private const val SCREEN_HEIGHT_TO_TASK_HEIGHT_RATIO = 4f diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java index da9fefab0b66..33021e3cde47 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java @@ -48,7 +48,6 @@ import android.view.accessibility.AccessibilityManager; import androidx.annotation.NonNull; -import com.android.keyguard.KeyguardViewController; import com.android.systemui.Dumpable; import com.android.systemui.accessibility.AccessibilityButtonModeObserver; import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver; @@ -61,6 +60,7 @@ import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode; import com.android.systemui.statusbar.phone.CentralSurfaces; +import com.android.systemui.statusbar.policy.KeyguardStateController; import java.io.PrintWriter; import java.util.ArrayList; @@ -90,7 +90,7 @@ public final class NavBarHelper implements private final AccessibilityManager mAccessibilityManager; private final Lazy<AssistManager> mAssistManagerLazy; private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; - private final KeyguardViewController mKeyguardViewController; + private final KeyguardStateController mKeyguardStateController; private final UserTracker mUserTracker; private final SystemActions mSystemActions; private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver; @@ -125,7 +125,7 @@ public final class NavBarHelper implements OverviewProxyService overviewProxyService, Lazy<AssistManager> assistManagerLazy, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, - KeyguardViewController keyguardViewController, + KeyguardStateController keyguardStateController, NavigationModeController navigationModeController, UserTracker userTracker, DumpManager dumpManager) { @@ -134,7 +134,7 @@ public final class NavBarHelper implements mAccessibilityManager = accessibilityManager; mAssistManagerLazy = assistManagerLazy; mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy; - mKeyguardViewController = keyguardViewController; + mKeyguardStateController = keyguardStateController; mUserTracker = userTracker; mSystemActions = systemActions; accessibilityManager.addAccessibilityServicesStateChangeListener(this); @@ -326,7 +326,7 @@ public final class NavBarHelper implements shadeWindowView = mCentralSurfacesOptionalLazy.get().get().getNotificationShadeWindowView(); } - boolean isKeyguardShowing = mKeyguardViewController.isShowing(); + boolean isKeyguardShowing = mKeyguardStateController.isShowing(); boolean imeVisibleOnShade = shadeWindowView != null && shadeWindowView.isAttachedToWindow() && shadeWindowView.getRootWindowInsets().isVisible(WindowInsets.Type.ime()); return imeVisibleOnShade diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 50a10bc0b15a..c089511a7ce9 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -114,6 +114,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope; import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener; @@ -211,6 +212,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private final NotificationShadeDepthController mNotificationShadeDepthController; private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener; private final UserContextProvider mUserContextProvider; + private final WakefulnessLifecycle mWakefulnessLifecycle; private final RegionSamplingHelper mRegionSamplingHelper; private final int mNavColorSampleMargin; private NavigationBarFrame mFrame; @@ -451,6 +453,28 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } }; + private final WakefulnessLifecycle.Observer mWakefulnessObserver = + new WakefulnessLifecycle.Observer() { + private void notifyScreenStateChanged(boolean isScreenOn) { + notifyNavigationBarScreenOn(); + mView.onScreenStateChanged(isScreenOn); + } + + @Override + public void onStartedWakingUp() { + notifyScreenStateChanged(true); + if (isGesturalModeOnDefaultDisplay(getContext(), mNavBarMode)) { + mRegionSamplingHelper.start(mSamplingBounds); + } + } + + @Override + public void onFinishedGoingToSleep() { + notifyScreenStateChanged(false); + mRegionSamplingHelper.stop(); + } + }; + @Inject NavigationBar( NavigationBarView navigationBarView, @@ -491,7 +515,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements NavigationBarTransitions navigationBarTransitions, EdgeBackGestureHandler edgeBackGestureHandler, Optional<BackAnimation> backAnimation, - UserContextProvider userContextProvider) { + UserContextProvider userContextProvider, + WakefulnessLifecycle wakefulnessLifecycle) { super(navigationBarView); mFrame = navigationBarFrame; mContext = context; @@ -529,6 +554,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mTelecomManagerOptional = telecomManagerOptional; mInputMethodManager = inputMethodManager; mUserContextProvider = userContextProvider; + mWakefulnessLifecycle = wakefulnessLifecycle; mNavColorSampleMargin = getResources() .getDimensionPixelSize(R.dimen.navigation_handle_sample_horizontal_margin); @@ -653,7 +679,9 @@ public class NavigationBar extends ViewController<NavigationBarView> implements public void onViewAttached() { final Display display = mView.getDisplay(); mView.setComponents(mRecentsOptional); - mView.setComponents(mCentralSurfacesOptionalLazy.get().get().getPanelController()); + if (mCentralSurfacesOptionalLazy.get().isPresent()) { + mView.setComponents(mCentralSurfacesOptionalLazy.get().get().getPanelController()); + } mView.setDisabledFlags(mDisabledFlags1, mSysUiFlagsContainer); mView.setOnVerticalChangedListener(this::onVerticalChanged); mView.setOnTouchListener(this::onNavigationTouch); @@ -682,11 +710,10 @@ public class NavigationBar extends ViewController<NavigationBarView> implements prepareNavigationBarView(); checkNavBarModes(); - IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF); - filter.addAction(Intent.ACTION_SCREEN_ON); - filter.addAction(Intent.ACTION_USER_SWITCHED); + IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED); mBroadcastDispatcher.registerReceiverWithHandler(mBroadcastReceiver, filter, Handler.getMain(), UserHandle.ALL); + mWakefulnessLifecycle.addObserver(mWakefulnessObserver); notifyNavigationBarScreenOn(); mOverviewProxyService.addCallback(mOverviewProxyListener); @@ -737,6 +764,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements getBarTransitions().destroy(); mOverviewProxyService.removeCallback(mOverviewProxyListener); mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver); + mWakefulnessLifecycle.removeObserver(mWakefulnessObserver); if (mOrientationHandle != null) { resetSecondaryHandle(); getBarTransitions().removeDarkIntensityListener(mOrientationHandleIntensityListener); @@ -1619,19 +1647,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements return; } String action = intent.getAction(); - if (Intent.ACTION_SCREEN_OFF.equals(action) - || Intent.ACTION_SCREEN_ON.equals(action)) { - notifyNavigationBarScreenOn(); - boolean isScreenOn = Intent.ACTION_SCREEN_ON.equals(action); - mView.onScreenStateChanged(isScreenOn); - if (isScreenOn) { - if (isGesturalModeOnDefaultDisplay(getContext(), mNavBarMode)) { - mRegionSamplingHelper.start(mSamplingBounds); - } - } else { - mRegionSamplingHelper.stop(); - } - } if (Intent.ACTION_USER_SWITCHED.equals(action)) { // The accessibility settings may be different for the new user updateAccessibilityStateFlags(); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java index 3789cbb1fb65..3fd1aa73c033 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java @@ -21,6 +21,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG; import static com.android.systemui.shared.recents.utilities.Utilities.isTablet; import android.content.ContentResolver; @@ -141,13 +142,22 @@ public class NavigationBarController implements public void onConfigChanged(Configuration newConfig) { boolean isOldConfigTablet = mIsTablet; mIsTablet = isTablet(mContext); + boolean willApplyConfig = mConfigChanges.applyNewConfig(mContext.getResources()); boolean largeScreenChanged = mIsTablet != isOldConfigTablet; + // TODO(b/243765256): Disable this logging once b/243765256 is fixed. + Log.d(DEBUG_MISSING_GESTURE_TAG, "NavbarController: newConfig=" + newConfig + + " mTaskbarDelegate initialized=" + mTaskbarDelegate.isInitialized() + + " willApplyConfigToNavbars=" + willApplyConfig + + " navBarCount=" + mNavigationBars.size()); + if (mTaskbarDelegate.isInitialized()) { + mTaskbarDelegate.onConfigurationChanged(newConfig); + } // If we folded/unfolded while in 3 button, show navbar in folded state, hide in unfolded if (largeScreenChanged && updateNavbarForTaskbar()) { return; } - if (mConfigChanges.applyNewConfig(mContext.getResources())) { + if (willApplyConfig) { for (int i = 0; i < mNavigationBars.size(); i++) { recreateNavigationBar(mNavigationBars.keyAt(i)); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java index 97024881ca62..403d276f8cbc 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java @@ -148,6 +148,7 @@ public class NavigationBarView extends FrameLayout { private NavigationBarInflaterView mNavigationInflaterView; private Optional<Recents> mRecentsOptional = Optional.empty(); + @Nullable private NotificationPanelViewController mPanelView; private RotationContextButton mRotationContextButton; private FloatingRotationButton mFloatingRotationButton; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index 9e0c49641e72..73fc21ef928c 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java @@ -40,7 +40,6 @@ import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode import android.app.StatusBarManager; import android.app.StatusBarManager.WindowVisibleState; -import android.content.ComponentCallbacks; import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; @@ -87,7 +86,7 @@ import javax.inject.Inject; @SysUISingleton public class TaskbarDelegate implements CommandQueue.Callbacks, OverviewProxyService.OverviewProxyListener, NavigationModeController.ModeChangedListener, - ComponentCallbacks, Dumpable { + Dumpable { private static final String TAG = TaskbarDelegate.class.getSimpleName(); private final EdgeBackGestureHandler mEdgeBackGestureHandler; @@ -225,7 +224,6 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, // Initialize component callback Display display = mDisplayManager.getDisplay(displayId); mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null); - mWindowContext.registerComponentCallbacks(this); mScreenPinningNotify = new ScreenPinningNotify(mWindowContext); // Set initial state for any listeners updateSysuiFlags(); @@ -233,6 +231,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, mLightBarController.setNavigationBar(mLightBarTransitionsController); mPipOptional.ifPresent(this::addPipExclusionBoundsChangeListener); mEdgeBackGestureHandler.setBackAnimation(mBackAnimation); + mEdgeBackGestureHandler.onConfigurationChanged(mContext.getResources().getConfiguration()); mInitialized = true; } @@ -247,10 +246,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, mNavBarHelper.destroy(); mEdgeBackGestureHandler.onNavBarDetached(); mScreenPinningNotify = null; - if (mWindowContext != null) { - mWindowContext.unregisterComponentCallbacks(this); - mWindowContext = null; - } + mWindowContext = null; mAutoHideController.setNavigationBar(null); mLightBarTransitionsController.destroy(); mLightBarController.setNavigationBar(null); @@ -267,8 +263,9 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, } /** - * Returns {@code true} if this taskBar is {@link #init(int)}. Returns {@code false} if this - * taskbar has not yet been {@link #init(int)} or has been {@link #destroy()}. + * Returns {@code true} if this taskBar is {@link #init(int)}. + * Returns {@code false} if this taskbar has not yet been {@link #init(int)} + * or has been {@link #destroy()}. */ public boolean isInitialized() { return mInitialized; @@ -460,15 +457,11 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, return mBehavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; } - @Override public void onConfigurationChanged(Configuration configuration) { mEdgeBackGestureHandler.onConfigurationChanged(configuration); } @Override - public void onLowMemory() {} - - @Override public void showPinningEnterExitToast(boolean entering) { updateSysuiFlags(); if (mScreenPinningNotify == null) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 0f1338e4e872..709467ffd3b5 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -19,6 +19,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_ import static com.android.systemui.classifier.Classifier.BACK_GESTURE; +import android.annotation.NonNull; import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; @@ -57,7 +58,6 @@ import android.window.BackEvent; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.policy.GestureNavigationSettingsObserver; -import com.android.internal.util.LatencyTracker; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; @@ -113,7 +113,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private static final int MAX_NUM_LOGGED_GESTURES = 10; static final boolean DEBUG_MISSING_GESTURE = false; - static final String DEBUG_MISSING_GESTURE_TAG = "NoBackGesture"; + public static final String DEBUG_MISSING_GESTURE_TAG = "NoBackGesture"; private ISystemGestureExclusionListener mGestureExclusionListener = new ISystemGestureExclusionListener.Stub() { @@ -199,7 +199,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private final Rect mNavBarOverlayExcludedBounds = new Rect(); private final Region mExcludeRegion = new Region(); private final Region mUnrestrictedExcludeRegion = new Region(); - private final LatencyTracker mLatencyTracker; + private final Provider<NavigationBarEdgePanel> mNavBarEdgePanelProvider; private final Provider<BackGestureTfClassifierProvider> mBackGestureTfClassifierProviderProvider; private final FeatureFlags mFeatureFlags; @@ -339,7 +339,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker IWindowManager windowManagerService, Optional<Pip> pipOptional, FalsingManager falsingManager, - LatencyTracker latencyTracker, + Provider<NavigationBarEdgePanel> navigationBarEdgePanelProvider, Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider, FeatureFlags featureFlags) { super(broadcastDispatcher); @@ -358,7 +358,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mWindowManagerService = windowManagerService; mPipOptional = pipOptional; mFalsingManager = falsingManager; - mLatencyTracker = latencyTracker; + mNavBarEdgePanelProvider = navigationBarEdgePanelProvider; mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider; mFeatureFlags = featureFlags; mLastReportedConfig.setTo(mContext.getResources().getConfiguration()); @@ -583,8 +583,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker setEdgeBackPlugin( mBackPanelControllerFactory.create(mContext)); } else { - setEdgeBackPlugin( - new NavigationBarEdgePanel(mContext, mLatencyTracker)); + setEdgeBackPlugin(mNavBarEdgePanelProvider.get()); } } @@ -957,7 +956,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mStartingQuickstepRotation != rotation; } - public void onConfigurationChanged(Configuration newConfig) { + public void onConfigurationChanged(@NonNull Configuration newConfig) { if (mStartingQuickstepRotation > -1) { updateDisabledForQuickstep(newConfig); } @@ -1091,7 +1090,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private final IWindowManager mWindowManagerService; private final Optional<Pip> mPipOptional; private final FalsingManager mFalsingManager; - private final LatencyTracker mLatencyTracker; + private final Provider<NavigationBarEdgePanel> mNavBarEdgePanelProvider; private final Provider<BackGestureTfClassifierProvider> mBackGestureTfClassifierProviderProvider; private final FeatureFlags mFeatureFlags; @@ -1111,7 +1110,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker IWindowManager windowManagerService, Optional<Pip> pipOptional, FalsingManager falsingManager, - LatencyTracker latencyTracker, + Provider<NavigationBarEdgePanel> navBarEdgePanelProvider, Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider, FeatureFlags featureFlags) { @@ -1129,7 +1128,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mWindowManagerService = windowManagerService; mPipOptional = pipOptional; mFalsingManager = falsingManager; - mLatencyTracker = latencyTracker; + mNavBarEdgePanelProvider = navBarEdgePanelProvider; mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider; mFeatureFlags = featureFlags; } @@ -1152,7 +1151,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mWindowManagerService, mPipOptional, mFalsingManager, - mLatencyTracker, + mNavBarEdgePanelProvider, mBackGestureTfClassifierProviderProvider, mFeatureFlags); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java index 24efc762b39b..1230708d780a 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java @@ -52,9 +52,9 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.internal.util.LatencyTracker; import com.android.settingslib.Utils; -import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.plugins.NavigationEdgeBackPlugin; import com.android.systemui.shared.navigationbar.RegionSamplingHelper; import com.android.systemui.statusbar.VibratorHelper; @@ -62,6 +62,8 @@ import com.android.systemui.statusbar.VibratorHelper; import java.io.PrintWriter; import java.util.concurrent.Executor; +import javax.inject.Inject; + public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPlugin { private static final String TAG = "NavigationBarEdgePanel"; @@ -282,11 +284,16 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl }; private BackCallback mBackCallback; - public NavigationBarEdgePanel(Context context, LatencyTracker latencyTracker) { + @Inject + public NavigationBarEdgePanel( + Context context, + LatencyTracker latencyTracker, + VibratorHelper vibratorHelper, + @Background Executor backgroundExecutor) { super(context); mWindowManager = context.getSystemService(WindowManager.class); - mVibratorHelper = Dependency.get(VibratorHelper.class); + mVibratorHelper = vibratorHelper; mDensity = context.getResources().getDisplayMetrics().density; @@ -358,7 +365,6 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl setVisibility(GONE); - Executor backgroundExecutor = Dependency.get(Dependency.BACKGROUND_EXECUTOR); boolean isPrimaryDisplay = mContext.getDisplayId() == DEFAULT_DISPLAY; mRegionSamplingHelper = new RegionSamplingHelper(this, new RegionSamplingHelper.SamplingCallback() { diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index 67dae9e7a0ea..1da866efc08d 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -46,6 +46,7 @@ import com.android.systemui.CoreStartable; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -59,7 +60,7 @@ import javax.inject.Inject; import dagger.Lazy; @SysUISingleton -public class PowerUI extends CoreStartable implements CommandQueue.Callbacks { +public class PowerUI implements CoreStartable, CommandQueue.Callbacks { static final String TAG = "PowerUI"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -78,6 +79,7 @@ public class PowerUI extends CoreStartable implements CommandQueue.Callbacks { private final PowerManager mPowerManager; private final WarningsUI mWarnings; + private final WakefulnessLifecycle mWakefulnessLifecycle; private InattentiveSleepWarningView mOverlayView; private final Configuration mLastConfiguration = new Configuration(); private int mPlugType = 0; @@ -103,22 +105,37 @@ public class PowerUI extends CoreStartable implements CommandQueue.Callbacks { private IThermalEventListener mSkinThermalEventListener; private IThermalEventListener mUsbThermalEventListener; + private final Context mContext; private final BroadcastDispatcher mBroadcastDispatcher; private final CommandQueue mCommandQueue; private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; + private final WakefulnessLifecycle.Observer mWakefulnessObserver = + new WakefulnessLifecycle.Observer() { + @Override + public void onStartedWakingUp() { + mScreenOffTime = -1; + } + + @Override + public void onFinishedGoingToSleep() { + mScreenOffTime = SystemClock.elapsedRealtime(); + } + }; @Inject public PowerUI(Context context, BroadcastDispatcher broadcastDispatcher, CommandQueue commandQueue, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, WarningsUI warningsUI, EnhancedEstimates enhancedEstimates, + WakefulnessLifecycle wakefulnessLifecycle, PowerManager powerManager) { - super(context); + mContext = context; mBroadcastDispatcher = broadcastDispatcher; mCommandQueue = commandQueue; mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy; mWarnings = warningsUI; mEnhancedEstimates = enhancedEstimates; mPowerManager = powerManager; + mWakefulnessLifecycle = wakefulnessLifecycle; } public void start() { @@ -137,6 +154,7 @@ public class PowerUI extends CoreStartable implements CommandQueue.Callbacks { false, obs, UserHandle.USER_ALL); updateBatteryWarningLevels(); mReceiver.init(); + mWakefulnessLifecycle.addObserver(mWakefulnessObserver); // Check to see if we need to let the user know that the phone previously shut down due // to the temperature being too high. @@ -169,7 +187,7 @@ public class PowerUI extends CoreStartable implements CommandQueue.Callbacks { } @Override - protected void onConfigurationChanged(Configuration newConfig) { + public void onConfigurationChanged(Configuration newConfig) { final int mask = ActivityInfo.CONFIG_MCC | ActivityInfo.CONFIG_MNC; // Safe to modify mLastConfiguration here as it's only updated by the main thread (here). @@ -232,8 +250,6 @@ public class PowerUI extends CoreStartable implements CommandQueue.Callbacks { IntentFilter filter = new IntentFilter(); filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); filter.addAction(Intent.ACTION_BATTERY_CHANGED); - filter.addAction(Intent.ACTION_SCREEN_OFF); - filter.addAction(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_USER_SWITCHED); mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mHandler); // Force get initial values. Relying on Sticky behavior until API for getting info. @@ -316,10 +332,6 @@ public class PowerUI extends CoreStartable implements CommandQueue.Callbacks { plugged, bucket); }); - } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { - mScreenOffTime = SystemClock.elapsedRealtime(); - } else if (Intent.ACTION_SCREEN_ON.equals(action)) { - mScreenOffTime = -1; } else if (Intent.ACTION_USER_SWITCHED.equals(action)) { mWarnings.userSwitched(); } else { diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt index 1ea93474f954..03503fd1ff61 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt @@ -17,10 +17,10 @@ package com.android.systemui.privacy.logging import android.permission.PermissionGroupUsage -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.PrivacyLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogMessage import com.android.systemui.privacy.PrivacyDialog import com.android.systemui.privacy.PrivacyItem import java.text.SimpleDateFormat diff --git a/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java b/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java index 5510eb172cd7..cd32a10a432b 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java +++ b/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java @@ -67,7 +67,7 @@ import javax.inject.Inject; * recording audio, accessing the camera or accessing the location. */ @SysUISingleton -public class TvOngoingPrivacyChip extends CoreStartable implements PrivacyItemController.Callback, +public class TvOngoingPrivacyChip implements CoreStartable, PrivacyItemController.Callback, PrivacyChipDrawable.PrivacyChipDrawableListener { private static final String TAG = "TvOngoingPrivacyChip"; private static final boolean DEBUG = false; @@ -134,7 +134,6 @@ public class TvOngoingPrivacyChip extends CoreStartable implements PrivacyItemCo @Inject public TvOngoingPrivacyChip(Context context, PrivacyItemController privacyItemController, IWindowManager iWindowManager) { - super(context); if (DEBUG) Log.d(TAG, "Privacy chip running"); mContext = context; mPrivacyItemController = privacyItemController; diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 482a1397642b..bb2b4419a80a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -52,6 +52,7 @@ import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -98,10 +99,10 @@ interface FgsManagerController { fun init() /** - * Show the foreground services dialog. The dialog will be expanded from [viewLaunchedFrom] if + * Show the foreground services dialog. The dialog will be expanded from [expandable] if * it's not `null`. */ - fun showDialog(viewLaunchedFrom: View?) + fun showDialog(expandable: Expandable?) /** Add a [OnNumberOfPackagesChangedListener]. */ fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) @@ -367,7 +368,7 @@ class FgsManagerControllerImpl @Inject constructor( override fun shouldUpdateFooterVisibility() = dialog == null - override fun showDialog(viewLaunchedFrom: View?) { + override fun showDialog(expandable: Expandable?) { synchronized(lock) { if (dialog == null) { @@ -403,16 +404,18 @@ class FgsManagerControllerImpl @Inject constructor( } mainExecutor.execute { - viewLaunchedFrom - ?.let { - dialogLaunchAnimator.showFromView( - dialog, it, - cuj = DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG - ) + val controller = + expandable?.dialogLaunchController( + DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG, ) - } ?: dialog.show() + ) + if (controller != null) { + dialogLaunchAnimator.show(dialog, controller) + } else { + dialog.show() + } } backgroundExecutor.execute { diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt index 9d64781ef2e9..a9943e886339 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt @@ -32,6 +32,7 @@ import com.android.internal.logging.nano.MetricsProto import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.globalactions.GlobalActionsDialogLite import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -156,7 +157,7 @@ internal class FooterActionsController @Inject constructor( startSettingsActivity() } else if (v === powerMenuLite) { uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS) - globalActionsDialog?.showOrHideDialog(false, true, v) + globalActionsDialog?.showOrHideDialog(false, true, Expandable.fromView(powerMenuLite)) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java index 7511278e0919..b1b9dd721eaf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java @@ -29,6 +29,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.dagger.QSScope; @@ -130,7 +131,7 @@ public class QSFgsManagerFooter implements View.OnClickListener, @Override public void onClick(View view) { - mFgsManagerController.showDialog(mRootView); + mFgsManagerController.showDialog(Expandable.fromView(view)); } public void refreshState() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 7b27cf45979f..20f1a8ed7689 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -49,7 +49,7 @@ import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; -import com.android.systemui.media.MediaHost; +import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.qs.QSContainerController; @@ -60,6 +60,7 @@ import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.BrightnessMirrorController; @@ -82,7 +83,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private static final String EXTRA_VISIBLE = "visible"; private final Rect mQsBounds = new Rect(); - private final StatusBarStateController mStatusBarStateController; + private final SysuiStatusBarStateController mStatusBarStateController; private final FalsingManager mFalsingManager; private final KeyguardBypassController mBypassController; private boolean mQsExpanded; @@ -159,7 +160,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca * Progress of pull down from the center of the lock screen. * @see com.android.systemui.statusbar.LockscreenShadeTransitionController */ - private float mFullShadeProgress; + private float mLockscreenToShadeProgress; private boolean mOverScrolling; @@ -177,7 +178,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca @Inject public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, QSTileHost qsTileHost, - StatusBarStateController statusBarStateController, CommandQueue commandQueue, + SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue, @Named(QS_PANEL) MediaHost qsMediaHost, @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost, KeyguardBypassController keyguardBypassController, @@ -442,20 +443,19 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } private void updateQsState() { - final boolean expanded = mQsExpanded || mInSplitShade; - final boolean expandVisually = expanded || mStackScrollerOverscrolling + final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling || mHeaderAnimating; - mQSPanelController.setExpanded(expanded); + mQSPanelController.setExpanded(mQsExpanded); boolean keyguardShowing = isKeyguardState(); - mHeader.setVisibility((expanded || !keyguardShowing || mHeaderAnimating + mHeader.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating || mShowCollapsedOnKeyguard) ? View.VISIBLE : View.INVISIBLE); mHeader.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) - || (expanded && !mStackScrollerOverscrolling), mQuickQSPanelController); + || (mQsExpanded && !mStackScrollerOverscrolling), mQuickQSPanelController); boolean qsPanelVisible = !mQsDisabled && expandVisually; - boolean footerVisible = qsPanelVisible && (expanded || !keyguardShowing || mHeaderAnimating - || mShowCollapsedOnKeyguard); + boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing + || mHeaderAnimating || mShowCollapsedOnKeyguard); mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); if (mQSFooterActionController != null) { mQSFooterActionController.setVisible(footerVisible); @@ -463,7 +463,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mQSFooterActionsViewModel.onVisibilityChangeRequested(footerVisible); } mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) - || (expanded && !mStackScrollerOverscrolling)); + || (mQsExpanded && !mStackScrollerOverscrolling)); mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE); if (DEBUG) { Log.d(TAG, "Footer: " + footerVisible + ", QS Panel: " + qsPanelVisible); @@ -586,7 +586,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mTransitioningToFullShade = isTransitioningToFullShade; updateShowCollapsedOnKeyguard(); } - mFullShadeProgress = qsTransitionFraction; + mLockscreenToShadeProgress = qsTransitionFraction; setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation, isTransitioningToFullShade ? qsSquishinessFraction : mSquishinessFraction); } @@ -691,6 +691,15 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca if (mQSAnimator != null) { mQSAnimator.setPosition(expansion); } + if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD + || mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) { + // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen + // and media player expect no change by squishiness in lock screen shade + mQsMediaHost.setSquishFraction(1.0F); + } else { + mQsMediaHost.setSquishFraction(mSquishinessFraction); + } + } private void setAlphaAnimationProgress(float progress) { @@ -710,10 +719,13 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } if (mInSplitShade) { // Large screens in landscape. - if (mTransitioningToFullShade || isKeyguardState()) { + // Need to check upcoming state as for unlocked -> AOD transition current state is + // not updated yet, but we're transitioning and UI should already follow KEYGUARD state + if (mTransitioningToFullShade || mStatusBarStateController.getCurrentOrUpcomingState() + == StatusBarState.KEYGUARD) { // Always use "mFullShadeProgress" on keyguard, because // "panelExpansionFractions" is always 1 on keyguard split shade. - return mFullShadeProgress; + return mLockscreenToShadeProgress; } else { return panelExpansionFraction; } @@ -722,7 +734,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca if (mTransitioningToFullShade) { // Only use this value during the standard lock screen shade expansion. During the // "quick" expansion from top, this value is 0. - return mFullShadeProgress; + return mLockscreenToShadeProgress; } else { return panelExpansionFraction; } @@ -930,7 +942,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca indentingPw.println("mLastHeaderTranslation: " + mLastHeaderTranslation); indentingPw.println("mInSplitShade: " + mInSplitShade); indentingPw.println("mTransitioningToFullShade: " + mTransitioningToFullShade); - indentingPw.println("mFullShadeProgress: " + mFullShadeProgress); + indentingPw.println("mLockscreenToShadeProgress: " + mLockscreenToShadeProgress); indentingPw.println("mOverScrolling: " + mOverScrolling); indentingPw.println("isCustomizing: " + mQSCustomizerController.isCustomizing()); View view = getView(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt index 8544f61d7031..c663aa605ca2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.qs -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.QSFragmentDisableLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.DisableFlagsLogger import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 184089f7eef4..6517ff33a49d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -105,6 +105,7 @@ public class QSPanel extends LinearLayout implements Tunable { private final Rect mClippingRect = new Rect(); private ViewGroup mMediaHostView; private boolean mShouldMoveMediaOnExpansion = true; + private boolean mUsingCombinedHeaders = false; public QSPanel(Context context, AttributeSet attrs) { super(context, attrs); @@ -148,6 +149,10 @@ public class QSPanel extends LinearLayout implements Tunable { } } + void setUsingCombinedHeaders(boolean usingCombinedHeaders) { + mUsingCombinedHeaders = usingCombinedHeaders; + } + protected void setHorizontalContentContainerClipping() { mHorizontalContentContainer.setClipChildren(true); mHorizontalContentContainer.setClipToPadding(false); @@ -371,7 +376,9 @@ public class QSPanel extends LinearLayout implements Tunable { protected void updatePadding() { final Resources res = mContext.getResources(); - int paddingTop = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top); + int paddingTop = res.getDimensionPixelSize( + mUsingCombinedHeaders ? R.dimen.qs_panel_padding_top_combined_headers + : R.dimen.qs_panel_padding_top); int paddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom); setPaddingRelative(getPaddingStart(), paddingTop, diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index 18bd6b7b3c32..abc0adecbfeb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -17,6 +17,7 @@ package com.android.systemui.qs; import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE; +import static com.android.systemui.flags.Flags.COMBINED_QS_HEADERS; import static com.android.systemui.media.dagger.MediaModule.QS_PANEL; import static com.android.systemui.qs.QSPanel.QS_SHOW_BRIGHTNESS; import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER; @@ -27,9 +28,10 @@ import android.view.View; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaHierarchyManager; -import com.android.systemui.media.MediaHost; -import com.android.systemui.media.MediaHostState; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; +import com.android.systemui.media.controls.ui.MediaHost; +import com.android.systemui.media.controls.ui.MediaHostState; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSScope; @@ -79,7 +81,8 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { QSLogger qsLogger, BrightnessController.Factory brightnessControllerFactory, BrightnessSliderController.Factory brightnessSliderFactory, FalsingManager falsingManager, - StatusBarKeyguardViewManager statusBarKeyguardViewManager) { + StatusBarKeyguardViewManager statusBarKeyguardViewManager, + FeatureFlags featureFlags) { super(view, qstileHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger, uiEventLogger, qsLogger, dumpManager); mTunerService = tunerService; @@ -93,6 +96,7 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { mBrightnessController = brightnessControllerFactory.create(mBrightnessSliderController); mBrightnessMirrorHandler = new BrightnessMirrorHandler(mBrightnessController); mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; + mView.setUsingCombinedHeaders(featureFlags.isEnabled(COMBINED_QS_HEADERS)); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index ded466a0cb25..2a80de0e24de 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -23,8 +23,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; import android.content.res.Configuration; +import android.content.res.Configuration.Orientation; import android.metrics.LogMaker; -import android.util.Log; import android.view.View; import com.android.internal.annotations.VisibleForTesting; @@ -32,7 +32,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dumpable; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaHost; +import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTileView; import com.android.systemui.qs.customize.QSCustomizerController; @@ -75,6 +75,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr @Nullable private Consumer<Boolean> mMediaVisibilityChangedListener; + @Orientation private int mLastOrientation; private String mCachedSpecs = ""; @Nullable @@ -88,21 +89,16 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr new QSPanel.OnConfigurationChangedListener() { @Override public void onConfigurationChange(Configuration newConfig) { + mQSLogger.logOnConfigurationChanged( + /* lastOrientation= */ mLastOrientation, + /* newOrientation= */ newConfig.orientation, + /* containerName= */ mView.getDumpableTag()); + mShouldUseSplitNotificationShade = - LargeScreenUtils.shouldUseSplitNotificationShade(getResources()); - // Logging to aid the investigation of b/216244185. - Log.d(TAG, - "onConfigurationChange: " - + "mShouldUseSplitNotificationShade=" - + mShouldUseSplitNotificationShade + ", " - + "newConfig.windowConfiguration=" - + newConfig.windowConfiguration); - mQSLogger.logOnConfigurationChanged(mLastOrientation, newConfig.orientation, - mView.getDumpableTag()); - if (newConfig.orientation != mLastOrientation) { - mLastOrientation = newConfig.orientation; - switchTileLayout(false); - } + LargeScreenUtils.shouldUseSplitNotificationShade(getResources()); + mLastOrientation = newConfig.orientation; + + switchTileLayoutIfNeeded(); onConfigurationChanged(); } }; @@ -334,6 +330,10 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr } } + private void switchTileLayoutIfNeeded() { + switchTileLayout(/* force= */ false); + } + boolean switchTileLayout(boolean force) { /* Whether or not the panel currently contains a media player. */ boolean horizontal = shouldUseHorizontalLayout(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java index 67bf3003deff..6c1e95645550 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java @@ -39,6 +39,7 @@ import androidx.annotation.Nullable; import com.android.internal.util.FrameworkStatsLog; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.common.shared.model.Icon; import com.android.systemui.dagger.qualifiers.Background; @@ -169,7 +170,7 @@ public class QSSecurityFooter extends ViewController<View> // TODO(b/242040009): Remove this. public void showDeviceMonitoringDialog() { - mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, mView); + mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, Expandable.fromView(mView)); } public void refreshState() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java index ae6ed2008a77..67bc76998597 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java @@ -75,6 +75,7 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.systemui.R; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.animation.Expandable; import com.android.systemui.common.shared.model.ContentDescription; import com.android.systemui.common.shared.model.Icon; import com.android.systemui.dagger.SysUISingleton; @@ -190,8 +191,9 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { } /** Show the device monitoring dialog. */ - public void showDeviceMonitoringDialog(Context quickSettingsContext, @Nullable View view) { - createDialog(quickSettingsContext, view); + public void showDeviceMonitoringDialog(Context quickSettingsContext, + @Nullable Expandable expandable) { + createDialog(quickSettingsContext, expandable); } /** @@ -440,7 +442,7 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { } } - private void createDialog(Context quickSettingsContext, @Nullable View view) { + private void createDialog(Context quickSettingsContext, @Nullable Expandable expandable) { mShouldUseSettingsButton.set(false); mBgHandler.post(() -> { String settingsButtonText = getSettingsButton(); @@ -453,9 +455,12 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { ? settingsButtonText : getNegativeButton(), this); mDialog.setView(dialogView); - if (view != null && view.isAggregatedVisible()) { - mDialogLaunchAnimator.showFromView(mDialog, view, new DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)); + DialogLaunchAnimator.Controller controller = + expandable != null ? expandable.dialogLaunchController(new DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)) + : null; + if (controller != null) { + mDialogLaunchAnimator.show(mDialog, controller); } else { mDialog.show(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index ac46c85c10a4..f37d66877069 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -34,10 +34,12 @@ import com.android.internal.logging.InstanceId; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.UiEventLogger; import com.android.systemui.Dumpable; +import com.android.systemui.ProtoDumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.dump.nano.SystemUIProtoDump; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.qs.QSTile; @@ -48,6 +50,7 @@ import com.android.systemui.qs.external.TileLifecycleManager; import com.android.systemui.qs.external.TileServiceKey; import com.android.systemui.qs.external.TileServiceRequestController; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.nano.QsTileState; import com.android.systemui.settings.UserFileManager; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.plugins.PluginManager; @@ -59,16 +62,20 @@ import com.android.systemui.tuner.TunerService.Tunable; import com.android.systemui.util.leak.GarbageMonitor; import com.android.systemui.util.settings.SecureSettings; +import org.jetbrains.annotations.NotNull; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Provider; @@ -82,7 +89,7 @@ import javax.inject.Provider; * This class also provides the interface for adding/removing/changing tiles. */ @SysUISingleton -public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, Dumpable { +public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable { private static final String TAG = "QSTileHost"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int MAX_QS_INSTANCE_ID = 1 << 20; @@ -671,4 +678,15 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D mTiles.values().stream().filter(obj -> obj instanceof Dumpable) .forEach(o -> ((Dumpable) o).dump(pw, args)); } + + @Override + public void dumpProto(@NotNull SystemUIProtoDump systemUIProtoDump, @NotNull String[] args) { + List<QsTileState> data = mTiles.values().stream() + .map(QSTile::getState) + .map(TileStateToProtoKt::toProto) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + systemUIProtoDump.tiles = data.toArray(new QsTileState[0]); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java index 9739974256f6..6aabe3b1ced1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java @@ -26,8 +26,8 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaHierarchyManager; -import com.android.systemui.media.MediaHost; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; +import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSScope; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index 264edb1ec9e4..27d9da6c2e1e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -25,6 +25,7 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.util.Pair; import android.view.DisplayCutout; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; @@ -231,6 +232,16 @@ public class QuickStatusBarHeader extends FrameLayout { } } + @Override + public boolean onTouchEvent(MotionEvent event) { + // If using combined headers, only react to touches inside QuickQSPanel + if (!mUseCombinedQSHeader || event.getY() > mHeaderQsPanel.getTop()) { + return super.onTouchEvent(event); + } else { + return false; + } + } + void updateResources() { Resources resources = mContext.getResources(); boolean largeScreenHeaderActive = @@ -410,9 +421,9 @@ public class QuickStatusBarHeader extends FrameLayout { // If forceExpanded (we are opening QS from lockscreen), the animators have been set to // position = 1f. if (forceExpanded) { - setTranslationY(panelTranslationY); + setAlpha(expansionFraction); } else { - setTranslationY(0); + setAlpha(1); } mKeyguardExpansionFraction = keyguardExpansionFraction; diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt new file mode 100644 index 000000000000..2c8a5a4981d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs + +import android.service.quicksettings.Tile +import android.text.TextUtils +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.external.CustomTile +import com.android.systemui.qs.nano.QsTileState +import com.android.systemui.util.nano.ComponentNameProto + +fun QSTile.State.toProto(): QsTileState? { + if (TextUtils.isEmpty(spec)) return null + val state = QsTileState() + if (spec.startsWith(CustomTile.PREFIX)) { + val protoComponentName = ComponentNameProto() + val tileComponentName = CustomTile.getComponentFromSpec(spec) + protoComponentName.packageName = tileComponentName.packageName + protoComponentName.className = tileComponentName.className + state.componentName = protoComponentName + } else { + state.spec = spec + } + state.state = + when (this.state) { + Tile.STATE_UNAVAILABLE -> QsTileState.UNAVAILABLE + Tile.STATE_INACTIVE -> QsTileState.INACTIVE + Tile.STATE_ACTIVE -> QsTileState.ACTIVE + else -> QsTileState.UNAVAILABLE + } + label?.let { state.label = it.toString() } + secondaryLabel?.let { state.secondaryLabel = it.toString() } + if (this is QSTile.BooleanState) { + state.booleanState = value + } + return state +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java index 3e445ddfc2a1..d39368012487 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java @@ -36,6 +36,7 @@ import android.util.ArraySet; import android.util.Log; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Main; @@ -182,6 +183,10 @@ public class TileLifecycleManager extends BroadcastReceiver implements setBindService(true); } + /** + * Binds or unbinds to IQSService + */ + @WorkerThread public void setBindService(boolean bind) { if (mBound && mUnbindImmediate) { // If we are already bound and expecting to unbind, this means we should stay bound diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java index 4cacbbacec2f..5d03da3cc113 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java @@ -35,6 +35,7 @@ import androidx.annotation.Nullable; import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.QSTileHost; import com.android.systemui.settings.UserTracker; @@ -53,6 +54,7 @@ import javax.inject.Provider; /** * Runs the day-to-day operations of which tiles should be bound and when. */ +@SysUISingleton public class TileServices extends IQSService.Stub { static final int DEFAULT_MAX_BOUND = 3; static final int REDUCED_MAX_BOUND = 1; diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt index cf9b41c25388..9ba3501c3434 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt @@ -23,13 +23,11 @@ import android.content.Intent import android.content.IntentFilter import android.os.UserHandle import android.provider.Settings -import android.view.View import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.MetricsLogger import com.android.internal.logging.UiEventLogger import com.android.internal.logging.nano.MetricsProto import com.android.internal.util.FrameworkStatsLog -import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton @@ -74,37 +72,27 @@ interface FooterActionsInteractor { val deviceMonitoringDialogRequests: Flow<Unit> /** - * Show the device monitoring dialog, expanded from [view]. - * - * Important: [view] must be associated to the same [Context] as the [Quick Settings fragment] - * [com.android.systemui.qs.QSFragment]. - */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showDeviceMonitoringDialog(view: View) - - /** - * Show the device monitoring dialog. + * Show the device monitoring dialog, expanded from [expandable] if it's not null. * * Important: [quickSettingsContext] *must* be the [Context] associated to the [Quick Settings * fragment][com.android.systemui.qs.QSFragment]. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showDeviceMonitoringDialog(quickSettingsContext: Context) + fun showDeviceMonitoringDialog(quickSettingsContext: Context, expandable: Expandable?) /** Show the foreground services dialog. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showForegroundServicesDialog(view: View) + fun showForegroundServicesDialog(expandable: Expandable) /** Show the power menu dialog. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) + fun showPowerMenuDialog( + globalActionsDialogLite: GlobalActionsDialogLite, + expandable: Expandable, + ) /** Show the settings. */ fun showSettings(expandable: Expandable) /** Show the user switcher. */ - // TODO(b/230830644): Replace view by Expandable interface. - fun showUserSwitcher(view: View) + fun showUserSwitcher(context: Context, expandable: Expandable) } @SysUISingleton @@ -147,28 +135,32 @@ constructor( null, ) - override fun showDeviceMonitoringDialog(view: View) { - qsSecurityFooterUtils.showDeviceMonitoringDialog(view.context, view) - DevicePolicyEventLogger.createEvent( - FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED - ) - .write() - } - - override fun showDeviceMonitoringDialog(quickSettingsContext: Context) { - qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, /* view= */ null) + override fun showDeviceMonitoringDialog( + quickSettingsContext: Context, + expandable: Expandable?, + ) { + qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, expandable) + if (expandable != null) { + DevicePolicyEventLogger.createEvent( + FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED + ) + .write() + } } - override fun showForegroundServicesDialog(view: View) { - fgsManagerController.showDialog(view) + override fun showForegroundServicesDialog(expandable: Expandable) { + fgsManagerController.showDialog(expandable) } - override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) { + override fun showPowerMenuDialog( + globalActionsDialogLite: GlobalActionsDialogLite, + expandable: Expandable, + ) { uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS) globalActionsDialogLite.showOrHideDialog( /* keyguardShowing= */ false, /* isDeviceProvisioned= */ true, - view, + expandable, ) } @@ -189,21 +181,21 @@ constructor( ) } - override fun showUserSwitcher(view: View) { + override fun showUserSwitcher(context: Context, expandable: Expandable) { if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { - userSwitchDialogController.showDialog(view) + userSwitchDialogController.showDialog(context, expandable) return } val intent = - Intent(view.context, UserSwitcherActivity::class.java).apply { + Intent(context, UserSwitcherActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) } activityStarter.startActivity( intent, true /* dismissShade */, - ActivityLaunchAnimator.Controller.fromView(view, null), + expandable.activityLaunchController(), true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt index 28ddead0bdd9..3e39c8ee62f1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.R +import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.people.ui.view.PeopleViewBinder.bind @@ -125,7 +126,7 @@ object FooterActionsViewBinder { launch { viewModel.security.collect { security -> if (previousSecurity != security) { - bindSecurity(securityHolder, security) + bindSecurity(view.context, securityHolder, security) previousSecurity = security } } @@ -159,6 +160,7 @@ object FooterActionsViewBinder { } private fun bindSecurity( + quickSettingsContext: Context, securityHolder: TextButtonViewHolder, security: FooterActionsSecurityButtonViewModel?, ) { @@ -171,9 +173,12 @@ object FooterActionsViewBinder { // Make sure that the chevron is visible and that the button is clickable if there is a // listener. val chevron = securityHolder.chevron - if (security.onClick != null) { + val onClick = security.onClick + if (onClick != null) { securityView.isClickable = true - securityView.setOnClickListener(security.onClick) + securityView.setOnClickListener { + onClick(quickSettingsContext, Expandable.fromView(securityView)) + } chevron.isVisible = true } else { securityView.isClickable = false @@ -205,7 +210,9 @@ object FooterActionsViewBinder { foregroundServicesWithNumberView.isVisible = false foregroundServicesWithTextView.isVisible = true - foregroundServicesWithTextView.setOnClickListener(foregroundServices.onClick) + foregroundServicesWithTextView.setOnClickListener { + foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView)) + } foregroundServicesWithTextHolder.text.text = foregroundServices.text foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges } else { @@ -213,7 +220,9 @@ object FooterActionsViewBinder { foregroundServicesWithTextView.isVisible = false foregroundServicesWithNumberView.visibility = View.VISIBLE - foregroundServicesWithNumberView.setOnClickListener(foregroundServices.onClick) + foregroundServicesWithNumberView.setOnClickListener { + foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView)) + } foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString() foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges @@ -222,13 +231,14 @@ object FooterActionsViewBinder { private fun bindButton(button: IconButtonViewHolder, model: FooterActionsButtonViewModel?) { val buttonView = button.view + buttonView.id = model?.id ?: View.NO_ID buttonView.isVisible = model != null if (model == null) { return } buttonView.setBackgroundResource(model.background) - buttonView.setOnClickListener(model.onClick) + buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) } val icon = model.icon val iconView = button.icon diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt index 2ad0513c2ace..8d819dacba67 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt @@ -17,7 +17,7 @@ package com.android.systemui.qs.footer.ui.viewmodel import android.annotation.DrawableRes -import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon /** @@ -25,10 +25,9 @@ import com.android.systemui.common.shared.model.Icon * power buttons. */ data class FooterActionsButtonViewModel( + val id: Int, val icon: Icon, val iconTint: Int?, @DrawableRes val background: Int, - // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog - // or activity. - val onClick: (View) -> Unit, + val onClick: (Expandable) -> Unit, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt index 98b53cb0ed5a..ff8130d3e6ec 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt @@ -16,7 +16,7 @@ package com.android.systemui.qs.footer.ui.viewmodel -import android.view.View +import com.android.systemui.animation.Expandable /** A ViewModel for the foreground services button. */ data class FooterActionsForegroundServicesButtonViewModel( @@ -24,5 +24,5 @@ data class FooterActionsForegroundServicesButtonViewModel( val text: String, val displayText: Boolean, val hasNewChanges: Boolean, - val onClick: (View) -> Unit, + val onClick: (Expandable) -> Unit, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt index 98ab129fc9de..3450505f9f86 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt @@ -16,12 +16,13 @@ package com.android.systemui.qs.footer.ui.viewmodel -import android.view.View +import android.content.Context +import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon /** A ViewModel for the security button. */ data class FooterActionsSecurityButtonViewModel( val icon: Icon, val text: String, - val onClick: ((View) -> Unit)?, + val onClick: ((quickSettingsContext: Context, Expandable) -> Unit)?, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index a935338c2565..dee6fadbc9cb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -18,12 +18,10 @@ package com.android.systemui.qs.footer.ui.viewmodel import android.content.Context import android.util.Log -import android.view.View import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import com.android.settingslib.Utils -import com.android.settingslib.drawable.UserIconDrawable import com.android.systemui.R import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription @@ -138,6 +136,7 @@ class FooterActionsViewModel( /** The model for the settings button. */ val settings: FooterActionsButtonViewModel = FooterActionsButtonViewModel( + id = R.id.settings_button_container, Icon.Resource( R.drawable.ic_settings, ContentDescription.Resource(R.string.accessibility_quick_settings_settings) @@ -151,6 +150,7 @@ class FooterActionsViewModel( val power: FooterActionsButtonViewModel? = if (showPowerButton) { FooterActionsButtonViewModel( + id = R.id.pm_lite, Icon.Resource( android.R.drawable.ic_lock_power_off, ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu) @@ -198,71 +198,70 @@ class FooterActionsViewModel( */ suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) { footerActionsInteractor.deviceMonitoringDialogRequests.collect { - footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext) + footerActionsInteractor.showDeviceMonitoringDialog( + quickSettingsContext, + expandable = null, + ) } } - private fun onSecurityButtonClicked(view: View) { + private fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showDeviceMonitoringDialog(view) + footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable) } - private fun onForegroundServiceButtonClicked(view: View) { + private fun onForegroundServiceButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showForegroundServicesDialog(view) + footerActionsInteractor.showForegroundServicesDialog(expandable) } - private fun onUserSwitcherClicked(view: View) { + private fun onUserSwitcherClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showUserSwitcher(view) + footerActionsInteractor.showUserSwitcher(context, expandable) } - // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog - // or activity. - private fun onSettingsButtonClicked(view: View) { + private fun onSettingsButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showSettings(Expandable.fromView(view)) + footerActionsInteractor.showSettings(expandable) } - private fun onPowerButtonClicked(view: View) { + private fun onPowerButtonClicked(expandable: Expandable) { if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { return } - footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, view) + footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable) } private fun userSwitcherButton( status: UserSwitcherStatusModel.Enabled ): FooterActionsButtonViewModel { val icon = status.currentUserImage!! - val iconTint = - if (status.isGuestUser && icon !is UserIconDrawable) { - Utils.getColorAttrDefaultColor(context, android.R.attr.colorForeground) - } else { - null - } return FooterActionsButtonViewModel( - Icon.Loaded( - icon, - ContentDescription.Loaded(userSwitcherContentDescription(status.currentUserName)), - ), - iconTint, - R.drawable.qs_footer_action_circle, - this::onUserSwitcherClicked, + id = R.id.multi_user_switch, + icon = + Icon.Loaded( + icon, + ContentDescription.Loaded( + userSwitcherContentDescription(status.currentUserName) + ), + ), + iconTint = null, + background = R.drawable.qs_footer_action_circle, + onClick = this::onUserSwitcherClicked, ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt index 60380064e098..931dc8df151a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt @@ -17,12 +17,12 @@ package com.android.systemui.qs.logging import android.service.quicksettings.Tile -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.VERBOSE -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.QSLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogMessage import com.android.systemui.plugins.qs.QSTile import com.android.systemui.statusbar.StatusBarState import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto new file mode 100644 index 000000000000..2a61033cb302 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package com.android.systemui.qs; + +import "frameworks/base/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto"; + +option java_multiple_files = true; + +message QsTileState { + oneof identifier { + string spec = 1; + com.android.systemui.util.ComponentNameProto component_name = 2; + } + + enum State { + UNAVAILABLE = 0; + INACTIVE = 1; + ACTIVE = 2; + } + + State state = 3; + oneof optional_boolean_state { + bool boolean_state = 4; + } + oneof optional_label { + string label = 5; + } + oneof optional_secondary_label { + string secondary_label = 6; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index 97476b2d1cde..57a00c9a1620 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -26,6 +26,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.logging.MetricsLogger; @@ -43,6 +44,9 @@ import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.user.data.source.UserRecord; +import java.util.List; +import java.util.stream.Collectors; + import javax.inject.Inject; /** @@ -83,6 +87,13 @@ public class UserDetailView extends PseudoGridView { private final FalsingManager mFalsingManager; private @Nullable UserSwitchDialogController.DialogShower mDialogShower; + @NonNull + @Override + protected List<UserRecord> getUsers() { + return super.getUsers().stream().filter( + userRecord -> !userRecord.isManageUsers).collect(Collectors.toList()); + } + @Inject public Adapter(Context context, UserSwitcherController controller, UiEventLogger uiEventLogger, FalsingManager falsingManager) { @@ -134,7 +145,7 @@ public class UserDetailView extends PseudoGridView { v.bind(name, drawable, item.info.id); } v.setActivated(item.isCurrent); - v.setDisabledByAdmin(mController.isDisabledByAdmin(item)); + v.setDisabledByAdmin(item.isDisabledByAdmin()); v.setEnabled(item.isSwitchToEnabled); UserSwitcherController.setSelectableAlpha(v); @@ -173,16 +184,16 @@ public class UserDetailView extends PseudoGridView { Trace.beginSection("UserDetailView.Adapter#onClick"); UserRecord userRecord = (UserRecord) view.getTag(); - if (mController.isDisabledByAdmin(userRecord)) { + if (userRecord.isDisabledByAdmin()) { final Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( - mContext, mController.getEnforcedAdmin(userRecord)); + mContext, userRecord.enforcedAdmin); mController.startActivity(intent); } else if (userRecord.isSwitchToEnabled) { MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER); mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH); if (!userRecord.isAddUser && !userRecord.isRestricted - && !mController.isDisabledByAdmin(userRecord)) { + && !userRecord.isDisabledByAdmin()) { if (mCurrentUserView != null) { mCurrentUserView.setActivated(false); } @@ -193,6 +204,15 @@ public class UserDetailView extends PseudoGridView { Trace.endSection(); } + @Override + public void onUserListItemClicked(@NonNull UserRecord record, + @Nullable UserSwitchDialogController.DialogShower dialogShower) { + if (dialogShower != null) { + mDialogShower.dismiss(); + } + super.onUserListItemClicked(record, dialogShower); + } + public void linkToViewGroup(ViewGroup viewGroup) { PseudoGridView.ViewGroupAdapterBridge.link(viewGroup, this); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java index 3c8775d01e2d..9c0a087c01b8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java @@ -197,7 +197,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi }; protected List<SubscriptionInfo> getSubscriptionInfo() { - return mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(false); + return mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(); } @Inject diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt index bdcc6b0b2a57..314252bf310b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt @@ -23,13 +23,13 @@ import android.content.DialogInterface.BUTTON_NEUTRAL import android.content.Intent import android.provider.Settings import android.view.LayoutInflater -import android.view.View import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger import com.android.systemui.R import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -77,10 +77,10 @@ class UserSwitchDialogController @VisibleForTesting constructor( * Show a [UserDialog]. * * Populate the dialog with information from and adapter obtained from - * [userDetailViewAdapterProvider] and show it as launched from [view]. + * [userDetailViewAdapterProvider] and show it as launched from [expandable]. */ - fun showDialog(view: View) { - with(dialogFactory(view.context)) { + fun showDialog(context: Context, expandable: Expandable) { + with(dialogFactory(context)) { setShowForAllUsers(true) setCanceledOnTouchOutside(true) @@ -112,13 +112,19 @@ class UserSwitchDialogController @VisibleForTesting constructor( adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid)) - dialogLaunchAnimator.showFromView( - this, view, - cuj = DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG + val controller = + expandable.dialogLaunchController( + DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) ) - ) + if (controller != null) { + dialogLaunchAnimator.show( + this, + controller, + ) + } else { + show() + } + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN) adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator)) } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 7e2a5c51786d..66be00d8de66 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -25,15 +25,6 @@ import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_RECENT_TASKS; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_BACK_ANIMATION; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_DESKTOP_MODE; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_FLOATING_TASKS; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_ONE_HANDED; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_PIP; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SHELL_TRANSITIONS; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SPLIT_SCREEN; -import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_STARTING_WINDOW; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SUPPORTS_WINDOW_CORNERS; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER; @@ -110,16 +101,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.StatusBarWindowCallback; import com.android.systemui.statusbar.policy.CallbackController; -import com.android.wm.shell.back.BackAnimation; -import com.android.wm.shell.desktopmode.DesktopMode; -import com.android.wm.shell.floating.FloatingTasks; -import com.android.wm.shell.onehanded.OneHanded; -import com.android.wm.shell.pip.Pip; -import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.recents.RecentTasks; -import com.android.wm.shell.splitscreen.SplitScreen; -import com.android.wm.shell.startingsurface.StartingSurface; -import com.android.wm.shell.transition.ShellTransitions; +import com.android.wm.shell.sysui.ShellInterface; import java.io.PrintWriter; import java.util.ArrayList; @@ -151,10 +133,8 @@ public class OverviewProxyService extends CurrentUserTracker implements private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000; private final Context mContext; - private final Optional<Pip> mPipOptional; + private final ShellInterface mShellInterface; private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; - private final Optional<SplitScreen> mSplitScreenOptional; - private final Optional<FloatingTasks> mFloatingTasksOptional; private SysUiState mSysUiState; private final Handler mHandler; private final Lazy<NavigationBarController> mNavBarControllerLazy; @@ -164,14 +144,8 @@ public class OverviewProxyService extends CurrentUserTracker implements private final List<OverviewProxyListener> mConnectionCallbacks = new ArrayList<>(); private final Intent mQuickStepIntent; private final ScreenshotHelper mScreenshotHelper; - private final Optional<OneHanded> mOneHandedOptional; private final CommandQueue mCommandQueue; - private final ShellTransitions mShellTransitions; - private final Optional<StartingSurface> mStartingSurface; private final KeyguardUnlockAnimationController mSysuiUnlockAnimationController; - private final Optional<RecentTasks> mRecentTasks; - private final Optional<BackAnimation> mBackAnimation; - private final Optional<DesktopMode> mDesktopModeOptional; private final UiEventLogger mUiEventLogger; private Region mActiveNavBarRegion; @@ -342,14 +316,6 @@ public class OverviewProxyService extends CurrentUserTracker implements } @Override - public void notifySwipeToHomeFinished() { - verifyCallerAndClearCallingIdentity("notifySwipeToHomeFinished", () -> - mPipOptional.ifPresent( - pip -> pip.setPinnedStackAnimationType( - PipAnimationController.ANIM_TYPE_ALPHA))); - } - - @Override public void notifySwipeUpGestureStarted() { verifyCallerAndClearCallingIdentityPostMain("notifySwipeUpGestureStarted", () -> notifySwipeUpGestureStartedInternal()); @@ -464,36 +430,10 @@ public class OverviewProxyService extends CurrentUserTracker implements params.putBinder(KEY_EXTRA_SYSUI_PROXY, mSysUiProxy.asBinder()); params.putFloat(KEY_EXTRA_WINDOW_CORNER_RADIUS, mWindowCornerRadius); params.putBoolean(KEY_EXTRA_SUPPORTS_WINDOW_CORNERS, mSupportsRoundedCornersOnWindows); - - mPipOptional.ifPresent((pip) -> params.putBinder( - KEY_EXTRA_SHELL_PIP, - pip.createExternalInterface().asBinder())); - mSplitScreenOptional.ifPresent((splitscreen) -> params.putBinder( - KEY_EXTRA_SHELL_SPLIT_SCREEN, - splitscreen.createExternalInterface().asBinder())); - mFloatingTasksOptional.ifPresent(floatingTasks -> params.putBinder( - KEY_EXTRA_SHELL_FLOATING_TASKS, - floatingTasks.createExternalInterface().asBinder())); - mOneHandedOptional.ifPresent((onehanded) -> params.putBinder( - KEY_EXTRA_SHELL_ONE_HANDED, - onehanded.createExternalInterface().asBinder())); - params.putBinder(KEY_EXTRA_SHELL_SHELL_TRANSITIONS, - mShellTransitions.createExternalInterface().asBinder()); - mStartingSurface.ifPresent((startingwindow) -> params.putBinder( - KEY_EXTRA_SHELL_STARTING_WINDOW, - startingwindow.createExternalInterface().asBinder())); - params.putBinder( - KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER, + params.putBinder(KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER, mSysuiUnlockAnimationController.asBinder()); - mRecentTasks.ifPresent(recentTasks -> params.putBinder( - KEY_EXTRA_RECENT_TASKS, - recentTasks.createExternalInterface().asBinder())); - mBackAnimation.ifPresent((backAnimation) -> params.putBinder( - KEY_EXTRA_SHELL_BACK_ANIMATION, - backAnimation.createExternalInterface().asBinder())); - mDesktopModeOptional.ifPresent((desktopMode -> params.putBinder( - KEY_EXTRA_SHELL_DESKTOP_MODE, - desktopMode.createExternalInterface().asBinder()))); + // Add all the interfaces exposed by the shell + mShellInterface.createExternalInterfaces(params); try { Log.d(TAG_OPS, "OverviewProxyService connected, initializing overview proxy"); @@ -567,21 +507,14 @@ public class OverviewProxyService extends CurrentUserTracker implements @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Inject - public OverviewProxyService(Context context, CommandQueue commandQueue, + public OverviewProxyService(Context context, + CommandQueue commandQueue, + ShellInterface shellInterface, Lazy<NavigationBarController> navBarControllerLazy, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, NavigationModeController navModeController, NotificationShadeWindowController statusBarWinController, SysUiState sysUiState, - Optional<Pip> pipOptional, - Optional<SplitScreen> splitScreenOptional, - Optional<FloatingTasks> floatingTasksOptional, - Optional<OneHanded> oneHandedOptional, - Optional<RecentTasks> recentTasks, - Optional<BackAnimation> backAnimation, - Optional<StartingSurface> startingSurface, - Optional<DesktopMode> desktopModeOptional, BroadcastDispatcher broadcastDispatcher, - ShellTransitions shellTransitions, ScreenLifecycle screenLifecycle, UiEventLogger uiEventLogger, KeyguardUnlockAnimationController sysuiUnlockAnimationController, @@ -595,7 +528,7 @@ public class OverviewProxyService extends CurrentUserTracker implements } mContext = context; - mPipOptional = pipOptional; + mShellInterface = shellInterface; mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy; mHandler = new Handler(); mNavBarControllerLazy = navBarControllerLazy; @@ -610,11 +543,6 @@ public class OverviewProxyService extends CurrentUserTracker implements .supportsRoundedCornersOnWindows(mContext.getResources()); mSysUiState = sysUiState; mSysUiState.addCallback(this::notifySystemUiStateFlags); - mOneHandedOptional = oneHandedOptional; - mShellTransitions = shellTransitions; - mRecentTasks = recentTasks; - mBackAnimation = backAnimation; - mDesktopModeOptional = desktopModeOptional; mUiEventLogger = uiEventLogger; dumpManager.registerDumpable(getClass().getSimpleName(), this); @@ -644,9 +572,6 @@ public class OverviewProxyService extends CurrentUserTracker implements }); mCommandQueue = commandQueue; - mSplitScreenOptional = splitScreenOptional; - mFloatingTasksOptional = floatingTasksOptional; - // Listen for user setup startTracking(); @@ -655,7 +580,6 @@ public class OverviewProxyService extends CurrentUserTracker implements // Connect to the service updateEnabledState(); startConnectionToCurrentUser(); - mStartingSurface = startingSurface; mSysuiUnlockAnimationController = sysuiUnlockAnimationController; // Listen for assistant changes diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java index 9b3b843c9848..b041f957d771 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java +++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java @@ -29,13 +29,14 @@ import java.io.PrintWriter; /** * A proxy to a Recents implementation. */ -public class Recents extends CoreStartable implements CommandQueue.Callbacks { +public class Recents implements CoreStartable, CommandQueue.Callbacks { + private final Context mContext; private final RecentsImplementation mImpl; private final CommandQueue mCommandQueue; public Recents(Context context, RecentsImplementation impl, CommandQueue commandQueue) { - super(context); + mContext = context; mImpl = impl; mCommandQueue = commandQueue; } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt new file mode 100644 index 000000000000..017e57fcaf62 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.content.ClipData +import android.content.ClipDescription +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.android.systemui.R + +object ActionIntentCreator { + /** @return a chooser intent to share the given URI with the optional provided subject. */ + fun createShareIntent(uri: Uri, subject: String?): Intent { + // Create a share intent, this will always go through the chooser activity first + // which should not trigger auto-enter PiP + val sharingIntent = + Intent(Intent.ACTION_SEND).apply { + setDataAndType(uri, "image/png") + putExtra(Intent.EXTRA_STREAM, uri) + + // Include URI in ClipData also, so that grantPermission picks it up. + // We don't use setData here because some apps interpret this as "to:". + clipData = + ClipData( + ClipDescription("content", arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)), + ClipData.Item(uri) + ) + + putExtra(Intent.EXTRA_SUBJECT, subject) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } + + return Intent.createChooser(sharingIntent, null) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + /** + * @return an ACTION_EDIT intent for the given URI, directed to config_screenshotEditor if + * available. + */ + fun createEditIntent(uri: Uri, context: Context): Intent { + val editIntent = Intent(Intent.ACTION_EDIT) + + context.getString(R.string.config_screenshotEditor)?.let { + if (it.isNotEmpty()) { + editIntent.component = ComponentName.unflattenFromString(it) + } + } + + return editIntent + .setDataAndType(uri, "image/png") + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt new file mode 100644 index 000000000000..5961635a0dba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.RemoteException +import android.os.UserHandle +import android.util.Log +import android.view.Display +import android.view.IRemoteAnimationFinishedCallback +import android.view.IRemoteAnimationRunner +import android.view.RemoteAnimationAdapter +import android.view.RemoteAnimationTarget +import android.view.WindowManager +import android.view.WindowManagerGlobal +import com.android.internal.infra.ServiceConnector +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import javax.inject.Inject +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@SysUISingleton +class ActionIntentExecutor +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, + private val context: Context, +) { + /** + * Execute the given intent with startActivity while performing operations for screenshot action + * launching. + * - Dismiss the keyguard first + * - If the userId is not the current user, proxy to a service running as that user to execute + * - After startActivity, optionally override the pending app transition. + */ + fun launchIntentAsync( + intent: Intent, + bundle: Bundle, + userId: Int, + overrideTransition: Boolean, + ) { + applicationScope.launch { launchIntent(intent, bundle, userId, overrideTransition) } + } + + suspend fun launchIntent( + intent: Intent, + bundle: Bundle, + userId: Int, + overrideTransition: Boolean, + ) { + withContext(bgDispatcher) { + dismissKeyguard() + + if (userId == UserHandle.myUserId()) { + context.startActivity(intent, bundle) + } else { + launchCrossProfileIntent(userId, intent, bundle) + } + + if (overrideTransition) { + val runner = RemoteAnimationAdapter(SCREENSHOT_REMOTE_RUNNER, 0, 0) + try { + WindowManagerGlobal.getWindowManagerService() + .overridePendingAppTransitionRemote(runner, Display.DEFAULT_DISPLAY) + } catch (e: Exception) { + Log.e(TAG, "Error overriding screenshot app transition", e) + } + } + } + } + + private val proxyConnector: ServiceConnector<IScreenshotProxy> = + ServiceConnector.Impl( + context, + Intent(context, ScreenshotProxyService::class.java), + Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE, + context.userId, + IScreenshotProxy.Stub::asInterface, + ) + + private suspend fun dismissKeyguard() { + val completion = CompletableDeferred<Unit>() + val onDoneBinder = + object : IOnDoneCallback.Stub() { + override fun onDone(success: Boolean) { + completion.complete(Unit) + } + } + proxyConnector.post { it.dismissKeyguard(onDoneBinder) } + completion.await() + } + + private fun getCrossProfileConnector(userId: Int): ServiceConnector<ICrossProfileService> = + ServiceConnector.Impl<ICrossProfileService>( + context, + Intent(context, ScreenshotCrossProfileService::class.java), + Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE, + userId, + ICrossProfileService.Stub::asInterface, + ) + + private suspend fun launchCrossProfileIntent(userId: Int, intent: Intent, bundle: Bundle) { + val connector = getCrossProfileConnector(userId) + val completion = CompletableDeferred<Unit>() + connector.post { + it.launchIntent(intent, bundle) + completion.complete(Unit) + } + completion.await() + } +} + +private const val TAG: String = "ActionIntentExecutor" +private const val SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)" + +/** + * This is effectively a no-op, but we need something non-null to pass in, in order to successfully + * override the pending activity entrance animation. + */ +private val SCREENSHOT_REMOTE_RUNNER: IRemoteAnimationRunner.Stub = + object : IRemoteAnimationRunner.Stub() { + override fun onAnimationStart( + @WindowManager.TransitionOldType transit: Int, + apps: Array<RemoteAnimationTarget>, + wallpapers: Array<RemoteAnimationTarget>, + nonApps: Array<RemoteAnimationTarget>, + finishedCallback: IRemoteAnimationFinishedCallback, + ) { + try { + finishedCallback.onAnimationFinished() + } catch (e: RemoteException) { + Log.e(TAG, "Error finishing screenshot remote animation", e) + } + } + + override fun onAnimationCancelled(isKeyguardOccluded: Boolean) {} + } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java index 950806d89422..ead3b7b1de53 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/DraggableConstraintLayout.java @@ -49,7 +49,6 @@ public class DraggableConstraintLayout extends ConstraintLayout private final SwipeDismissHandler mSwipeDismissHandler; private final GestureDetector mSwipeDetector; private View mActionsContainer; - private View mActionsContainerBackground; private SwipeDismissCallbacks mCallbacks; private final DisplayMetrics mDisplayMetrics; @@ -111,6 +110,9 @@ public class DraggableConstraintLayout extends ConstraintLayout } }); mSwipeDetector.setIsLongpressEnabled(false); + + mCallbacks = new SwipeDismissCallbacks() { + }; // default to unimplemented callbacks } public void setCallbacks(SwipeDismissCallbacks callbacks) { @@ -119,16 +121,13 @@ public class DraggableConstraintLayout extends ConstraintLayout @Override public boolean onInterceptHoverEvent(MotionEvent event) { - if (mCallbacks != null) { - mCallbacks.onInteraction(); - } + mCallbacks.onInteraction(); return super.onInterceptHoverEvent(event); } @Override // View protected void onFinishInflate() { mActionsContainer = findViewById(R.id.actions_container); - mActionsContainerBackground = findViewById(R.id.actions_container_background); } @Override @@ -186,6 +185,13 @@ public class DraggableConstraintLayout extends ConstraintLayout inoutInfo.touchableRegion.set(r); } + private int getBackgroundRight() { + // background expected to be null in testing. + // animation may have unexpected behavior if view is not present + View background = findViewById(R.id.actions_container_background); + return background == null ? 0 : background.getRight(); + } + /** * Allows a view to be swipe-dismissed, or returned to its location if distance threshold is not * met @@ -213,8 +219,6 @@ public class DraggableConstraintLayout extends ConstraintLayout mGestureDetector = new GestureDetector(context, gestureListener); mDisplayMetrics = new DisplayMetrics(); context.getDisplay().getRealMetrics(mDisplayMetrics); - mCallbacks = new SwipeDismissCallbacks() { - }; // default to unimplemented callbacks } @Override @@ -230,7 +234,9 @@ public class DraggableConstraintLayout extends ConstraintLayout return true; } if (isPastDismissThreshold()) { - dismiss(); + ValueAnimator anim = createSwipeDismissAnimation(); + mCallbacks.onSwipeDismissInitiated(anim); + dismiss(anim); } else { // if we've moved, but not past the threshold, start the return animation if (DEBUG_DISMISS) { @@ -295,10 +301,7 @@ public class DraggableConstraintLayout extends ConstraintLayout } void dismiss() { - float velocityPxPerMs = FloatingWindowUtil.dpToPx(mDisplayMetrics, VELOCITY_DP_PER_MS); - ValueAnimator anim = createSwipeDismissAnimation(velocityPxPerMs); - mCallbacks.onSwipeDismissInitiated(anim); - dismiss(anim); + dismiss(createSwipeDismissAnimation()); } private void dismiss(ValueAnimator animator) { @@ -323,6 +326,11 @@ public class DraggableConstraintLayout extends ConstraintLayout mDismissAnimation.start(); } + private ValueAnimator createSwipeDismissAnimation() { + float velocityPxPerMs = FloatingWindowUtil.dpToPx(mDisplayMetrics, VELOCITY_DP_PER_MS); + return createSwipeDismissAnimation(velocityPxPerMs); + } + private ValueAnimator createSwipeDismissAnimation(float velocity) { // velocity is measured in pixels per millisecond velocity = Math.min(3, Math.max(1, velocity)); @@ -337,7 +345,7 @@ public class DraggableConstraintLayout extends ConstraintLayout if (startX > 0 || (startX == 0 && layoutDir == LAYOUT_DIRECTION_RTL)) { finalX = mDisplayMetrics.widthPixels; } else { - finalX = -1 * mActionsContainerBackground.getRight(); + finalX = -1 * getBackgroundRight(); } float distance = Math.abs(finalX - startX); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ICrossProfileService.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/ICrossProfileService.aidl new file mode 100644 index 000000000000..da834729d319 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ICrossProfileService.aidl @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2009, 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.screenshot; + +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; + +/** Interface implemented by ScreenshotCrossProfileService */ +interface ICrossProfileService { + + void launchIntent(in Intent intent, in Bundle bundle); +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/IOnDoneCallback.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/IOnDoneCallback.aidl new file mode 100644 index 000000000000..e15030f78234 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/IOnDoneCallback.aidl @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2022, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot; + +interface IOnDoneCallback { + void onDone(boolean success); +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl index f7c4dadc6605..d2e3fbd65762 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl +++ b/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl @@ -16,9 +16,14 @@ package com.android.systemui.screenshot; +import com.android.systemui.screenshot.IOnDoneCallback; + /** Interface implemented by ScreenshotProxyService */ interface IScreenshotProxy { /** Is the notification shade currently exanded? */ boolean isNotificationShadeExpanded(); -}
\ No newline at end of file + + /** Attempts to dismiss the keyguard. */ + void dismissKeyguard(IOnDoneCallback callback); +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index 55602a98b8c5..e3658defc52a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -19,6 +19,7 @@ package com.android.systemui.screenshot; import static android.os.FileUtils.closeQuietly; import android.annotation.IntRange; +import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.graphics.Bitmap; @@ -29,6 +30,7 @@ import android.os.Environment; import android.os.ParcelFileDescriptor; import android.os.SystemClock; import android.os.Trace; +import android.os.UserHandle; import android.provider.MediaStore; import android.util.Log; @@ -142,8 +144,9 @@ class ImageExporter { * * @return a listenable future result */ - ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap) { - return export(executor, requestId, bitmap, ZonedDateTime.now()); + ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, + UserHandle owner) { + return export(executor, requestId, bitmap, ZonedDateTime.now(), owner); } /** @@ -155,10 +158,10 @@ class ImageExporter { * @return a listenable future result */ ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, - ZonedDateTime captureTime) { + ZonedDateTime captureTime, UserHandle owner) { final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat, - mQuality, /* publish */ true); + mQuality, /* publish */ true, owner); return CallbackToFutureAdapter.getFuture( (completer) -> { @@ -174,28 +177,6 @@ class ImageExporter { ); } - /** - * Delete the entry. - * - * @param executor the thread for execution - * @param uri the uri of the image to publish - * - * @return a listenable future result - */ - ListenableFuture<Result> delete(Executor executor, Uri uri) { - return CallbackToFutureAdapter.getFuture((completer) -> { - executor.execute(() -> { - mResolver.delete(uri, null); - - Result result = new Result(); - result.uri = uri; - result.deleted = true; - completer.set(result); - }); - return "ContentResolver#delete"; - }); - } - static class Result { Uri uri; UUID requestId; @@ -203,7 +184,6 @@ class ImageExporter { long timestamp; CompressFormat format; boolean published; - boolean deleted; @Override public String toString() { @@ -214,7 +194,6 @@ class ImageExporter { sb.append(", timestamp=").append(timestamp); sb.append(", format=").append(format); sb.append(", published=").append(published); - sb.append(", deleted=").append(deleted); sb.append('}'); return sb.toString(); } @@ -227,17 +206,19 @@ class ImageExporter { private final ZonedDateTime mCaptureTime; private final CompressFormat mFormat; private final int mQuality; + private final UserHandle mOwner; private final String mFileName; private final boolean mPublish; Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime, - CompressFormat format, int quality, boolean publish) { + CompressFormat format, int quality, boolean publish, UserHandle owner) { mResolver = resolver; mRequestId = requestId; mBitmap = bitmap; mCaptureTime = captureTime; mFormat = format; mQuality = quality; + mOwner = owner; mFileName = createFilename(mCaptureTime, mFormat); mPublish = publish; } @@ -253,7 +234,7 @@ class ImageExporter { start = Instant.now(); } - uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName); + uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName, mOwner); throwIfInterrupted(); writeImage(mResolver, mBitmap, mFormat, mQuality, uri); @@ -297,15 +278,20 @@ class ImageExporter { } private static Uri createEntry(ContentResolver resolver, CompressFormat format, - ZonedDateTime time, String fileName) throws ImageExportException { + ZonedDateTime time, String fileName, UserHandle owner) throws ImageExportException { Trace.beginSection("ImageExporter_createEntry"); try { final ContentValues values = createMetadata(time, format, fileName); - Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + if (UserHandle.myUserId() != owner.getIdentifier()) { + baseUri = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier()); + } + Uri uri = resolver.insert(baseUri, values); if (uri == null) { throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL); } + Log.d(TAG, "Inserted new URI: " + uri); return uri; } finally { Trace.endSection(); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java index ba6e98e79ac0..8bf956b86683 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java @@ -30,6 +30,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; +import android.os.Process; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; @@ -387,7 +388,9 @@ public class LongScreenshotActivity extends Activity { mOutputBitmap = renderBitmap(drawable, bounds); ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export( - mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now()); + mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now(), + // TODO: Owner must match the owner of the captured window. + Process.myUserHandle()); exportFuture.addListener(() -> onExportCompleted(action, exportFuture), mUiExecutor); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt index 309059fdb9ad..95cc0dcadfb4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt @@ -76,7 +76,7 @@ class RequestProcessor @Inject constructor( ) } else { // Create a new request of the same type which includes the top component - ScreenshotRequest(request.source, request.type, info.component) + ScreenshotRequest(request.type, request.source, info.component) } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index f248d6913878..7143ba263570 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -48,6 +48,8 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.systemui.R; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition; import com.google.common.util.concurrent.ListenableFuture; @@ -71,6 +73,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"; private final Context mContext; + private FeatureFlags mFlags; private final ScreenshotSmartActions mScreenshotSmartActions; private final ScreenshotController.SaveImageInBackgroundData mParams; private final ScreenshotController.SavedImageData mImageData; @@ -84,7 +87,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private final ImageExporter mImageExporter; private long mImageTime; - SaveImageInBackgroundTask(Context context, ImageExporter exporter, + SaveImageInBackgroundTask( + Context context, + FeatureFlags flags, + ImageExporter exporter, ScreenshotSmartActions screenshotSmartActions, ScreenshotController.SaveImageInBackgroundData data, Supplier<ActionTransition> sharedElementTransition, @@ -92,6 +98,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { screenshotNotificationSmartActionsProvider ) { mContext = context; + mFlags = flags; mScreenshotSmartActions = screenshotSmartActions; mImageData = new ScreenshotController.SavedImageData(); mQuickShareData = new ScreenshotController.QuickShareData(); @@ -117,7 +124,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { } // TODO: move to constructor / from ScreenshotRequest final UUID requestId = UUID.randomUUID(); - final UserHandle user = getUserHandleOfForegroundApplication(mContext); + final UserHandle user = mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY) + ? mParams.owner : getUserHandleOfForegroundApplication(mContext); Thread.currentThread().setPriority(Thread.MAX_PRIORITY); @@ -133,8 +141,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // Call synchronously here since already on a background thread. ListenableFuture<ImageExporter.Result> future = - mImageExporter.export(Runnable::run, requestId, image); + mImageExporter.export(Runnable::run, requestId, image, mParams.owner); ImageExporter.Result result = future.get(); + Log.d(TAG, "Saved screenshot: " + result); final Uri uri = result.uri; mImageTime = result.timestamp; @@ -157,12 +166,14 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { } mImageData.uri = uri; + mImageData.owner = user; mImageData.smartActions = smartActions; mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri); mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri); mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri); mImageData.quickShareAction = createQuickShareAction(mContext, mQuickShareData.quickShareAction, uri); + mImageData.subject = getSubjectString(); mParams.mActionsReadyListener.onActionsReady(mImageData); if (DEBUG_CALLBACK) { @@ -227,8 +238,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // Create a share intent, this will always go through the chooser activity first // which should not trigger auto-enter PiP - String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime)); - String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); Intent sharingIntent = new Intent(Intent.ACTION_SEND); sharingIntent.setDataAndType(uri, "image/png"); sharingIntent.putExtra(Intent.EXTRA_STREAM, uri); @@ -238,7 +247,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}), new ClipData.Item(uri)); sharingIntent.setClipData(clipdata); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject); + sharingIntent.putExtra(Intent.EXTRA_SUBJECT, getSubjectString()); sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); @@ -308,7 +317,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // by setting the (otherwise unused) request code to the current user id. int requestCode = mContext.getUserId(); - // Create a edit action + // Create an edit action PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode, new Intent(context, ActionProxyReceiver.class) .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent) @@ -469,4 +478,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData); } } + + private String getSubjectString() { + String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime)); + return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); + } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 3fee232b3465..d524a356a323 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -20,6 +20,7 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; +import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS; @@ -28,12 +29,14 @@ import static com.android.systemui.screenshot.LogConfig.DEBUG_UI; import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW; import static com.android.systemui.screenshot.LogConfig.logTag; import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER; +import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT; import static java.util.Objects.requireNonNull; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.MainThread; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; @@ -57,7 +60,9 @@ import android.media.AudioSystem; import android.media.MediaPlayer; import android.net.Uri; import android.os.Bundle; +import android.os.Process; import android.os.RemoteException; +import android.os.UserHandle; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; @@ -90,6 +95,7 @@ import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.clipboardoverlay.ClipboardOverlayController; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition; import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback; import com.android.systemui.util.Assert; @@ -151,6 +157,7 @@ public class ScreenshotController { public Consumer<Uri> finisher; public ScreenshotController.ActionsReadyListener mActionsReadyListener; public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener; + public UserHandle owner; void clearImage() { image = null; @@ -167,6 +174,8 @@ public class ScreenshotController { public Notification.Action deleteAction; public List<Notification.Action> smartActions; public Notification.Action quickShareAction; + public UserHandle owner; + public String subject; // Title for sharing /** * POD for shared element transition. @@ -187,6 +196,7 @@ public class ScreenshotController { deleteAction = null; smartActions = null; quickShareAction = null; + subject = null; } } @@ -242,6 +252,7 @@ public class ScreenshotController { private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; private final WindowContext mContext; + private final FeatureFlags mFlags; private final ScreenshotNotificationsController mNotificationsController; private final ScreenshotSmartActions mScreenshotSmartActions; private final UiEventLogger mUiEventLogger; @@ -264,6 +275,7 @@ public class ScreenshotController { private final ScreenshotNotificationSmartActionsProvider mScreenshotNotificationSmartActionsProvider; private final TimeoutHandler mScreenshotHandler; + private final ActionIntentExecutor mActionExecutor; private ScreenshotView mScreenshotView; private Bitmap mScreenBitmap; @@ -288,6 +300,7 @@ public class ScreenshotController { @Inject ScreenshotController( Context context, + FeatureFlags flags, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController screenshotNotificationsController, ScrollCaptureClient scrollCaptureClient, @@ -300,7 +313,8 @@ public class ScreenshotController { ActivityManager activityManager, TimeoutHandler timeoutHandler, BroadcastSender broadcastSender, - ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider + ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, + ActionIntentExecutor actionExecutor ) { mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsController; @@ -322,15 +336,15 @@ public class ScreenshotController { if (DEBUG_UI) { Log.d(TAG, "Corner timeout hit"); } - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT, 0, - mPackageName); - ScreenshotController.this.dismissScreenshot(false); + dismissScreenshot(SCREENSHOT_INTERACTION_TIMEOUT); }); mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class)); final Context displayContext = context.createDisplayContext(getDefaultDisplay()); mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); mWindowManager = mContext.getSystemService(WindowManager.class); + mFlags = flags; + mActionExecutor = actionExecutor; mAccessibilityManager = AccessibilityManager.getInstance(mContext); @@ -351,8 +365,7 @@ public class ScreenshotController { @Override public void onReceive(Context context, Intent intent) { if (ClipboardOverlayController.COPY_OVERLAY_ACTION.equals(intent.getAction())) { - mUiEventLogger.log(SCREENSHOT_DISMISSED_OTHER); - dismissScreenshot(false); + dismissScreenshot(SCREENSHOT_DISMISSED_OTHER); } } }; @@ -377,7 +390,6 @@ public class ScreenshotController { void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, Insets visibleInsets, int taskId, int userId, ComponentName topComponent, Consumer<Uri> finisher, RequestCallback requestCallback) { - // TODO: use task Id, userId, topComponent for smart handler Assert.isMainThread(); if (screenshot == null) { Log.e(TAG, "Got null bitmap from screenshot message"); @@ -395,48 +407,26 @@ public class ScreenshotController { } mCurrentRequestCallback = requestCallback; saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent, - showFlash); - } - - /** - * Displays a screenshot selector - */ - @MainThread - void takeScreenshotPartial(ComponentName topComponent, - final Consumer<Uri> finisher, RequestCallback requestCallback) { - Assert.isMainThread(); - mScreenshotView.reset(); - mCurrentRequestCallback = requestCallback; - - attachWindow(); - mWindow.setContentView(mScreenshotView); - mScreenshotView.requestApplyInsets(); - - mScreenshotView.takePartialScreenshot( - rect -> takeScreenshotInternal(topComponent, finisher, rect)); + showFlash, UserHandle.of(userId)); } /** * Clears current screenshot */ - void dismissScreenshot(boolean immediate) { + void dismissScreenshot(ScreenshotEvent event) { if (DEBUG_DISMISS) { - Log.d(TAG, "dismissScreenshot(immediate=" + immediate + ")"); + Log.d(TAG, "dismissScreenshot"); } // If we're already animating out, don't restart the animation - // (but do obey an immediate dismissal) - if (!immediate && mScreenshotView.isDismissing()) { + if (mScreenshotView.isDismissing()) { if (DEBUG_DISMISS) { Log.v(TAG, "Already dismissing, ignoring duplicate command"); } return; } + mUiEventLogger.log(event, 0, mPackageName); mScreenshotHandler.cancelTimeout(); - if (immediate) { - finishDismiss(); - } else { - mScreenshotView.animateDismissal(); - } + mScreenshotView.animateDismissal(); } boolean isPendingSharedTransition() { @@ -501,7 +491,7 @@ public class ScreenshotController { // TODO(159460485): Remove this when focus is handled properly in the system setWindowFocusable(false); } - }); + }, mActionExecutor, mFlags); mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis()); mScreenshotView.setOnKeyListener((v, keyCode, event) -> { @@ -509,7 +499,7 @@ public class ScreenshotController { if (DEBUG_INPUT) { Log.d(TAG, "onKeyEvent: KeyEvent.KEYCODE_BACK"); } - dismissScreenshot(false); + dismissScreenshot(SCREENSHOT_DISMISSED_OTHER); return true; } return false; @@ -543,14 +533,15 @@ public class ScreenshotController { return; } - saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true); + saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true, + Process.myUserHandle()); mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION), ClipboardOverlayController.SELF_PERMISSION); } private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, - Insets screenInsets, ComponentName topComponent, boolean showFlash) { + Insets screenInsets, ComponentName topComponent, boolean showFlash, UserHandle owner) { withWindowAttached(() -> mScreenshotView.announceForAccessibility( mContext.getResources().getString(R.string.screenshot_saving_title))); @@ -575,11 +566,11 @@ public class ScreenshotController { mScreenBitmap = screenshot; - if (!isUserSetupComplete()) { + if (!isUserSetupComplete(owner)) { Log.w(TAG, "User setup not complete, displaying toast only"); // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing // and sharing shouldn't be exposed to the user. - saveScreenshotAndToast(finisher); + saveScreenshotAndToast(owner, finisher); return; } @@ -587,7 +578,7 @@ public class ScreenshotController { mScreenBitmap.setHasAlpha(false); mScreenBitmap.prepareToDraw(); - saveScreenshotInWorkerThread(finisher, this::showUiOnActionsReady, + saveScreenshotInWorkerThread(owner, finisher, this::showUiOnActionsReady, this::showUiOnQuickShareActionReady); // The window is focusable by default @@ -644,6 +635,11 @@ public class ScreenshotController { return true; } }); + + if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { + mScreenshotView.badgeScreenshot( + mContext.getPackageManager().getUserBadgeForDensity(owner, 0)); + } mScreenshotView.setScreenshot(mScreenBitmap, screenInsets); if (DEBUG_WINDOW) { Log.d(TAG, "setContentView: " + mScreenshotView); @@ -853,11 +849,12 @@ public class ScreenshotController { * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on * failure). */ - private void saveScreenshotAndToast(Consumer<Uri> finisher) { + private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) { // Play the shutter sound to notify that we've taken a screenshot playCameraSound(); saveScreenshotInWorkerThread( + owner, /* onComplete */ finisher, /* actionsReadyListener */ imageData -> { if (DEBUG_CALLBACK) { @@ -925,9 +922,11 @@ public class ScreenshotController { /** * Creates a new worker thread and saves the screenshot to the media store. */ - private void saveScreenshotInWorkerThread(Consumer<Uri> finisher, - @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener, - @Nullable ScreenshotController.QuickShareActionReadyListener + private void saveScreenshotInWorkerThread( + UserHandle owner, + @NonNull Consumer<Uri> finisher, + @Nullable ActionsReadyListener actionsReadyListener, + @Nullable QuickShareActionReadyListener quickShareActionsReadyListener) { ScreenshotController.SaveImageInBackgroundData data = new ScreenshotController.SaveImageInBackgroundData(); @@ -935,13 +934,14 @@ public class ScreenshotController { data.finisher = finisher; data.mActionsReadyListener = actionsReadyListener; data.mQuickShareActionsReadyListener = quickShareActionsReadyListener; + data.owner = owner; if (mSaveInBgTask != null) { // just log success/failure for the pre-existing screenshot mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); } - mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter, + mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter, mScreenshotSmartActions, data, getActionTransitionSupplier(), mScreenshotNotificationSmartActionsProvider); mSaveInBgTask.execute(); @@ -960,6 +960,15 @@ public class ScreenshotController { mScreenshotHandler.resetTimeout(); if (imageData.uri != null) { + if (!imageData.owner.equals(Process.myUserHandle())) { + // TODO: Handle non-primary user ownership (e.g. Work Profile) + // This image is owned by another user. Special treatment will be + // required in the UI (badging) as well as sending intents which can + // correctly forward those URIs on to be read (actions). + + Log.d(TAG, "*** Screenshot saved to a non-primary user (" + + imageData.owner + ") as " + imageData.uri); + } mScreenshotHandler.post(() -> { if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { @@ -1033,9 +1042,9 @@ public class ScreenshotController { } } - private boolean isUserSetupComplete() { - return Settings.Secure.getInt(mContext.getContentResolver(), - SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; + private boolean isUserSetupComplete(UserHandle owner) { + return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0) + .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt new file mode 100644 index 000000000000..2e6c7567259f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.app.Service +import android.content.Intent +import android.os.Bundle +import android.os.IBinder +import android.util.Log + +/** + * If a screenshot is saved to the work profile, any intents that grant access to the screenshot + * must come from a service running as the work profile user. This service is meant to be started as + * the desired user and just startActivity for the given intent. + */ +class ScreenshotCrossProfileService : Service() { + + private val mBinder: IBinder = + object : ICrossProfileService.Stub() { + override fun launchIntent(intent: Intent, bundle: Bundle) { + startActivity(intent, bundle) + } + } + + override fun onBind(intent: Intent): IBinder? { + Log.d(TAG, "onBind: $intent") + return mBinder + } + + companion object { + const val TAG = "ScreenshotProxyService" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt index c2a50609b6a5..3a3528606302 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt @@ -68,7 +68,9 @@ internal open class ScreenshotPolicyImpl @Inject constructor( } override suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean { - return withContext(bgDispatcher) { userMgr.isManagedProfile(userId) } + val managed = withContext(bgDispatcher) { userMgr.isManagedProfile(userId) } + Log.d(TAG, "isManagedProfile: $managed") + return managed } private fun nonPipVisibleTask(info: RootTaskInfo): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt index 9654e03e506e..c41e2bc14afc 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt @@ -19,14 +19,17 @@ import android.app.Service import android.content.Intent import android.os.IBinder import android.util.Log -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager +import com.android.systemui.shade.ShadeExpansionStateManager +import com.android.systemui.statusbar.phone.CentralSurfaces +import java.util.Optional import javax.inject.Inject /** * Provides state from the main SystemUI process on behalf of the Screenshot process. */ internal class ScreenshotProxyService @Inject constructor( - private val mExpansionMgr: PanelExpansionStateManager + private val mExpansionMgr: ShadeExpansionStateManager, + private val mCentralSurfacesOptional: Optional<CentralSurfaces>, ) : Service() { private val mBinder: IBinder = object : IScreenshotProxy.Stub() { @@ -38,6 +41,20 @@ internal class ScreenshotProxyService @Inject constructor( Log.d(TAG, "isNotificationShadeExpanded(): $expanded") return expanded } + + override fun dismissKeyguard(callback: IOnDoneCallback) { + if (mCentralSurfacesOptional.isPresent) { + mCentralSurfacesOptional.get().executeRunnableDismissingKeyguard( + Runnable { + callback.onDone(true) + }, null, + true /* dismissShade */, true /* afterKeyguardGone */, + true /* deferred */ + ) + } else { + callback.onDone(false) + } + } } override fun onBind(intent: Intent): IBinder? { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java deleted file mode 100644 index c793b5b9639e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.screenshot; - -import android.annotation.Nullable; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - -import java.util.function.Consumer; - -/** - * Draws a selection rectangle while taking screenshot - */ -public class ScreenshotSelectorView extends View { - private Point mStartPoint; - private Rect mSelectionRect; - private final Paint mPaintSelection, mPaintBackground; - - private Consumer<Rect> mOnScreenshotSelected; - - public ScreenshotSelectorView(Context context) { - this(context, null); - } - - public ScreenshotSelectorView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - mPaintBackground = new Paint(Color.BLACK); - mPaintBackground.setAlpha(160); - mPaintSelection = new Paint(Color.TRANSPARENT); - mPaintSelection.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); - - setOnTouchListener((v, event) -> { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - startSelection((int) event.getX(), (int) event.getY()); - return true; - case MotionEvent.ACTION_MOVE: - updateSelection((int) event.getX(), (int) event.getY()); - return true; - case MotionEvent.ACTION_UP: - setVisibility(View.GONE); - final Rect rect = getSelectionRect(); - if (mOnScreenshotSelected != null - && rect != null - && rect.width() != 0 && rect.height() != 0) { - mOnScreenshotSelected.accept(rect); - } - stopSelection(); - return true; - } - return false; - }); - } - - @Override - public void draw(Canvas canvas) { - canvas.drawRect(mLeft, mTop, mRight, mBottom, mPaintBackground); - if (mSelectionRect != null) { - canvas.drawRect(mSelectionRect, mPaintSelection); - } - } - - void setOnScreenshotSelected(Consumer<Rect> onScreenshotSelected) { - mOnScreenshotSelected = onScreenshotSelected; - } - - void stop() { - if (getSelectionRect() != null) { - stopSelection(); - } - } - - private void startSelection(int x, int y) { - mStartPoint = new Point(x, y); - mSelectionRect = new Rect(x, y, x, y); - } - - private void updateSelection(int x, int y) { - if (mSelectionRect != null) { - mSelectionRect.left = Math.min(mStartPoint.x, x); - mSelectionRect.right = Math.max(mStartPoint.x, x); - mSelectionRect.top = Math.min(mStartPoint.y, y); - mSelectionRect.bottom = Math.max(mStartPoint.y, y); - invalidate(); - } - } - - private Rect getSelectionRect() { - return mSelectionRect; - } - - private void stopSelection() { - mStartPoint = null; - mSelectionRect = null; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 360fc879731c..27331ae7a389 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -74,7 +74,6 @@ import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; import android.view.accessibility.AccessibilityManager; -import android.view.animation.AccelerateInterpolator; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.FrameLayout; @@ -87,13 +86,14 @@ import androidx.constraintlayout.widget.ConstraintLayout; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.shared.system.InputMonitorCompat; import com.android.systemui.shared.system.QuickStepContract; import java.util.ArrayList; -import java.util.function.Consumer; /** * Handles the visual elements and animations for the screenshot flow. @@ -121,15 +121,9 @@ public class ScreenshotView extends FrameLayout implements private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234; private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; - private static final long SCREENSHOT_DISMISS_X_DURATION_MS = 350; - private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 350; - private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; - private static final float ROUNDED_CORNER_RADIUS = .25f; private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe - private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator(); - private final Resources mResources; private final Interpolator mFastOutSlowIn; private final DisplayMetrics mDisplayMetrics; @@ -141,10 +135,10 @@ public class ScreenshotView extends FrameLayout implements private boolean mOrientationPortrait; private boolean mDirectionLTR; - private ScreenshotSelectorView mScreenshotSelectorView; private ImageView mScrollingScrim; private DraggableConstraintLayout mScreenshotStatic; private ImageView mScreenshotPreview; + private ImageView mScreenshotBadge; private View mScreenshotPreviewBorder; private ImageView mScrollablePreview; private ImageView mScreenshotFlash; @@ -170,6 +164,8 @@ public class ScreenshotView extends FrameLayout implements private final InteractionJankMonitor mInteractionJankMonitor; private long mDefaultTimeoutOfTimeoutHandler; + private ActionIntentExecutor mActionExecutor; + private FeatureFlags mFlags; private enum PendingInteraction { PREVIEW, @@ -353,6 +349,7 @@ public class ScreenshotView extends FrameLayout implements mScreenshotPreviewBorder = requireNonNull( findViewById(R.id.screenshot_preview_border)); mScreenshotPreview.setClipToOutline(true); + mScreenshotBadge = requireNonNull(findViewById(R.id.screenshot_badge)); mActionsContainerBackground = requireNonNull(findViewById( R.id.actions_container_background)); @@ -361,7 +358,6 @@ public class ScreenshotView extends FrameLayout implements mDismissButton = requireNonNull(findViewById(R.id.screenshot_dismiss_button)); mScrollablePreview = requireNonNull(findViewById(R.id.screenshot_scrollable_preview)); mScreenshotFlash = requireNonNull(findViewById(R.id.screenshot_flash)); - mScreenshotSelectorView = requireNonNull(findViewById(R.id.screenshot_selector)); mShareChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_share_chip)); mEditChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_edit_chip)); mScrollChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_scroll_chip)); @@ -377,8 +373,6 @@ public class ScreenshotView extends FrameLayout implements mActionsContainerBackground.setTouchDelegate(actionsDelegate); setFocusable(true); - mScreenshotSelectorView.setFocusable(true); - mScreenshotSelectorView.setFocusableInTouchMode(true); mActionsContainer.setScrollX(0); mNavMode = getResources().getInteger( @@ -427,15 +421,12 @@ public class ScreenshotView extends FrameLayout implements * Note: must be called before any other (non-constructor) method or null pointer exceptions * may occur. */ - void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks) { + void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks, + ActionIntentExecutor actionExecutor, FeatureFlags flags) { mUiEventLogger = uiEventLogger; mCallbacks = callbacks; - } - - void takePartialScreenshot(Consumer<Rect> onPartialScreenshotSelected) { - mScreenshotSelectorView.setOnScreenshotSelected(onPartialScreenshotSelected); - mScreenshotSelectorView.setVisibility(View.VISIBLE); - mScreenshotSelectorView.requestFocus(); + mActionExecutor = actionExecutor; + mFlags = flags; } void setScreenshot(Bitmap bitmap, Insets screenInsets) { @@ -599,8 +590,11 @@ public class ScreenshotView extends FrameLayout implements ValueAnimator borderFadeIn = ValueAnimator.ofFloat(0, 1); borderFadeIn.setDuration(100); - borderFadeIn.addUpdateListener((animation) -> - mScreenshotPreviewBorder.setAlpha(animation.getAnimatedFraction())); + borderFadeIn.addUpdateListener((animation) -> { + float borderAlpha = animation.getAnimatedFraction(); + mScreenshotPreviewBorder.setAlpha(borderAlpha); + mScreenshotBadge.setAlpha(borderAlpha); + }); if (showFlash) { dropInAnimation.play(flashOutAnimator).after(flashInAnimator); @@ -767,21 +761,49 @@ public class ScreenshotView extends FrameLayout implements return animator; } + void badgeScreenshot(Drawable badge) { + mScreenshotBadge.setImageDrawable(badge); + mScreenshotBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE); + } + void setChipIntents(ScreenshotController.SavedImageData imageData) { mShareChip.setOnClickListener(v -> { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName); - startSharedTransition( - imageData.shareTransition.get()); + if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) { + prepareSharedTransition(); + mActionExecutor.launchIntentAsync( + ActionIntentCreator.INSTANCE.createShareIntent( + imageData.uri, imageData.subject), + imageData.shareTransition.get().bundle, + imageData.owner.getIdentifier(), false); + } else { + startSharedTransition(imageData.shareTransition.get()); + } }); mEditChip.setOnClickListener(v -> { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName); - startSharedTransition( - imageData.editTransition.get()); + if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) { + prepareSharedTransition(); + mActionExecutor.launchIntentAsync( + ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext), + imageData.editTransition.get().bundle, + imageData.owner.getIdentifier(), true); + } else { + startSharedTransition(imageData.editTransition.get()); + } }); mScreenshotPreview.setOnClickListener(v -> { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName); - startSharedTransition( - imageData.editTransition.get()); + if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) { + prepareSharedTransition(); + mActionExecutor.launchIntentAsync( + ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext), + imageData.editTransition.get().bundle, + imageData.owner.getIdentifier(), true); + } else { + startSharedTransition( + imageData.editTransition.get()); + } }); if (mQuickShareChip != null) { mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent, @@ -1008,6 +1030,9 @@ public class ScreenshotView extends FrameLayout implements mScreenshotPreview.setVisibility(View.INVISIBLE); mScreenshotPreview.setAlpha(1f); mScreenshotPreviewBorder.setAlpha(0); + mScreenshotBadge.setAlpha(0f); + mScreenshotBadge.setVisibility(View.GONE); + mScreenshotBadge.setImageDrawable(null); mPendingSharedTransition = false; mActionsContainerBackground.setVisibility(View.GONE); mActionsContainer.setVisibility(View.GONE); @@ -1031,7 +1056,6 @@ public class ScreenshotView extends FrameLayout implements mQuickShareChip = null; setAlpha(1); mScreenshotStatic.setAlpha(1); - mScreenshotSelectorView.stop(); } private void startSharedTransition(ActionTransition transition) { @@ -1050,6 +1074,12 @@ public class ScreenshotView extends FrameLayout implements } } + private void prepareSharedTransition() { + mPendingSharedTransition = true; + // fade out non-preview UI + createScreenshotFadeDismissAnimation().start(); + } + ValueAnimator createScreenshotFadeDismissAnimation() { ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); alphaAnim.addUpdateListener(animation -> { @@ -1058,6 +1088,7 @@ public class ScreenshotView extends FrameLayout implements mActionsContainerBackground.setAlpha(alpha); mActionsContainer.setAlpha(alpha); mScreenshotPreviewBorder.setAlpha(alpha); + mScreenshotBadge.setAlpha(alpha); }); alphaAnim.setDuration(600); return alphaAnim; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java index 83b60fb23b90..30a0b8f2d76f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -78,6 +78,7 @@ public class ScrollCaptureController { static class LongScreenshot { private final ImageTileSet mImageTileSet; private final Session mSession; + // TODO: Add UserHandle so LongScreenshots can adhere to work profile screenshot policy LongScreenshot(Session session, ImageTileSet imageTileSet) { mSession = session; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index 695a80b2b95d..2176825d8b38 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -89,8 +89,7 @@ public class TakeScreenshotService extends Service { Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS"); } if (!mScreenshot.isPendingSharedTransition()) { - mUiEventLogger.log(SCREENSHOT_DISMISSED_OTHER); - mScreenshot.dismissScreenshot(false); + mScreenshot.dismissScreenshot(SCREENSHOT_DISMISSED_OTHER); } } } @@ -249,12 +248,6 @@ public class TakeScreenshotService extends Service { } mScreenshot.takeScreenshotFullscreen(topComponent, uriConsumer, callback); break; - case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION: - if (DEBUG_SERVICE) { - Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_SELECTED_REGION"); - } - mScreenshot.takeScreenshotPartial(topComponent, uriConsumer, callback); - break; case WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE: if (DEBUG_SERVICE) { Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_PROVIDED_IMAGE"); diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java index bbba0071094b..b36f0d7bacfc 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java @@ -33,13 +33,13 @@ import android.view.animation.DecelerateInterpolator; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; +import com.android.systemui.statusbar.notification.stack.StackStateAnimator; /** * Drawable used on SysUI scrims. */ public class ScrimDrawable extends Drawable { private static final String TAG = "ScrimDrawable"; - private static final long COLOR_ANIMATION_DURATION = 2000; private final Paint mPaint; private int mAlpha = 255; @@ -76,7 +76,7 @@ public class ScrimDrawable extends Drawable { final int mainFrom = mMainColor; ValueAnimator anim = ValueAnimator.ofFloat(0, 1); - anim.setDuration(COLOR_ANIMATION_DURATION); + anim.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); anim.addUpdateListener(animation -> { float ratio = (float) animation.getAnimatedValue(); mMainColor = ColorUtils.blendARGB(mainFrom, mainColor, ratio); diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt index ad073c073ed8..d450afa59c7d 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt @@ -42,11 +42,11 @@ import javax.inject.Inject @SysUISingleton class UserFileManagerImpl @Inject constructor( // Context of system process and system user. - val context: Context, + private val context: Context, val userManager: UserManager, val broadcastDispatcher: BroadcastDispatcher, @Background val backgroundExecutor: DelayableExecutor -) : UserFileManager, CoreStartable(context) { +) : UserFileManager, CoreStartable { companion object { private const val FILES = "files" @VisibleForTesting internal const val SHARED_PREFS = "shared_prefs" diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java index a22fda7f9a47..d5a395436271 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java @@ -20,11 +20,13 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.app.Activity; +import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; +import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; @@ -35,6 +37,8 @@ import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; +import java.util.List; + import javax.inject.Inject; /** A dialog that provides controls for adjusting the screen brightness. */ @@ -76,6 +80,21 @@ public class BrightnessDialog extends Activity { FrameLayout frame = findViewById(R.id.brightness_mirror_container); // The brightness mirror container is INVISIBLE by default. frame.setVisibility(View.VISIBLE); + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) frame.getLayoutParams(); + int horizontalMargin = + getResources().getDimensionPixelSize(R.dimen.notification_side_paddings); + lp.leftMargin = horizontalMargin; + lp.rightMargin = horizontalMargin; + frame.setLayoutParams(lp); + Rect bounds = new Rect(); + frame.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + // Exclude this view (and its horizontal margins) from triggering gestures. + // This prevents back gesture from being triggered by dragging close to the + // edge of the slider (0% or 100%). + bounds.set(-horizontalMargin, 0, right - left + horizontalMargin, bottom - top); + v.setSystemGestureExclusionRects(List.of(bounds)); + }); BrightnessSliderController controller = mToggleSliderFactory.create(this, frame); controller.init(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt index d3ed47407b9d..6b540aa9f392 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt @@ -280,6 +280,9 @@ class LargeScreenShadeHeaderController @Inject constructor( context.getString(com.android.internal.R.string.status_bar_alarm_clock) ) } + if (combinedHeaders) { + privacyIconsController.onParentVisible() + } } override fun onViewAttached() { @@ -289,6 +292,7 @@ class LargeScreenShadeHeaderController @Inject constructor( clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ -> val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f v.pivotX = newPivot + v.pivotY = v.height.toFloat() / 2 } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt index 07e8b9fe3123..754036d3baa9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt @@ -16,7 +16,7 @@ package com.android.systemui.shade import android.view.MotionEvent import com.android.systemui.dump.DumpsysTableLogger import com.android.systemui.dump.Row -import com.android.systemui.util.collection.RingBuffer +import com.android.systemui.plugins.util.RingBuffer import java.text.SimpleDateFormat import java.util.Locale diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt index e0cd482166b8..ba779c6233fb 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt @@ -20,8 +20,8 @@ import android.content.Context import android.view.ViewGroup import com.android.systemui.R import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator -import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.LEFT -import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.RIGHT +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate import com.android.systemui.unfold.SysUIUnfoldScope import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider @@ -36,11 +36,11 @@ constructor(private val context: Context, progressProvider: NaturalRotationUnfol UnfoldConstantTranslateAnimator( viewsIdToTranslate = setOf( - ViewIdToTranslate(R.id.quick_settings_panel, LEFT), - ViewIdToTranslate(R.id.notification_stack_scroller, RIGHT), - ViewIdToTranslate(R.id.rightLayout, RIGHT), - ViewIdToTranslate(R.id.clock, LEFT), - ViewIdToTranslate(R.id.date, LEFT)), + ViewIdToTranslate(R.id.quick_settings_panel, START), + ViewIdToTranslate(R.id.notification_stack_scroller, END), + ViewIdToTranslate(R.id.rightLayout, END), + ViewIdToTranslate(R.id.clock, START), + ViewIdToTranslate(R.id.date, START)), progressProvider = progressProvider) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index d7e86b6e2919..ddb57f74cacf 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -17,6 +17,8 @@ package com.android.systemui.shade; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; import static androidx.constraintlayout.widget.ConstraintSet.END; import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; @@ -26,8 +28,15 @@ import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; import static com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE; import static com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE; +import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; +import static com.android.systemui.classifier.Classifier.GENERIC; import static com.android.systemui.classifier.Classifier.QS_COLLAPSE; import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; +import static com.android.systemui.classifier.Classifier.UNLOCK; +import static com.android.systemui.shade.NotificationPanelView.DEBUG; +import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED; +import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN; +import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPENING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; @@ -36,11 +45,10 @@ import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED; import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD; -import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED; -import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN; -import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING; import static com.android.systemui.util.DumpUtilsKt.asIndenting; +import static java.lang.Float.isNaN; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -48,6 +56,8 @@ import android.annotation.NonNull; import android.app.Fragment; import android.app.StatusBarManager; import android.content.ContentResolver; +import android.content.res.Configuration; +import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Canvas; import android.graphics.Color; @@ -69,15 +79,20 @@ import android.os.UserManager; import android.os.VibrationEffect; import android.provider.Settings; import android.transition.ChangeBounds; +import android.transition.Transition; import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.transition.TransitionValues; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; +import android.view.InputDevice; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.View.AccessibilityDelegate; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewPropertyAnimator; import android.view.ViewStub; @@ -86,6 +101,7 @@ import android.view.WindowInsets; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.Interpolator; import android.widget.FrameLayout; import androidx.annotation.Nullable; @@ -131,10 +147,12 @@ import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; -import com.android.systemui.media.KeyguardMediaController; -import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.MediaHierarchyManager; +import com.android.systemui.media.controls.pipeline.MediaDataManager; +import com.android.systemui.media.controls.ui.KeyguardMediaController; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; import com.android.systemui.model.SysUiState; +import com.android.systemui.navigationbar.NavigationBarController; +import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.FalsingManager.FalsingTapListener; @@ -178,6 +196,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; +import com.android.systemui.statusbar.phone.BounceInterpolator; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; @@ -202,8 +221,6 @@ import com.android.systemui.statusbar.phone.TapAgainViewController; import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; -import com.android.systemui.statusbar.phone.panelstate.PanelState; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -232,8 +249,13 @@ import javax.inject.Inject; import javax.inject.Provider; @CentralSurfacesComponent.CentralSurfacesScope -public final class NotificationPanelViewController extends PanelViewController { +public final class NotificationPanelViewController { + public static final String TAG = NotificationPanelView.class.getSimpleName(); + public static final float FLING_MAX_LENGTH_SECONDS = 0.6f; + public static final float FLING_SPEED_UP_FACTOR = 0.6f; + public static final float FLING_CLOSING_MAX_LENGTH_SECONDS = 0.6f; + public static final float FLING_CLOSING_SPEED_UP_FACTOR = 0.6f; private static final boolean DEBUG_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG); private static final boolean SPEW_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE); private static final boolean DEBUG_DRAWABLE = false; @@ -264,6 +286,22 @@ public final class NotificationPanelViewController extends PanelViewController { ActivityLaunchAnimator.TIMINGS.getTotalDuration() - CollapsedStatusBarFragment.FADE_IN_DURATION - CollapsedStatusBarFragment.FADE_IN_DELAY - 48; + private static final int NO_FIXED_DURATION = -1; + private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L; + private static final long SHADE_OPEN_SPRING_BACK_DURATION = 400L; + /** + * The factor of the usual high velocity that is needed in order to reach the maximum overshoot + * when flinging. A low value will make it that most flings will reach the maximum overshoot. + */ + private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f; + private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager; + private final Resources mResources; + private final KeyguardStateController mKeyguardStateController; + private final SysuiStatusBarStateController mStatusBarStateController; + private final AmbientState mAmbientState; + private final LockscreenGestureLogger mLockscreenGestureLogger; + private final SystemClock mSystemClock; + private final ShadeLogger mShadeLog; private final DozeParameters mDozeParameters; private final OnHeightChangedListener mOnHeightChangedListener = new OnHeightChangedListener(); @@ -335,6 +373,28 @@ public final class NotificationPanelViewController extends PanelViewController { private final LargeScreenShadeHeaderController mLargeScreenShadeHeaderController; private final RecordingController mRecordingController; private final PanelEventsEmitter mPanelEventsEmitter; + private final boolean mVibrateOnOpening; + private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); + private final FlingAnimationUtils mFlingAnimationUtilsClosing; + private final FlingAnimationUtils mFlingAnimationUtilsDismissing; + private final LatencyTracker mLatencyTracker; + private final DozeLog mDozeLog; + /** Whether or not the NotificationPanelView can be expanded or collapsed with a drag. */ + private final boolean mNotificationsDragEnabled; + private final Interpolator mBounceInterpolator; + private final NotificationShadeWindowController mNotificationShadeWindowController; + private final ShadeExpansionStateManager mShadeExpansionStateManager; + private long mDownTime; + private boolean mTouchSlopExceededBeforeDown; + private boolean mIsLaunchAnimationRunning; + private float mOverExpansion; + private CentralSurfaces mCentralSurfaces; + private HeadsUpManagerPhone mHeadsUpManager; + private float mExpandedHeight = 0; + private boolean mTracking; + private boolean mHintAnimationRunning; + private KeyguardBottomAreaView mKeyguardBottomArea; + private boolean mExpanding; private boolean mSplitShadeEnabled; /** The bottom padding reserved for elements of the keyguard measuring notifications. */ private float mKeyguardNotificationBottomPadding; @@ -425,11 +485,10 @@ public final class NotificationPanelViewController extends PanelViewController { new KeyguardClockPositionAlgorithm.Result(); private boolean mIsExpanding; - private boolean mBlockTouches; - /** * Determines if QS should be already expanded when expanding shade. * Used for split shade, two finger gesture as well as accessibility shortcut to QS. + * It needs to be set when movement starts as it resets at the end of expansion/collapse. */ @VisibleForTesting boolean mQsExpandImmediate; @@ -526,6 +585,7 @@ public final class NotificationPanelViewController extends PanelViewController { private final SysUiState mSysUiState; private final NotificationShadeDepthController mDepthController; + private final NavigationBarController mNavigationBarController; private final int mDisplayId; private KeyguardIndicationController mKeyguardIndicationController; @@ -635,6 +695,7 @@ public final class NotificationPanelViewController extends PanelViewController { private int mScreenCornerRadius; private boolean mQSAnimatingHiddenFromCollapsed; private boolean mUseLargeScreenShadeHeader; + private boolean mEnableQsClipping; private int mQsClipTop; private int mQsClipBottom; @@ -709,6 +770,54 @@ public final class NotificationPanelViewController extends PanelViewController { private final CameraGestureHelper mCameraGestureHelper; private final KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel; private final KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; + private float mMinExpandHeight; + private boolean mPanelUpdateWhenAnimatorEnds; + private boolean mHasVibratedOnOpen = false; + private int mFixedDuration = NO_FIXED_DURATION; + /** The overshoot amount when the panel flings open. */ + private float mPanelFlingOvershootAmount; + /** The amount of pixels that we have overexpanded the last time with a gesture. */ + private float mLastGesturedOverExpansion = -1; + /** Whether the current animator is the spring back animation. */ + private boolean mIsSpringBackAnimation; + private boolean mInSplitShade; + private float mHintDistance; + private float mInitialOffsetOnTouch; + private boolean mCollapsedAndHeadsUpOnDown; + private float mExpandedFraction = 0; + private float mExpansionDragDownAmountPx = 0; + private boolean mPanelClosedOnDown; + private boolean mHasLayoutedSinceDown; + private float mUpdateFlingVelocity; + private boolean mUpdateFlingOnLayout; + private boolean mClosing; + private boolean mTouchSlopExceeded; + private int mTrackingPointer; + private int mTouchSlop; + private float mSlopMultiplier; + private boolean mTouchAboveFalsingThreshold; + private boolean mTouchStartedInEmptyArea; + private boolean mMotionAborted; + private boolean mUpwardsWhenThresholdReached; + private boolean mAnimatingOnDown; + private boolean mHandlingPointerUp; + private ValueAnimator mHeightAnimator; + /** Whether an instant expand request is currently pending and we are waiting for layout. */ + private boolean mInstantExpanding; + private boolean mAnimateAfterExpanding; + private boolean mIsFlinging; + private String mViewName; + private float mInitialExpandY; + private float mInitialExpandX; + private boolean mTouchDisabled; + private boolean mInitialTouchFromKeyguard; + /** Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time. */ + private float mNextCollapseSpeedUpFactor = 1.0f; + private boolean mGestureWaitForTouchSlop; + private boolean mIgnoreXTouchSlop; + private boolean mExpandLatencyTracking; + private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */, + mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */); @Inject public NotificationPanelViewController(NotificationPanelView view, @@ -755,13 +864,14 @@ public final class NotificationPanelViewController extends PanelViewController { PrivacyDotViewController privacyDotViewController, TapAgainViewController tapAgainViewController, NavigationModeController navigationModeController, + NavigationBarController navigationBarController, FragmentService fragmentService, ContentResolver contentResolver, RecordingController recordingController, LargeScreenShadeHeaderController largeScreenShadeHeaderController, ScreenOffAnimationController screenOffAnimationController, LockscreenGestureLogger lockscreenGestureLogger, - PanelExpansionStateManager panelExpansionStateManager, + ShadeExpansionStateManager shadeExpansionStateManager, NotificationRemoteInputManager remoteInputManager, Optional<SysUIUnfoldComponent> unfoldComponent, InteractionJankMonitor interactionJankMonitor, @@ -778,35 +888,77 @@ public final class NotificationPanelViewController extends PanelViewController { CameraGestureHelper cameraGestureHelper, KeyguardBottomAreaViewModel keyguardBottomAreaViewModel, KeyguardBottomAreaInteractor keyguardBottomAreaInteractor) { - super(view, - falsingManager, - dozeLog, - keyguardStateController, - (SysuiStatusBarStateController) statusBarStateController, - notificationShadeWindowController, - vibratorHelper, - statusBarKeyguardViewManager, - latencyTracker, - flingAnimationUtilsBuilder.get(), - statusBarTouchableRegionManager, - lockscreenGestureLogger, - panelExpansionStateManager, - ambientState, - interactionJankMonitor, - shadeLogger, - systemClock); + keyguardStateController.addCallback(new KeyguardStateController.Callback() { + @Override + public void onKeyguardFadingAwayChanged() { + updateExpandedHeightToMaxHeight(); + } + }); + mAmbientState = ambientState; mView = view; + mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; + mLockscreenGestureLogger = lockscreenGestureLogger; + mShadeExpansionStateManager = shadeExpansionStateManager; + mShadeLog = shadeLogger; + TouchHandler touchHandler = createTouchHandler(); + mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + mViewName = mResources.getResourceName(mView.getId()); + } + + @Override + public void onViewDetachedFromWindow(View v) { + } + }); + + mView.addOnLayoutChangeListener(createLayoutChangeListener()); + mView.setOnTouchListener(touchHandler); + mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener()); + + mResources = mView.getResources(); + mKeyguardStateController = keyguardStateController; + mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController; + mNotificationShadeWindowController = notificationShadeWindowController; + FlingAnimationUtils.Builder fauBuilder = flingAnimationUtilsBuilder.get(); + mFlingAnimationUtils = fauBuilder + .reset() + .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS) + .setSpeedUpFactor(FLING_SPEED_UP_FACTOR) + .build(); + mFlingAnimationUtilsClosing = fauBuilder + .reset() + .setMaxLengthSeconds(FLING_CLOSING_MAX_LENGTH_SECONDS) + .setSpeedUpFactor(FLING_CLOSING_SPEED_UP_FACTOR) + .build(); + mFlingAnimationUtilsDismissing = fauBuilder + .reset() + .setMaxLengthSeconds(0.5f) + .setSpeedUpFactor(0.6f) + .setX2(0.6f) + .setY2(0.84f) + .build(); + mLatencyTracker = latencyTracker; + mBounceInterpolator = new BounceInterpolator(); + mFalsingManager = falsingManager; + mDozeLog = dozeLog; + mNotificationsDragEnabled = mResources.getBoolean( + R.bool.config_enableNotificationShadeDrag); mVibratorHelper = vibratorHelper; + mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation); + mStatusBarTouchableRegionManager = statusBarTouchableRegionManager; + mInteractionJankMonitor = interactionJankMonitor; + mSystemClock = systemClock; mKeyguardMediaController = keyguardMediaController; mPrivacyDotViewController = privacyDotViewController; mMetricsLogger = metricsLogger; mConfigurationController = configurationController; mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder; mMediaHierarchyManager = mediaHierarchyManager; - mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mNotificationsQSContainerController = notificationsQSContainerController; mNotificationListContainer = notificationListContainer; mNotificationStackSizeCalculator = notificationStackSizeCalculator; + mNavigationBarController = navigationBarController; mKeyguardBottomAreaViewControllerProvider = keyguardBottomAreaViewControllerProvider; mNotificationsQSContainerController.init(); mNotificationStackScrollLayoutController = notificationStackScrollLayoutController; @@ -825,7 +977,6 @@ public final class NotificationPanelViewController extends PanelViewController { mLargeScreenShadeHeaderController = largeScreenShadeHeaderController; mLayoutInflater = layoutInflater; mFeatureFlags = featureFlags; - mFalsingManager = falsingManager; mFalsingCollector = falsingCollector; mPowerManager = powerManager; mWakeUpCoordinator = coordinator; @@ -841,7 +992,6 @@ public final class NotificationPanelViewController extends PanelViewController { mUserManager = userManager; mMediaDataManager = mediaDataManager; mTapAgainViewController = tapAgainViewController; - mInteractionJankMonitor = interactionJankMonitor; mSysUiState = sysUiState; mPanelEventsEmitter = panelEventsEmitter; pulseExpansionHandler.setPulseExpandAbortListener(() -> { @@ -861,7 +1011,7 @@ public final class NotificationPanelViewController extends PanelViewController { new DynamicPrivacyControlListener(); dynamicPrivacyController.addListener(dynamicPrivacyControlListener); - panelExpansionStateManager.addStateListener(this::onPanelStateChanged); + shadeExpansionStateManager.addStateListener(this::onPanelStateChanged); mBottomAreaShadeAlphaAnimator = ValueAnimator.ofFloat(1f, 0); mBottomAreaShadeAlphaAnimator.addUpdateListener(animation -> { @@ -1043,9 +1193,14 @@ public final class NotificationPanelViewController extends PanelViewController { controller.setup(mNotificationContainerParent)); } - @Override - protected void loadDimens() { - super.loadDimens(); + @VisibleForTesting + void loadDimens() { + final ViewConfiguration configuration = ViewConfiguration.get(this.mView.getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier(); + mHintDistance = mResources.getDimension(R.dimen.hint_move_distance); + mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount); + mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade); mFlingAnimationUtils = mFlingAnimationUtilsBuilder.get() .setMaxLengthSeconds(0.4f).build(); mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mView.getContext()); @@ -1152,6 +1307,8 @@ public final class NotificationPanelViewController extends PanelViewController { mSplitShadeFullTransitionDistance = mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance); + + mEnableQsClipping = mResources.getBoolean(R.bool.qs_enable_clipping); } private void onSplitShadeEnabledChanged() { @@ -1291,6 +1448,16 @@ public final class NotificationPanelViewController extends PanelViewController { mMaxAllowedKeyguardNotifications = maxAllowed; } + @VisibleForTesting + boolean getClosing() { + return mClosing; + } + + @VisibleForTesting + boolean getIsFlinging() { + return mIsFlinging; + } + private void updateMaxDisplayedNotifications(boolean recompute) { if (recompute) { setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1)); @@ -1518,9 +1685,40 @@ public final class NotificationPanelViewController extends PanelViewController { // horizontally properly. transition.excludeTarget(R.id.status_view_media_container, true); } + transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); - TransitionManager.beginDelayedTransition(mNotificationContainerParent, transition); + + boolean customClockAnimation = + mKeyguardStatusViewController.getClockAnimations() != null + && mKeyguardStatusViewController.getClockAnimations() + .getHasCustomPositionUpdatedAnimation(); + + if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) { + // Find the clock, so we can exclude it from this transition. + FrameLayout clockContainerView = + mView.findViewById(R.id.lockscreen_clock_view_large); + View clockView = clockContainerView.getChildAt(0); + + transition.excludeTarget(clockView, /* exclude= */ true); + + TransitionSet set = new TransitionSet(); + set.addTransition(transition); + + SplitShadeTransitionAdapter adapter = + new SplitShadeTransitionAdapter(mKeyguardStatusViewController); + + // Use linear here, so the actual clock can pick its own interpolator. + adapter.setInterpolator(Interpolators.LINEAR); + adapter.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); + adapter.addTarget(clockView); + set.addTransition(adapter); + + TransitionManager.beginDelayedTransition(mNotificationContainerParent, set); + } else { + TransitionManager.beginDelayedTransition( + mNotificationContainerParent, transition); + } } constraintSet.applyTo(mNotificationContainerParent); @@ -1692,7 +1890,6 @@ public final class NotificationPanelViewController extends PanelViewController { public void resetViews(boolean animate) { mIsLaunchTransitionFinished = false; - mBlockTouches = false; mCentralSurfaces.getGutsManager().closeAndSaveGuts(true /* leavebehind */, true /* force */, true /* controls */, -1 /* x */, -1 /* y */, true /* resetMenu */); if (animate && !isFullyCollapsed()) { @@ -1719,11 +1916,10 @@ public final class NotificationPanelViewController extends PanelViewController { // it's possible that nothing animated, so we replicate the termination // conditions of panelExpansionChanged here // TODO(b/200063118): This can likely go away in a future refactor CL. - getPanelExpansionStateManager().updateState(STATE_CLOSED); + getShadeExpansionStateManager().updateState(STATE_CLOSED); } } - @Override public void collapse(boolean delayed, float speedUpFactor) { if (!canPanelBeCollapsed()) { return; @@ -1733,12 +1929,27 @@ public final class NotificationPanelViewController extends PanelViewController { setQsExpandImmediate(true); setShowShelfOnly(true); } - super.collapse(delayed, speedUpFactor); + if (DEBUG) this.logf("collapse: " + this); + if (canPanelBeCollapsed()) { + cancelHeightAnimator(); + notifyExpandingStarted(); + + // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state. + setIsClosing(true); + if (delayed) { + mNextCollapseSpeedUpFactor = speedUpFactor; + this.mView.postDelayed(mFlingCollapseRunnable, 120); + } else { + fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */); + } + } } private void setQsExpandImmediate(boolean expandImmediate) { - mQsExpandImmediate = expandImmediate; - mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate); + if (expandImmediate != mQsExpandImmediate) { + mQsExpandImmediate = expandImmediate; + mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate); + } } private void setShowShelfOnly(boolean shelfOnly) { @@ -1751,10 +1962,15 @@ public final class NotificationPanelViewController extends PanelViewController { setQsExpansion(mQsMinExpansionHeight); } - @Override @VisibleForTesting - protected void cancelHeightAnimator() { - super.cancelHeightAnimator(); + void cancelHeightAnimator() { + if (mHeightAnimator != null) { + if (mHeightAnimator.isRunning()) { + mPanelUpdateWhenAnimatorEnds = false; + } + mHeightAnimator.cancel(); + } + endClosing(); } public void cancelAnimation() { @@ -1822,28 +2038,124 @@ public final class NotificationPanelViewController extends PanelViewController { } } - @Override public void fling(float vel, boolean expand) { GestureRecorder gr = mCentralSurfaces.getGestureRecorder(); if (gr != null) { gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel); } - super.fling(vel, expand); + fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false); } - @Override - protected void flingToHeight(float vel, boolean expand, float target, + @VisibleForTesting + void flingToHeight(float vel, boolean expand, float target, float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) { mHeadsUpTouchHelper.notifyFling(!expand); mKeyguardStateController.notifyPanelFlingStart(!expand /* flingingToDismiss */); setClosingWithAlphaFadeout(!expand && !isOnKeyguard() && getFadeoutAlpha() == 1.0f); mNotificationStackScrollLayoutController.setPanelFlinging(true); - super.flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing); + if (target == mExpandedHeight && mOverExpansion == 0.0f) { + // We're at the target and didn't fling and there's no overshoot + onFlingEnd(false /* cancelled */); + return; + } + mIsFlinging = true; + // we want to perform an overshoot animation when flinging open + final boolean addOverscroll = + expand + && !mInSplitShade // Split shade has its own overscroll logic + && mStatusBarStateController.getState() != KEYGUARD + && mOverExpansion == 0.0f + && vel >= 0; + final boolean shouldSpringBack = addOverscroll || (mOverExpansion != 0.0f && expand); + float overshootAmount = 0.0f; + if (addOverscroll) { + // Let's overshoot depending on the amount of velocity + overshootAmount = MathUtils.lerp( + 0.2f, + 1.0f, + MathUtils.saturate(vel + / (this.mFlingAnimationUtils.getHighVelocityPxPerSecond() + * FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT))); + overshootAmount += mOverExpansion / mPanelFlingOvershootAmount; + } + ValueAnimator animator = createHeightAnimator(target, overshootAmount); + if (expand) { + if (expandBecauseOfFalsing && vel < 0) { + vel = 0; + } + this.mFlingAnimationUtils.apply(animator, mExpandedHeight, + target + overshootAmount * mPanelFlingOvershootAmount, vel, + this.mView.getHeight()); + if (vel == 0) { + animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION); + } + } else { + if (shouldUseDismissingAnimation()) { + if (vel == 0) { + animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED); + long duration = (long) (200 + mExpandedHeight / this.mView.getHeight() * 100); + animator.setDuration(duration); + } else { + mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel, + this.mView.getHeight()); + } + } else { + mFlingAnimationUtilsClosing.apply( + animator, mExpandedHeight, target, vel, this.mView.getHeight()); + } + + // Make it shorter if we run a canned animation + if (vel == 0) { + animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor)); + } + if (mFixedDuration != NO_FIXED_DURATION) { + animator.setDuration(mFixedDuration); + } + } + animator.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationStart(Animator animation) { + if (!mStatusBarStateController.isDozing()) { + beginJankMonitoring(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (shouldSpringBack && !mCancelled) { + // After the shade is flinged open to an overscrolled state, spring back + // the shade by reducing section padding to 0. + springBack(); + } else { + onFlingEnd(mCancelled); + } + } + }); + setAnimator(animator); + animator.start(); } - @Override - protected void onFlingEnd(boolean cancelled) { - super.onFlingEnd(cancelled); + @VisibleForTesting + void onFlingEnd(boolean cancelled) { + mIsFlinging = false; + // No overshoot when the animation ends + setOverExpansionInternal(0, false /* isFromGesture */); + setAnimator(null); + mKeyguardStateController.notifyPanelFlingEnd(); + if (!cancelled) { + endJankMonitoring(); + notifyExpandingFinished(); + } else { + cancelJankMonitoring(); + } + updatePanelExpansionAndVisibility(); mNotificationStackScrollLayoutController.setPanelFlinging(false); } @@ -1905,7 +2217,8 @@ public final class NotificationPanelViewController extends PanelViewController { mShadeLog.logMotionEvent(event, "onQsIntercept: move ignored because qs tracking disabled"); } - if ((h > getTouchSlop(event) || (h < -getTouchSlop(event) && mQsExpanded)) + float touchSlop = getTouchSlop(event); + if ((h > touchSlop || (h < -touchSlop && mQsExpanded)) && Math.abs(h) > Math.abs(x - mInitialTouchX) && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) { if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept - start tracking expansion"); @@ -1920,6 +2233,9 @@ public final class NotificationPanelViewController extends PanelViewController { mInitialTouchX = x; mNotificationStackScrollLayoutController.cancelLongPress(); return true; + } else { + mShadeLog.logQsTrackingNotStarted(mInitialTouchY, y, h, touchSlop, mQsExpanded, + mCollapsedOnDown, mKeyguardShowing, isQsExpansionEnabled()); } break; @@ -1938,8 +2254,7 @@ public final class NotificationPanelViewController extends PanelViewController { return mQsTracking; } - @Override - protected boolean isInContentBounds(float x, float y) { + private boolean isInContentBounds(float x, float y) { float stackScrollerX = mNotificationStackScrollLayoutController.getX(); return !mNotificationStackScrollLayoutController .isBelowLastNotification(x - stackScrollerX, y) @@ -2072,9 +2387,8 @@ public final class NotificationPanelViewController extends PanelViewController { - mQsMinExpansionHeight)); } - @Override - protected boolean shouldExpandWhenNotFlinging() { - if (super.shouldExpandWhenNotFlinging()) { + private boolean shouldExpandWhenNotFlinging() { + if (getExpandedFraction() > 0.5f) { return true; } if (mAllowExpandForSmallExpansion) { @@ -2086,8 +2400,7 @@ public final class NotificationPanelViewController extends PanelViewController { return false; } - @Override - protected float getOpeningHeight() { + private float getOpeningHeight() { return mNotificationStackScrollLayoutController.getOpeningHeight(); } @@ -2238,9 +2551,20 @@ public final class NotificationPanelViewController extends PanelViewController { } } - @Override - protected boolean flingExpands(float vel, float vectorVel, float x, float y) { - boolean expands = super.flingExpands(vel, vectorVel, x, y); + private boolean flingExpands(float vel, float vectorVel, float x, float y) { + boolean expands = true; + if (!this.mFalsingManager.isUnlockingDisabled()) { + @Classifier.InteractionType int interactionType = y - mInitialExpandY > 0 + ? QUICK_SETTINGS : ( + mKeyguardStateController.canDismissLockScreen() ? UNLOCK : BOUNCER_UNLOCK); + if (!isFalseTouch(x, y, interactionType)) { + if (Math.abs(vectorVel) < this.mFlingAnimationUtils.getMinVelocityPxPerSecond()) { + expands = shouldExpandWhenNotFlinging(); + } else { + expands = vel > 0; + } + } + } // If we are already running a QS expansion, make sure that we keep the panel open. if (mQsExpansionAnimator != null) { @@ -2249,8 +2573,7 @@ public final class NotificationPanelViewController extends PanelViewController { return expands; } - @Override - protected boolean shouldGestureWaitForTouchSlop() { + private boolean shouldGestureWaitForTouchSlop() { if (mExpectingSynthesizedDown) { mExpectingSynthesizedDown = false; return false; @@ -2328,7 +2651,7 @@ public final class NotificationPanelViewController extends PanelViewController { } } - protected int getFalsingThreshold() { + private int getFalsingThreshold() { float factor = mCentralSurfaces.isWakeUpComingFromTouch() ? 1.5f : 1.0f; return (int) (mQsFalsingThreshold * factor); } @@ -2363,12 +2686,16 @@ public final class NotificationPanelViewController extends PanelViewController { mQsExpanded = expanded; updateQsState(); updateExpandedHeightToMaxHeight(); - mFalsingCollector.setQsExpanded(expanded); - mCentralSurfaces.setQsExpanded(expanded); - mNotificationsQSContainerController.setQsExpanded(expanded); - mPulseExpansionHandler.setQsExpanded(expanded); - mKeyguardBypassController.setQSExpanded(expanded); - mPrivacyDotViewController.setQsExpanded(expanded); + setStatusAccessibilityImportance(expanded + ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); + updateSystemUiStateFlags(); + NavigationBarView navigationBarView = + mNavigationBarController.getNavigationBarView(mDisplayId); + if (navigationBarView != null) { + navigationBarView.onStatusBarPanelStateChanged(); + } + mShadeExpansionStateManager.onQsExpansionChanged(expanded); } } @@ -2479,17 +2806,23 @@ public final class NotificationPanelViewController extends PanelViewController { mDepthController.setQsPanelExpansion(qsExpansionFraction); mStatusBarKeyguardViewManager.setQsExpansion(qsExpansionFraction); - // updateQsExpansion will get called whenever mTransitionToFullShadeProgress or - // mLockscreenShadeTransitionController.getDragProgress change. - // When in lockscreen, getDragProgress indicates the true expanded fraction of QS - float shadeExpandedFraction = mTransitioningToFullShadeProgress > 0 - ? mLockscreenShadeTransitionController.getQSDragProgress() + float shadeExpandedFraction = isOnKeyguard() + ? getLockscreenShadeDragProgress() : getExpandedFraction(); mLargeScreenShadeHeaderController.setShadeExpandedFraction(shadeExpandedFraction); mLargeScreenShadeHeaderController.setQsExpandedFraction(qsExpansionFraction); mLargeScreenShadeHeaderController.setQsVisible(mQsVisible); } + private float getLockscreenShadeDragProgress() { + // mTransitioningToFullShadeProgress > 0 means we're doing regular lockscreen to shade + // transition. If that's not the case we should follow QS expansion fraction for when + // user is pulling from the same top to go directly to expanded QS + return mTransitioningToFullShadeProgress > 0 + ? mLockscreenShadeTransitionController.getQSDragProgress() + : computeQsExpansionFraction(); + } + private void onStackYChanged(boolean shouldAnimate) { if (mQs != null) { if (shouldAnimate) { @@ -2676,8 +3009,10 @@ public final class NotificationPanelViewController extends PanelViewController { mQsTranslationForFullShadeTransition = qsTranslation; updateQsFrameTranslation(); float currentTranslation = mQsFrame.getTranslationY(); - mQsClipTop = (int) (top - currentTranslation - mQsFrame.getTop()); - mQsClipBottom = (int) (bottom - currentTranslation - mQsFrame.getTop()); + mQsClipTop = mEnableQsClipping + ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0; + mQsClipBottom = mEnableQsClipping + ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0; mQsVisible = qsVisible; mQs.setQsVisible(mQsVisible); mQs.setFancyClipping( @@ -3062,8 +3397,8 @@ public final class NotificationPanelViewController extends PanelViewController { } } - @Override - protected boolean canCollapsePanelOnTouch() { + @VisibleForTesting + boolean canCollapsePanelOnTouch() { if (!isInSettings() && mBarState == KEYGUARD) { return true; } @@ -3075,7 +3410,6 @@ public final class NotificationPanelViewController extends PanelViewController { return !mSplitShadeEnabled && (isInSettings() || mIsPanelCollapseOnQQS); } - @Override public int getMaxPanelHeight() { int min = mStatusBarMinHeight; if (!(mBarState == KEYGUARD) @@ -3109,8 +3443,7 @@ public final class NotificationPanelViewController extends PanelViewController { return mIsExpanding; } - @Override - protected void onHeightUpdated(float expandedHeight) { + private void onHeightUpdated(float expandedHeight) { if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) { // Updating the clock position will set the top padding which might // trigger a new panel height and re-position the clock. @@ -3124,26 +3457,24 @@ public final class NotificationPanelViewController extends PanelViewController { } if (mQsExpandImmediate || (mQsExpanded && !mQsTracking && mQsExpansionAnimator == null && !mQsExpansionFromOverscroll)) { - float t; - if (mKeyguardShowing) { - + float qsExpansionFraction; + if (mSplitShadeEnabled) { + qsExpansionFraction = 1; + } else if (mKeyguardShowing) { // On Keyguard, interpolate the QS expansion linearly to the panel expansion - t = expandedHeight / (getMaxPanelHeight()); + qsExpansionFraction = expandedHeight / (getMaxPanelHeight()); } else { // In Shade, interpolate linearly such that QS is closed whenever panel height is // minimum QS expansion + minStackHeight - float - panelHeightQsCollapsed = + float panelHeightQsCollapsed = mNotificationStackScrollLayoutController.getIntrinsicPadding() + mNotificationStackScrollLayoutController.getLayoutMinHeight(); float panelHeightQsExpanded = calculatePanelHeightQsExpanded(); - t = - (expandedHeight - panelHeightQsCollapsed) / (panelHeightQsExpanded - - panelHeightQsCollapsed); + qsExpansionFraction = (expandedHeight - panelHeightQsCollapsed) + / (panelHeightQsExpanded - panelHeightQsCollapsed); } - float - targetHeight = - mQsMinExpansionHeight + t * (mQsMaxExpansionHeight - mQsMinExpansionHeight); + float targetHeight = mQsMinExpansionHeight + + qsExpansionFraction * (mQsMaxExpansionHeight - mQsMinExpansionHeight); setQsExpansion(targetHeight); } updateExpandedHeight(expandedHeight); @@ -3291,9 +3622,7 @@ public final class NotificationPanelViewController extends PanelViewController { mLockIconViewController.setAlpha(alpha); } - @Override - protected void onExpandingStarted() { - super.onExpandingStarted(); + private void onExpandingStarted() { mNotificationStackScrollLayoutController.onExpansionStarted(); mIsExpanding = true; mQsExpandedWhenExpandingStarted = mQsFullyExpanded; @@ -3309,8 +3638,7 @@ public final class NotificationPanelViewController extends PanelViewController { mQs.setHeaderListening(true); } - @Override - protected void onExpandingFinished() { + private void onExpandingFinished() { mScrimController.onExpandingFinished(); mNotificationStackScrollLayoutController.onExpansionStopped(); mHeadsUpManager.onExpandingFinished(); @@ -3329,7 +3657,11 @@ public final class NotificationPanelViewController extends PanelViewController { } else { setListening(true); } - setQsExpandImmediate(false); + if (mBarState != SHADE) { + // updating qsExpandImmediate is done in onPanelStateChanged for unlocked shade but + // on keyguard panel state is always OPEN so we need to have that extra update + setQsExpandImmediate(false); + } setShowShelfOnly(false); mTwoFingerQsExpandPossible = false; updateTrackingHeadsUp(null); @@ -3358,18 +3690,63 @@ public final class NotificationPanelViewController extends PanelViewController { mQs.setListening(listening); } - @Override public void expand(boolean animate) { - super.expand(animate); + if (isFullyCollapsed() || isCollapsing()) { + mInstantExpanding = true; + mAnimateAfterExpanding = animate; + mUpdateFlingOnLayout = false; + abortAnimations(); + if (mTracking) { + // The panel is expanded after this call. + onTrackingStopped(true /* expands */); + } + if (mExpanding) { + notifyExpandingFinished(); + } + updatePanelExpansionAndVisibility(); + // Wait for window manager to pickup the change, so we know the maximum height of the + // panel then. + this.mView.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (!mInstantExpanding) { + mView.getViewTreeObserver().removeOnGlobalLayoutListener( + this); + return; + } + if (mCentralSurfaces.getNotificationShadeWindowView() + .isVisibleToUser()) { + mView.getViewTreeObserver().removeOnGlobalLayoutListener( + this); + if (mAnimateAfterExpanding) { + notifyExpandingStarted(); + beginJankMonitoring(); + fling(0, true /* expand */); + } else { + setExpandedFraction(1f); + } + mInstantExpanding = false; + } + } + }); + // Make sure a layout really happens. + this.mView.requestLayout(); + } + setListening(true); } - @Override + @VisibleForTesting + void setTouchSlopExceeded(boolean isTouchSlopExceeded) { + mTouchSlopExceeded = isTouchSlopExceeded; + } + public void setOverExpansion(float overExpansion) { if (overExpansion == mOverExpansion) { return; } - super.setOverExpansion(overExpansion); + mOverExpansion = overExpansion; // Translating the quick settings by half the overexpansion to center it in the background // frame updateQsFrameTranslation(); @@ -3377,14 +3754,18 @@ public final class NotificationPanelViewController extends PanelViewController { } private void updateQsFrameTranslation() { - mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs, mOverExpansion, - mQsTranslationForFullShadeTransition); + mQsFrameTranslateController.translateQsFrame(mQsFrame, mQs, + mNavigationBarBottomHeight + mAmbientState.getStackTopMargin()); + } - @Override - protected void onTrackingStarted() { + private void onTrackingStarted() { mFalsingCollector.onTrackingStarted(!mKeyguardStateController.canDismissLockScreen()); - super.onTrackingStarted(); + endClosing(); + mTracking = true; + mCentralSurfaces.onTrackingStarted(); + notifyExpandingStarted(); + updatePanelExpansionAndVisibility(); mScrimController.onTrackingStarted(); if (mQsFullyExpanded) { setQsExpandImmediate(true); @@ -3394,10 +3775,11 @@ public final class NotificationPanelViewController extends PanelViewController { cancelPendingPanelCollapse(); } - @Override - protected void onTrackingStopped(boolean expand) { + private void onTrackingStopped(boolean expand) { mFalsingCollector.onTrackingStopped(); - super.onTrackingStopped(expand); + mTracking = false; + mCentralSurfaces.onTrackingStopped(expand); + updatePanelExpansionAndVisibility(); if (expand) { mNotificationStackScrollLayoutController.setOverScrollAmount(0.0f, true /* onTop */, true /* animate */); @@ -3414,37 +3796,48 @@ public final class NotificationPanelViewController extends PanelViewController { getHeight(), mNavigationBarBottomHeight); } - @Override - protected void startUnlockHintAnimation() { + @VisibleForTesting + void startUnlockHintAnimation() { if (mPowerManager.isPowerSaveMode() || mAmbientState.getDozeAmount() > 0f) { onUnlockHintStarted(); onUnlockHintFinished(); return; } - super.startUnlockHintAnimation(); + + // We don't need to hint the user if an animation is already running or the user is changing + // the expansion. + if (mHeightAnimator != null || mTracking) { + return; + } + notifyExpandingStarted(); + startUnlockHintAnimationPhase1(() -> { + notifyExpandingFinished(); + onUnlockHintFinished(); + mHintAnimationRunning = false; + }); + onUnlockHintStarted(); + mHintAnimationRunning = true; } - @Override - protected void onUnlockHintFinished() { - super.onUnlockHintFinished(); + @VisibleForTesting + void onUnlockHintFinished() { + mCentralSurfaces.onHintFinished(); mScrimController.setExpansionAffectsAlpha(true); mNotificationStackScrollLayoutController.setUnlockHintRunning(false); } - @Override - protected void onUnlockHintStarted() { - super.onUnlockHintStarted(); + @VisibleForTesting + void onUnlockHintStarted() { + mCentralSurfaces.onUnlockHintStarted(); mScrimController.setExpansionAffectsAlpha(false); mNotificationStackScrollLayoutController.setUnlockHintRunning(true); } - @Override - protected boolean shouldUseDismissingAnimation() { + private boolean shouldUseDismissingAnimation() { return mBarState != StatusBarState.SHADE && (mKeyguardStateController.canDismissLockScreen() || !isTracking()); } - @Override public int getMaxPanelTransitionDistance() { // Traditionally the value is based on the number of notifications. On split-shade, we want // the required distance to be a specific and constant value, to make sure the expansion @@ -3469,8 +3862,8 @@ public final class NotificationPanelViewController extends PanelViewController { } } - @Override - protected boolean isTrackingBlocked() { + @VisibleForTesting + boolean isTrackingBlocked() { return mConflictingQsExpansionGesture && mQsExpanded || mBlockingExpansionForCurrentTouch; } @@ -3492,22 +3885,22 @@ public final class NotificationPanelViewController extends PanelViewController { return mIsLaunchTransitionFinished; } - @Override public void setIsLaunchAnimationRunning(boolean running) { boolean wasRunning = mIsLaunchAnimationRunning; - super.setIsLaunchAnimationRunning(running); + mIsLaunchAnimationRunning = running; if (wasRunning != mIsLaunchAnimationRunning) { mPanelEventsEmitter.notifyLaunchingActivityChanged(running); } } - @Override - protected void setIsClosing(boolean isClosing) { + @VisibleForTesting + void setIsClosing(boolean isClosing) { boolean wasClosing = isClosing(); - super.setIsClosing(isClosing); + mClosing = isClosing; if (wasClosing != isClosing) { mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing); } + mAmbientState.setIsClosing(isClosing); } private void updateDozingVisibilities(boolean animate) { @@ -3517,7 +3910,6 @@ public final class NotificationPanelViewController extends PanelViewController { } } - @Override public boolean isDozing() { return mDozing; } @@ -3534,17 +3926,20 @@ public final class NotificationPanelViewController extends PanelViewController { mKeyguardStatusViewController.dozeTimeTick(); } - @Override - protected boolean onMiddleClicked() { + private boolean onMiddleClicked() { switch (mBarState) { case KEYGUARD: if (!mDozingOnDown) { - if (mUpdateMonitor.isFaceEnrolled() - && !mUpdateMonitor.isFaceDetectionRunning() - && !mUpdateMonitor.getUserCanSkipBouncer( - KeyguardUpdateMonitor.getCurrentUser())) { - mUpdateMonitor.requestFaceAuth(true, - FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); + mShadeLog.v("onMiddleClicked on Keyguard, mDozingOnDown: false"); + // Try triggering face auth, this "might" run. Check + // KeyguardUpdateMonitor#shouldListenForFace to see when face auth won't run. + boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); + + if (didFaceAuthRun) { + mUpdateMonitor.requestActiveUnlock( + ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT, + "lockScreenEmptySpaceTap"); } else { mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT, 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */); @@ -3552,11 +3947,6 @@ public final class NotificationPanelViewController extends PanelViewController { .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT); startUnlockHintAnimation(); } - if (mUpdateMonitor.isFaceEnrolled()) { - mUpdateMonitor.requestActiveUnlock( - ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT, - "lockScreenEmptySpaceTap"); - } } return true; case StatusBarState.SHADE_LOCKED: @@ -3594,15 +3984,13 @@ public final class NotificationPanelViewController extends PanelViewController { updateVisibility(); } - @Override - protected boolean shouldPanelBeVisible() { + private boolean shouldPanelBeVisible() { boolean headsUpVisible = mHeadsUpAnimatingAway || mHeadsUpPinnedMode; return headsUpVisible || isExpanded() || mBouncerShowing; } - @Override public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) { - super.setHeadsUpManager(headsUpManager); + mHeadsUpManager = headsUpManager; mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager, mNotificationStackScrollLayoutController.getHeadsUpCallback(), NotificationPanelViewController.this); @@ -3616,8 +4004,7 @@ public final class NotificationPanelViewController extends PanelViewController { // otherwise we update the state when the expansion is finished } - @Override - protected void onClosingFinished() { + private void onClosingFinished() { mCentralSurfaces.onClosingFinished(); setClosingWithAlphaFadeout(false); mMediaHierarchyManager.closeGuts(); @@ -3706,8 +4093,7 @@ public final class NotificationPanelViewController extends PanelViewController { mCentralSurfaces.clearNotificationEffects(); } - @Override - protected boolean isPanelVisibleBecauseOfHeadsUp() { + private boolean isPanelVisibleBecauseOfHeadsUp() { return (mHeadsUpManager.hasPinnedHeadsUp() || mHeadsUpAnimatingAway) && mBarState == StatusBarState.SHADE; } @@ -3822,17 +4208,23 @@ public final class NotificationPanelViewController extends PanelViewController { mNotificationBoundsAnimationDelay = delay; } - @Override public void setTouchAndAnimationDisabled(boolean disabled) { - super.setTouchAndAnimationDisabled(disabled); + mTouchDisabled = disabled; + if (mTouchDisabled) { + cancelHeightAnimator(); + if (mTracking) { + onTrackingStopped(true /* expanded */); + } + notifyExpandingFinished(); + } mNotificationStackScrollLayoutController.setAnimationsEnabled(!disabled); } /** * Sets the dozing state. * - * @param dozing {@code true} when dozing. - * @param animate if transition should be animated. + * @param dozing {@code true} when dozing. + * @param animate if transition should be animated. */ public void setDozing(boolean dozing, boolean animate) { if (dozing == mDozing) return; @@ -3972,35 +4364,35 @@ public final class NotificationPanelViewController extends PanelViewController { /** * Starts fold to AOD animation. * - * @param startAction invoked when the animation starts. - * @param endAction invoked when the animation finishes, also if it was cancelled. + * @param startAction invoked when the animation starts. + * @param endAction invoked when the animation finishes, also if it was cancelled. * @param cancelAction invoked when the animation is cancelled, before endAction. */ public void startFoldToAodAnimation(Runnable startAction, Runnable endAction, Runnable cancelAction) { mView.animate() - .translationX(0) - .alpha(1f) - .setDuration(ANIMATION_DURATION_FOLD_TO_AOD) - .setInterpolator(EMPHASIZED_DECELERATE) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - startAction.run(); - } + .translationX(0) + .alpha(1f) + .setDuration(ANIMATION_DURATION_FOLD_TO_AOD) + .setInterpolator(EMPHASIZED_DECELERATE) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + startAction.run(); + } - @Override - public void onAnimationCancel(Animator animation) { - cancelAction.run(); - } + @Override + public void onAnimationCancel(Animator animation) { + cancelAction.run(); + } - @Override - public void onAnimationEnd(Animator animation) { - endAction.run(); - } - }).setUpdateListener(anim -> { - mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction()); - }).start(); + @Override + public void onAnimationEnd(Animator animation) { + endAction.run(); + } + }).setUpdateListener(anim -> { + mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction()); + }).start(); } /** @@ -4024,9 +4416,14 @@ public final class NotificationPanelViewController extends PanelViewController { mBlockingExpansionForCurrentTouch = mTracking; } - @Override public void dump(PrintWriter pw, String[] args) { - super.dump(pw, args); + pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s" + + " tracking=%s timeAnim=%s%s " + + "touchDisabled=%s" + "]", + this.getClass().getSimpleName(), getExpandedHeight(), getMaxPanelHeight(), + mClosing ? "T" : "f", mTracking ? "T" : "f", mHeightAnimator, + ((mHeightAnimator != null && mHeightAnimator.isStarted()) ? " (started)" : ""), + mTouchDisabled ? "T" : "f")); IndentingPrintWriter ipw = asIndenting(pw); ipw.increaseIndent(); ipw.println("gestureExclusionRect:" + calculateGestureExclusionRect()); @@ -4169,127 +4566,13 @@ public final class NotificationPanelViewController extends PanelViewController { mConfigurationListener.onThemeChanged(); } - @Override - protected OnLayoutChangeListener createLayoutChangeListener() { - return new OnLayoutChangeListenerImpl(); + private OnLayoutChangeListener createLayoutChangeListener() { + return new OnLayoutChangeListener(); } - @Override - protected TouchHandler createTouchHandler() { - return new TouchHandler() { - - private long mLastTouchDownTime = -1L; - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - if (SPEW_LOGCAT) { - Log.v(TAG, - "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX() - + "," + event.getY() + ")"); - } - if (mBlockTouches || mQs.disallowPanelTouches()) { - return false; - } - initDownStates(event); - // Do not let touches go to shade or QS if the bouncer is visible, - // but still let user swipe down to expand the panel, dismissing the bouncer. - if (mCentralSurfaces.isBouncerShowing()) { - return true; - } - if (mCommandQueue.panelsEnabled() - && !mNotificationStackScrollLayoutController.isLongPressInProgress() - && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { - mMetricsLogger.count(COUNTER_PANEL_OPEN, 1); - mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1); - return true; - } - if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0) - && mPulseExpansionHandler.onInterceptTouchEvent(event)) { - return true; - } - - if (!isFullyCollapsed() && onQsIntercept(event)) { - if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true"); - return true; - } - return super.onInterceptTouchEvent(event); - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - if (event.getDownTime() == mLastTouchDownTime) { - // An issue can occur when swiping down after unlock, where multiple down - // events are received in this handler with identical downTimes. Until the - // source of the issue can be located, detect this case and ignore. - // see b/193350347 - Log.w(TAG, "Duplicate down event detected... ignoring"); - return true; - } - mLastTouchDownTime = event.getDownTime(); - } - - - if (mBlockTouches || (mQsFullyExpanded && mQs != null - && mQs.disallowPanelTouches())) { - return false; - } - - // Do not allow panel expansion if bouncer is scrimmed or showing over a dream, - // otherwise user would be able to pull down QS or expand the shade. - if (mCentralSurfaces.isBouncerShowingScrimmed() - || mCentralSurfaces.isBouncerShowingOverDream()) { - return false; - } - - // Make sure the next touch won't the blocked after the current ends. - if (event.getAction() == MotionEvent.ACTION_UP - || event.getAction() == MotionEvent.ACTION_CANCEL) { - mBlockingExpansionForCurrentTouch = false; - } - // When touch focus transfer happens, ACTION_DOWN->ACTION_UP may happen immediately - // without any ACTION_MOVE event. - // In such case, simply expand the panel instead of being stuck at the bottom bar. - if (mLastEventSynthesizedDown && event.getAction() == MotionEvent.ACTION_UP) { - expand(true /* animate */); - } - initDownStates(event); - - // If pulse is expanding already, let's give it the touch. There are situations - // where the panel starts expanding even though we're also pulsing - boolean pulseShouldGetTouch = (!mIsExpanding - && !shouldQuickSettingsIntercept(mDownX, mDownY, 0)) - || mPulseExpansionHandler.isExpanding(); - if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) { - // We're expanding all the other ones shouldn't get this anymore - mShadeLog.logMotionEvent(event, "onTouch: PulseExpansionHandler handled event"); - return true; - } - if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp() - && !mNotificationStackScrollLayoutController.isLongPressInProgress() - && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { - mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1); - } - boolean handled = mHeadsUpTouchHelper.onTouchEvent(event); - - if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) { - mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event"); - return true; - } - if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) { - mMetricsLogger.count(COUNTER_PANEL_OPEN, 1); - handled = true; - } - - if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded() - && mStatusBarKeyguardViewManager.isShowing()) { - mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX()); - } - - handled |= super.onTouch(v, event); - return !mDozing || mPulsing || handled; - } - }; + @VisibleForTesting + TouchHandler createTouchHandler() { + return new TouchHandler(); } private final PhoneStatusBarView.TouchEventHandler mStatusBarViewTouchEventHandler = @@ -4341,8 +4624,7 @@ public final class NotificationPanelViewController extends PanelViewController { } }; - @Override - protected OnConfigurationChangedListener createOnConfigurationChangedListener() { + private OnConfigurationChangedListener createOnConfigurationChangedListener() { return new OnConfigurationChangedListener(); } @@ -4400,10 +4682,605 @@ public final class NotificationPanelViewController extends PanelViewController { } mSysUiState.setFlag(SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED, isFullyExpanded() && !isInSettings()) - .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, isInSettings()) + .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, isFullyExpanded() && isInSettings()) .commitUpdate(mDisplayId); } + private void logf(String fmt, Object... args) { + Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); + } + + @VisibleForTesting + void notifyExpandingStarted() { + if (!mExpanding) { + mExpanding = true; + onExpandingStarted(); + } + } + + @VisibleForTesting + void notifyExpandingFinished() { + endClosing(); + if (mExpanding) { + mExpanding = false; + onExpandingFinished(); + } + } + + private float getTouchSlop(MotionEvent event) { + // Adjust the touch slop if another gesture may be being performed. + return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE + ? mTouchSlop * mSlopMultiplier + : mTouchSlop; + } + + private void addMovement(MotionEvent event) { + // Add movement to velocity tracker using raw screen X and Y coordinates instead + // of window coordinates because the window frame may be moving at the same time. + float deltaX = event.getRawX() - event.getX(); + float deltaY = event.getRawY() - event.getY(); + event.offsetLocation(deltaX, deltaY); + mVelocityTracker.addMovement(event); + event.offsetLocation(-deltaX, -deltaY); + } + + /** If the latency tracker is enabled, begins tracking expand latency. */ + public void startExpandLatencyTracking() { + if (mLatencyTracker.isEnabled()) { + mLatencyTracker.onActionStart(LatencyTracker.ACTION_EXPAND_PANEL); + mExpandLatencyTracking = true; + } + } + + private void startOpening(MotionEvent event) { + updatePanelExpansionAndVisibility(); + // Reset at start so haptic can be triggered as soon as panel starts to open. + mHasVibratedOnOpen = false; + //TODO: keyguard opens QS a different way; log that too? + + // Log the position of the swipe that opened the panel + float width = mCentralSurfaces.getDisplayWidth(); + float height = mCentralSurfaces.getDisplayHeight(); + int rot = mCentralSurfaces.getRotation(); + + mLockscreenGestureLogger.writeAtFractionalPosition(MetricsEvent.ACTION_PANEL_VIEW_EXPAND, + (int) (event.getX() / width * 100), (int) (event.getY() / height * 100), rot); + mLockscreenGestureLogger + .log(LockscreenUiEvent.LOCKSCREEN_UNLOCKED_NOTIFICATION_PANEL_EXPAND); + } + + /** + * Maybe vibrate as panel is opened. + * + * @param openingWithTouch Whether the panel is being opened with touch. If the panel is + * instead + * being opened programmatically (such as by the open panel gesture), we + * always play haptic. + */ + private void maybeVibrateOnOpening(boolean openingWithTouch) { + if (mVibrateOnOpening) { + if (!openingWithTouch || !mHasVibratedOnOpen) { + mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); + mHasVibratedOnOpen = true; + } + } + } + + /** + * @return whether the swiping direction is upwards and above a 45 degree angle compared to the + * horizontal direction + */ + private boolean isDirectionUpwards(float x, float y) { + float xDiff = x - mInitialExpandX; + float yDiff = y - mInitialExpandY; + if (yDiff >= 0) { + return false; + } + return Math.abs(yDiff) >= Math.abs(xDiff); + } + + /** Called when a MotionEvent is about to trigger Shade expansion. */ + public void startExpandMotion(float newX, float newY, boolean startTracking, + float expandedHeight) { + if (!mHandlingPointerUp && !mStatusBarStateController.isDozing()) { + beginJankMonitoring(); + } + mInitialOffsetOnTouch = expandedHeight; + mInitialExpandY = newY; + mInitialExpandX = newX; + mInitialTouchFromKeyguard = mKeyguardStateController.isShowing(); + if (startTracking) { + mTouchSlopExceeded = true; + setExpandedHeight(mInitialOffsetOnTouch); + onTrackingStarted(); + } + } + + private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) { + mTrackingPointer = -1; + mAmbientState.setSwipingUp(false); + if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop + || Math.abs(y - mInitialExpandY) > mTouchSlop + || (!isFullyExpanded() && !isFullyCollapsed()) + || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { + mVelocityTracker.computeCurrentVelocity(1000); + float vel = mVelocityTracker.getYVelocity(); + float vectorVel = (float) Math.hypot( + mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); + + final boolean onKeyguard = mKeyguardStateController.isShowing(); + final boolean expand; + if (mKeyguardStateController.isKeyguardFadingAway() + || (mInitialTouchFromKeyguard && !onKeyguard)) { + // Don't expand for any touches that started from the keyguard and ended after the + // keyguard is gone. + expand = false; + } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { + if (onKeyguard) { + expand = true; + } else if (mCentralSurfaces.isBouncerShowingOverDream()) { + expand = false; + } else { + // If we get a cancel, put the shade back to the state it was in when the + // gesture started + expand = !mPanelClosedOnDown; + } + } else { + expand = flingExpands(vel, vectorVel, x, y); + } + + mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold, + mCentralSurfaces.isFalsingThresholdNeeded(), + mCentralSurfaces.isWakeUpComingFromTouch()); + // Log collapse gesture if on lock screen. + if (!expand && onKeyguard) { + float displayDensity = mCentralSurfaces.getDisplayDensity(); + int heightDp = (int) Math.abs((y - mInitialExpandY) / displayDensity); + int velocityDp = (int) Math.abs(vel / displayDensity); + mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_UNLOCK, heightDp, velocityDp); + mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_UNLOCK); + } + @Classifier.InteractionType int interactionType = vel == 0 ? GENERIC + : y - mInitialExpandY > 0 ? QUICK_SETTINGS + : (mKeyguardStateController.canDismissLockScreen() + ? UNLOCK : BOUNCER_UNLOCK); + + fling(vel, expand, isFalseTouch(x, y, interactionType)); + onTrackingStopped(expand); + mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown; + if (mUpdateFlingOnLayout) { + mUpdateFlingVelocity = vel; + } + } else if (!mCentralSurfaces.isBouncerShowing() + && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating() + && !mKeyguardStateController.isKeyguardGoingAway()) { + boolean expands = onEmptySpaceClick(); + onTrackingStopped(expands); + } + mVelocityTracker.clear(); + } + + private float getCurrentExpandVelocity() { + mVelocityTracker.computeCurrentVelocity(1000); + return mVelocityTracker.getYVelocity(); + } + + private void endClosing() { + if (mClosing) { + setIsClosing(false); + onClosingFinished(); + } + } + + /** + * @param x the final x-coordinate when the finger was lifted + * @param y the final y-coordinate when the finger was lifted + * @return whether this motion should be regarded as a false touch + */ + private boolean isFalseTouch(float x, float y, + @Classifier.InteractionType int interactionType) { + if (!mCentralSurfaces.isFalsingThresholdNeeded()) { + return false; + } + if (mFalsingManager.isClassifierEnabled()) { + return mFalsingManager.isFalseTouch(interactionType); + } + if (!mTouchAboveFalsingThreshold) { + return true; + } + if (mUpwardsWhenThresholdReached) { + return false; + } + return !isDirectionUpwards(x, y); + } + + private void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) { + fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing); + } + + private void fling(float vel, boolean expand, float collapseSpeedUpFactor, + boolean expandBecauseOfFalsing) { + float target = expand ? getMaxPanelHeight() : 0; + if (!expand) { + setIsClosing(true); + } + flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing); + } + + private void springBack() { + if (mOverExpansion == 0) { + onFlingEnd(false /* cancelled */); + return; + } + mIsSpringBackAnimation = true; + ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0); + animator.addUpdateListener( + animation -> setOverExpansionInternal((float) animation.getAnimatedValue(), + false /* isFromGesture */)); + animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION); + animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + animator.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + mIsSpringBackAnimation = false; + onFlingEnd(mCancelled); + } + }); + setAnimator(animator); + animator.start(); + } + + public String getName() { + return mViewName; + } + + @VisibleForTesting + void setExpandedHeight(float height) { + if (DEBUG) logf("setExpandedHeight(%.1f)", height); + setExpandedHeightInternal(height); + } + + private void updateExpandedHeightToMaxHeight() { + float currentMaxPanelHeight = getMaxPanelHeight(); + + if (isFullyCollapsed()) { + return; + } + + if (currentMaxPanelHeight == mExpandedHeight) { + return; + } + + if (mTracking && !isTrackingBlocked()) { + return; + } + + if (mHeightAnimator != null && !mIsSpringBackAnimation) { + mPanelUpdateWhenAnimatorEnds = true; + return; + } + + setExpandedHeight(currentMaxPanelHeight); + } + + private void setExpandedHeightInternal(float h) { + if (isNaN(h)) { + Log.wtf(TAG, "ExpandedHeight set to NaN"); + } + mNotificationShadeWindowController.batchApplyWindowLayoutParams(() -> { + if (mExpandLatencyTracking && h != 0f) { + DejankUtils.postAfterTraversal( + () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL)); + mExpandLatencyTracking = false; + } + float maxPanelHeight = getMaxPanelTransitionDistance(); + if (mHeightAnimator == null) { + // Split shade has its own overscroll logic + if (mTracking && !mInSplitShade) { + float overExpansionPixels = Math.max(0, h - maxPanelHeight); + setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */); + } + } + mExpandedHeight = Math.min(h, maxPanelHeight); + // If we are closing the panel and we are almost there due to a slow decelerating + // interpolator, abort the animation. + if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) { + mExpandedHeight = 0f; + if (mHeightAnimator != null) { + mHeightAnimator.end(); + } + } + mExpansionDragDownAmountPx = h; + mExpandedFraction = Math.min(1f, + maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight); + mAmbientState.setExpansionFraction(mExpandedFraction); + onHeightUpdated(mExpandedHeight); + updatePanelExpansionAndVisibility(); + }); + } + + /** + * Set the current overexpansion + * + * @param overExpansion the amount of overexpansion to apply + * @param isFromGesture is this amount from a gesture and needs to be rubberBanded? + */ + private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) { + if (!isFromGesture) { + mLastGesturedOverExpansion = -1; + setOverExpansion(overExpansion); + } else if (mLastGesturedOverExpansion != overExpansion) { + mLastGesturedOverExpansion = overExpansion; + final float heightForFullOvershoot = mView.getHeight() / 3.0f; + float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot); + newExpansion = Interpolators.getOvershootInterpolation(newExpansion); + setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f); + } + } + + /** Sets the expanded height relative to a number from 0 to 1. */ + public void setExpandedFraction(float frac) { + setExpandedHeight(getMaxPanelTransitionDistance() * frac); + } + + @VisibleForTesting + float getExpandedHeight() { + return mExpandedHeight; + } + + public float getExpandedFraction() { + return mExpandedFraction; + } + + public boolean isFullyExpanded() { + return mExpandedHeight >= getMaxPanelHeight(); + } + + public boolean isFullyCollapsed() { + return mExpandedFraction <= 0.0f; + } + + public boolean isCollapsing() { + return mClosing || mIsLaunchAnimationRunning; + } + + public boolean isFlinging() { + return mIsFlinging; + } + + public boolean isTracking() { + return mTracking; + } + + /** Returns whether the shade can be collapsed. */ + public boolean canPanelBeCollapsed() { + return !isFullyCollapsed() && !mTracking && !mClosing; + } + + /** Collapses the shade instantly without animation. */ + public void instantCollapse() { + abortAnimations(); + setExpandedFraction(0f); + if (mExpanding) { + notifyExpandingFinished(); + } + if (mInstantExpanding) { + mInstantExpanding = false; + updatePanelExpansionAndVisibility(); + } + } + + private void abortAnimations() { + cancelHeightAnimator(); + mView.removeCallbacks(mFlingCollapseRunnable); + } + + public boolean isUnlockHintRunning() { + return mHintAnimationRunning; + } + + /** + * Phase 1: Move everything upwards. + */ + private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) { + float target = Math.max(0, getMaxPanelHeight() - mHintDistance); + ValueAnimator animator = createHeightAnimator(target); + animator.setDuration(250); + animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + animator.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mCancelled) { + setAnimator(null); + onAnimationFinished.run(); + } else { + startUnlockHintAnimationPhase2(onAnimationFinished); + } + } + }); + animator.start(); + setAnimator(animator); + + final List<ViewPropertyAnimator> indicationAnimators = + mKeyguardBottomArea.getIndicationAreaAnimators(); + for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) { + indicationAreaAnimator + .translationY(-mHintDistance) + .setDuration(250) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .withEndAction(() -> indicationAreaAnimator + .translationY(0) + .setDuration(450) + .setInterpolator(mBounceInterpolator) + .start()) + .start(); + } + } + + private void setAnimator(ValueAnimator animator) { + mHeightAnimator = animator; + if (animator == null && mPanelUpdateWhenAnimatorEnds) { + mPanelUpdateWhenAnimatorEnds = false; + updateExpandedHeightToMaxHeight(); + } + } + + /** + * Phase 2: Bounce down. + */ + private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) { + ValueAnimator animator = createHeightAnimator(getMaxPanelHeight()); + animator.setDuration(450); + animator.setInterpolator(mBounceInterpolator); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setAnimator(null); + onAnimationFinished.run(); + updatePanelExpansionAndVisibility(); + } + }); + animator.start(); + setAnimator(animator); + } + + private ValueAnimator createHeightAnimator(float targetHeight) { + return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */); + } + + /** + * Create an animator that can also overshoot + * + * @param targetHeight the target height + * @param overshootAmount the amount of overshoot desired + */ + private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) { + float startExpansion = mOverExpansion; + ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight); + animator.addUpdateListener( + animation -> { + if (overshootAmount > 0.0f + // Also remove the overExpansion when collapsing + || (targetHeight == 0.0f && startExpansion != 0)) { + final float expansion = MathUtils.lerp( + startExpansion, + mPanelFlingOvershootAmount * overshootAmount, + Interpolators.FAST_OUT_SLOW_IN.getInterpolation( + animator.getAnimatedFraction())); + setOverExpansionInternal(expansion, false /* isFromGesture */); + } + setExpandedHeightInternal((float) animation.getAnimatedValue()); + }); + return animator; + } + + /** Update the visibility of {@link NotificationPanelView} if necessary. */ + private void updateVisibility() { + mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE); + } + + /** + * Updates the panel expansion and {@link NotificationPanelView} visibility if necessary. + * + * TODO(b/200063118): Could public calls to this method be replaced with calls to + * {@link #updateVisibility()}? That would allow us to make this method private. + */ + public void updatePanelExpansionAndVisibility() { + mShadeExpansionStateManager.onPanelExpansionChanged( + mExpandedFraction, isExpanded(), + mTracking, mExpansionDragDownAmountPx); + updateVisibility(); + } + + public boolean isExpanded() { + return mExpandedFraction > 0f + || mInstantExpanding + || isPanelVisibleBecauseOfHeadsUp() + || mTracking + || mHeightAnimator != null + && !mIsSpringBackAnimation; + } + + /** + * Gets called when the user performs a click anywhere in the empty area of the panel. + * + * @return whether the panel will be expanded after the action performed by this method + */ + private boolean onEmptySpaceClick() { + if (mHintAnimationRunning) { + return true; + } + return onMiddleClicked(); + } + + @VisibleForTesting + boolean isClosing() { + return mClosing; + } + + /** Collapses the shade with an animation duration in milliseconds. */ + public void collapseWithDuration(int animationDuration) { + mFixedDuration = animationDuration; + collapse(false /* delayed */, 1.0f /* speedUpFactor */); + mFixedDuration = NO_FIXED_DURATION; + } + + /** Returns the NotificationPanelView. */ + public ViewGroup getView() { + // TODO: remove this method, or at least reduce references to it. + return mView; + } + + private void beginJankMonitoring() { + if (mInteractionJankMonitor == null) { + return; + } + InteractionJankMonitor.Configuration.Builder builder = + InteractionJankMonitor.Configuration.Builder.withView( + InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, + mView) + .setTag(isFullyCollapsed() ? "Expand" : "Collapse"); + mInteractionJankMonitor.begin(builder); + } + + private void endJankMonitoring() { + if (mInteractionJankMonitor == null) { + return; + } + InteractionJankMonitor.getInstance().end( + InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); + } + + private void cancelJankMonitoring() { + if (mInteractionJankMonitor == null) { + return; + } + InteractionJankMonitor.getInstance().cancel( + InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); + } + + private float getExpansionFraction() { + return mExpandedFraction; + } + + private ShadeExpansionStateManager getShadeExpansionStateManager() { + return mShadeExpansionStateManager; + } + private class OnHeightChangedListener implements ExpandableView.OnHeightChangedListener { @Override public void onHeightChanged(ExpandableView view, boolean needsAnimation) { @@ -4678,12 +5555,19 @@ public final class NotificationPanelViewController extends PanelViewController { } } } else { + // this else branch means we are doing one of: + // - from KEYGUARD and SHADE (but not expanded shade) + // - from SHADE to KEYGUARD + // - from SHADE_LOCKED to SHADE + // - getting notified again about the current SHADE or KEYGUARD state final boolean animatingUnlockedShadeToKeyguard = oldState == SHADE && statusBarState == KEYGUARD && mScreenOffAnimationController.isKeyguardShowDelayed(); if (!animatingUnlockedShadeToKeyguard) { // Only make the status bar visible if we're not animating the screen off, since // we only want to be showing the clock/notifications during the animation. + mShadeLog.v("Updating keyguard status bar state to " + + (keyguardShowing ? "visible" : "invisible")); mKeyguardStatusBarViewController.updateViewState( /* alpha= */ 1f, keyguardShowing ? View.VISIBLE : View.INVISIBLE); @@ -4749,9 +5633,7 @@ public final class NotificationPanelViewController extends PanelViewController { @Override public float getLockscreenShadeDragProgress() { - return mTransitioningToFullShadeProgress > 0 - ? mLockscreenShadeTransitionController.getQSDragProgress() - : computeQsExpansionFraction(); + return NotificationPanelViewController.this.getLockscreenShadeDragProgress(); } }; @@ -4807,13 +5689,18 @@ public final class NotificationPanelViewController extends PanelViewController { } } - private class OnLayoutChangeListenerImpl extends OnLayoutChangeListener { - + private final class OnLayoutChangeListener implements View.OnLayoutChangeListener { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { DejankUtils.startDetectingBlockingIpcs("NVP#onLayout"); - super.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom); + updateExpandedHeightToMaxHeight(); + mHasLayoutedSinceDown = true; + if (mUpdateFlingOnLayout) { + abortAnimations(); + fling(mUpdateFlingVelocity, true /* expands */); + mUpdateFlingOnLayout = false; + } updateMaxDisplayedNotifications(!shouldAvoidChangingNotificationsCount()); setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth()); @@ -4988,6 +5875,7 @@ public final class NotificationPanelViewController extends PanelViewController { updateQSExpansionEnabledAmbient(); if (state == STATE_OPEN && mCurrentPanelState != state) { + setQsExpandImmediate(false); mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } if (state == STATE_OPENING) { @@ -5000,6 +5888,7 @@ public final class NotificationPanelViewController extends PanelViewController { mCentralSurfaces.makeExpandedVisible(false); } if (state == STATE_CLOSED) { + setQsExpandImmediate(false); // Close the status bar in the next frame so we can show the end of the // animation. mView.post(mMaybeHideExpandedRunnable); @@ -5069,4 +5958,415 @@ public final class NotificationPanelViewController extends PanelViewController { } } } + + /** Handles MotionEvents for the Shade. */ + public final class TouchHandler implements View.OnTouchListener { + private long mLastTouchDownTime = -1L; + + /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */ + public boolean onInterceptTouchEvent(MotionEvent event) { + if (SPEW_LOGCAT) { + Log.v(TAG, + "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX() + + "," + event.getY() + ")"); + } + if (mQs.disallowPanelTouches()) { + return false; + } + initDownStates(event); + // Do not let touches go to shade or QS if the bouncer is visible, + // but still let user swipe down to expand the panel, dismissing the bouncer. + if (mCentralSurfaces.isBouncerShowing()) { + return true; + } + if (mCommandQueue.panelsEnabled() + && !mNotificationStackScrollLayoutController.isLongPressInProgress() + && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { + mMetricsLogger.count(COUNTER_PANEL_OPEN, 1); + mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1); + return true; + } + if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0) + && mPulseExpansionHandler.onInterceptTouchEvent(event)) { + return true; + } + + if (!isFullyCollapsed() && onQsIntercept(event)) { + if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true"); + return true; + } + if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted + && event.getActionMasked() != MotionEvent.ACTION_DOWN)) { + return false; + } + + /* If the user drags anywhere inside the panel we intercept it if the movement is + upwards. This allows closing the shade from anywhere inside the panel. + We only do this if the current content is scrolled to the bottom, i.e. + canCollapsePanelOnTouch() is true and therefore there is no conflicting scrolling + gesture possible. */ + int pointerIndex = event.findPointerIndex(mTrackingPointer); + if (pointerIndex < 0) { + pointerIndex = 0; + mTrackingPointer = event.getPointerId(pointerIndex); + } + final float x = event.getX(pointerIndex); + final float y = event.getY(pointerIndex); + boolean canCollapsePanel = canCollapsePanelOnTouch(); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mCentralSurfaces.userActivity(); + mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation; + mMinExpandHeight = 0.0f; + mDownTime = mSystemClock.uptimeMillis(); + if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) { + cancelHeightAnimator(); + mTouchSlopExceeded = true; + return true; + } + mInitialExpandY = y; + mInitialExpandX = x; + mTouchStartedInEmptyArea = !isInContentBounds(x, y); + mTouchSlopExceeded = mTouchSlopExceededBeforeDown; + mMotionAborted = false; + mPanelClosedOnDown = isFullyCollapsed(); + mCollapsedAndHeadsUpOnDown = false; + mHasLayoutedSinceDown = false; + mUpdateFlingOnLayout = false; + mTouchAboveFalsingThreshold = false; + addMovement(event); + break; + case MotionEvent.ACTION_POINTER_UP: + final int upPointer = event.getPointerId(event.getActionIndex()); + if (mTrackingPointer == upPointer) { + // gesture is ongoing, find a new pointer to track + final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; + mTrackingPointer = event.getPointerId(newIndex); + mInitialExpandX = event.getX(newIndex); + mInitialExpandY = event.getY(newIndex); + } + break; + case MotionEvent.ACTION_POINTER_DOWN: + if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { + mMotionAborted = true; + mVelocityTracker.clear(); + } + break; + case MotionEvent.ACTION_MOVE: + final float h = y - mInitialExpandY; + addMovement(event); + final boolean openShadeWithoutHun = + mPanelClosedOnDown && !mCollapsedAndHeadsUpOnDown; + if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown + || openShadeWithoutHun) { + float hAbs = Math.abs(h); + float touchSlop = getTouchSlop(event); + if ((h < -touchSlop + || ((openShadeWithoutHun || mAnimatingOnDown) && hAbs > touchSlop)) + && hAbs > Math.abs(x - mInitialExpandX)) { + cancelHeightAnimator(); + startExpandMotion(x, y, true /* startTracking */, mExpandedHeight); + return true; + } + } + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + mVelocityTracker.clear(); + break; + } + return false; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (event.getDownTime() == mLastTouchDownTime) { + // An issue can occur when swiping down after unlock, where multiple down + // events are received in this handler with identical downTimes. Until the + // source of the issue can be located, detect this case and ignore. + // see b/193350347 + Log.w(TAG, "Duplicate down event detected... ignoring"); + return true; + } + mLastTouchDownTime = event.getDownTime(); + } + + + if (mQsFullyExpanded && mQs != null && mQs.disallowPanelTouches()) { + return false; + } + + // Do not allow panel expansion if bouncer is scrimmed or showing over a dream, + // otherwise user would be able to pull down QS or expand the shade. + if (mCentralSurfaces.isBouncerShowingScrimmed() + || mCentralSurfaces.isBouncerShowingOverDream()) { + return false; + } + + // Make sure the next touch won't the blocked after the current ends. + if (event.getAction() == MotionEvent.ACTION_UP + || event.getAction() == MotionEvent.ACTION_CANCEL) { + mBlockingExpansionForCurrentTouch = false; + } + // When touch focus transfer happens, ACTION_DOWN->ACTION_UP may happen immediately + // without any ACTION_MOVE event. + // In such case, simply expand the panel instead of being stuck at the bottom bar. + if (mLastEventSynthesizedDown && event.getAction() == MotionEvent.ACTION_UP) { + expand(true /* animate */); + } + initDownStates(event); + + // If pulse is expanding already, let's give it the touch. There are situations + // where the panel starts expanding even though we're also pulsing + boolean pulseShouldGetTouch = (!mIsExpanding + && !shouldQuickSettingsIntercept(mDownX, mDownY, 0)) + || mPulseExpansionHandler.isExpanding(); + if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) { + // We're expanding all the other ones shouldn't get this anymore + mShadeLog.logMotionEvent(event, "onTouch: PulseExpansionHandler handled event"); + return true; + } + if (mPulsing) { + mShadeLog.logMotionEvent(event, "onTouch: eat touch, device pulsing"); + return true; + } + if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp() + && !mNotificationStackScrollLayoutController.isLongPressInProgress() + && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) { + mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1); + } + boolean handled = mHeadsUpTouchHelper.onTouchEvent(event); + + if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) { + mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event"); + return true; + } + if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) { + mMetricsLogger.count(COUNTER_PANEL_OPEN, 1); + handled = true; + } + + if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyExpanded() + && mKeyguardStateController.isShowing()) { + mStatusBarKeyguardViewManager.updateKeyguardPosition(event.getX()); + } + + handled |= handleTouch(event); + return !mDozing || handled; + } + + private boolean handleTouch(MotionEvent event) { + if (mInstantExpanding) { + mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding"); + return false; + } + if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) { + mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled"); + return false; + } + if (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN) { + mShadeLog.logMotionEvent(event, "onTouch: non-down action, motion was aborted"); + return false; + } + + // If dragging should not expand the notifications shade, then return false. + if (!mNotificationsDragEnabled) { + if (mTracking) { + // Turn off tracking if it's on or the shade can get stuck in the down position. + onTrackingStopped(true /* expand */); + } + mShadeLog.logMotionEvent(event, "onTouch: drag not enabled"); + return false; + } + + // On expanding, single mouse click expands the panel instead of dragging. + if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) { + if (event.getAction() == MotionEvent.ACTION_UP) { + expand(true); + } + return true; + } + + /* + * We capture touch events here and update the expand height here in case according to + * the users fingers. This also handles multi-touch. + * + * Flinging is also enabled in order to open or close the shade. + */ + + int pointerIndex = event.findPointerIndex(mTrackingPointer); + if (pointerIndex < 0) { + pointerIndex = 0; + mTrackingPointer = event.getPointerId(pointerIndex); + } + final float x = event.getX(pointerIndex); + final float y = event.getY(pointerIndex); + + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop(); + mIgnoreXTouchSlop = true; + } + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); + mMinExpandHeight = 0.0f; + mPanelClosedOnDown = isFullyCollapsed(); + mHasLayoutedSinceDown = false; + mUpdateFlingOnLayout = false; + mMotionAborted = false; + mDownTime = mSystemClock.uptimeMillis(); + mTouchAboveFalsingThreshold = false; + mCollapsedAndHeadsUpOnDown = + isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp(); + addMovement(event); + boolean regularHeightAnimationRunning = mHeightAnimator != null + && !mHintAnimationRunning && !mIsSpringBackAnimation; + if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) { + mTouchSlopExceeded = regularHeightAnimationRunning + || mTouchSlopExceededBeforeDown; + cancelHeightAnimator(); + onTrackingStarted(); + } + if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp() + && !mCentralSurfaces.isBouncerShowing()) { + startOpening(event); + } + break; + + case MotionEvent.ACTION_POINTER_UP: + final int upPointer = event.getPointerId(event.getActionIndex()); + if (mTrackingPointer == upPointer) { + // gesture is ongoing, find a new pointer to track + final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; + final float newY = event.getY(newIndex); + final float newX = event.getX(newIndex); + mTrackingPointer = event.getPointerId(newIndex); + mHandlingPointerUp = true; + startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight); + mHandlingPointerUp = false; + } + break; + case MotionEvent.ACTION_POINTER_DOWN: + if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { + mMotionAborted = true; + endMotionEvent(event, x, y, true /* forceCancel */); + return false; + } + break; + case MotionEvent.ACTION_MOVE: + addMovement(event); + if (!isFullyCollapsed()) { + maybeVibrateOnOpening(true /* openingWithTouch */); + } + float h = y - mInitialExpandY; + + // If the panel was collapsed when touching, we only need to check for the + // y-component of the gesture, as we have no conflicting horizontal gesture. + if (Math.abs(h) > getTouchSlop(event) + && (Math.abs(h) > Math.abs(x - mInitialExpandX) + || mIgnoreXTouchSlop)) { + mTouchSlopExceeded = true; + if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) { + if (mInitialOffsetOnTouch != 0f) { + startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); + h = 0; + } + cancelHeightAnimator(); + onTrackingStarted(); + } + } + float newHeight = Math.max(0, h + mInitialOffsetOnTouch); + newHeight = Math.max(newHeight, mMinExpandHeight); + if (-h >= getFalsingThreshold()) { + mTouchAboveFalsingThreshold = true; + mUpwardsWhenThresholdReached = isDirectionUpwards(x, y); + } + if ((!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) { + // Count h==0 as part of swipe-up, + // otherwise {@link NotificationStackScrollLayout} + // wrongly enables stack height updates at the start of lockscreen swipe-up + mAmbientState.setSwipingUp(h <= 0); + setExpandedHeightInternal(newHeight); + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + addMovement(event); + endMotionEvent(event, x, y, false /* forceCancel */); + // mHeightAnimator is null, there is no remaining frame, ends instrumenting. + if (mHeightAnimator == null) { + if (event.getActionMasked() == MotionEvent.ACTION_UP) { + endJankMonitoring(); + } else { + cancelJankMonitoring(); + } + } + break; + } + return !mGestureWaitForTouchSlop || mTracking; + } + } + + /** Listens for config changes. */ + public class OnConfigurationChangedListener implements + NotificationPanelView.OnConfigurationChangedListener { + @Override + public void onConfigurationChanged(Configuration newConfig) { + loadDimens(); + } + } + + static class SplitShadeTransitionAdapter extends Transition { + private static final String PROP_BOUNDS = "splitShadeTransitionAdapter:bounds"; + private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS }; + + private final KeyguardStatusViewController mController; + + SplitShadeTransitionAdapter(KeyguardStatusViewController controller) { + mController = controller; + } + + private void captureValues(TransitionValues transitionValues) { + Rect boundsRect = new Rect(); + boundsRect.left = transitionValues.view.getLeft(); + boundsRect.top = transitionValues.view.getTop(); + boundsRect.right = transitionValues.view.getRight(); + boundsRect.bottom = transitionValues.view.getBottom(); + transitionValues.values.put(PROP_BOUNDS, boundsRect); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + ValueAnimator anim = ValueAnimator.ofFloat(0, 1); + + Rect from = (Rect) startValues.values.get(PROP_BOUNDS); + Rect to = (Rect) endValues.values.get(PROP_BOUNDS); + + anim.addUpdateListener( + animation -> mController.getClockAnimations().onPositionUpdated( + from, to, animation.getAnimatedFraction())); + + return anim; + } + + @Override + public String[] getTransitionProperties() { + return TRANSITION_PROPERTIES; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 1d9210592b78..66a22f4ddc0d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -135,7 +135,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW DumpManager dumpManager, KeyguardStateController keyguardStateController, ScreenOffAnimationController screenOffAnimationController, - AuthController authController) { + AuthController authController, + ShadeExpansionStateManager shadeExpansionStateManager) { mContext = context; mWindowManager = windowManager; mActivityManager = activityManager; @@ -156,6 +157,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW .addCallback(mStateListener, SysuiStatusBarStateController.RANK_STATUS_BAR_WINDOW_CONTROLLER); configurationController.addCallback(this); + shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged); float desiredPreferredRefreshRate = context.getResources() .getInteger(R.integer.config_keyguardRefreshRate); @@ -607,8 +609,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW apply(mCurrentState); } - @Override - public void setQsExpanded(boolean expanded) { + private void onQsExpansionChanged(Boolean expanded) { mCurrentState.mQsExpanded = expanded; apply(mCurrentState); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 6be9bbbf4e0d..65bd58d0d801 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -52,7 +52,6 @@ import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.PhoneStatusBarViewController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import java.io.PrintWriter; @@ -91,7 +90,7 @@ public class NotificationShadeWindowViewController { private boolean mExpandingBelowNotch; private final DockManager mDockManager; private final NotificationPanelViewController mNotificationPanelViewController; - private final PanelExpansionStateManager mPanelExpansionStateManager; + private final ShadeExpansionStateManager mShadeExpansionStateManager; private boolean mIsTrackingBarGesture = false; @@ -104,7 +103,7 @@ public class NotificationShadeWindowViewController { NotificationShadeDepthController depthController, NotificationShadeWindowView notificationShadeWindowView, NotificationPanelViewController notificationPanelViewController, - PanelExpansionStateManager panelExpansionStateManager, + ShadeExpansionStateManager shadeExpansionStateManager, NotificationStackScrollLayoutController notificationStackScrollLayoutController, StatusBarKeyguardViewManager statusBarKeyguardViewManager, StatusBarWindowStateController statusBarWindowStateController, @@ -124,7 +123,7 @@ public class NotificationShadeWindowViewController { mView = notificationShadeWindowView; mDockManager = dockManager; mNotificationPanelViewController = notificationPanelViewController; - mPanelExpansionStateManager = panelExpansionStateManager; + mShadeExpansionStateManager = shadeExpansionStateManager; mDepthController = depthController; mNotificationStackScrollLayoutController = notificationStackScrollLayoutController; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; @@ -404,7 +403,7 @@ public class NotificationShadeWindowViewController { setDragDownHelper(mLockscreenShadeTransitionController.getTouchHelper()); mDepthController.setRoot(mView); - mPanelExpansionStateManager.addExpansionListener(mDepthController); + mShadeExpansionStateManager.addExpansionListener(mDepthController); } public NotificationShadeWindowView getView() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index d6f0de83ecc1..73c6d507f035 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -36,17 +36,12 @@ class NotificationsQSContainerController @Inject constructor( private val navigationModeController: NavigationModeController, private val overviewProxyService: OverviewProxyService, private val largeScreenShadeHeaderController: LargeScreenShadeHeaderController, + private val shadeExpansionStateManager: ShadeExpansionStateManager, private val featureFlags: FeatureFlags, @Main private val delayableExecutor: DelayableExecutor ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController { - var qsExpanded = false - set(value) { - if (field != value) { - field = value - mView.invalidate() - } - } + private var qsExpanded = false private var splitShadeEnabled = false private var isQSDetailShowing = false private var isQSCustomizing = false @@ -71,6 +66,13 @@ class NotificationsQSContainerController @Inject constructor( taskbarVisible = visible } } + private val shadeQsExpansionListener: ShadeQsExpansionListener = + ShadeQsExpansionListener { isQsExpanded -> + if (qsExpanded != isQsExpanded) { + qsExpanded = isQsExpanded + mView.invalidate() + } + } // With certain configuration changes (like light/dark changes), the nav bar will disappear // for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value @@ -106,6 +108,7 @@ class NotificationsQSContainerController @Inject constructor( public override fun onViewAttached() { updateResources() overviewProxyService.addCallback(taskbarVisibilityListener) + shadeExpansionStateManager.addQsExpansionListener(shadeQsExpansionListener) mView.setInsetsChangedListener(delayedInsetSetter) mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) } mView.setConfigurationChangedListener { updateResources() } @@ -113,6 +116,7 @@ class NotificationsQSContainerController @Inject constructor( override fun onViewDetached() { overviewProxyService.removeCallback(taskbarVisibilityListener) + shadeExpansionStateManager.removeQsExpansionListener(shadeQsExpansionListener) mView.removeOnInsetsChangedListener() mView.removeQSFragmentAttachedListener() mView.setConfigurationChangedListener(null) diff --git a/packages/SystemUI/src/com/android/systemui/shade/OWNERS b/packages/SystemUI/src/com/android/systemui/shade/OWNERS index 7dc9dc7efeb7..d71fbf656e5f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/shade/OWNERS @@ -1,3 +1,6 @@ +justinweir@google.com +syeonlee@google.com + per-file *Notification* = set noparent per-file *Notification* = file:../statusbar/notification/OWNERS @@ -11,4 +14,4 @@ per-file NotificationShadeWindowView.java = pixel@google.com, cinek@google.com, per-file NotificationPanelUnfoldAnimationController.kt = alexflo@google.com, jeffdq@google.com, juliacr@google.com per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com -per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com
\ No newline at end of file +per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com diff --git a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java deleted file mode 100644 index b4ce95c434fc..000000000000 --- a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java +++ /dev/null @@ -1,1494 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.shade; - -import static android.view.View.INVISIBLE; -import static android.view.View.VISIBLE; - -import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; -import static com.android.systemui.classifier.Classifier.GENERIC; -import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; -import static com.android.systemui.classifier.Classifier.UNLOCK; -import static com.android.systemui.shade.NotificationPanelView.DEBUG; - -import static java.lang.Float.isNaN; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.os.VibrationEffect; -import android.util.Log; -import android.util.MathUtils; -import android.view.InputDevice; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.ViewPropertyAnimator; -import android.view.ViewTreeObserver; -import android.view.animation.Interpolator; - -import com.android.internal.jank.InteractionJankMonitor; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.util.LatencyTracker; -import com.android.systemui.DejankUtils; -import com.android.systemui.R; -import com.android.systemui.animation.Interpolators; -import com.android.systemui.classifier.Classifier; -import com.android.systemui.doze.DozeLog; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.statusbar.NotificationShadeWindowController; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.SysuiStatusBarStateController; -import com.android.systemui.statusbar.VibratorHelper; -import com.android.systemui.statusbar.notification.stack.AmbientState; -import com.android.systemui.statusbar.phone.BounceInterpolator; -import com.android.systemui.statusbar.phone.CentralSurfaces; -import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; -import com.android.systemui.statusbar.phone.KeyguardBottomAreaView; -import com.android.systemui.statusbar.phone.LockscreenGestureLogger; -import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent; -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.time.SystemClock; -import com.android.wm.shell.animation.FlingAnimationUtils; - -import java.io.PrintWriter; -import java.util.List; - -public abstract class PanelViewController { - public static final String TAG = NotificationPanelView.class.getSimpleName(); - public static final float FLING_MAX_LENGTH_SECONDS = 0.6f; - public static final float FLING_SPEED_UP_FACTOR = 0.6f; - public static final float FLING_CLOSING_MAX_LENGTH_SECONDS = 0.6f; - public static final float FLING_CLOSING_SPEED_UP_FACTOR = 0.6f; - private static final int NO_FIXED_DURATION = -1; - private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L; - private static final long SHADE_OPEN_SPRING_BACK_DURATION = 400L; - - /** - * The factor of the usual high velocity that is needed in order to reach the maximum overshoot - * when flinging. A low value will make it that most flings will reach the maximum overshoot. - */ - private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f; - - protected long mDownTime; - protected boolean mTouchSlopExceededBeforeDown; - private float mMinExpandHeight; - private boolean mPanelUpdateWhenAnimatorEnds; - private final boolean mVibrateOnOpening; - private boolean mHasVibratedOnOpen = false; - protected boolean mIsLaunchAnimationRunning; - private int mFixedDuration = NO_FIXED_DURATION; - protected float mOverExpansion; - - /** - * The overshoot amount when the panel flings open - */ - private float mPanelFlingOvershootAmount; - - /** - * The amount of pixels that we have overexpanded the last time with a gesture - */ - private float mLastGesturedOverExpansion = -1; - - /** - * Is the current animator the spring back animation? - */ - private boolean mIsSpringBackAnimation; - - private boolean mInSplitShade; - - private void logf(String fmt, Object... args) { - Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); - } - - protected CentralSurfaces mCentralSurfaces; - protected HeadsUpManagerPhone mHeadsUpManager; - protected final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager; - - private float mHintDistance; - private float mInitialOffsetOnTouch; - private boolean mCollapsedAndHeadsUpOnDown; - private float mExpandedFraction = 0; - private float mExpansionDragDownAmountPx = 0; - protected float mExpandedHeight = 0; - private boolean mPanelClosedOnDown; - private boolean mHasLayoutedSinceDown; - private float mUpdateFlingVelocity; - private boolean mUpdateFlingOnLayout; - private boolean mClosing; - protected boolean mTracking; - private boolean mTouchSlopExceeded; - private int mTrackingPointer; - private int mTouchSlop; - private float mSlopMultiplier; - protected boolean mHintAnimationRunning; - private boolean mTouchAboveFalsingThreshold; - private boolean mTouchStartedInEmptyArea; - private boolean mMotionAborted; - private boolean mUpwardsWhenThresholdReached; - private boolean mAnimatingOnDown; - private boolean mHandlingPointerUp; - - private ValueAnimator mHeightAnimator; - private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); - private final FlingAnimationUtils mFlingAnimationUtils; - private final FlingAnimationUtils mFlingAnimationUtilsClosing; - private final FlingAnimationUtils mFlingAnimationUtilsDismissing; - private final LatencyTracker mLatencyTracker; - private final FalsingManager mFalsingManager; - private final DozeLog mDozeLog; - private final VibratorHelper mVibratorHelper; - - /** - * Whether an instant expand request is currently pending and we are just waiting for layout. - */ - private boolean mInstantExpanding; - private boolean mAnimateAfterExpanding; - private boolean mIsFlinging; - - private String mViewName; - private float mInitialExpandY; - private float mInitialExpandX; - private boolean mTouchDisabled; - private boolean mInitialTouchFromKeyguard; - - /** - * Whether or not the NotificationPanelView can be expanded or collapsed with a drag. - */ - private final boolean mNotificationsDragEnabled; - - private final Interpolator mBounceInterpolator; - protected KeyguardBottomAreaView mKeyguardBottomArea; - - /** - * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time. - */ - private float mNextCollapseSpeedUpFactor = 1.0f; - - protected boolean mExpanding; - private boolean mGestureWaitForTouchSlop; - private boolean mIgnoreXTouchSlop; - private boolean mExpandLatencyTracking; - private final NotificationPanelView mView; - private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - private final NotificationShadeWindowController mNotificationShadeWindowController; - protected final Resources mResources; - protected final KeyguardStateController mKeyguardStateController; - protected final SysuiStatusBarStateController mStatusBarStateController; - protected final AmbientState mAmbientState; - protected final LockscreenGestureLogger mLockscreenGestureLogger; - private final PanelExpansionStateManager mPanelExpansionStateManager; - private final InteractionJankMonitor mInteractionJankMonitor; - protected final SystemClock mSystemClock; - - protected final ShadeLogger mShadeLog; - - protected abstract void onExpandingFinished(); - - protected void onExpandingStarted() { - } - - protected void notifyExpandingStarted() { - if (!mExpanding) { - mExpanding = true; - onExpandingStarted(); - } - } - - protected final void notifyExpandingFinished() { - endClosing(); - if (mExpanding) { - mExpanding = false; - onExpandingFinished(); - } - } - - protected AmbientState getAmbientState() { - return mAmbientState; - } - - public PanelViewController( - NotificationPanelView view, - FalsingManager falsingManager, - DozeLog dozeLog, - KeyguardStateController keyguardStateController, - SysuiStatusBarStateController statusBarStateController, - NotificationShadeWindowController notificationShadeWindowController, - VibratorHelper vibratorHelper, - StatusBarKeyguardViewManager statusBarKeyguardViewManager, - LatencyTracker latencyTracker, - FlingAnimationUtils.Builder flingAnimationUtilsBuilder, - StatusBarTouchableRegionManager statusBarTouchableRegionManager, - LockscreenGestureLogger lockscreenGestureLogger, - PanelExpansionStateManager panelExpansionStateManager, - AmbientState ambientState, - InteractionJankMonitor interactionJankMonitor, - ShadeLogger shadeLogger, - SystemClock systemClock) { - keyguardStateController.addCallback(new KeyguardStateController.Callback() { - @Override - public void onKeyguardFadingAwayChanged() { - updateExpandedHeightToMaxHeight(); - } - }); - mAmbientState = ambientState; - mView = view; - mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; - mLockscreenGestureLogger = lockscreenGestureLogger; - mPanelExpansionStateManager = panelExpansionStateManager; - mShadeLog = shadeLogger; - TouchHandler touchHandler = createTouchHandler(); - mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - mViewName = mResources.getResourceName(mView.getId()); - } - - @Override - public void onViewDetachedFromWindow(View v) { - } - }); - - mView.addOnLayoutChangeListener(createLayoutChangeListener()); - mView.setOnTouchListener(touchHandler); - mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener()); - - mResources = mView.getResources(); - mKeyguardStateController = keyguardStateController; - mStatusBarStateController = statusBarStateController; - mNotificationShadeWindowController = notificationShadeWindowController; - mFlingAnimationUtils = flingAnimationUtilsBuilder - .reset() - .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS) - .setSpeedUpFactor(FLING_SPEED_UP_FACTOR) - .build(); - mFlingAnimationUtilsClosing = flingAnimationUtilsBuilder - .reset() - .setMaxLengthSeconds(FLING_CLOSING_MAX_LENGTH_SECONDS) - .setSpeedUpFactor(FLING_CLOSING_SPEED_UP_FACTOR) - .build(); - mFlingAnimationUtilsDismissing = flingAnimationUtilsBuilder - .reset() - .setMaxLengthSeconds(0.5f) - .setSpeedUpFactor(0.6f) - .setX2(0.6f) - .setY2(0.84f) - .build(); - mLatencyTracker = latencyTracker; - mBounceInterpolator = new BounceInterpolator(); - mFalsingManager = falsingManager; - mDozeLog = dozeLog; - mNotificationsDragEnabled = mResources.getBoolean( - R.bool.config_enableNotificationShadeDrag); - mVibratorHelper = vibratorHelper; - mVibrateOnOpening = mResources.getBoolean(R.bool.config_vibrateOnIconAnimation); - mStatusBarTouchableRegionManager = statusBarTouchableRegionManager; - mInteractionJankMonitor = interactionJankMonitor; - mSystemClock = systemClock; - } - - protected void loadDimens() { - final ViewConfiguration configuration = ViewConfiguration.get(mView.getContext()); - mTouchSlop = configuration.getScaledTouchSlop(); - mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier(); - mHintDistance = mResources.getDimension(R.dimen.hint_move_distance); - mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount); - mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade); - } - - protected float getTouchSlop(MotionEvent event) { - // Adjust the touch slop if another gesture may be being performed. - return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE - ? mTouchSlop * mSlopMultiplier - : mTouchSlop; - } - - private void addMovement(MotionEvent event) { - // Add movement to velocity tracker using raw screen X and Y coordinates instead - // of window coordinates because the window frame may be moving at the same time. - float deltaX = event.getRawX() - event.getX(); - float deltaY = event.getRawY() - event.getY(); - event.offsetLocation(deltaX, deltaY); - mVelocityTracker.addMovement(event); - event.offsetLocation(-deltaX, -deltaY); - } - - public void setTouchAndAnimationDisabled(boolean disabled) { - mTouchDisabled = disabled; - if (mTouchDisabled) { - cancelHeightAnimator(); - if (mTracking) { - onTrackingStopped(true /* expanded */); - } - notifyExpandingFinished(); - } - } - - public void startExpandLatencyTracking() { - if (mLatencyTracker.isEnabled()) { - mLatencyTracker.onActionStart(LatencyTracker.ACTION_EXPAND_PANEL); - mExpandLatencyTracking = true; - } - } - - private void startOpening(MotionEvent event) { - updatePanelExpansionAndVisibility(); - // Reset at start so haptic can be triggered as soon as panel starts to open. - mHasVibratedOnOpen = false; - //TODO: keyguard opens QS a different way; log that too? - - // Log the position of the swipe that opened the panel - float width = mCentralSurfaces.getDisplayWidth(); - float height = mCentralSurfaces.getDisplayHeight(); - int rot = mCentralSurfaces.getRotation(); - - mLockscreenGestureLogger.writeAtFractionalPosition(MetricsEvent.ACTION_PANEL_VIEW_EXPAND, - (int) (event.getX() / width * 100), (int) (event.getY() / height * 100), rot); - mLockscreenGestureLogger - .log(LockscreenUiEvent.LOCKSCREEN_UNLOCKED_NOTIFICATION_PANEL_EXPAND); - } - - /** - * Maybe vibrate as panel is opened. - * - * @param openingWithTouch Whether the panel is being opened with touch. If the panel is instead - * being opened programmatically (such as by the open panel gesture), we always play haptic. - */ - protected void maybeVibrateOnOpening(boolean openingWithTouch) { - if (mVibrateOnOpening) { - if (!openingWithTouch || !mHasVibratedOnOpen) { - mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); - mHasVibratedOnOpen = true; - } - } - } - - protected abstract float getOpeningHeight(); - - /** - * @return whether the swiping direction is upwards and above a 45 degree angle compared to the - * horizontal direction - */ - private boolean isDirectionUpwards(float x, float y) { - float xDiff = x - mInitialExpandX; - float yDiff = y - mInitialExpandY; - if (yDiff >= 0) { - return false; - } - return Math.abs(yDiff) >= Math.abs(xDiff); - } - - public void startExpandMotion(float newX, float newY, boolean startTracking, - float expandedHeight) { - if (!mHandlingPointerUp && !mStatusBarStateController.isDozing()) { - beginJankMonitoring(); - } - mInitialOffsetOnTouch = expandedHeight; - mInitialExpandY = newY; - mInitialExpandX = newX; - mInitialTouchFromKeyguard = mKeyguardStateController.isShowing(); - if (startTracking) { - mTouchSlopExceeded = true; - setExpandedHeight(mInitialOffsetOnTouch); - onTrackingStarted(); - } - } - - private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) { - mTrackingPointer = -1; - mAmbientState.setSwipingUp(false); - if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop - || Math.abs(y - mInitialExpandY) > mTouchSlop - || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { - mVelocityTracker.computeCurrentVelocity(1000); - float vel = mVelocityTracker.getYVelocity(); - float vectorVel = (float) Math.hypot( - mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); - - final boolean onKeyguard = mKeyguardStateController.isShowing(); - final boolean expand; - if (mKeyguardStateController.isKeyguardFadingAway() - || (mInitialTouchFromKeyguard && !onKeyguard)) { - // Don't expand for any touches that started from the keyguard and ended after the - // keyguard is gone. - expand = false; - } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) { - if (onKeyguard) { - expand = true; - } else if (mCentralSurfaces.isBouncerShowingOverDream()) { - expand = false; - } else { - // If we get a cancel, put the shade back to the state it was in when the - // gesture started - expand = !mPanelClosedOnDown; - } - } else { - expand = flingExpands(vel, vectorVel, x, y); - } - - mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold, - mCentralSurfaces.isFalsingThresholdNeeded(), - mCentralSurfaces.isWakeUpComingFromTouch()); - // Log collapse gesture if on lock screen. - if (!expand && onKeyguard) { - float displayDensity = mCentralSurfaces.getDisplayDensity(); - int heightDp = (int) Math.abs((y - mInitialExpandY) / displayDensity); - int velocityDp = (int) Math.abs(vel / displayDensity); - mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_UNLOCK, heightDp, velocityDp); - mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_UNLOCK); - } - @Classifier.InteractionType int interactionType = vel == 0 ? GENERIC - : y - mInitialExpandY > 0 ? QUICK_SETTINGS - : (mKeyguardStateController.canDismissLockScreen() - ? UNLOCK : BOUNCER_UNLOCK); - - fling(vel, expand, isFalseTouch(x, y, interactionType)); - onTrackingStopped(expand); - mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown; - if (mUpdateFlingOnLayout) { - mUpdateFlingVelocity = vel; - } - } else if (!mCentralSurfaces.isBouncerShowing() - && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating() - && !mKeyguardStateController.isKeyguardGoingAway()) { - boolean expands = onEmptySpaceClick(); - onTrackingStopped(expands); - } - mVelocityTracker.clear(); - } - - protected float getCurrentExpandVelocity() { - mVelocityTracker.computeCurrentVelocity(1000); - return mVelocityTracker.getYVelocity(); - } - - protected abstract int getFalsingThreshold(); - - protected abstract boolean shouldGestureWaitForTouchSlop(); - - protected void onTrackingStopped(boolean expand) { - mTracking = false; - mCentralSurfaces.onTrackingStopped(expand); - updatePanelExpansionAndVisibility(); - } - - protected void onTrackingStarted() { - endClosing(); - mTracking = true; - mCentralSurfaces.onTrackingStarted(); - notifyExpandingStarted(); - updatePanelExpansionAndVisibility(); - } - - /** - * @return Whether a pair of coordinates are inside the visible view content bounds. - */ - protected abstract boolean isInContentBounds(float x, float y); - - protected void cancelHeightAnimator() { - if (mHeightAnimator != null) { - if (mHeightAnimator.isRunning()) { - mPanelUpdateWhenAnimatorEnds = false; - } - mHeightAnimator.cancel(); - } - endClosing(); - } - - private void endClosing() { - if (mClosing) { - setIsClosing(false); - onClosingFinished(); - } - } - - protected abstract boolean canCollapsePanelOnTouch(); - - protected float getContentHeight() { - return mExpandedHeight; - } - - /** - * @param vel the current vertical velocity of the motion - * @param vectorVel the length of the vectorial velocity - * @return whether a fling should expands the panel; contracts otherwise - */ - protected boolean flingExpands(float vel, float vectorVel, float x, float y) { - if (mFalsingManager.isUnlockingDisabled()) { - return true; - } - - @Classifier.InteractionType int interactionType = y - mInitialExpandY > 0 - ? QUICK_SETTINGS : ( - mKeyguardStateController.canDismissLockScreen() ? UNLOCK : BOUNCER_UNLOCK); - - if (isFalseTouch(x, y, interactionType)) { - return true; - } - if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) { - return shouldExpandWhenNotFlinging(); - } else { - return vel > 0; - } - } - - protected boolean shouldExpandWhenNotFlinging() { - return getExpandedFraction() > 0.5f; - } - - /** - * @param x the final x-coordinate when the finger was lifted - * @param y the final y-coordinate when the finger was lifted - * @return whether this motion should be regarded as a false touch - */ - private boolean isFalseTouch(float x, float y, - @Classifier.InteractionType int interactionType) { - if (!mCentralSurfaces.isFalsingThresholdNeeded()) { - return false; - } - if (mFalsingManager.isClassifierEnabled()) { - return mFalsingManager.isFalseTouch(interactionType); - } - if (!mTouchAboveFalsingThreshold) { - return true; - } - if (mUpwardsWhenThresholdReached) { - return false; - } - return !isDirectionUpwards(x, y); - } - - protected void fling(float vel, boolean expand) { - fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false); - } - - protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) { - fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing); - } - - protected void fling(float vel, boolean expand, float collapseSpeedUpFactor, - boolean expandBecauseOfFalsing) { - float target = expand ? getMaxPanelHeight() : 0; - if (!expand) { - setIsClosing(true); - } - flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing); - } - - protected void flingToHeight(float vel, boolean expand, float target, - float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) { - if (target == mExpandedHeight && mOverExpansion == 0.0f) { - // We're at the target and didn't fling and there's no overshoot - onFlingEnd(false /* cancelled */); - return; - } - mIsFlinging = true; - // we want to perform an overshoot animation when flinging open - final boolean addOverscroll = - expand - && !mInSplitShade // Split shade has its own overscroll logic - && mStatusBarStateController.getState() != StatusBarState.KEYGUARD - && mOverExpansion == 0.0f - && vel >= 0; - final boolean shouldSpringBack = addOverscroll || (mOverExpansion != 0.0f && expand); - float overshootAmount = 0.0f; - if (addOverscroll) { - // Let's overshoot depending on the amount of velocity - overshootAmount = MathUtils.lerp( - 0.2f, - 1.0f, - MathUtils.saturate(vel - / (mFlingAnimationUtils.getHighVelocityPxPerSecond() - * FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT))); - overshootAmount += mOverExpansion / mPanelFlingOvershootAmount; - } - ValueAnimator animator = createHeightAnimator(target, overshootAmount); - if (expand) { - if (expandBecauseOfFalsing && vel < 0) { - vel = 0; - } - mFlingAnimationUtils.apply(animator, mExpandedHeight, - target + overshootAmount * mPanelFlingOvershootAmount, vel, mView.getHeight()); - if (vel == 0) { - animator.setDuration(SHADE_OPEN_SPRING_OUT_DURATION); - } - } else { - if (shouldUseDismissingAnimation()) { - if (vel == 0) { - animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED); - long duration = (long) (200 + mExpandedHeight / mView.getHeight() * 100); - animator.setDuration(duration); - } else { - mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel, - mView.getHeight()); - } - } else { - mFlingAnimationUtilsClosing.apply( - animator, mExpandedHeight, target, vel, mView.getHeight()); - } - - // Make it shorter if we run a canned animation - if (vel == 0) { - animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor)); - } - if (mFixedDuration != NO_FIXED_DURATION) { - animator.setDuration(mFixedDuration); - } - } - animator.addListener(new AnimatorListenerAdapter() { - private boolean mCancelled; - - @Override - public void onAnimationStart(Animator animation) { - if (!mStatusBarStateController.isDozing()) { - beginJankMonitoring(); - } - } - - @Override - public void onAnimationCancel(Animator animation) { - mCancelled = true; - } - - @Override - public void onAnimationEnd(Animator animation) { - if (shouldSpringBack && !mCancelled) { - // After the shade is flinged open to an overscrolled state, spring back - // the shade by reducing section padding to 0. - springBack(); - } else { - onFlingEnd(mCancelled); - } - } - }); - setAnimator(animator); - animator.start(); - } - - private void springBack() { - if (mOverExpansion == 0) { - onFlingEnd(false /* cancelled */); - return; - } - mIsSpringBackAnimation = true; - ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0); - animator.addUpdateListener( - animation -> setOverExpansionInternal((float) animation.getAnimatedValue(), - false /* isFromGesture */)); - animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION); - animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - animator.addListener(new AnimatorListenerAdapter() { - private boolean mCancelled; - @Override - public void onAnimationCancel(Animator animation) { - mCancelled = true; - } - @Override - public void onAnimationEnd(Animator animation) { - mIsSpringBackAnimation = false; - onFlingEnd(mCancelled); - } - }); - setAnimator(animator); - animator.start(); - } - - protected void onFlingEnd(boolean cancelled) { - mIsFlinging = false; - // No overshoot when the animation ends - setOverExpansionInternal(0, false /* isFromGesture */); - setAnimator(null); - mKeyguardStateController.notifyPanelFlingEnd(); - if (!cancelled) { - endJankMonitoring(); - notifyExpandingFinished(); - } else { - cancelJankMonitoring(); - } - updatePanelExpansionAndVisibility(); - } - - protected abstract boolean shouldUseDismissingAnimation(); - - public String getName() { - return mViewName; - } - - public void setExpandedHeight(float height) { - if (DEBUG) logf("setExpandedHeight(%.1f)", height); - setExpandedHeightInternal(height); - } - - void updateExpandedHeightToMaxHeight() { - float currentMaxPanelHeight = getMaxPanelHeight(); - - if (isFullyCollapsed()) { - return; - } - - if (currentMaxPanelHeight == mExpandedHeight) { - return; - } - - if (mTracking && !isTrackingBlocked()) { - return; - } - - if (mHeightAnimator != null && !mIsSpringBackAnimation) { - mPanelUpdateWhenAnimatorEnds = true; - return; - } - - setExpandedHeight(currentMaxPanelHeight); - } - - /** - * Returns drag down distance after which panel should be fully expanded. Usually it's the - * same as max panel height but for large screen devices (especially split shade) we might - * want to return different value to shorten drag distance - */ - public abstract int getMaxPanelTransitionDistance(); - - public void setExpandedHeightInternal(float h) { - if (isNaN(h)) { - Log.wtf(TAG, "ExpandedHeight set to NaN"); - } - mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> { - if (mExpandLatencyTracking && h != 0f) { - DejankUtils.postAfterTraversal( - () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL)); - mExpandLatencyTracking = false; - } - float maxPanelHeight = getMaxPanelTransitionDistance(); - if (mHeightAnimator == null) { - // Split shade has its own overscroll logic - if (mTracking && !mInSplitShade) { - float overExpansionPixels = Math.max(0, h - maxPanelHeight); - setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */); - } - } - mExpandedHeight = Math.min(h, maxPanelHeight); - // If we are closing the panel and we are almost there due to a slow decelerating - // interpolator, abort the animation. - if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) { - mExpandedHeight = 0f; - if (mHeightAnimator != null) { - mHeightAnimator.end(); - } - } - mExpansionDragDownAmountPx = h; - mExpandedFraction = Math.min(1f, - maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight); - mAmbientState.setExpansionFraction(mExpandedFraction); - onHeightUpdated(mExpandedHeight); - updatePanelExpansionAndVisibility(); - }); - } - - /** - * @return true if the panel tracking should be temporarily blocked; this is used when a - * conflicting gesture (opening QS) is happening - */ - protected abstract boolean isTrackingBlocked(); - - protected void setOverExpansion(float overExpansion) { - mOverExpansion = overExpansion; - } - - /** - * Set the current overexpansion - * - * @param overExpansion the amount of overexpansion to apply - * @param isFromGesture is this amount from a gesture and needs to be rubberBanded? - */ - private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) { - if (!isFromGesture) { - mLastGesturedOverExpansion = -1; - setOverExpansion(overExpansion); - } else if (mLastGesturedOverExpansion != overExpansion) { - mLastGesturedOverExpansion = overExpansion; - final float heightForFullOvershoot = mView.getHeight() / 3.0f; - float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot); - newExpansion = Interpolators.getOvershootInterpolation(newExpansion); - setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f); - } - } - - protected abstract void onHeightUpdated(float expandedHeight); - - /** - * This returns the maximum height of the panel. Children should override this if their - * desired height is not the full height. - * - * @return the default implementation simply returns the maximum height. - */ - protected abstract int getMaxPanelHeight(); - - public void setExpandedFraction(float frac) { - setExpandedHeight(getMaxPanelTransitionDistance() * frac); - } - - public float getExpandedHeight() { - return mExpandedHeight; - } - - public float getExpandedFraction() { - return mExpandedFraction; - } - - public boolean isFullyExpanded() { - return mExpandedHeight >= getMaxPanelHeight(); - } - - public boolean isFullyCollapsed() { - return mExpandedFraction <= 0.0f; - } - - public boolean isCollapsing() { - return mClosing || mIsLaunchAnimationRunning; - } - - public boolean isFlinging() { - return mIsFlinging; - } - - public boolean isTracking() { - return mTracking; - } - - public void collapse(boolean delayed, float speedUpFactor) { - if (DEBUG) logf("collapse: " + this); - if (canPanelBeCollapsed()) { - cancelHeightAnimator(); - notifyExpandingStarted(); - - // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state. - setIsClosing(true); - if (delayed) { - mNextCollapseSpeedUpFactor = speedUpFactor; - mView.postDelayed(mFlingCollapseRunnable, 120); - } else { - fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */); - } - } - } - - public boolean canPanelBeCollapsed() { - return !isFullyCollapsed() && !mTracking && !mClosing; - } - - private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */, - mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */); - - public void expand(final boolean animate) { - if (!isFullyCollapsed() && !isCollapsing()) { - return; - } - - mInstantExpanding = true; - mAnimateAfterExpanding = animate; - mUpdateFlingOnLayout = false; - abortAnimations(); - if (mTracking) { - onTrackingStopped(true /* expands */); // The panel is expanded after this call. - } - if (mExpanding) { - notifyExpandingFinished(); - } - updatePanelExpansionAndVisibility(); - - // Wait for window manager to pickup the change, so we know the maximum height of the panel - // then. - mView.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - if (!mInstantExpanding) { - mView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - return; - } - if (mCentralSurfaces.getNotificationShadeWindowView().isVisibleToUser()) { - mView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - if (mAnimateAfterExpanding) { - notifyExpandingStarted(); - beginJankMonitoring(); - fling(0, true /* expand */); - } else { - setExpandedFraction(1f); - } - mInstantExpanding = false; - } - } - }); - - // Make sure a layout really happens. - mView.requestLayout(); - } - - public void instantCollapse() { - abortAnimations(); - setExpandedFraction(0f); - if (mExpanding) { - notifyExpandingFinished(); - } - if (mInstantExpanding) { - mInstantExpanding = false; - updatePanelExpansionAndVisibility(); - } - } - - private void abortAnimations() { - cancelHeightAnimator(); - mView.removeCallbacks(mFlingCollapseRunnable); - } - - protected abstract void onClosingFinished(); - - protected void startUnlockHintAnimation() { - - // We don't need to hint the user if an animation is already running or the user is changing - // the expansion. - if (mHeightAnimator != null || mTracking) { - return; - } - notifyExpandingStarted(); - startUnlockHintAnimationPhase1(() -> { - notifyExpandingFinished(); - onUnlockHintFinished(); - mHintAnimationRunning = false; - }); - onUnlockHintStarted(); - mHintAnimationRunning = true; - } - - protected void onUnlockHintFinished() { - mCentralSurfaces.onHintFinished(); - } - - protected void onUnlockHintStarted() { - mCentralSurfaces.onUnlockHintStarted(); - } - - public boolean isUnlockHintRunning() { - return mHintAnimationRunning; - } - - /** - * Phase 1: Move everything upwards. - */ - private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) { - float target = Math.max(0, getMaxPanelHeight() - mHintDistance); - ValueAnimator animator = createHeightAnimator(target); - animator.setDuration(250); - animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - animator.addListener(new AnimatorListenerAdapter() { - private boolean mCancelled; - - @Override - public void onAnimationCancel(Animator animation) { - mCancelled = true; - } - - @Override - public void onAnimationEnd(Animator animation) { - if (mCancelled) { - setAnimator(null); - onAnimationFinished.run(); - } else { - startUnlockHintAnimationPhase2(onAnimationFinished); - } - } - }); - animator.start(); - setAnimator(animator); - - final List<ViewPropertyAnimator> indicationAnimators = - mKeyguardBottomArea.getIndicationAreaAnimators(); - for (final ViewPropertyAnimator indicationAreaAnimator : indicationAnimators) { - indicationAreaAnimator - .translationY(-mHintDistance) - .setDuration(250) - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .withEndAction(() -> indicationAreaAnimator - .translationY(0) - .setDuration(450) - .setInterpolator(mBounceInterpolator) - .start()) - .start(); - } - } - - private void setAnimator(ValueAnimator animator) { - mHeightAnimator = animator; - if (animator == null && mPanelUpdateWhenAnimatorEnds) { - mPanelUpdateWhenAnimatorEnds = false; - updateExpandedHeightToMaxHeight(); - } - } - - /** - * Phase 2: Bounce down. - */ - private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) { - ValueAnimator animator = createHeightAnimator(getMaxPanelHeight()); - animator.setDuration(450); - animator.setInterpolator(mBounceInterpolator); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - setAnimator(null); - onAnimationFinished.run(); - updatePanelExpansionAndVisibility(); - } - }); - animator.start(); - setAnimator(animator); - } - - private ValueAnimator createHeightAnimator(float targetHeight) { - return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */); - } - - /** - * Create an animator that can also overshoot - * - * @param targetHeight the target height - * @param overshootAmount the amount of overshoot desired - */ - private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) { - float startExpansion = mOverExpansion; - ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight); - animator.addUpdateListener( - animation -> { - if (overshootAmount > 0.0f - // Also remove the overExpansion when collapsing - || (targetHeight == 0.0f && startExpansion != 0)) { - final float expansion = MathUtils.lerp( - startExpansion, - mPanelFlingOvershootAmount * overshootAmount, - Interpolators.FAST_OUT_SLOW_IN.getInterpolation( - animator.getAnimatedFraction())); - setOverExpansionInternal(expansion, false /* isFromGesture */); - } - setExpandedHeightInternal((float) animation.getAnimatedValue()); - }); - return animator; - } - - /** Update the visibility of {@link NotificationPanelView} if necessary. */ - public void updateVisibility() { - mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE); - } - - /** Returns true if {@link NotificationPanelView} should be visible. */ - abstract protected boolean shouldPanelBeVisible(); - - /** - * Updates the panel expansion and {@link NotificationPanelView} visibility if necessary. - * - * TODO(b/200063118): Could public calls to this method be replaced with calls to - * {@link #updateVisibility()}? That would allow us to make this method private. - */ - public void updatePanelExpansionAndVisibility() { - mPanelExpansionStateManager.onPanelExpansionChanged( - mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx); - updateVisibility(); - } - - public boolean isExpanded() { - return mExpandedFraction > 0f - || mInstantExpanding - || isPanelVisibleBecauseOfHeadsUp() - || mTracking - || mHeightAnimator != null - && !mIsSpringBackAnimation; - } - - protected abstract boolean isPanelVisibleBecauseOfHeadsUp(); - - /** - * Gets called when the user performs a click anywhere in the empty area of the panel. - * - * @return whether the panel will be expanded after the action performed by this method - */ - protected boolean onEmptySpaceClick() { - if (mHintAnimationRunning) { - return true; - } - return onMiddleClicked(); - } - - protected abstract boolean onMiddleClicked(); - - protected abstract boolean isDozing(); - - public void dump(PrintWriter pw, String[] args) { - pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s" - + " tracking=%s timeAnim=%s%s " - + "touchDisabled=%s" + "]", - this.getClass().getSimpleName(), getExpandedHeight(), getMaxPanelHeight(), - mClosing ? "T" : "f", mTracking ? "T" : "f", mHeightAnimator, - ((mHeightAnimator != null && mHeightAnimator.isStarted()) ? " (started)" : ""), - mTouchDisabled ? "T" : "f")); - } - - public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) { - mHeadsUpManager = headsUpManager; - } - - public void setIsLaunchAnimationRunning(boolean running) { - mIsLaunchAnimationRunning = running; - } - - protected void setIsClosing(boolean isClosing) { - mClosing = isClosing; - } - - protected boolean isClosing() { - return mClosing; - } - - public void collapseWithDuration(int animationDuration) { - mFixedDuration = animationDuration; - collapse(false /* delayed */, 1.0f /* speedUpFactor */); - mFixedDuration = NO_FIXED_DURATION; - } - - public ViewGroup getView() { - // TODO: remove this method, or at least reduce references to it. - return mView; - } - - protected abstract OnLayoutChangeListener createLayoutChangeListener(); - - protected abstract TouchHandler createTouchHandler(); - - protected OnConfigurationChangedListener createOnConfigurationChangedListener() { - return new OnConfigurationChangedListener(); - } - - public class TouchHandler implements View.OnTouchListener { - - public boolean onInterceptTouchEvent(MotionEvent event) { - if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted - && event.getActionMasked() != MotionEvent.ACTION_DOWN)) { - return false; - } - - /* - * If the user drags anywhere inside the panel we intercept it if the movement is - * upwards. This allows closing the shade from anywhere inside the panel. - * - * We only do this if the current content is scrolled to the bottom, - * i.e canCollapsePanelOnTouch() is true and therefore there is no conflicting scrolling - * gesture - * possible. - */ - int pointerIndex = event.findPointerIndex(mTrackingPointer); - if (pointerIndex < 0) { - pointerIndex = 0; - mTrackingPointer = event.getPointerId(pointerIndex); - } - final float x = event.getX(pointerIndex); - final float y = event.getY(pointerIndex); - boolean canCollapsePanel = canCollapsePanelOnTouch(); - - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - mCentralSurfaces.userActivity(); - mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation; - mMinExpandHeight = 0.0f; - mDownTime = mSystemClock.uptimeMillis(); - if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) { - cancelHeightAnimator(); - mTouchSlopExceeded = true; - return true; - } - mInitialExpandY = y; - mInitialExpandX = x; - mTouchStartedInEmptyArea = !isInContentBounds(x, y); - mTouchSlopExceeded = mTouchSlopExceededBeforeDown; - mMotionAborted = false; - mPanelClosedOnDown = isFullyCollapsed(); - mCollapsedAndHeadsUpOnDown = false; - mHasLayoutedSinceDown = false; - mUpdateFlingOnLayout = false; - mTouchAboveFalsingThreshold = false; - addMovement(event); - break; - case MotionEvent.ACTION_POINTER_UP: - final int upPointer = event.getPointerId(event.getActionIndex()); - if (mTrackingPointer == upPointer) { - // gesture is ongoing, find a new pointer to track - final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; - mTrackingPointer = event.getPointerId(newIndex); - mInitialExpandX = event.getX(newIndex); - mInitialExpandY = event.getY(newIndex); - } - break; - case MotionEvent.ACTION_POINTER_DOWN: - if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { - mMotionAborted = true; - mVelocityTracker.clear(); - } - break; - case MotionEvent.ACTION_MOVE: - final float h = y - mInitialExpandY; - addMovement(event); - final boolean openShadeWithoutHun = - mPanelClosedOnDown && !mCollapsedAndHeadsUpOnDown; - if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown - || openShadeWithoutHun) { - float hAbs = Math.abs(h); - float touchSlop = getTouchSlop(event); - if ((h < -touchSlop - || ((openShadeWithoutHun || mAnimatingOnDown) && hAbs > touchSlop)) - && hAbs > Math.abs(x - mInitialExpandX)) { - cancelHeightAnimator(); - startExpandMotion(x, y, true /* startTracking */, mExpandedHeight); - return true; - } - } - break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - mVelocityTracker.clear(); - break; - } - return false; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - if (mInstantExpanding) { - mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding"); - return false; - } - if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) { - mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled"); - return false; - } - if (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN) { - mShadeLog.logMotionEvent(event, "onTouch: non-down action, motion was aborted"); - return false; - } - - // If dragging should not expand the notifications shade, then return false. - if (!mNotificationsDragEnabled) { - if (mTracking) { - // Turn off tracking if it's on or the shade can get stuck in the down position. - onTrackingStopped(true /* expand */); - } - mShadeLog.logMotionEvent(event, "onTouch: drag not enabled"); - return false; - } - - // On expanding, single mouse click expands the panel instead of dragging. - if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) { - if (event.getAction() == MotionEvent.ACTION_UP) { - expand(true); - } - return true; - } - - /* - * We capture touch events here and update the expand height here in case according to - * the users fingers. This also handles multi-touch. - * - * Flinging is also enabled in order to open or close the shade. - */ - - int pointerIndex = event.findPointerIndex(mTrackingPointer); - if (pointerIndex < 0) { - pointerIndex = 0; - mTrackingPointer = event.getPointerId(pointerIndex); - } - final float x = event.getX(pointerIndex); - final float y = event.getY(pointerIndex); - - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop(); - mIgnoreXTouchSlop = true; - } - - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); - mMinExpandHeight = 0.0f; - mPanelClosedOnDown = isFullyCollapsed(); - mHasLayoutedSinceDown = false; - mUpdateFlingOnLayout = false; - mMotionAborted = false; - mDownTime = mSystemClock.uptimeMillis(); - mTouchAboveFalsingThreshold = false; - mCollapsedAndHeadsUpOnDown = - isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp(); - addMovement(event); - boolean regularHeightAnimationRunning = mHeightAnimator != null - && !mHintAnimationRunning && !mIsSpringBackAnimation; - if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) { - mTouchSlopExceeded = regularHeightAnimationRunning - || mTouchSlopExceededBeforeDown; - cancelHeightAnimator(); - onTrackingStarted(); - } - if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp() - && !mCentralSurfaces.isBouncerShowing()) { - startOpening(event); - } - break; - - case MotionEvent.ACTION_POINTER_UP: - final int upPointer = event.getPointerId(event.getActionIndex()); - if (mTrackingPointer == upPointer) { - // gesture is ongoing, find a new pointer to track - final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; - final float newY = event.getY(newIndex); - final float newX = event.getX(newIndex); - mTrackingPointer = event.getPointerId(newIndex); - mHandlingPointerUp = true; - startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight); - mHandlingPointerUp = false; - } - break; - case MotionEvent.ACTION_POINTER_DOWN: - if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { - mMotionAborted = true; - endMotionEvent(event, x, y, true /* forceCancel */); - return false; - } - break; - case MotionEvent.ACTION_MOVE: - addMovement(event); - if (!isFullyCollapsed()) { - maybeVibrateOnOpening(true /* openingWithTouch */); - } - float h = y - mInitialExpandY; - - // If the panel was collapsed when touching, we only need to check for the - // y-component of the gesture, as we have no conflicting horizontal gesture. - if (Math.abs(h) > getTouchSlop(event) - && (Math.abs(h) > Math.abs(x - mInitialExpandX) - || mIgnoreXTouchSlop)) { - mTouchSlopExceeded = true; - if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) { - if (mInitialOffsetOnTouch != 0f) { - startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); - h = 0; - } - cancelHeightAnimator(); - onTrackingStarted(); - } - } - float newHeight = Math.max(0, h + mInitialOffsetOnTouch); - newHeight = Math.max(newHeight, mMinExpandHeight); - if (-h >= getFalsingThreshold()) { - mTouchAboveFalsingThreshold = true; - mUpwardsWhenThresholdReached = isDirectionUpwards(x, y); - } - if ((!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) { - // Count h==0 as part of swipe-up, - // otherwise {@link NotificationStackScrollLayout} - // wrongly enables stack height updates at the start of lockscreen swipe-up - mAmbientState.setSwipingUp(h <= 0); - setExpandedHeightInternal(newHeight); - } - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - addMovement(event); - endMotionEvent(event, x, y, false /* forceCancel */); - // mHeightAnimator is null, there is no remaining frame, ends instrumenting. - if (mHeightAnimator == null) { - if (event.getActionMasked() == MotionEvent.ACTION_UP) { - endJankMonitoring(); - } else { - cancelJankMonitoring(); - } - } - break; - } - return !mGestureWaitForTouchSlop || mTracking; - } - } - - protected abstract class OnLayoutChangeListener implements View.OnLayoutChangeListener { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, - int oldTop, int oldRight, int oldBottom) { - updateExpandedHeightToMaxHeight(); - mHasLayoutedSinceDown = true; - if (mUpdateFlingOnLayout) { - abortAnimations(); - fling(mUpdateFlingVelocity, true /* expands */); - mUpdateFlingOnLayout = false; - } - } - } - - public class OnConfigurationChangedListener implements - NotificationPanelView.OnConfigurationChangedListener { - @Override - public void onConfigurationChanged(Configuration newConfig) { - loadDimens(); - } - } - - private void beginJankMonitoring() { - if (mInteractionJankMonitor == null) { - return; - } - InteractionJankMonitor.Configuration.Builder builder = - InteractionJankMonitor.Configuration.Builder.withView( - InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, - mView) - .setTag(isFullyCollapsed() ? "Expand" : "Collapse"); - mInteractionJankMonitor.begin(builder); - } - - private void endJankMonitoring() { - if (mInteractionJankMonitor == null) { - return; - } - InteractionJankMonitor.getInstance().end( - InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); - } - - private void cancelJankMonitoring() { - if (mInteractionJankMonitor == null) { - return; - } - InteractionJankMonitor.getInstance().cancel( - InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); - } - - protected float getExpansionFraction() { - return mExpandedFraction; - } - - protected PanelExpansionStateManager getPanelExpansionStateManager() { - return mPanelExpansionStateManager; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionChangeEvent.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionChangeEvent.kt index 7c61b299cff9..71dfafa09d52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionChangeEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionChangeEvent.kt @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.statusbar.phone.panelstate +package com.android.systemui.shade import android.annotation.FloatRange -data class PanelExpansionChangeEvent( +data class ShadeExpansionChangeEvent( /** 0 when collapsed, 1 when fully expanded. */ @FloatRange(from = 0.0, to = 1.0) val fraction: Float, /** Whether the panel should be considered expanded */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionListener.kt index d0038243122b..a5a9ffd653a0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionListener.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.statusbar.phone.panelstate +package com.android.systemui.shade /** A listener interface to be notified of expansion events for the notification panel. */ -fun interface PanelExpansionListener { +fun interface ShadeExpansionListener { /** * Invoked whenever the notification panel expansion changes, at every animation frame. This is * the main expansion that happens when the user is swiping up to dismiss the lock screen and * swiping to pull down the notification shade. */ - fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) + fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt index 6b7c42e3884a..7bba74a8b125 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt @@ -14,13 +14,14 @@ * limitations under the License. */ -package com.android.systemui.statusbar.phone.panelstate +package com.android.systemui.shade import android.annotation.IntDef import android.util.Log import androidx.annotation.FloatRange import com.android.systemui.dagger.SysUISingleton import com.android.systemui.util.Compile +import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject /** @@ -29,14 +30,16 @@ import javax.inject.Inject * TODO(b/200063118): Make this class the one source of truth for the state of panel expansion. */ @SysUISingleton -class PanelExpansionStateManager @Inject constructor() { +class ShadeExpansionStateManager @Inject constructor() { - private val expansionListeners = mutableListOf<PanelExpansionListener>() - private val stateListeners = mutableListOf<PanelStateListener>() + private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>() + private val qsExpansionListeners = CopyOnWriteArrayList<ShadeQsExpansionListener>() + private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>() @PanelState private var state: Int = STATE_CLOSED @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f private var expanded: Boolean = false + private var qsExpanded: Boolean = false private var tracking: Boolean = false private var dragDownPxAmount: Float = 0f @@ -45,24 +48,34 @@ class PanelExpansionStateManager @Inject constructor() { * * Listener will also be immediately notified with the current values. */ - fun addExpansionListener(listener: PanelExpansionListener) { + fun addExpansionListener(listener: ShadeExpansionListener) { expansionListeners.add(listener) listener.onPanelExpansionChanged( - PanelExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount)) + ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount) + ) } /** Removes an expansion listener. */ - fun removeExpansionListener(listener: PanelExpansionListener) { + fun removeExpansionListener(listener: ShadeExpansionListener) { expansionListeners.remove(listener) } + fun addQsExpansionListener(listener: ShadeQsExpansionListener) { + qsExpansionListeners.add(listener) + listener.onQsExpansionChanged(qsExpanded) + } + + fun removeQsExpansionListener(listener: ShadeQsExpansionListener) { + qsExpansionListeners.remove(listener) + } + /** Adds a listener that will be notified when the panel state has changed. */ - fun addStateListener(listener: PanelStateListener) { + fun addStateListener(listener: ShadeStateListener) { stateListeners.add(listener) } /** Removes a state listener. */ - fun removeStateListener(listener: PanelStateListener) { + fun removeStateListener(listener: ShadeStateListener) { stateListeners.remove(listener) } @@ -110,25 +123,34 @@ class PanelExpansionStateManager @Inject constructor() { debugLog( "panelExpansionChanged:" + - "start state=${oldState.panelStateToString()} " + - "end state=${state.panelStateToString()} " + - "f=$fraction " + - "expanded=$expanded " + - "tracking=$tracking " + - "dragDownPxAmount=$dragDownPxAmount " + - "${if (fullyOpened) " fullyOpened" else ""} " + - if (fullyClosed) " fullyClosed" else "" + "start state=${oldState.panelStateToString()} " + + "end state=${state.panelStateToString()} " + + "f=$fraction " + + "expanded=$expanded " + + "tracking=$tracking " + + "dragDownPxAmount=$dragDownPxAmount " + + "${if (fullyOpened) " fullyOpened" else ""} " + + if (fullyClosed) " fullyClosed" else "" ) val expansionChangeEvent = - PanelExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount) + ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount) expansionListeners.forEach { it.onPanelExpansionChanged(expansionChangeEvent) } } + /** Called when the quick settings expansion changes to fully expanded or collapsed. */ + fun onQsExpansionChanged(qsExpanded: Boolean) { + this.qsExpanded = qsExpanded + + debugLog("qsExpanded=$qsExpanded") + qsExpansionListeners.forEach { it.onQsExpansionChanged(qsExpanded) } + } + /** Updates the panel state if necessary. */ fun updateState(@PanelState state: Int) { debugLog( - "update state: ${this.state.panelStateToString()} -> ${state.panelStateToString()}") + "update state: ${this.state.panelStateToString()} -> ${state.panelStateToString()}" + ) if (this.state != state) { updateStateInternal(state) } @@ -165,5 +187,5 @@ fun Int.panelStateToString(): String { } } -private val TAG = PanelExpansionStateManager::class.simpleName +private val TAG = ShadeExpansionStateManager::class.simpleName private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG) diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt index f1e44ce5736e..2b788d85a14c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt @@ -1,20 +1,17 @@ package com.android.systemui.shade import android.view.MotionEvent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.ShadeLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogMessage import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject private const val TAG = "systemui.shade" /** Lightweight logging utility for the Shade. */ -class ShadeLogger @Inject constructor( - @ShadeLog - private val buffer: LogBuffer -) { +class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { fun v(@CompileTimeConstant msg: String) { buffer.log(TAG, LogLevel.VERBOSE, msg) } @@ -28,21 +25,56 @@ class ShadeLogger @Inject constructor( } fun onQsInterceptMoveQsTrackingEnabled(h: Float) { - log(LogLevel.VERBOSE, + log( + LogLevel.VERBOSE, { double1 = h.toDouble() }, - { "onQsIn[tercept: move action, QS tracking enabled. h = $double1" }) + { "onQsIntercept: move action, QS tracking enabled. h = $double1" } + ) + } + + fun logQsTrackingNotStarted( + initialTouchY: Float, + y: Float, + h: Float, + touchSlop: Float, + qsExpanded: Boolean, + collapsedOnDown: Boolean, + keyguardShowing: Boolean, + qsExpansionEnabled: Boolean + ) { + log( + LogLevel.VERBOSE, + { + int1 = initialTouchY.toInt() + int2 = y.toInt() + long1 = h.toLong() + double1 = touchSlop.toDouble() + bool1 = qsExpanded + bool2 = collapsedOnDown + bool3 = keyguardShowing + bool4 = qsExpansionEnabled + }, + { + "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded" + + "=$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4" + } + ) } fun logMotionEvent(event: MotionEvent, message: String) { - log(LogLevel.VERBOSE, { - str1 = message - long1 = event.eventTime - long2 = event.downTime - int1 = event.action - int2 = event.classification - double1 = event.y.toDouble() - }, { - "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,classification=$int2" - }) + log( + LogLevel.VERBOSE, + { + str1 = message + long1 = event.eventTime + long2 = event.downTime + int1 = event.action + int2 = event.classification + double1 = event.y.toDouble() + }, + { + "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,class=$int2" + } + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt new file mode 100644 index 000000000000..14882b9afd2f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +/** A listener interface to be notified of expansion events for the quick settings panel. */ +fun interface ShadeQsExpansionListener { + /** + * Invoked whenever the quick settings expansion changes, when it is fully collapsed or expanded + */ + fun onQsExpansionChanged(isQsExpanded: Boolean) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateListener.kt index ca667dddbe8a..74468a0dd10a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateListener.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package com.android.systemui.statusbar.phone.panelstate +package com.android.systemui.shade /** A listener interface to be notified of state change events for the notification panel. */ -fun interface PanelStateListener { - /** Called when the panel's expansion state has changed. */ +fun interface ShadeStateListener { + /** Called when the panel's expansion state has changed. */ fun onPanelStateChanged(@PanelState state: Int) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt new file mode 100644 index 000000000000..09019a69df47 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.shade.data.repository + +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.ShadeExpansionListener +import com.android.systemui.shade.ShadeExpansionStateManager +import com.android.systemui.shade.domain.model.ShadeModel +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +/** Business logic for shade interactions */ +@SysUISingleton +class ShadeRepository @Inject constructor(shadeExpansionStateManager: ShadeExpansionStateManager) { + + val shadeModel: Flow<ShadeModel> = + conflatedCallbackFlow { + val callback = + object : ShadeExpansionListener { + override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) { + // Don't propagate ShadeExpansionChangeEvent.dragDownPxAmount field. + // It is too noisy and produces extra events that consumers won't care + // about + val info = + ShadeModel( + expansionAmount = event.fraction, + isExpanded = event.expanded, + isUserDragging = event.tracking + ) + trySendWithFailureLogging(info, TAG, "updated shade expansion info") + } + } + + shadeExpansionStateManager.addExpansionListener(callback) + trySendWithFailureLogging(ShadeModel(), TAG, "initial shade expansion info") + + awaitClose { shadeExpansionStateManager.removeExpansionListener(callback) } + } + .distinctUntilChanged() + + companion object { + private const val TAG = "ShadeRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt new file mode 100644 index 000000000000..ce0f4283ff83 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.shade.domain.model + +import android.annotation.FloatRange + +/** Information about shade (NotificationPanel) expansion */ +data class ShadeModel( + /** 0 when collapsed, 1 when fully expanded. */ + @FloatRange(from = 0.0, to = 1.0) val expansionAmount: Float = 0f, + /** Whether the panel should be considered expanded */ + val isExpanded: Boolean = false, + /** Whether the user is actively dragging the panel. */ + val isUserDragging: Boolean = false, +) diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt index 618c8924cd21..a77c21a8da57 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt @@ -7,12 +7,12 @@ import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.shade.PanelState +import com.android.systemui.shade.STATE_OPENING +import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.ScrimController -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent -import com.android.systemui.statusbar.phone.panelstate.PanelState -import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.util.LargeScreenUtils @@ -35,7 +35,7 @@ constructor( private var inSplitShade = false private var splitShadeScrimTransitionDistance = 0 private var lastExpansionFraction: Float? = null - private var lastExpansionEvent: PanelExpansionChangeEvent? = null + private var lastExpansionEvent: ShadeExpansionChangeEvent? = null private var currentPanelState: Int? = null init { @@ -61,8 +61,8 @@ constructor( onStateChanged() } - fun onPanelExpansionChanged(panelExpansionChangeEvent: PanelExpansionChangeEvent) { - lastExpansionEvent = panelExpansionChangeEvent + fun onPanelExpansionChanged(shadeExpansionChangeEvent: ShadeExpansionChangeEvent) { + lastExpansionEvent = shadeExpansionChangeEvent onStateChanged() } @@ -75,7 +75,7 @@ constructor( } private fun calculateScrimExpansionFraction( - expansionEvent: PanelExpansionChangeEvent, + expansionEvent: ShadeExpansionChangeEvent, @PanelState panelState: Int? ): Float { return if (canUseCustomFraction(panelState)) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt index 6c3a028c2380..22e847deb7b2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt @@ -1,6 +1,6 @@ package com.android.systemui.shade.transition -import com.android.systemui.statusbar.phone.panelstate.PanelState +import com.android.systemui.shade.PanelState /** Represents an over scroller for the non-lockscreen shade. */ interface ShadeOverScroller { diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt index 58acfb40ee44..1e8208f52fdc 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt @@ -7,13 +7,13 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.qs.QS import com.android.systemui.shade.NotificationPanelViewController +import com.android.systemui.shade.PanelState +import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.ShadeExpansionStateManager +import com.android.systemui.shade.panelStateToString import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager -import com.android.systemui.statusbar.phone.panelstate.PanelState -import com.android.systemui.statusbar.phone.panelstate.panelStateToString import com.android.systemui.statusbar.policy.ConfigurationController import java.io.PrintWriter import javax.inject.Inject @@ -24,7 +24,7 @@ class ShadeTransitionController @Inject constructor( configurationController: ConfigurationController, - panelExpansionStateManager: PanelExpansionStateManager, + shadeExpansionStateManager: ShadeExpansionStateManager, dumpManager: DumpManager, private val context: Context, private val splitShadeOverScrollerFactory: SplitShadeOverScroller.Factory, @@ -39,7 +39,7 @@ constructor( private var inSplitShade = false private var currentPanelState: Int? = null - private var lastPanelExpansionChangeEvent: PanelExpansionChangeEvent? = null + private var lastShadeExpansionChangeEvent: ShadeExpansionChangeEvent? = null private val splitShadeOverScroller by lazy { splitShadeOverScrollerFactory.create({ qs }, { notificationStackScrollLayoutController }) @@ -60,8 +60,8 @@ constructor( updateResources() } }) - panelExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged) - panelExpansionStateManager.addStateListener(this::onPanelStateChanged) + shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged) + shadeExpansionStateManager.addStateListener(this::onPanelStateChanged) dumpManager.registerDumpable("ShadeTransitionController") { printWriter, _ -> dump(printWriter) } @@ -77,8 +77,8 @@ constructor( scrimShadeTransitionController.onPanelStateChanged(state) } - private fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) { - lastPanelExpansionChangeEvent = event + private fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) { + lastShadeExpansionChangeEvent = event shadeOverScroller.onDragDownAmountChanged(event.dragDownPxAmount) scrimShadeTransitionController.onPanelExpansionChanged(event) } @@ -95,7 +95,7 @@ constructor( inSplitShade: $inSplitShade isScreenUnlocked: ${isScreenUnlocked()} currentPanelState: ${currentPanelState?.panelStateToString()} - lastPanelExpansionChangeEvent: $lastPanelExpansionChangeEvent + lastPanelExpansionChangeEvent: $lastShadeExpansionChangeEvent qs.isInitialized: ${this::qs.isInitialized} npvc.isInitialized: ${this::notificationPanelViewController.isInitialized} nssl.isInitialized: ${this::notificationStackScrollLayoutController.isInitialized} diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt index 204dd3c07d8e..8c57194c0950 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt @@ -10,11 +10,11 @@ import com.android.systemui.R import com.android.systemui.animation.Interpolators import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.qs.QS +import com.android.systemui.shade.PanelState +import com.android.systemui.shade.STATE_CLOSED +import com.android.systemui.shade.STATE_OPENING import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.phone.ScrimController -import com.android.systemui.statusbar.phone.panelstate.PanelState -import com.android.systemui.statusbar.phone.panelstate.STATE_CLOSED -import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING import com.android.systemui.statusbar.policy.ConfigurationController import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -37,6 +37,7 @@ constructor( private var previousOverscrollAmount = 0 private var dragDownAmount: Float = 0f @PanelState private var panelState: Int = STATE_CLOSED + private var releaseOverScrollAnimator: Animator? = null private val qS: QS diff --git a/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java b/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java index 6abf339685e4..ff26766fba66 100644 --- a/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java +++ b/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java @@ -32,10 +32,10 @@ import javax.inject.Inject; * Dispatches shortcut to System UI components */ @SysUISingleton -public class ShortcutKeyDispatcher extends CoreStartable - implements ShortcutKeyServiceProxy.Callbacks { +public class ShortcutKeyDispatcher implements CoreStartable, ShortcutKeyServiceProxy.Callbacks { private static final String TAG = "ShortcutKeyDispatcher"; + private final Context mContext; private ShortcutKeyServiceProxy mShortcutKeyServiceProxy = new ShortcutKeyServiceProxy(this); private IWindowManager mWindowManagerService = WindowManagerGlobal.getWindowManagerService(); @@ -50,7 +50,7 @@ public class ShortcutKeyDispatcher extends CoreStartable @Inject public ShortcutKeyDispatcher(Context context) { - super(context); + mContext = context; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt index 7f7ff9cf4881..90c52bd8c9f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt @@ -17,9 +17,9 @@ package com.android.systemui.statusbar import android.app.PendingIntent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.collection.NotificationEntry import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt index 4d53064d047d..ce730baeed0d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt @@ -20,14 +20,16 @@ import android.util.AttributeSet import android.widget.FrameLayout /** - * A temporary base class that's shared between our old status bar wifi view implementation - * ([StatusBarWifiView]) and our new status bar wifi view implementation - * ([ModernStatusBarWifiView]). + * A temporary base class that's shared between our old status bar connectivity view implementations + * ([StatusBarWifiView], [StatusBarMobileView]) and our new status bar implementations ( + * [ModernStatusBarWifiView], [ModernStatusBarMobileView]). * * Once our refactor is over, we should be able to delete this go-between class and the old view * class. */ -abstract class BaseStatusBarWifiView @JvmOverloads constructor( +abstract class BaseStatusBarFrameLayout +@JvmOverloads +constructor( context: Context, attrs: AttributeSet? = null, defStyleAttrs: Int = 0, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 04621168493b..e6d7e4124d01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -69,12 +69,15 @@ import com.android.internal.statusbar.LetterboxDetails; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.util.GcUtils; import com.android.internal.view.AppearanceRegion; +import com.android.systemui.dump.DumpHandler; import com.android.systemui.statusbar.CommandQueue.Callbacks; import com.android.systemui.statusbar.commandline.CommandRegistry; import com.android.systemui.statusbar.policy.CallbackController; import com.android.systemui.tracing.ProtoTracer; +import java.io.FileDescriptor; import java.io.FileOutputStream; +import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; @@ -184,6 +187,7 @@ public class CommandQueue extends IStatusBar.Stub implements private int mLastUpdatedImeDisplayId = INVALID_DISPLAY; private ProtoTracer mProtoTracer; private final @Nullable CommandRegistry mRegistry; + private final @Nullable DumpHandler mDumpHandler; /** * These methods are called back on the main thread. @@ -473,12 +477,18 @@ public class CommandQueue extends IStatusBar.Stub implements } public CommandQueue(Context context) { - this(context, null, null); + this(context, null, null, null); } - public CommandQueue(Context context, ProtoTracer protoTracer, CommandRegistry registry) { + public CommandQueue( + Context context, + ProtoTracer protoTracer, + CommandRegistry registry, + DumpHandler dumpHandler + ) { mProtoTracer = protoTracer; mRegistry = registry; + mDumpHandler = dumpHandler; context.getSystemService(DisplayManager.class).registerDisplayListener(this, mHandler); // We always have default display. setDisabled(DEFAULT_DISPLAY, DISABLE_NONE, DISABLE2_NONE); @@ -1178,6 +1188,35 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override + public void dumpProto(String[] args, ParcelFileDescriptor pfd) { + final FileDescriptor fd = pfd.getFileDescriptor(); + // This is mimicking Binder#dumpAsync, but on this side of the binder. Might be possible + // to just throw this work onto the handler just like the other messages + Thread thr = new Thread("Sysui.dumpProto") { + public void run() { + try { + if (mDumpHandler == null) { + return; + } + // We won't be using the PrintWriter. + OutputStream o = new OutputStream() { + @Override + public void write(int b) {} + }; + mDumpHandler.dump(fd, new PrintWriter(o), args); + } finally { + try { + // Close the file descriptor so the TransferPipe finishes its thread + pfd.close(); + } catch (Exception e) { + } + } + } + }; + thr.start(); + } + + @Override public void runGcForTest() { // Gc sysui GcUtils.runGcAndFinalizersSync(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt index 886ad684649f..5fb500247697 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt @@ -5,7 +5,7 @@ import android.util.IndentingPrintWriter import android.util.MathUtils import com.android.systemui.R import com.android.systemui.dump.DumpManager -import com.android.systemui.media.MediaHierarchyManager +import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.shade.NotificationPanelViewController import com.android.systemui.statusbar.policy.ConfigurationController import dagger.assisted.Assisted diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index 80069319601f..a2e4536ce45f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -24,7 +24,7 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.WakefulnessLifecycle -import com.android.systemui.media.MediaHierarchyManager +import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QS diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 8699441da726..184dc253bfc6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -42,7 +42,6 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.widget.LockPatternUtils; -import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; @@ -97,6 +96,7 @@ public class NotificationLockscreenUserManagerImpl implements private final List<UserChangedListener> mListeners = new ArrayList<>(); private final BroadcastDispatcher mBroadcastDispatcher; private final NotificationClickNotifier mClickNotifier; + private final Lazy<OverviewProxyService> mOverviewProxyServiceLazy; private boolean mShowLockscreenNotifications; private boolean mAllowLockscreenRemoteInput; @@ -157,7 +157,7 @@ public class NotificationLockscreenUserManagerImpl implements break; case Intent.ACTION_USER_UNLOCKED: // Start the overview connection to the launcher service - Dependency.get(OverviewProxyService.class).startConnectionToCurrentUser(); + mOverviewProxyServiceLazy.get().startConnectionToCurrentUser(); break; case NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION: final IntentSender intentSender = intent.getParcelableExtra( @@ -189,7 +189,6 @@ public class NotificationLockscreenUserManagerImpl implements protected NotificationPresenter mPresenter; protected ContentObserver mLockscreenSettingsObserver; protected ContentObserver mSettingsObserver; - private boolean mHideSilentNotificationsOnLockscreen; @Inject public NotificationLockscreenUserManagerImpl(Context context, @@ -199,6 +198,7 @@ public class NotificationLockscreenUserManagerImpl implements Lazy<NotificationVisibilityProvider> visibilityProviderLazy, Lazy<CommonNotifCollection> commonNotifCollectionLazy, NotificationClickNotifier clickNotifier, + Lazy<OverviewProxyService> overviewProxyServiceLazy, KeyguardManager keyguardManager, StatusBarStateController statusBarStateController, @Main Handler mainHandler, @@ -214,6 +214,7 @@ public class NotificationLockscreenUserManagerImpl implements mVisibilityProviderLazy = visibilityProviderLazy; mCommonNotifCollectionLazy = commonNotifCollectionLazy; mClickNotifier = clickNotifier; + mOverviewProxyServiceLazy = overviewProxyServiceLazy; statusBarStateController.addCallback(this); mLockPatternUtils = new LockPatternUtils(context); mKeyguardManager = keyguardManager; @@ -264,12 +265,6 @@ public class NotificationLockscreenUserManagerImpl implements UserHandle.USER_ALL); mContext.getContentResolver().registerContentObserver( - mSecureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS), - true, - mLockscreenSettingsObserver, - UserHandle.USER_ALL); - - mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false, mSettingsObserver); @@ -338,9 +333,6 @@ public class NotificationLockscreenUserManagerImpl implements final boolean allowedByDpm = (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS) == 0; - mHideSilentNotificationsOnLockscreen = mSecureSettings.getIntForUser( - Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 1, mCurrentUserId) == 0; - setShowLockscreenNotifications(show && allowedByDpm); if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index c900c5a2ff0b..ced725e0b1d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -43,15 +43,14 @@ import android.util.Log; import android.view.View; import android.widget.ImageView; -import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.animation.Interpolators; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaData; -import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.SmartspaceMediaData; +import com.android.systemui.media.controls.models.player.MediaData; +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData; +import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.CentralSurfacesModule; import com.android.systemui.statusbar.notification.collection.NotifCollection; @@ -89,11 +88,9 @@ public class NotificationMediaManager implements Dumpable { private static final String TAG = "NotificationMediaManager"; public static final boolean DEBUG_MEDIA = false; - private final StatusBarStateController mStatusBarStateController - = Dependency.get(StatusBarStateController.class); - private final SysuiColorExtractor mColorExtractor = Dependency.get(SysuiColorExtractor.class); - private final KeyguardStateController mKeyguardStateController = Dependency.get( - KeyguardStateController.class); + private final StatusBarStateController mStatusBarStateController; + private final SysuiColorExtractor mColorExtractor; + private final KeyguardStateController mKeyguardStateController; private final KeyguardBypassController mKeyguardBypassController; private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>(); private static final HashSet<Integer> CONNECTING_MEDIA_STATES = new HashSet<>(); @@ -179,6 +176,9 @@ public class NotificationMediaManager implements Dumpable { NotifCollection notifCollection, @Main DelayableExecutor mainExecutor, MediaDataManager mediaDataManager, + StatusBarStateController statusBarStateController, + SysuiColorExtractor colorExtractor, + KeyguardStateController keyguardStateController, DumpManager dumpManager) { mContext = context; mMediaArtworkProcessor = mediaArtworkProcessor; @@ -192,6 +192,9 @@ public class NotificationMediaManager implements Dumpable { mMediaDataManager = mediaDataManager; mNotifPipeline = notifPipeline; mNotifCollection = notifCollection; + mStatusBarStateController = statusBarStateController; + mColorExtractor = colorExtractor; + mKeyguardStateController = keyguardStateController; setupNotifPipeline(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index cb13fcf246cb..b5879ec19d21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -38,12 +38,12 @@ import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.ShadeExpansionListener import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.ScrimController -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.LargeScreenUtils @@ -69,7 +69,7 @@ class NotificationShadeDepthController @Inject constructor( private val context: Context, dumpManager: DumpManager, configurationController: ConfigurationController -) : PanelExpansionListener, Dumpable { +) : ShadeExpansionListener, Dumpable { companion object { private const val WAKE_UP_ANIMATION_ENABLED = true private const val VELOCITY_SCALE = 100f @@ -338,7 +338,7 @@ class NotificationShadeDepthController @Inject constructor( /** * Update blurs when pulling down the shade */ - override fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) { + override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) { val rawFraction = event.fraction val tracking = event.tracking val timestamp = SystemClock.elapsedRealtimeNanos() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java index 0c9e1ec1ff77..e21acb7e0f68 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java @@ -92,9 +92,6 @@ public interface NotificationShadeWindowController extends RemoteInputController /** Sets the state of whether the keyguard is fading away or not. */ default void setKeyguardFadingAway(boolean keyguardFadingAway) {} - /** Sets the state of whether the quick settings is expanded or not. */ - default void setQsExpanded(boolean expanded) {} - /** Sets the state of whether the user activities are forced or not. */ default void setForceUserActivity(boolean forceUserActivity) {} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index d67f94f11e65..87ef92a28d5d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -40,6 +40,7 @@ import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; @@ -110,8 +111,8 @@ public class NotificationShelf extends ActivatableNotificationView implements setClipChildren(false); setClipToPadding(false); mShelfIcons.setIsStaticLayout(false); - setBottomRoundness(1.0f, false /* animate */); - setTopRoundness(1f, false /* animate */); + requestBottomRoundness(1.0f, /* animate = */ false, SourceType.DefaultValue); + requestTopRoundness(1f, false, SourceType.DefaultValue); // Setting this to first in section to get the clipping to the top roundness correct. This // value determines the way we are clipping to the top roundness of the overall shade @@ -189,22 +190,22 @@ public class NotificationShelf extends ActivatableNotificationView implements viewState.copyFrom(lastViewState); viewState.height = getIntrinsicHeight(); - viewState.zTranslation = ambientState.getBaseZHeight(); + viewState.setZTranslation(ambientState.getBaseZHeight()); viewState.clipTopAmount = 0; if (ambientState.isExpansionChanging() && !ambientState.isOnKeyguard()) { float expansion = ambientState.getExpansionFraction(); if (ambientState.isBouncerInTransit()) { - viewState.alpha = aboutToShowBouncerProgress(expansion); + viewState.setAlpha(aboutToShowBouncerProgress(expansion)); } else { - viewState.alpha = ShadeInterpolation.getContentAlpha(expansion); + viewState.setAlpha(ShadeInterpolation.getContentAlpha(expansion)); } } else { - viewState.alpha = 1f - ambientState.getHideAmount(); + viewState.setAlpha(1f - ambientState.getHideAmount()); } viewState.belowSpeedBump = mHostLayoutController.getSpeedBumpIndex() == 0; viewState.hideSensitive = false; - viewState.xTranslation = getTranslationX(); + viewState.setXTranslation(getTranslationX()); viewState.hasItemsInStableShelf = lastViewState.inShelf; viewState.firstViewInShelf = algorithmState.firstViewInShelf; if (mNotGoneIndex != -1) { @@ -230,7 +231,7 @@ public class NotificationShelf extends ActivatableNotificationView implements } final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight(); - viewState.yTranslation = stackEnd - viewState.height; + viewState.setYTranslation(stackEnd - viewState.height); } else { viewState.hidden = true; viewState.location = ExpandableViewState.LOCATION_GONE; @@ -413,7 +414,7 @@ public class NotificationShelf extends ActivatableNotificationView implements if (iconState != null && iconState.clampedAppearAmount == 1.0f) { // only if the first icon is fully in the shelf we want to clip to it! backgroundTop = (int) (child.getTranslationY() - getTranslationY()); - firstElementRoundness = expandableRow.getCurrentTopRoundness(); + firstElementRoundness = expandableRow.getTopRoundness(); } } @@ -507,28 +508,36 @@ public class NotificationShelf extends ActivatableNotificationView implements // Round bottom corners within animation bounds final float changeFraction = MathUtils.saturate( (viewEnd - cornerAnimationTop) / cornerAnimationDistance); - anv.setBottomRoundness(anv.isLastInSection() ? 1f : changeFraction, - false /* animate */); + anv.requestBottomRoundness( + anv.isLastInSection() ? 1f : changeFraction, + /* animate = */ false, + SourceType.OnScroll); } else if (viewEnd < cornerAnimationTop) { // Fast scroll skips frames and leaves corners with unfinished rounding. // Reset top and bottom corners outside of animation bounds. - anv.setBottomRoundness(anv.isLastInSection() ? 1f : smallCornerRadius, - false /* animate */); + anv.requestBottomRoundness( + anv.isLastInSection() ? 1f : smallCornerRadius, + /* animate = */ false, + SourceType.OnScroll); } if (viewStart >= cornerAnimationTop) { // Round top corners within animation bounds final float changeFraction = MathUtils.saturate( (viewStart - cornerAnimationTop) / cornerAnimationDistance); - anv.setTopRoundness(anv.isFirstInSection() ? 1f : changeFraction, - false /* animate */); + anv.requestTopRoundness( + anv.isFirstInSection() ? 1f : changeFraction, + false, + SourceType.OnScroll); } else if (viewStart < cornerAnimationTop) { // Fast scroll skips frames and leaves corners with unfinished rounding. // Reset top and bottom corners outside of animation bounds. - anv.setTopRoundness(anv.isFirstInSection() ? 1f : smallCornerRadius, - false /* animate */); + anv.requestTopRoundness( + anv.isFirstInSection() ? 1f : smallCornerRadius, + false, + SourceType.OnScroll); } } @@ -794,7 +803,7 @@ public class NotificationShelf extends ActivatableNotificationView implements if (iconState == null) { return; } - iconState.alpha = ICON_ALPHA_INTERPOLATOR.getInterpolation(transitionAmount); + iconState.setAlpha(ICON_ALPHA_INTERPOLATOR.getInterpolation(transitionAmount)); boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf(); iconState.hidden = isAppearing || (view instanceof ExpandableNotificationRow @@ -809,12 +818,12 @@ public class NotificationShelf extends ActivatableNotificationView implements // Fade in icons at shelf start // This is important for conversation icons, which are badged and need x reset - iconState.xTranslation = mShelfIcons.getActualPaddingStart(); + iconState.setXTranslation(mShelfIcons.getActualPaddingStart()); final boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf(); if (stayingInShelf) { iconState.iconAppearAmount = 1.0f; - iconState.alpha = 1.0f; + iconState.setAlpha(1.0f); iconState.hidden = false; } int backgroundColor = getBackgroundColorWithoutTint(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt index bbff0466cf50..c630feba1dcb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt @@ -39,6 +39,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView @@ -52,7 +53,10 @@ import javax.inject.Inject import kotlin.math.max /** - * A utility class to enable the downward swipe on when pulsing. + * A utility class that handles notification panel expansion when a user swipes downward on a + * notification from the pulsing state. + * If face-bypass is enabled, the user can swipe down anywhere on the screen (not just from a + * notification) to trigger the notification panel expansion. */ @SysUISingleton class PulseExpansionHandler @Inject @@ -62,9 +66,10 @@ constructor( private val bypassController: KeyguardBypassController, private val headsUpManager: HeadsUpManagerPhone, private val roundnessManager: NotificationRoundnessManager, - private val configurationController: ConfigurationController, + configurationController: ConfigurationController, private val statusBarStateController: StatusBarStateController, private val falsingManager: FalsingManager, + shadeExpansionStateManager: ShadeExpansionStateManager, private val lockscreenShadeTransitionController: LockscreenShadeTransitionController, private val falsingCollector: FalsingCollector, dumpManager: DumpManager @@ -123,6 +128,13 @@ constructor( initResources(context) } }) + + shadeExpansionStateManager.addQsExpansionListener { isQsExpanded -> + if (qsExpanded != isQsExpanded) { + qsExpanded = isQsExpanded + } + } + mPowerManager = context.getSystemService(PowerManager::class.java) dumpManager.registerDumpable(this) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java index 78077386179a..59afb18195dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateController.java @@ -36,8 +36,7 @@ public abstract class QsFrameTranslateController { /** * Calculate and translate the QS Frame on the Y-axis. */ - public abstract void translateQsFrame(View qsFrame, QS qs, float overExpansion, - float qsTranslationForFullShadeTransition); + public abstract void translateQsFrame(View qsFrame, QS qs, int bottomInset); /** * Calculate the top padding for notifications panel. This could be the supplied diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java index 33e224579bef..85b522cbd9d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/QsFrameTranslateImpl.java @@ -27,6 +27,8 @@ import javax.inject.Inject; /** * Default implementation of QS Translation. This by default does not do much. + * This class can be subclassed to allow System UI variants the flexibility to change position of + * the Quick Settings frame. */ @SysUISingleton public class QsFrameTranslateImpl extends QsFrameTranslateController { @@ -37,8 +39,8 @@ public class QsFrameTranslateImpl extends QsFrameTranslateController { } @Override - public void translateQsFrame(View qsFrame, QS qs, float overExpansion, - float qsTranslationForFullShadeTransition) { + public void translateQsFrame(View qsFrame, QS qs, int bottomInset) { + // Empty implementation by default, meant to be overridden by subclasses. } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java index 48c6e273bbb4..fdad101ae0f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java @@ -29,7 +29,6 @@ import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; -import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; @@ -43,7 +42,10 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconStat import java.util.ArrayList; -public class StatusBarMobileView extends FrameLayout implements DarkReceiver, +/** + * View group for the mobile icon in the status bar + */ +public class StatusBarMobileView extends BaseStatusBarFrameLayout implements DarkReceiver, StatusIconDisplayable { private static final String TAG = "StatusBarMobileView"; @@ -101,11 +103,6 @@ public class StatusBarMobileView extends FrameLayout implements DarkReceiver, super(context, attrs, defStyleAttr); } - public StatusBarMobileView(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - @Override public void getDrawingRect(Rect outRect) { super.getDrawingRect(outRect); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java index f3e74d92fc8a..decc70d175b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java @@ -40,7 +40,7 @@ import java.util.ArrayList; /** * Start small: StatusBarWifiView will be able to layout from a WifiIconState */ -public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkReceiver { +public class StatusBarWifiView extends BaseStatusBarFrameLayout implements DarkReceiver { private static final String TAG = "StatusBarWifiView"; /// Used to show etc dots diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java index b3dd853cd2e1..402217dac185 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -71,9 +71,9 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; -import com.android.systemui.log.LogLevel; import com.android.systemui.log.dagger.StatusBarNetworkControllerLog; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.qs.tiles.dialog.InternetDialogFactory; import com.android.systemui.settings.CurrentUserTracker; import com.android.systemui.statusbar.policy.ConfigurationController; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index 7cd79cac8928..eacb18e3c50c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -26,10 +26,12 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dump.DumpHandler; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaDataManager; +import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.carrier.QSCarrierGroupController; @@ -130,6 +132,9 @@ public interface CentralSurfacesDependenciesModule { NotifCollection notifCollection, @Main DelayableExecutor mainExecutor, MediaDataManager mediaDataManager, + StatusBarStateController statusBarStateController, + SysuiColorExtractor colorExtractor, + KeyguardStateController keyguardStateController, DumpManager dumpManager) { return new NotificationMediaManager( context, @@ -142,6 +147,9 @@ public interface CentralSurfacesDependenciesModule { notifCollection, mainExecutor, mediaDataManager, + statusBarStateController, + colorExtractor, + keyguardStateController, dumpManager); } @@ -174,8 +182,10 @@ public interface CentralSurfacesDependenciesModule { static CommandQueue provideCommandQueue( Context context, ProtoTracer protoTracer, - CommandRegistry registry) { - return new CommandQueue(context, protoTracer, registry); + CommandRegistry registry, + DumpHandler dumpHandler + ) { + return new CommandQueue(context, protoTracer, registry, dumpHandler); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt index d88f07ca304c..737b4812d4fb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -25,11 +25,12 @@ import android.view.Gravity import android.view.View import android.widget.FrameLayout import com.android.internal.annotations.GuardedBy -import com.android.systemui.animation.Interpolators import com.android.systemui.R +import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.StatusBarState.SHADE import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener @@ -42,7 +43,6 @@ import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN import com.android.systemui.util.leak.RotationUtils.Rotation - import java.util.concurrent.Executor import javax.inject.Inject @@ -67,7 +67,8 @@ class PrivacyDotViewController @Inject constructor( private val stateController: StatusBarStateController, private val configurationController: ConfigurationController, private val contentInsetsProvider: StatusBarContentInsetsProvider, - private val animationScheduler: SystemStatusAnimationScheduler + private val animationScheduler: SystemStatusAnimationScheduler, + shadeExpansionStateManager: ShadeExpansionStateManager ) { private lateinit var tl: View private lateinit var tr: View @@ -128,6 +129,13 @@ class PrivacyDotViewController @Inject constructor( updateStatusBarState() } }) + + shadeExpansionStateManager.addQsExpansionListener { isQsExpanded -> + dlog("setQsExpanded $isQsExpanded") + synchronized(lock) { + nextViewState = nextViewState.copy(qsExpanded = isQsExpanded) + } + } } fun setUiExecutor(e: DelayableExecutor) { @@ -138,13 +146,6 @@ class PrivacyDotViewController @Inject constructor( showingListener = l } - fun setQsExpanded(expanded: Boolean) { - dlog("setQsExpanded $expanded") - synchronized(lock) { - nextViewState = nextViewState.copy(qsExpanded = expanded) - } - } - @UiThread fun setNewRotation(rot: Int) { dlog("updateRotation: $rot") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt index 17feaa842165..9bdff928c44b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.gesture -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.SwipeStatusBarAwayLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject /** Log messages for [SwipeStatusBarAwayGestureHandler]. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 8f8813b80b5f..842204bbf621 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -118,6 +118,7 @@ class LockscreenSmartspaceController @Inject constructor( regionSamplingEnabled, updateFun ) + initializeTextColors(regionSamplingInstance) regionSamplingInstance.startRegionSampler() regionSamplingInstances.put(v, regionSamplingInstance) connectSession() @@ -361,18 +362,20 @@ class LockscreenSmartspaceController @Inject constructor( } } + private fun initializeTextColors(regionSamplingInstance: RegionSamplingInstance) { + val lightThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_LightWallpaper) + val darkColor = Utils.getColorAttrDefaultColor(lightThemeContext, R.attr.wallpaperTextColor) + + val darkThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI) + val lightColor = Utils.getColorAttrDefaultColor(darkThemeContext, R.attr.wallpaperTextColor) + + regionSamplingInstance.setForegroundColors(lightColor, darkColor) + } + private fun updateTextColorFromRegionSampler() { smartspaceViews.forEach { - val isRegionDark = regionSamplingInstances.getValue(it).currentRegionDarkness() - val themeID = if (isRegionDark.isDark) { - R.style.Theme_SystemUI - } else { - R.style.Theme_SystemUI_LightWallpaper - } - val themedContext = ContextThemeWrapper(context, themeID) - val wallpaperTextColor = - Utils.getColorAttrDefaultColor(themedContext, R.attr.wallpaperTextColor) - it.setPrimaryTextColor(wallpaperTextColor) + val textColor = regionSamplingInstances.getValue(it).currentForegroundColor() + it.setPrimaryTextColor(textColor) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java index 1be4c04ef804..b5c7ef5f7630 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification; -import android.annotation.Nullable; import android.util.ArraySet; import androidx.annotation.VisibleForTesting; @@ -25,7 +24,6 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import javax.inject.Inject; @@ -43,7 +41,6 @@ public class DynamicPrivacyController implements KeyguardStateController.Callbac private boolean mLastDynamicUnlocked; private boolean mCacheInvalid; - @Nullable private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @Inject DynamicPrivacyController(NotificationLockscreenUserManager notificationLockscreenUserManager, @@ -100,7 +97,7 @@ public class DynamicPrivacyController implements KeyguardStateController.Callbac * contents aren't revealed yet? */ public boolean isInLockedDownShade() { - if (!isStatusBarKeyguardShowing() || !mKeyguardStateController.isMethodSecure()) { + if (!mKeyguardStateController.isShowing() || !mKeyguardStateController.isMethodSecure()) { return false; } int state = mStateController.getState(); @@ -113,16 +110,7 @@ public class DynamicPrivacyController implements KeyguardStateController.Callbac return true; } - private boolean isStatusBarKeyguardShowing() { - return mStatusBarKeyguardViewManager != null && mStatusBarKeyguardViewManager.isShowing(); - } - - public void setStatusBarKeyguardViewManager( - StatusBarKeyguardViewManager statusBarKeyguardViewManager) { - mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; - } - public interface Listener { void onDynamicPrivacyChanged(); } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java index 59022c0ffbf2..0a5e9867a17f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java @@ -66,11 +66,12 @@ import javax.inject.Inject; * splitted screen. */ @SysUISingleton -public class InstantAppNotifier extends CoreStartable - implements CommandQueue.Callbacks, KeyguardStateController.Callback { +public class InstantAppNotifier + implements CoreStartable, CommandQueue.Callbacks, KeyguardStateController.Callback { private static final String TAG = "InstantAppNotifier"; public static final int NUM_TASKS_FOR_INSTANT_APP_INFO = 5; + private final Context mContext; private final Handler mHandler = new Handler(); private final Executor mUiBgExecutor; private final ArraySet<Pair<String, Integer>> mCurrentNotifs = new ArraySet<>(); @@ -83,7 +84,7 @@ public class InstantAppNotifier extends CoreStartable CommandQueue commandQueue, @UiBackground Executor uiBgExecutor, KeyguardStateController keyguardStateController) { - super(context); + mContext = context; mCommandQueue = commandQueue; mUiBgExecutor = uiBgExecutor; mKeyguardStateController = keyguardStateController; @@ -289,7 +290,7 @@ public class InstantAppNotifier extends CoreStartable .setComponent(aiaComponent) .setAction(Intent.ACTION_VIEW) .addCategory(Intent.CATEGORY_BROWSABLE) - .addCategory("unique:" + System.currentTimeMillis()) + .setIdentifier("unique:" + System.currentTimeMillis()) .putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.packageName) .putExtra( Intent.EXTRA_VERSION_CODE, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt index 7fbdd35796c1..2734511de78c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt @@ -17,40 +17,27 @@ package com.android.systemui.statusbar.notification import android.content.Context -import android.util.Log -import android.widget.Toast import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.util.Compile import javax.inject.Inject class NotifPipelineFlags @Inject constructor( val context: Context, val featureFlags: FeatureFlags ) { - fun checkLegacyPipelineEnabled(): Boolean { - if (Compile.IS_DEBUG) { - Toast.makeText(context, "Old pipeline code running!", Toast.LENGTH_SHORT).show() - } - if (featureFlags.isEnabled(Flags.NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE)) { - throw RuntimeException("Old pipeline code running with new pipeline enabled") - } else { - Log.d("NotifPipeline", "Old pipeline code running with new pipeline enabled", - Exception()) - } - return false - } - fun isDevLoggingEnabled(): Boolean = featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING) - fun isSmartspaceDedupingEnabled(): Boolean = - featureFlags.isEnabled(Flags.SMARTSPACE) && - featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING) - - fun removeUnrankedNotifs(): Boolean = - featureFlags.isEnabled(Flags.REMOVE_UNRANKED_NOTIFICATIONS) + fun isSmartspaceDedupingEnabled(): Boolean = featureFlags.isEnabled(Flags.SMARTSPACE) fun fullScreenIntentRequiresKeyguard(): Boolean = featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD) + + val isStabilityIndexFixEnabled: Boolean by lazy { + featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX) + } + + val isSemiStableSortEnabled: Boolean by lazy { + featureFlags.isEnabled(Flags.SEMI_STABLE_SORT) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt index ad3dfedcdb96..3058fbbc1031 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.collection.NotificationEntry import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt index 553826dda919..0d35fdce953e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt @@ -70,8 +70,8 @@ class NotificationLaunchAnimatorController( val height = max(0, notification.actualHeight - notification.clipBottomAmount) val location = notification.locationOnScreen - val clipStartLocation = notificationListContainer.getTopClippingStartLocation() - val roundedTopClipping = Math.max(clipStartLocation - location[1], 0) + val clipStartLocation = notificationListContainer.topClippingStartLocation + val roundedTopClipping = (clipStartLocation - location[1]).coerceAtLeast(0) val windowTop = location[1] + roundedTopClipping val topCornerRadius = if (roundedTopClipping > 0) { // Because the rounded Rect clipping is complex, we start the top rounding at @@ -80,7 +80,7 @@ class NotificationLaunchAnimatorController( // if we'd like to have this perfect, but this is close enough. 0f } else { - notification.currentBackgroundRadiusTop + notification.topCornerRadius } val params = LaunchAnimationParameters( top = windowTop, @@ -88,7 +88,7 @@ class NotificationLaunchAnimatorController( left = location[0], right = location[0] + notification.width, topCornerRadius = topCornerRadius, - bottomCornerRadius = notification.currentBackgroundRadiusBottom + bottomCornerRadius = notification.bottomCornerRadius ) params.startTranslationZ = notification.translationZ @@ -97,8 +97,8 @@ class NotificationLaunchAnimatorController( params.startClipTopAmount = notification.clipTopAmount if (notification.isChildInGroup) { params.startNotificationTop += notification.notificationParent.translationY - val parentRoundedClip = Math.max( - clipStartLocation - notification.notificationParent.locationOnScreen[1], 0) + val locationOnScreen = notification.notificationParent.locationOnScreen[1] + val parentRoundedClip = (clipStartLocation - locationOnScreen).coerceAtLeast(0) params.parentStartRoundedTopClipping = parentRoundedClip val parentClip = notification.notificationParent.clipTopAmount diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt index 126a986ee5f4..d97b712df030 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -18,9 +18,13 @@ package com.android.systemui.statusbar.notification import android.animation.ObjectAnimator import android.util.FloatProperty +import com.android.systemui.Dumpable import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.ShadeExpansionListener import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController @@ -28,21 +32,22 @@ import com.android.systemui.statusbar.notification.stack.StackStateAnimator import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.ScreenOffAnimationController -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener +import java.io.PrintWriter import javax.inject.Inject import kotlin.math.min @SysUISingleton class NotificationWakeUpCoordinator @Inject constructor( + dumpManager: DumpManager, private val mHeadsUpManager: HeadsUpManager, private val statusBarStateController: StatusBarStateController, private val bypassController: KeyguardBypassController, private val dozeParameters: DozeParameters, private val screenOffAnimationController: ScreenOffAnimationController -) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, PanelExpansionListener { +) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener, + Dumpable { private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>( "notificationVisibility") { @@ -60,6 +65,7 @@ class NotificationWakeUpCoordinator @Inject constructor( private var mLinearDozeAmount: Float = 0.0f private var mDozeAmount: Float = 0.0f + private var mDozeAmountSource: String = "init" private var mNotificationVisibleAmount = 0.0f private var mNotificationsVisible = false private var mNotificationsVisibleForExpansion = false @@ -142,6 +148,7 @@ class NotificationWakeUpCoordinator @Inject constructor( } init { + dumpManager.registerDumpable(this) mHeadsUpManager.addListener(this) statusBarStateController.addCallback(this) addListener(object : WakeUpListener { @@ -248,13 +255,14 @@ class NotificationWakeUpCoordinator @Inject constructor( // Let's notify the scroller that an animation started notifyAnimationStart(mLinearDozeAmount == 1.0f) } - setDozeAmount(linear, eased) + setDozeAmount(linear, eased, source = "StatusBar") } - fun setDozeAmount(linear: Float, eased: Float) { + fun setDozeAmount(linear: Float, eased: Float, source: String) { val changed = linear != mLinearDozeAmount mLinearDozeAmount = linear mDozeAmount = eased + mDozeAmountSource = source mStackScrollerController.setDozeAmount(mDozeAmount) updateHideAmount() if (changed && linear == 0.0f) { @@ -271,7 +279,7 @@ class NotificationWakeUpCoordinator @Inject constructor( // undefined state, so it's an indication that we should do state cleanup. We override // the doze amount to 0f (not dozing) so that the notifications are no longer hidden. // See: UnlockedScreenOffAnimationController.onFinishedWakingUp() - setDozeAmount(0f, 0f) + setDozeAmount(0f, 0f, source = "Override: Shade->Shade (lock cancelled by unlock)") } if (overrideDozeAmountIfAnimatingScreenOff(mLinearDozeAmount)) { @@ -293,7 +301,7 @@ class NotificationWakeUpCoordinator @Inject constructor( this.state = newState } - override fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) { + override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) { val collapsedEnough = event.fraction <= 0.9f if (collapsedEnough != this.collapsedEnoughToHide) { val couldShowPulsingHuns = canShowPulsingHuns @@ -311,12 +319,11 @@ class NotificationWakeUpCoordinator @Inject constructor( */ private fun overrideDozeAmountIfBypass(): Boolean { if (bypassController.bypassEnabled) { - var amount = 1.0f - if (statusBarStateController.state == StatusBarState.SHADE || - statusBarStateController.state == StatusBarState.SHADE_LOCKED) { - amount = 0.0f + if (statusBarStateController.state == StatusBarState.KEYGUARD) { + setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)") + } else { + setDozeAmount(0f, 0f, source = "Override: bypass (shade)") } - setDozeAmount(amount, amount) return true } return false @@ -332,7 +339,7 @@ class NotificationWakeUpCoordinator @Inject constructor( */ private fun overrideDozeAmountIfAnimatingScreenOff(linearDozeAmount: Float): Boolean { if (screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()) { - setDozeAmount(1f, 1f) + setDozeAmount(1f, 1f, source = "Override: animating screen off") return true } @@ -414,6 +421,26 @@ class NotificationWakeUpCoordinator @Inject constructor( private fun shouldAnimateVisibility() = dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("mLinearDozeAmount: $mLinearDozeAmount") + pw.println("mDozeAmount: $mDozeAmount") + pw.println("mDozeAmountSource: $mDozeAmountSource") + pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount") + pw.println("mNotificationsVisible: $mNotificationsVisible") + pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion") + pw.println("mVisibilityAmount: $mVisibilityAmount") + pw.println("mLinearVisibilityAmount: $mLinearVisibilityAmount") + pw.println("pulseExpanding: $pulseExpanding") + pw.println("state: ${StatusBarState.toString(state)}") + pw.println("fullyAwake: $fullyAwake") + pw.println("wakingUp: $wakingUp") + pw.println("willWakeUp: $willWakeUp") + pw.println("collapsedEnoughToHide: $collapsedEnoughToHide") + pw.println("pulsing: $pulsing") + pw.println("notificationsFullyHidden: $notificationsFullyHidden") + pw.println("canShowPulsingHuns: $canShowPulsingHuns") + } + interface WakeUpListener { /** * Called whenever the notifications are fully hidden or shown @@ -426,4 +453,4 @@ class NotificationWakeUpCoordinator @Inject constructor( */ @JvmDefault fun onPulseExpansionChanged(expandingChanged: Boolean) {} } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt new file mode 100644 index 000000000000..ed7f648081c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt @@ -0,0 +1,284 @@ +package com.android.systemui.statusbar.notification + +import android.util.FloatProperty +import android.view.View +import androidx.annotation.FloatRange +import com.android.systemui.R +import com.android.systemui.statusbar.notification.stack.AnimationProperties +import com.android.systemui.statusbar.notification.stack.StackStateAnimator +import kotlin.math.abs + +/** + * Interface that allows to request/retrieve top and bottom roundness (a value between 0f and 1f). + * + * To request a roundness value, an [SourceType] must be specified. In case more origins require + * different roundness, for the same property, the maximum value will always be chosen. + * + * It also returns the current radius for all corners ([updatedRadii]). + */ +interface Roundable { + /** Properties required for a Roundable */ + val roundableState: RoundableState + + /** Current top roundness */ + @get:FloatRange(from = 0.0, to = 1.0) + @JvmDefault + val topRoundness: Float + get() = roundableState.topRoundness + + /** Current bottom roundness */ + @get:FloatRange(from = 0.0, to = 1.0) + @JvmDefault + val bottomRoundness: Float + get() = roundableState.bottomRoundness + + /** Max radius in pixel */ + @JvmDefault + val maxRadius: Float + get() = roundableState.maxRadius + + /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */ + @JvmDefault + val topCornerRadius: Float + get() = topRoundness * maxRadius + + /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */ + @JvmDefault + val bottomCornerRadius: Float + get() = bottomRoundness * maxRadius + + /** Get and update the current radii */ + @JvmDefault + val updatedRadii: FloatArray + get() = + roundableState.radiiBuffer.also { radii -> + updateRadii( + topCornerRadius = topCornerRadius, + bottomCornerRadius = bottomCornerRadius, + radii = radii, + ) + } + + /** + * Request the top roundness [value] for a specific [sourceType]. + * + * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more + * origins require different roundness, for the same property, the maximum value will always be + * chosen. + * + * @param value a value between 0f and 1f. + * @param animate true if it should animate to that value. + * @param sourceType the source from which the request for roundness comes. + * @return Whether the roundness was changed. + */ + @JvmDefault + fun requestTopRoundness( + @FloatRange(from = 0.0, to = 1.0) value: Float, + animate: Boolean, + sourceType: SourceType, + ): Boolean { + val roundnessMap = roundableState.topRoundnessMap + val lastValue = roundnessMap.values.maxOrNull() ?: 0f + if (value == 0f) { + // we should only take the largest value, and since the smallest value is 0f, we can + // remove this value from the list. In the worst case, the list is empty and the + // default value is 0f. + roundnessMap.remove(sourceType) + } else { + roundnessMap[sourceType] = value + } + val newValue = roundnessMap.values.maxOrNull() ?: 0f + + if (lastValue != newValue) { + val wasAnimating = roundableState.isTopAnimating() + + // Fail safe: + // when we've been animating previously and we're now getting an update in the + // other direction, make sure to animate it too, otherwise, the localized updating + // may make the start larger than 1.0. + val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f + + roundableState.setTopRoundness(value = newValue, animated = shouldAnimate || animate) + return true + } + return false + } + + /** + * Request the bottom roundness [value] for a specific [sourceType]. + * + * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more + * origins require different roundness, for the same property, the maximum value will always be + * chosen. + * + * @param value value between 0f and 1f. + * @param animate true if it should animate to that value. + * @param sourceType the source from which the request for roundness comes. + * @return Whether the roundness was changed. + */ + @JvmDefault + fun requestBottomRoundness( + @FloatRange(from = 0.0, to = 1.0) value: Float, + animate: Boolean, + sourceType: SourceType, + ): Boolean { + val roundnessMap = roundableState.bottomRoundnessMap + val lastValue = roundnessMap.values.maxOrNull() ?: 0f + if (value == 0f) { + // we should only take the largest value, and since the smallest value is 0f, we can + // remove this value from the list. In the worst case, the list is empty and the + // default value is 0f. + roundnessMap.remove(sourceType) + } else { + roundnessMap[sourceType] = value + } + val newValue = roundnessMap.values.maxOrNull() ?: 0f + + if (lastValue != newValue) { + val wasAnimating = roundableState.isBottomAnimating() + + // Fail safe: + // when we've been animating previously and we're now getting an update in the + // other direction, make sure to animate it too, otherwise, the localized updating + // may make the start larger than 1.0. + val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f + + roundableState.setBottomRoundness(value = newValue, animated = shouldAnimate || animate) + return true + } + return false + } + + /** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */ + @JvmDefault + fun applyRoundness() { + roundableState.targetView.invalidate() + } + + /** @return true if top or bottom roundness is not zero. */ + @JvmDefault + fun hasRoundedCorner(): Boolean { + return topRoundness != 0f || bottomRoundness != 0f + } + + /** + * Update an Array of 8 values, 4 pairs of [X,Y] radii. As expected by param radii of + * [android.graphics.Path.addRoundRect]. + * + * This method reuses the previous [radii] for performance reasons. + */ + @JvmDefault + fun updateRadii( + topCornerRadius: Float, + bottomCornerRadius: Float, + radii: FloatArray, + ) { + if (radii.size != 8) error("Unexpected radiiBuffer size ${radii.size}") + + if (radii[0] != topCornerRadius || radii[4] != bottomCornerRadius) { + (0..3).forEach { radii[it] = topCornerRadius } + (4..7).forEach { radii[it] = bottomCornerRadius } + } + } +} + +/** + * State object for a `Roundable` class. + * @param targetView Will handle the [AnimatableProperty] + * @param roundable Target of the radius animation + * @param maxRadius Max corner radius in pixels + */ +class RoundableState( + internal val targetView: View, + roundable: Roundable, + internal val maxRadius: Float, +) { + /** Animatable for top roundness */ + private val topAnimatable = topAnimatable(roundable) + + /** Animatable for bottom roundness */ + private val bottomAnimatable = bottomAnimatable(roundable) + + /** Current top roundness. Use [setTopRoundness] to update this value */ + @set:FloatRange(from = 0.0, to = 1.0) + internal var topRoundness = 0f + private set + + /** Current bottom roundness. Use [setBottomRoundness] to update this value */ + @set:FloatRange(from = 0.0, to = 1.0) + internal var bottomRoundness = 0f + private set + + /** Last requested top roundness associated by [SourceType] */ + internal val topRoundnessMap = mutableMapOf<SourceType, Float>() + + /** Last requested bottom roundness associated by [SourceType] */ + internal val bottomRoundnessMap = mutableMapOf<SourceType, Float>() + + /** Last cached radii */ + internal val radiiBuffer = FloatArray(8) + + /** Is top roundness animation in progress? */ + internal fun isTopAnimating() = PropertyAnimator.isAnimating(targetView, topAnimatable) + + /** Is bottom roundness animation in progress? */ + internal fun isBottomAnimating() = PropertyAnimator.isAnimating(targetView, bottomAnimatable) + + /** Set the current top roundness */ + internal fun setTopRoundness( + value: Float, + animated: Boolean = targetView.isShown, + ) { + PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated) + } + + /** Set the current bottom roundness */ + internal fun setBottomRoundness( + value: Float, + animated: Boolean = targetView.isShown, + ) { + PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated) + } + + companion object { + private val DURATION: AnimationProperties = + AnimationProperties() + .setDuration(StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS.toLong()) + + private fun topAnimatable(roundable: Roundable): AnimatableProperty = + AnimatableProperty.from( + object : FloatProperty<View>("topRoundness") { + override fun get(view: View): Float = roundable.topRoundness + + override fun setValue(view: View, value: Float) { + roundable.roundableState.topRoundness = value + roundable.applyRoundness() + } + }, + R.id.top_roundess_animator_tag, + R.id.top_roundess_animator_end_tag, + R.id.top_roundess_animator_start_tag, + ) + + private fun bottomAnimatable(roundable: Roundable): AnimatableProperty = + AnimatableProperty.from( + object : FloatProperty<View>("bottomRoundness") { + override fun get(view: View): Float = roundable.bottomRoundness + + override fun setValue(view: View, value: Float) { + roundable.roundableState.bottomRoundness = value + roundable.applyRoundness() + } + }, + R.id.bottom_roundess_animator_tag, + R.id.bottom_roundess_animator_end_tag, + R.id.bottom_roundess_animator_start_tag, + ) + } +} + +enum class SourceType { + DefaultValue, + OnDismissAnimation, + OnScroll, +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt index f8449ae8807b..84ab0d1190f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt @@ -68,6 +68,9 @@ data class ListAttachState private constructor( */ var stableIndex: Int = -1 + /** Access the index of the [section] or -1 if the entry does not have one */ + val sectionIndex: Int get() = section?.index ?: -1 + /** Copies the state of another instance. */ fun clone(other: ListAttachState) { parent = other.parent @@ -95,11 +98,13 @@ data class ListAttachState private constructor( * This can happen if the entry is removed from a group that was broken up or if the entry was * filtered out during any of the filtering steps. */ - fun detach() { + fun detach(includingStableIndex: Boolean) { parent = null section = null promoter = null - // stableIndex = -1 // TODO(b/241229236): Clear this once we fix the stability fragility + if (includingStableIndex) { + stableIndex = -1 + } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index 2887f975d46c..df35c9e6832a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -602,7 +602,7 @@ public class NotifCollection implements Dumpable, PipelineDumpable { mInconsistencyTracker.logNewMissingNotifications(rankingMap); mInconsistencyTracker.logNewInconsistentRankings(currentEntriesWithoutRankings, rankingMap); - if (currentEntriesWithoutRankings != null && mNotifPipelineFlags.removeUnrankedNotifs()) { + if (currentEntriesWithoutRankings != null) { for (NotificationEntry entry : currentEntriesWithoutRankings.values()) { entry.mCancellationReason = REASON_UNKNOWN; tryRemoveNotification(entry); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index 3eaa988e8389..3ae2545e4e10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -54,6 +54,9 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener; import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState; +import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort; +import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder; +import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper; import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator; @@ -96,11 +99,14 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated // TODO replace temp with collection pool for readability private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>(); + private NotifPipelineFlags mFlags; private final boolean mAlwaysLogList; private List<ListEntry> mNotifList = new ArrayList<>(); private List<ListEntry> mNewNotifList = new ArrayList<>(); + private final SemiStableSort mSemiStableSort = new SemiStableSort(); + private final StableOrder<ListEntry> mStableOrder = this::getStableOrderRank; private final PipelineState mPipelineState = new PipelineState(); private final Map<String, GroupEntry> mGroups = new ArrayMap<>(); private Collection<NotificationEntry> mAllEntries = Collections.emptyList(); @@ -141,6 +147,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { ) { mSystemClock = systemClock; mLogger = logger; + mFlags = flags; mAlwaysLogList = flags.isDevLoggingEnabled(); mInteractionTracker = interactionTracker; mChoreographer = pipelineChoreographer; @@ -527,7 +534,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { List<NotifFilter> filters) { Trace.beginSection("ShadeListBuilder.filterNotifs"); final long now = mSystemClock.uptimeMillis(); - for (ListEntry entry : entries) { + for (ListEntry entry : entries) { if (entry instanceof GroupEntry) { final GroupEntry groupEntry = (GroupEntry) entry; @@ -958,7 +965,8 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { * filtered out during any of the filtering steps. */ private void annulAddition(ListEntry entry) { - entry.getAttachState().detach(); + // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility + entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled()); } private void assignSections() { @@ -978,7 +986,16 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private void sortListAndGroups() { Trace.beginSection("ShadeListBuilder.sortListAndGroups"); - // Assign sections to top-level elements and sort their children + if (mFlags.isSemiStableSortEnabled()) { + sortWithSemiStableSort(); + } else { + sortWithLegacyStability(); + } + Trace.endSection(); + } + + private void sortWithLegacyStability() { + // Sort all groups and the top level list for (ListEntry entry : mNotifList) { if (entry instanceof GroupEntry) { GroupEntry parent = (GroupEntry) entry; @@ -991,16 +1008,15 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // Check for suppressed order changes if (!getStabilityManager().isEveryChangeAllowed()) { mForceReorderable = true; - boolean isSorted = isShadeSorted(); + boolean isSorted = isShadeSortedLegacy(); mForceReorderable = false; if (!isSorted) { getStabilityManager().onEntryReorderSuppressed(); } } - Trace.endSection(); } - private boolean isShadeSorted() { + private boolean isShadeSortedLegacy() { if (!isSorted(mNotifList, mTopLevelComparator)) { return false; } @@ -1014,6 +1030,43 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { return true; } + private void sortWithSemiStableSort() { + // Sort each group's children + boolean allSorted = true; + for (ListEntry entry : mNotifList) { + if (entry instanceof GroupEntry) { + GroupEntry parent = (GroupEntry) entry; + allSorted &= sortGroupChildren(parent.getRawChildren()); + } + } + // Sort each section within the top level list + mNotifList.sort(mTopLevelComparator); + if (!getStabilityManager().isEveryChangeAllowed()) { + for (List<ListEntry> subList : getSectionSubLists(mNotifList)) { + allSorted &= mSemiStableSort.stabilizeTo(subList, mStableOrder, mNewNotifList); + } + applyNewNotifList(); + } + assignIndexes(mNotifList); + if (!allSorted) { + // Report suppressed order changes + getStabilityManager().onEntryReorderSuppressed(); + } + } + + private Iterable<List<ListEntry>> getSectionSubLists(List<ListEntry> entries) { + return ShadeListBuilderHelper.INSTANCE.getSectionSubLists(entries); + } + + private boolean sortGroupChildren(List<NotificationEntry> entries) { + if (getStabilityManager().isEveryChangeAllowed()) { + entries.sort(mGroupChildrenComparator); + return true; + } else { + return mSemiStableSort.sort(entries, mStableOrder, mGroupChildrenComparator); + } + } + /** Determine whether the items in the list are sorted according to the comparator */ @VisibleForTesting public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) { @@ -1036,27 +1089,41 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { /** * Assign the index of each notification relative to the total order */ - private static void assignIndexes(List<ListEntry> notifList) { + private void assignIndexes(List<ListEntry> notifList) { if (notifList.size() == 0) return; NotifSection currentSection = requireNonNull(notifList.get(0).getSection()); int sectionMemberIndex = 0; for (int i = 0; i < notifList.size(); i++) { - ListEntry entry = notifList.get(i); + final ListEntry entry = notifList.get(i); NotifSection section = requireNonNull(entry.getSection()); if (section.getIndex() != currentSection.getIndex()) { sectionMemberIndex = 0; currentSection = section; } - entry.getAttachState().setStableIndex(sectionMemberIndex); - if (entry instanceof GroupEntry) { - GroupEntry parent = (GroupEntry) entry; - for (int j = 0; j < parent.getChildren().size(); j++) { - entry = parent.getChildren().get(j); - entry.getAttachState().setStableIndex(sectionMemberIndex); - sectionMemberIndex++; + if (mFlags.isStabilityIndexFixEnabled()) { + entry.getAttachState().setStableIndex(sectionMemberIndex++); + if (entry instanceof GroupEntry) { + final GroupEntry parent = (GroupEntry) entry; + final NotificationEntry summary = parent.getSummary(); + if (summary != null) { + summary.getAttachState().setStableIndex(sectionMemberIndex++); + } + for (NotificationEntry child : parent.getChildren()) { + child.getAttachState().setStableIndex(sectionMemberIndex++); + } + } + } else { + // This old implementation uses the same index number for the group as the first + // child, and fails to assign an index to the summary. Remove once tested. + entry.getAttachState().setStableIndex(sectionMemberIndex); + if (entry instanceof GroupEntry) { + final GroupEntry parent = (GroupEntry) entry; + for (NotificationEntry child : parent.getChildren()) { + child.getAttachState().setStableIndex(sectionMemberIndex++); + } } + sectionMemberIndex++; } - sectionMemberIndex++; } } @@ -1196,7 +1263,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { o2.getSectionIndex()); if (cmp != 0) return cmp; - cmp = Integer.compare( + cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( getStableOrderIndex(o1), getStableOrderIndex(o2)); if (cmp != 0) return cmp; @@ -1225,7 +1292,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> { - int cmp = Integer.compare( + int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( getStableOrderIndex(o1), getStableOrderIndex(o2)); if (cmp != 0) return cmp; @@ -1256,9 +1323,25 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // let the stability manager constrain or allow reordering return -1; } + // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility return entry.getPreviousAttachState().getStableIndex(); } + @Nullable + private Integer getStableOrderRank(ListEntry entry) { + if (getStabilityManager().isEntryReorderingAllowed(entry)) { + // let the stability manager constrain or allow reordering + return null; + } + if (entry.getAttachState().getSectionIndex() + != entry.getPreviousAttachState().getSectionIndex()) { + // stable index is only valid within the same section; otherwise we allow reordering + return null; + } + final int stableIndex = entry.getPreviousAttachState().getStableIndex(); + return stableIndex == -1 ? null : stableIndex; + } + private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) { final NotifFilter filter = findRejectingFilter(entry, now, filters); entry.getAttachState().setExcludingFilter(filter); @@ -1398,7 +1481,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { throw exception; } - Log.e(TAG, "Allowing " + mConsecutiveReentrantRebuilds + Log.wtf(TAG, "Allowing " + mConsecutiveReentrantRebuilds + " consecutive reentrant notification pipeline rebuild(s).", exception); mChoreographer.schedule(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt index 211e37473a70..68d1319699d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.coalescer -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject class GroupCoalescerLogger @Inject constructor( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt index e8f352f60da0..2919def16304 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.row.NotificationGuts import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 8278b549a7a0..8a31ed9271ad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification import android.app.Notification.GROUP_ALERT_SUMMARY import android.util.ArrayMap +import android.util.ArraySet +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.collection.GroupEntry @@ -70,6 +72,7 @@ class HeadsUpCoordinator @Inject constructor( @Main private val mExecutor: DelayableExecutor, ) : Coordinator { private val mEntriesBindingUntil = ArrayMap<String, Long>() + private val mEntriesUpdateTimes = ArrayMap<String, Long>() private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null private lateinit var mNotifPipeline: NotifPipeline private var mNow: Long = -1 @@ -264,6 +267,9 @@ class HeadsUpCoordinator @Inject constructor( } // After this method runs, all posted entries should have been handled (or skipped). mPostedEntries.clear() + + // Also take this opportunity to clean up any stale entry update times + cleanUpEntryUpdateTimes() } /** @@ -378,6 +384,9 @@ class HeadsUpCoordinator @Inject constructor( isAlerting = false, isBinding = false, ) + + // Record the last updated time for this key + setUpdateTime(entry, mSystemClock.currentTimeMillis()) } /** @@ -393,7 +402,7 @@ class HeadsUpCoordinator @Inject constructor( val posted = mPostedEntries.compute(entry.key) { _, value -> value?.also { update -> update.wasUpdated = true - update.shouldHeadsUpEver = update.shouldHeadsUpEver || shouldHeadsUpEver + update.shouldHeadsUpEver = shouldHeadsUpEver update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain update.isAlerting = isAlerting update.isBinding = isBinding @@ -419,6 +428,9 @@ class HeadsUpCoordinator @Inject constructor( cancelHeadsUpBind(posted.entry) } } + + // Update last updated time for this entry + setUpdateTime(entry, mSystemClock.currentTimeMillis()) } /** @@ -426,6 +438,7 @@ class HeadsUpCoordinator @Inject constructor( */ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { mPostedEntries.remove(entry.key) + mEntriesUpdateTimes.remove(entry.key) cancelHeadsUpBind(entry) val entryKey = entry.key if (mHeadsUpManager.isAlerting(entryKey)) { @@ -440,6 +453,47 @@ class HeadsUpCoordinator @Inject constructor( override fun onEntryCleanUp(entry: NotificationEntry) { mHeadsUpViewBinder.abortBindCallback(entry) } + + /** + * Identify notifications whose heads-up state changes when the notification rankings are + * updated, and have those changed notifications alert if necessary. + * + * This method will occur after any operations in onEntryAdded or onEntryUpdated, so any + * handling of ranking changes needs to take into account that we may have just made a + * PostedEntry for some of these notifications. + */ + override fun onRankingApplied() { + // Because a ranking update may cause some notifications that are no longer (or were + // never) in mPostedEntries to need to alert, we need to check every notification + // known to the pipeline. + for (entry in mNotifPipeline.allNotifs) { + // Only consider entries that are recent enough, since we want to apply a fairly + // strict threshold for when an entry should be updated via only ranking and not an + // app-provided notification update. + if (!isNewEnoughForRankingUpdate(entry)) continue + + // The only entries we consider alerting for here are entries that have never + // interrupted and that now say they should heads up; if they've alerted in the + // past, we don't want to incorrectly alert a second time if there wasn't an + // explicit notification update. + if (entry.hasInterrupted()) continue + + // The cases where we should consider this notification to be updated: + // - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp + // state + // - if it is present in PostedEntries and the previous state of shouldHeadsUp + // differs from the updated one + val shouldHeadsUpEver = mNotificationInterruptStateProvider.checkHeadsUp(entry, + /* log= */ false) + val postedShouldHeadsUpEver = mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false + val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver + + if (shouldUpdateEntry) { + mLogger.logEntryUpdatedByRanking(entry.key, shouldHeadsUpEver) + onEntryUpdated(entry) + } + } + } } /** @@ -450,6 +504,41 @@ class HeadsUpCoordinator @Inject constructor( (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0) } + /** + * Sets the updated time for the given entry to the specified time. + */ + @VisibleForTesting + fun setUpdateTime(entry: NotificationEntry, time: Long) { + mEntriesUpdateTimes[entry.key] = time + } + + /** + * Checks whether the entry is new enough to be updated via ranking update. + * We want to avoid updating an entry too long after it was originally posted/updated when we're + * only reacting to a ranking change, as relevant ranking updates are expected to come in + * fairly soon after the posting of a notification. + */ + private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean { + // If we don't have an update time for this key, default to "too old" + if (!mEntriesUpdateTimes.containsKey(entry.key)) return false + + val updateTime = mEntriesUpdateTimes[entry.key] ?: return false + return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS + } + + private fun cleanUpEntryUpdateTimes() { + // Because we won't update entries that are older than this amount of time anyway, clean + // up any entries that are too old to notify. + val toRemove = ArraySet<String>() + for ((key, updateTime) in mEntriesUpdateTimes) { + if (updateTime == null || + (mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) { + toRemove.add(key) + } + } + mEntriesUpdateTimes.removeAll(toRemove) + } + /** When an action is pressed on a notification, end HeadsUp lifetime extension. */ private val mActionPressListener = Consumer<NotificationEntry> { entry -> if (mNotifsExtendingLifetime.contains(entry)) { @@ -561,6 +650,9 @@ class HeadsUpCoordinator @Inject constructor( companion object { private const val TAG = "HeadsUpCoordinator" private const val BIND_TIMEOUT = 1000L + + // This value is set to match MAX_SOUND_DELAY_MS in NotificationRecord. + private const val MAX_RANKING_UPDATE_DELAY_MS: Long = 2000 } data class PostedEntry( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt index 204a494c32e8..dfaa291c6bb6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt @@ -1,9 +1,10 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.util.Log -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel + import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "HeadsUpCoordinator" @@ -59,4 +60,13 @@ class HeadsUpCoordinatorLogger constructor( " numPostedEntries=$int1 logicalGroupSize=$int2" }) } + + fun logEntryUpdatedByRanking(key: String, shouldHun: Boolean) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = key + bool1 = shouldHun + }, { + "updating entry via ranking applied: $str1 updated shouldHeadsUp=$bool1" + }) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java index 2480ff65d8fc..0be4bde749f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java @@ -16,14 +16,14 @@ package com.android.systemui.statusbar.notification.collection.coordinator; -import static com.android.systemui.media.MediaDataManagerKt.isMediaNotification; +import static com.android.systemui.media.controls.pipeline.MediaDataManagerKt.isMediaNotification; import android.os.RemoteException; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; import com.android.internal.statusbar.IStatusBarService; -import com.android.systemui.media.MediaFeatureFlag; +import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 6e76691ae1b1..d2db6224ef52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -407,7 +407,10 @@ public class PreparationCoordinator implements Coordinator { mLogger.logGroupInflationTookTooLong(group); return false; } - if (mInflatingNotifs.contains(group.getSummary())) { + // Only delay release if the summary is not inflated. + // TODO(253454977): Once we ensure that all other pipeline filtering and pruning has been + // done by this point, we can revert back to checking for mInflatingNotifs.contains(...) + if (group.getSummary() != null && !isInflated(group.getSummary())) { mLogger.logDelayingGroupRelease(group, group.getSummary()); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt index c4f4ed54e2fa..9558f47af795 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt index c687e1bacbc9..d80445491bda 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "ShadeEventCoordinator" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java index a7719d3d82a4..e71d80c130da 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.collection.inflation; import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; +import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; + import android.os.SystemClock; import android.service.notification.NotificationStats; @@ -70,6 +72,8 @@ public class OnUserInteractionCallbackImpl implements OnUserInteractionCallback dismissalSurface = NotificationStats.DISMISSAL_PEEK; } else if (mStatusBarStateController.isDozing()) { dismissalSurface = NotificationStats.DISMISSAL_AOD; + } else if (mStatusBarStateController.getState() == KEYGUARD) { + dismissalSurface = NotificationStats.DISMISSAL_LOCKSCREEN; } return new DismissedByUserStats( dismissalSurface, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt new file mode 100644 index 000000000000..9ec8e07e73b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.listbuilder + +import androidx.annotation.VisibleForTesting +import kotlin.math.sign + +class SemiStableSort { + val preallocatedWorkspace by lazy { ArrayList<Any>() } + val preallocatedAdditions by lazy { ArrayList<Any>() } + val preallocatedMapToIndex by lazy { HashMap<Any, Int>() } + val preallocatedMapToIndexComparator: Comparator<Any> by lazy { + Comparator.comparingInt { item -> preallocatedMapToIndex[item] ?: -1 } + } + + /** + * Sort the given [items] such that items which have a [stableOrder] will all be in that order, + * items without a [stableOrder] will be sorted according to the comparator, and the two sets of + * items will be combined to have the fewest elements out of order according to the [comparator] + * . The result will be placed into the original [items] list. + */ + fun <T : Any> sort( + items: MutableList<T>, + stableOrder: StableOrder<in T>, + comparator: Comparator<in T>, + ): Boolean = + withWorkspace<T, Boolean> { workspace -> + val ordered = + sortTo( + items, + stableOrder, + comparator, + workspace, + ) + items.clear() + items.addAll(workspace) + return ordered + } + + /** + * Sort the given [items] such that items which have a [stableOrder] will all be in that order, + * items without a [stableOrder] will be sorted according to the comparator, and the two sets of + * items will be combined to have the fewest elements out of order according to the [comparator] + * . The result will be put into [output]. + */ + fun <T : Any> sortTo( + items: Iterable<T>, + stableOrder: StableOrder<in T>, + comparator: Comparator<in T>, + output: MutableList<T>, + ): Boolean { + if (DEBUG) println("\n> START from ${items.map { it to stableOrder.getRank(it) }}") + // If array already has elements, use subList to ensure we only append + val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size) + items.filterTo(result) { stableOrder.getRank(it) != null } + result.sortBy { stableOrder.getRank(it)!! } + val isOrdered = result.isSorted(comparator) + withAdditions<T> { additions -> + items.filterTo(additions) { stableOrder.getRank(it) == null } + additions.sortWith(comparator) + insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator) + } + return isOrdered + } + + /** + * Rearrange the [sortedItems] to enforce that items are in the [stableOrder], and store the + * result in [output]. Items with a [stableOrder] will be in that order, items without a + * [stableOrder] will remain in same relative order as the input, and the two sets of items will + * be combined to have the fewest elements moved from their locations in the original. + */ + fun <T : Any> stabilizeTo( + sortedItems: Iterable<T>, + stableOrder: StableOrder<in T>, + output: MutableList<T>, + ): Boolean { + // Append to the output array if present + val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size) + sortedItems.filterTo(result) { stableOrder.getRank(it) != null } + val stableRankComparator = compareBy<T> { stableOrder.getRank(it)!! } + val isOrdered = result.isSorted(stableRankComparator) + if (!isOrdered) { + result.sortWith(stableRankComparator) + } + if (result.isEmpty()) { + sortedItems.filterTo(result) { stableOrder.getRank(it) == null } + return isOrdered + } + withAdditions<T> { additions -> + sortedItems.filterTo(additions) { stableOrder.getRank(it) == null } + if (additions.isNotEmpty()) { + withIndexOfComparator(sortedItems) { comparator -> + insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator) + } + } + } + return isOrdered + } + + private inline fun <T : Any, R> withWorkspace(block: (ArrayList<T>) -> R): R { + preallocatedWorkspace.clear() + val result = block(preallocatedWorkspace as ArrayList<T>) + preallocatedWorkspace.clear() + return result + } + + private inline fun <T : Any> withAdditions(block: (ArrayList<T>) -> Unit) { + preallocatedAdditions.clear() + block(preallocatedAdditions as ArrayList<T>) + preallocatedAdditions.clear() + } + + private inline fun <T : Any> withIndexOfComparator( + sortedItems: Iterable<T>, + block: (Comparator<in T>) -> Unit + ) { + preallocatedMapToIndex.clear() + sortedItems.forEachIndexed { i, item -> preallocatedMapToIndex[item] = i } + block(preallocatedMapToIndexComparator as Comparator<in T>) + preallocatedMapToIndex.clear() + } + + companion object { + + /** + * This is the core of the algorithm. + * + * Insert [preSortedAdditions] (the elements to be inserted) into [existing] without + * changing the relative order of any elements already in [existing], even though those + * elements may be mis-ordered relative to the [comparator], such that the total number of + * elements which are ordered incorrectly according to the [comparator] is fewest. + */ + private fun <T> insertPreSortedElementsWithFewestMisOrderings( + existing: MutableList<T>, + preSortedAdditions: Iterable<T>, + comparator: Comparator<in T>, + ) { + if (DEBUG) println(" To $existing insert $preSortedAdditions with fewest misordering") + var iStart = 0 + preSortedAdditions.forEach { toAdd -> + if (DEBUG) println(" need to add $toAdd to $existing, starting at $iStart") + var cmpSum = 0 + var cmpSumMax = 0 + var iCmpSumMax = iStart + if (DEBUG) print(" ") + for (i in iCmpSumMax until existing.size) { + val cmp = comparator.compare(toAdd, existing[i]).sign + cmpSum += cmp + if (cmpSum > cmpSumMax) { + cmpSumMax = cmpSum + iCmpSumMax = i + 1 + } + if (DEBUG) print("sum[$i]=$cmpSum, ") + } + if (DEBUG) println("inserting $toAdd at $iCmpSumMax") + existing.add(iCmpSumMax, toAdd) + iStart = iCmpSumMax + 1 + } + } + + /** Determines if a list is correctly sorted according to the given comparator */ + @VisibleForTesting + fun <T> List<T>.isSorted(comparator: Comparator<T>): Boolean { + if (this.size <= 1) { + return true + } + val iterator = this.iterator() + var previous = iterator.next() + var current: T? + while (iterator.hasNext()) { + current = iterator.next() + if (comparator.compare(previous, current) > 0) { + return false + } + previous = current + } + return true + } + } + + fun interface StableOrder<T> { + fun getRank(item: T): Int? + } +} + +val DEBUG = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt new file mode 100644 index 000000000000..d8f75f61c05a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.listbuilder + +import com.android.systemui.statusbar.notification.collection.ListEntry + +object ShadeListBuilderHelper { + fun getSectionSubLists(entries: List<ListEntry>): Iterable<List<ListEntry>> = + getContiguousSubLists(entries, minLength = 1) { it.sectionIndex } + + inline fun <T : Any, K : Any> getContiguousSubLists( + itemList: List<T>, + minLength: Int = 1, + key: (T) -> K, + ): Iterable<List<T>> { + val subLists = mutableListOf<List<T>>() + val numEntries = itemList.size + var currentSectionStartIndex = 0 + var currentSectionKey: K? = null + for (i in 0 until numEntries) { + val sectionKey = key(itemList[i]) + if (currentSectionKey == null) { + currentSectionKey = sectionKey + } else if (currentSectionKey != sectionKey) { + val length = i - currentSectionStartIndex + if (length >= minLength) { + subLists.add(itemList.subList(currentSectionStartIndex, i)) + } + currentSectionStartIndex = i + currentSectionKey = sectionKey + } + } + val length = numEntries - currentSectionStartIndex + if (length >= minLength) { + subLists.add(itemList.subList(currentSectionStartIndex, numEntries)) + } + return subLists + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt index d8dae5d23f42..8e052c7dcc5d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt @@ -16,11 +16,11 @@ package com.android.systemui.statusbar.notification.collection.listbuilder -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt index aa27e1e407f0..911a2d0c2b36 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt @@ -20,13 +20,13 @@ import android.os.RemoteException import android.service.notification.NotificationListenerService import android.service.notification.NotificationListenerService.RankingMap import android.service.notification.StatusBarNotification -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING -import com.android.systemui.log.LogLevel.WTF import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING +import com.android.systemui.plugins.log.LogLevel.WTF import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason import com.android.systemui.statusbar.notification.collection.NotifCollection.FutureDismissal diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt index 38e3d496a60c..9c71e5c1054c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.render -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.util.Compile diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt index b6278d1d5f01..fde4ecb7bcaa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.collection.render +import javax.inject.Inject + /** An interface by which the pipeline can make updates to the notification root view. */ interface NotifStackController { /** Provides stats about the list of notifications attached to the shade */ @@ -42,6 +44,6 @@ data class NotifStats( * methods, rather than forcing us to add no-op implementations in their implementation every time * a method is added. */ -open class DefaultNotifStackController : NotifStackController { +open class DefaultNotifStackController @Inject constructor() : NotifStackController { override fun setNotifStats(stats: NotifStats) {} }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt index 6d1071c283e3..b4b9438cd6be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.collection.render -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import java.lang.RuntimeException import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt index b2cb23bd11aa..a5278c3d0ad3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt @@ -23,6 +23,7 @@ import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.stack.NotificationListContainer +import com.android.systemui.statusbar.phone.CentralSurfaces /** * The master controller for all notifications-related work @@ -32,6 +33,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationListContain */ interface NotificationsController { fun initialize( + centralSurfaces: CentralSurfaces, presenter: NotificationPresenter, listContainer: NotificationListContainer, stackController: NotifStackController, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 1aa02951f3f7..8eef3f36433d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -19,9 +19,12 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification import com.android.systemui.ForegroundServiceNotificationListener import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.people.widget.PeopleSpaceWidgetManager import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationListener +import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.AnimatedImageNotificationManager import com.android.systemui.statusbar.notification.NotificationActivityStarter @@ -36,6 +39,8 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Co import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder +import com.android.systemui.statusbar.notification.logging.NotificationLogger +import com.android.systemui.statusbar.notification.logging.NotificationMemoryMonitor import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.statusbar.phone.CentralSurfaces @@ -53,7 +58,6 @@ import javax.inject.Inject */ @SysUISingleton class NotificationsControllerImpl @Inject constructor( - private val centralSurfaces: Lazy<CentralSurfaces>, private val notificationListener: NotificationListener, private val commonNotifCollection: Lazy<CommonNotifCollection>, private val notifPipeline: Lazy<NotifPipeline>, @@ -61,16 +65,21 @@ class NotificationsControllerImpl @Inject constructor( private val targetSdkResolver: TargetSdkResolver, private val notifPipelineInitializer: Lazy<NotifPipelineInitializer>, private val notifBindPipelineInitializer: NotifBindPipelineInitializer, + private val notificationLogger: NotificationLogger, private val notificationRowBinder: NotificationRowBinderImpl, + private val notificationsMediaManager: NotificationMediaManager, private val headsUpViewBinder: HeadsUpViewBinder, private val clickerBuilder: NotificationClicker.Builder, private val animatedImageNotificationManager: AnimatedImageNotificationManager, private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager, private val bubblesOptional: Optional<Bubbles>, private val fgsNotifListener: ForegroundServiceNotificationListener, + private val memoryMonitor: Lazy<NotificationMemoryMonitor>, + private val featureFlags: FeatureFlags ) : NotificationsController { override fun initialize( + centralSurfaces: CentralSurfaces, presenter: NotificationPresenter, listContainer: NotificationListContainer, stackController: NotifStackController, @@ -87,8 +96,8 @@ class NotificationsControllerImpl @Inject constructor( notificationRowBinder.setNotificationClicker( clickerBuilder.build( - Optional.of( - centralSurfaces.get()), bubblesOptional, notificationActivityStarter)) + Optional.ofNullable(centralSurfaces), bubblesOptional, + notificationActivityStarter)) notificationRowBinder.setUpWithPresenter( presenter, listContainer, @@ -104,9 +113,13 @@ class NotificationsControllerImpl @Inject constructor( stackController) targetSdkResolver.initialize(notifPipeline.get()) - + notificationsMediaManager.setUpWithPresenter(presenter) + notificationLogger.setUpWithContainer(listContainer) peopleSpaceWidgetManager.attach(notificationListener) fgsNotifListener.init() + if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_MONITOR_ENABLED)) { + memoryMonitor.get().init() + } } // TODO: Convert all functions below this line into listeners instead of public methods diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt index 744166d87907..14856dafdb11 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.stack.NotificationListContainer +import com.android.systemui.statusbar.phone.CentralSurfaces import javax.inject.Inject /** @@ -34,6 +35,7 @@ class NotificationsControllerStub @Inject constructor( ) : NotificationsController { override fun initialize( + centralSurfaces: CentralSurfaces, presenter: NotificationPresenter, listContainer: NotificationListContainer, stackController: NotifStackController, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java index 6f41425b506d..9a7610ddd354 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java @@ -114,7 +114,18 @@ public class HeadsUpViewBinder { */ public void unbindHeadsUpView(NotificationEntry entry) { abortBindCallback(entry); - mStage.getStageParams(entry).markContentViewsFreeable(FLAG_CONTENT_VIEW_HEADS_UP); + + // params may be null if the notification was already removed from the collection but we let + // it stick around during a launch animation. In this case, the heads up view has already + // been unbound, so we don't need to unbind it. + // TODO(b/253081345): Change this back to getStageParams and remove null check. + RowContentBindParams params = mStage.tryGetStageParams(entry); + if (params == null) { + mLogger.entryBindStageParamsNullOnUnbind(entry); + return; + } + + params.markContentViewsFreeable(FLAG_CONTENT_VIEW_HEADS_UP); mLogger.entryContentViewMarkedFreeable(entry); mStage.requestRebind(entry, e -> mLogger.entryUnbound(e)); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt index d1feaa05c653..d4f11fc141f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.interruption -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject @@ -47,6 +47,14 @@ class HeadsUpViewBinderLogger @Inject constructor(@NotificationHeadsUpLog val bu "start unbinding heads up entry $str1 " }) } + + fun entryBindStageParamsNullOnUnbind(entry: NotificationEntry) { + buffer.log(TAG, INFO, { + str1 = entry.logKey + }, { + "heads up entry bind stage params null on unbind $str1 " + }) + } } private const val TAG = "HeadsUpViewBinder" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt index c956a2ea1836..e6dbcee10f60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt @@ -72,7 +72,6 @@ private interface KeyguardNotificationVisibilityProviderImplModule { @SysUISingleton private class KeyguardNotificationVisibilityProviderImpl @Inject constructor( - context: Context, @Main private val handler: Handler, private val keyguardStateController: KeyguardStateController, private val lockscreenUserManager: NotificationLockscreenUserManager, @@ -82,7 +81,7 @@ private class KeyguardNotificationVisibilityProviderImpl @Inject constructor( private val broadcastDispatcher: BroadcastDispatcher, private val secureSettings: SecureSettings, private val globalSettings: GlobalSettings -) : CoreStartable(context), KeyguardNotificationVisibilityProvider { +) : CoreStartable, KeyguardNotificationVisibilityProvider { private val showSilentNotifsUri = secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS) private val onStateChangedListeners = ListenerSet<Consumer<String>>() @@ -232,7 +231,7 @@ private class KeyguardNotificationVisibilityProviderImpl @Inject constructor( private fun readShowSilentNotificationSetting() { val showSilentNotifs = secureSettings.getBoolForUser(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, - true, UserHandle.USER_CURRENT) + false, UserHandle.USER_CURRENT) hideSilentNotificationsOnLockscreen = !showSilentNotifs } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt index 99d320d1c7ca..073b6b041b81 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt @@ -16,11 +16,11 @@ package com.android.systemui.statusbar.notification.interruption -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.NotificationInterruptLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java index 3292a8fcdb50..6cf4bf318c99 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java @@ -37,6 +37,19 @@ public interface NotificationInterruptStateProvider { boolean shouldHeadsUp(NotificationEntry entry); /** + * Returns the value of whether this entry should peek (from shouldHeadsUp(entry)), but only + * optionally logs the status. + * + * This method should be used in cases where the caller needs to check whether a notification + * qualifies for a heads up, but is not necessarily guaranteed to make the heads-up happen. + * + * @param entry the entry to check + * @param log whether or not to log the results of this check + * @return true if the entry should heads up, false otherwise + */ + boolean checkHeadsUp(NotificationEntry entry, boolean log); + + /** * Whether the notification should appear as a bubble with a fly-out on top of the screen. * * @param entry the entry to check diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index 558fd62c78bf..c4f5a3a30608 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.interruption; import static com.android.systemui.statusbar.StatusBarState.SHADE; +import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD; +import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR; import android.app.NotificationManager; import android.content.ContentResolver; @@ -32,6 +34,8 @@ import android.service.notification.StatusBarNotification; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -68,10 +72,30 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter private final NotificationInterruptLogger mLogger; private final NotifPipelineFlags mFlags; private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider; + private final UiEventLogger mUiEventLogger; @VisibleForTesting protected boolean mUseHeadsUp = false; + public enum NotificationInterruptEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "FSI suppressed for suppressive GroupAlertBehavior") + FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(1235), + + @UiEvent(doc = "FSI suppressed for requiring neither HUN nor keyguard") + FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236); + + private final int mId; + + NotificationInterruptEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } + @Inject public NotificationInterruptStateProviderImpl( ContentResolver contentResolver, @@ -85,7 +109,8 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter NotificationInterruptLogger logger, @Main Handler mainHandler, NotifPipelineFlags flags, - KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) { + KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, + UiEventLogger uiEventLogger) { mContentResolver = contentResolver; mPowerManager = powerManager; mDreamManager = dreamManager; @@ -97,6 +122,7 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter mLogger = logger; mFlags = flags; mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider; + mUiEventLogger = uiEventLogger; ContentObserver headsUpObserver = new ContentObserver(mainHandler) { @Override public void onChange(boolean selfChange) { @@ -137,11 +163,11 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter public boolean shouldBubbleUp(NotificationEntry entry) { final StatusBarNotification sbn = entry.getSbn(); - if (!canAlertCommon(entry)) { + if (!canAlertCommon(entry, true)) { return false; } - if (!canAlertAwakeCommon(entry)) { + if (!canAlertAwakeCommon(entry, true)) { return false; } @@ -163,10 +189,15 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter @Override public boolean shouldHeadsUp(NotificationEntry entry) { + return checkHeadsUp(entry, true); + } + + @Override + public boolean checkHeadsUp(NotificationEntry entry, boolean log) { if (mStatusBarStateController.isDozing()) { - return shouldHeadsUpWhenDozing(entry); + return shouldHeadsUpWhenDozing(entry, log); } else { - return shouldHeadsUpWhenAwake(entry); + return shouldHeadsUpWhenAwake(entry, log); } } @@ -198,7 +229,9 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter // b/231322873: Detect and report an event when a notification has both an FSI and a // suppressive groupAlertBehavior, and now correctly block the FSI from firing. final int uid = entry.getSbn().getUid(); + final String packageName = entry.getSbn().getPackageName(); android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "groupAlertBehavior"); + mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, uid, packageName); mLogger.logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN"); return false; } @@ -244,7 +277,9 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter // Detect the case determined by b/231322873 to launch FSI while device is in use, // as blocked by the correct implementation, and report the event. final int uid = entry.getSbn().getUid(); + final String packageName = entry.getSbn().getPackageName(); android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "no hun or keyguard"); + mUiEventLogger.log(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, uid, packageName); mLogger.logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard"); return false; } @@ -263,61 +298,61 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter } } - private boolean shouldHeadsUpWhenAwake(NotificationEntry entry) { + private boolean shouldHeadsUpWhenAwake(NotificationEntry entry, boolean log) { StatusBarNotification sbn = entry.getSbn(); if (!mUseHeadsUp) { - mLogger.logNoHeadsUpFeatureDisabled(); + if (log) mLogger.logNoHeadsUpFeatureDisabled(); return false; } - if (!canAlertCommon(entry)) { + if (!canAlertCommon(entry, log)) { return false; } - if (!canAlertHeadsUpCommon(entry)) { + if (!canAlertHeadsUpCommon(entry, log)) { return false; } - if (!canAlertAwakeCommon(entry)) { + if (!canAlertAwakeCommon(entry, log)) { return false; } if (isSnoozedPackage(sbn)) { - mLogger.logNoHeadsUpPackageSnoozed(entry); + if (log) mLogger.logNoHeadsUpPackageSnoozed(entry); return false; } boolean inShade = mStatusBarStateController.getState() == SHADE; if (entry.isBubble() && inShade) { - mLogger.logNoHeadsUpAlreadyBubbled(entry); + if (log) mLogger.logNoHeadsUpAlreadyBubbled(entry); return false; } if (entry.shouldSuppressPeek()) { - mLogger.logNoHeadsUpSuppressedByDnd(entry); + if (log) mLogger.logNoHeadsUpSuppressedByDnd(entry); return false; } if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) { - mLogger.logNoHeadsUpNotImportant(entry); + if (log) mLogger.logNoHeadsUpNotImportant(entry); return false; } boolean inUse = mPowerManager.isScreenOn() && !isDreaming(); if (!inUse) { - mLogger.logNoHeadsUpNotInUse(entry); + if (log) mLogger.logNoHeadsUpNotInUse(entry); return false; } for (int i = 0; i < mSuppressors.size(); i++) { if (mSuppressors.get(i).suppressAwakeHeadsUp(entry)) { - mLogger.logNoHeadsUpSuppressedBy(entry, mSuppressors.get(i)); + if (log) mLogger.logNoHeadsUpSuppressedBy(entry, mSuppressors.get(i)); return false; } } - mLogger.logHeadsUp(entry); + if (log) mLogger.logHeadsUp(entry); return true; } @@ -328,37 +363,37 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter * @param entry the entry to check * @return true if the entry should ambient pulse, false otherwise */ - private boolean shouldHeadsUpWhenDozing(NotificationEntry entry) { + private boolean shouldHeadsUpWhenDozing(NotificationEntry entry, boolean log) { if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) { - mLogger.logNoPulsingSettingDisabled(entry); + if (log) mLogger.logNoPulsingSettingDisabled(entry); return false; } if (mBatteryController.isAodPowerSave()) { - mLogger.logNoPulsingBatteryDisabled(entry); + if (log) mLogger.logNoPulsingBatteryDisabled(entry); return false; } - if (!canAlertCommon(entry)) { - mLogger.logNoPulsingNoAlert(entry); + if (!canAlertCommon(entry, log)) { + if (log) mLogger.logNoPulsingNoAlert(entry); return false; } - if (!canAlertHeadsUpCommon(entry)) { - mLogger.logNoPulsingNoAlert(entry); + if (!canAlertHeadsUpCommon(entry, log)) { + if (log) mLogger.logNoPulsingNoAlert(entry); return false; } if (entry.shouldSuppressAmbient()) { - mLogger.logNoPulsingNoAmbientEffect(entry); + if (log) mLogger.logNoPulsingNoAmbientEffect(entry); return false; } if (entry.getImportance() < NotificationManager.IMPORTANCE_DEFAULT) { - mLogger.logNoPulsingNotImportant(entry); + if (log) mLogger.logNoPulsingNotImportant(entry); return false; } - mLogger.logPulsing(entry); + if (log) mLogger.logPulsing(entry); return true; } @@ -366,18 +401,22 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter * Common checks between regular & AOD heads up and bubbles. * * @param entry the entry to check + * @param log whether or not to log the results of these checks * @return true if these checks pass, false if the notification should not alert */ - private boolean canAlertCommon(NotificationEntry entry) { + private boolean canAlertCommon(NotificationEntry entry, boolean log) { for (int i = 0; i < mSuppressors.size(); i++) { if (mSuppressors.get(i).suppressInterruptions(entry)) { - mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), /* awake */ false); + if (log) { + mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), + /* awake */ false); + } return false; } } if (mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry)) { - mLogger.keyguardHideNotification(entry); + if (log) mLogger.keyguardHideNotification(entry); return false; } @@ -388,19 +427,20 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter * Common checks for heads up notifications on regular and AOD displays. * * @param entry the entry to check + * @param log whether or not to log the results of these checks * @return true if these checks pass, false if the notification should not alert */ - private boolean canAlertHeadsUpCommon(NotificationEntry entry) { + private boolean canAlertHeadsUpCommon(NotificationEntry entry, boolean log) { StatusBarNotification sbn = entry.getSbn(); // Don't alert notifications that are suppressed due to group alert behavior if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) { - mLogger.logNoAlertingGroupAlertBehavior(entry); + if (log) mLogger.logNoAlertingGroupAlertBehavior(entry); return false; } if (entry.hasJustLaunchedFullScreenIntent()) { - mLogger.logNoAlertingRecentFullscreen(entry); + if (log) mLogger.logNoAlertingRecentFullscreen(entry); return false; } @@ -413,12 +453,14 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter * @param entry the entry to check * @return true if these checks pass, false if the notification should not alert */ - private boolean canAlertAwakeCommon(NotificationEntry entry) { + private boolean canAlertAwakeCommon(NotificationEntry entry, boolean log) { StatusBarNotification sbn = entry.getSbn(); for (int i = 0; i < mSuppressors.size(); i++) { if (mSuppressors.get(i).suppressAwakeInterruptions(entry)) { - mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), /* awake */ true); + if (log) { + mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), /* awake */ true); + } return false; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt new file mode 100644 index 000000000000..0380fff1e2af --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt @@ -0,0 +1,65 @@ +/* + * + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +/** Describes usage of a notification. */ +data class NotificationMemoryUsage( + val packageName: String, + val notificationKey: String, + val objectUsage: NotificationObjectUsage, + val viewUsage: List<NotificationViewUsage> +) + +/** + * Describes current memory usage of a [android.app.Notification] object. + * + * The values are in bytes. + */ +data class NotificationObjectUsage( + val smallIcon: Int, + val largeIcon: Int, + val extras: Int, + val style: String?, + val styleIcon: Int, + val bigPicture: Int, + val extender: Int, + val hasCustomView: Boolean, +) + +enum class ViewType { + PUBLIC_VIEW, + PRIVATE_CONTRACTED_VIEW, + PRIVATE_EXPANDED_VIEW, + PRIVATE_HEADS_UP_VIEW, + TOTAL +} + +/** + * Describes current memory of a notification view hierarchy. + * + * The values are in bytes. + */ +data class NotificationViewUsage( + val viewType: ViewType, + val smallIcon: Int, + val largeIcon: Int, + val systemIcons: Int, + val style: Int, + val customViews: Int, + val softwareBitmapsPenalty: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt new file mode 100644 index 000000000000..7d39e18ab349 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt @@ -0,0 +1,212 @@ +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.app.Person +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import androidx.annotation.WorkerThread +import com.android.systemui.statusbar.notification.NotificationUtils +import com.android.systemui.statusbar.notification.collection.NotificationEntry + +/** Calculates estimated memory usage of [Notification] and [NotificationEntry] objects. */ +internal object NotificationMemoryMeter { + + private const val CAR_EXTENSIONS = "android.car.EXTENSIONS" + private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon" + private const val TV_EXTENSIONS = "android.tv.EXTENSIONS" + private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS" + private const val WEARABLE_EXTENSIONS_BACKGROUND = "background" + + /** Returns a list of memory use entries for currently shown notifications. */ + @WorkerThread + fun notificationMemoryUse( + notifications: Collection<NotificationEntry>, + ): List<NotificationMemoryUsage> { + return notifications + .asSequence() + .map { entry -> + val packageName = entry.sbn.packageName + val notificationObjectUsage = + notificationMemoryUse(entry.sbn.notification, hashSetOf()) + val notificationViewUsage = NotificationMemoryViewWalker.getViewUsage(entry.row) + NotificationMemoryUsage( + packageName, + NotificationUtils.logKey(entry.sbn.key), + notificationObjectUsage, + notificationViewUsage + ) + } + .toList() + } + + @WorkerThread + fun notificationMemoryUse( + entry: NotificationEntry, + seenBitmaps: HashSet<Int> = hashSetOf(), + ): NotificationMemoryUsage { + return NotificationMemoryUsage( + entry.sbn.packageName, + NotificationUtils.logKey(entry.sbn.key), + notificationMemoryUse(entry.sbn.notification, seenBitmaps), + NotificationMemoryViewWalker.getViewUsage(entry.row) + ) + } + + /** + * Computes the estimated memory usage of a given [Notification] object. It'll attempt to + * inspect Bitmaps in the object and provide summary of memory usage. + */ + @WorkerThread + fun notificationMemoryUse( + notification: Notification, + seenBitmaps: HashSet<Int> = hashSetOf(), + ): NotificationObjectUsage { + val extras = notification.extras + val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps) + val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps) + + // Collect memory usage of extra styles + + // Big Picture + val bigPictureIconUse = + computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps) + val bigPictureUse = + computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) + + computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) + + // People + val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST) + val peopleUse = + peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0 + + // Calling + val callingPersonUse = + computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps) + val verificationIconUse = + computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps) + + // Messages + val messages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_MESSAGES) + ) + val messagesUse = + messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } + val historicMessages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES) + ) + val historyicMessagesUse = + historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } + + // Extenders + val carExtender = extras.getBundle(CAR_EXTENSIONS) + val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0 + val carExtenderIcon = + computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps) + + val tvExtender = extras.getBundle(TV_EXTENSIONS) + val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0 + + val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS) + val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0 + val wearExtenderBackground = + computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps) + + val style = notification.notificationStyle + val hasCustomView = notification.contentView != null || notification.bigContentView != null + val extrasSize = computeBundleSize(extras) + + return NotificationObjectUsage( + smallIcon = smallIconUse, + largeIcon = largeIconUse, + extras = extrasSize, + style = style?.simpleName, + styleIcon = + bigPictureIconUse + + peopleUse + + callingPersonUse + + verificationIconUse + + messagesUse + + historyicMessagesUse, + bigPicture = bigPictureUse, + extender = + carExtenderSize + + carExtenderIcon + + tvExtenderSize + + wearExtenderSize + + wearExtenderBackground, + hasCustomView = hasCustomView + ) + } + + /** + * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem + * bitmaps). Can be slow. + */ + private fun computeBundleSize(extras: Bundle): Int { + val parcel = Parcel.obtain() + try { + extras.writeToParcel(parcel, 0) + return parcel.dataSize() + } finally { + parcel.recycle() + } + } + + /** + * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0 + * if the key does not exist in extras. + */ + private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int { + return when (val parcelable = extras?.getParcelable<Parcelable>(key)) { + is Bitmap -> computeBitmapUse(parcelable, seenBitmaps) + is Icon -> computeIconUse(parcelable, seenBitmaps) + is Person -> computeIconUse(parcelable.icon, seenBitmaps) + else -> 0 + } + } + + /** + * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is + * defined via Uri or a resource. + * + * @return memory usage in bytes or 0 if the icon is Uri/Resource based + */ + private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) = + when (icon?.type) { + Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) + Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) + Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps) + else -> 0 + } + + /** + * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of + * seenBitmaps set, this method returns 0 to avoid double counting. + * + * @return memory usage of the bitmap in bytes + */ + private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int { + val refId = System.identityHashCode(bitmap) + if (seenBitmaps?.contains(refId) == true) { + return 0 + } + + seenBitmaps?.add(refId) + return bitmap.allocationByteCount + } + + private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int { + val refId = System.identityHashCode(icon.dataBytes) + if (seenBitmaps.contains(refId)) { + return 0 + } + + seenBitmaps.add(refId) + return icon.dataLength + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt new file mode 100644 index 000000000000..c09cc4306ced --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt @@ -0,0 +1,166 @@ +/* + * + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +import android.util.Log +import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import java.io.PrintWriter +import javax.inject.Inject + +/** This class monitors and logs current Notification memory use. */ +@SysUISingleton +class NotificationMemoryMonitor +@Inject +constructor( + val notificationPipeline: NotifPipeline, + val dumpManager: DumpManager, +) : Dumpable { + + companion object { + private const val TAG = "NotificationMemory" + } + + fun init() { + Log.d(TAG, "NotificationMemoryMonitor initialized.") + dumpManager.registerDumpable(javaClass.simpleName, this) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(notificationPipeline.allNotifs) + .sortedWith(compareBy({ it.packageName }, { it.notificationKey })) + dumpNotificationObjects(pw, memoryUse) + dumpNotificationViewUsage(pw, memoryUse) + } + + /** Renders a table of notification object usage into passed [PrintWriter]. */ + private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List<NotificationMemoryUsage>) { + pw.println("Notification Object Usage") + pw.println("-----------") + pw.println( + "Package".padEnd(35) + + "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom" + ) + pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView") + pw.println() + + memoryUse.forEach { use -> + pw.println( + use.packageName.padEnd(35) + + "\t\t" + + "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" + + (use.objectUsage.style?.take(15) ?: "").padEnd(15) + + "\t\t${use.objectUsage.styleIcon}\t" + + "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" + + "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" + + use.notificationKey + ) + } + + // Calculate totals for easily glanceable summary. + data class Totals( + var smallIcon: Int = 0, + var largeIcon: Int = 0, + var styleIcon: Int = 0, + var bigPicture: Int = 0, + var extender: Int = 0, + var extras: Int = 0, + ) + + val totals = + memoryUse.fold(Totals()) { t, usage -> + t.smallIcon += usage.objectUsage.smallIcon + t.largeIcon += usage.objectUsage.largeIcon + t.styleIcon += usage.objectUsage.styleIcon + t.bigPicture += usage.objectUsage.bigPicture + t.extender += usage.objectUsage.extender + t.extras += usage.objectUsage.extras + t + } + + pw.println() + pw.println("TOTALS") + pw.println( + "".padEnd(35) + + "\t\t" + + "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" + + "".padEnd(15) + + "\t\t${toKb(totals.styleIcon)}\t" + + "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" + + toKb(totals.extras) + ) + pw.println() + } + + /** Renders a table of notification view usage into passed [PrintWriter] */ + private fun dumpNotificationViewUsage( + pw: PrintWriter, + memoryUse: List<NotificationMemoryUsage>, + ) { + + data class Totals( + var smallIcon: Int = 0, + var largeIcon: Int = 0, + var style: Int = 0, + var customViews: Int = 0, + var softwareBitmapsPenalty: Int = 0, + ) + + val totals = Totals() + pw.println("Notification View Usage") + pw.println("-----------") + pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware") + pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps") + pw.println() + memoryUse + .filter { it.viewUsage.isNotEmpty() } + .forEach { use -> + pw.println(use.packageName + " " + use.notificationKey) + use.viewUsage.forEach { view -> + pw.println( + " ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" + + "\t${view.largeIcon}\t${view.style}" + + "\t${view.customViews}\t${view.softwareBitmapsPenalty}" + ) + + if (view.viewType == ViewType.TOTAL) { + totals.smallIcon += view.smallIcon + totals.largeIcon += view.largeIcon + totals.style += view.style + totals.customViews += view.customViews + totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty + } + } + } + pw.println() + pw.println("TOTALS") + pw.println( + " ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" + + "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" + + "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}" + ) + pw.println() + } + + private fun toKb(bytes: Int): String { + return (bytes / 1024).toString() + " KB" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt new file mode 100644 index 000000000000..a0bee1502f51 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt @@ -0,0 +1,173 @@ +package com.android.systemui.statusbar.notification.logging + +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import com.android.internal.R +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.util.children + +/** Walks view hiearchy of a given notification to estimate its memory use. */ +internal object NotificationMemoryViewWalker { + + private const val TAG = "NotificationMemory" + + /** Builder for [NotificationViewUsage] objects. */ + private class UsageBuilder { + private var smallIcon: Int = 0 + private var largeIcon: Int = 0 + private var systemIcons: Int = 0 + private var style: Int = 0 + private var customViews: Int = 0 + private var softwareBitmaps = 0 + + fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse } + fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse } + fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse } + fun addStyle(styleUse: Int) = apply { style += styleUse } + fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply { + softwareBitmaps += softwareBitmapUse + } + + fun addCustomViews(customViewsUse: Int) = apply { customViews += customViewsUse } + + fun build(viewType: ViewType): NotificationViewUsage { + return NotificationViewUsage( + viewType = viewType, + smallIcon = smallIcon, + largeIcon = largeIcon, + systemIcons = systemIcons, + style = style, + customViews = customViews, + softwareBitmapsPenalty = softwareBitmaps, + ) + } + } + + /** + * Returns memory usage of public and private views contained in passed + * [ExpandableNotificationRow] + */ + fun getViewUsage(row: ExpandableNotificationRow?): List<NotificationViewUsage> { + if (row == null) { + return listOf() + } + + // The ordering here is significant since it determines deduplication of seen drawables. + return listOf( + getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild), + getViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, row.privateLayout?.contractedChild), + getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild), + getViewUsage(ViewType.PUBLIC_VIEW, row.publicLayout), + getTotalUsage(row) + ) + } + + /** + * Calculate total usage of all views - we need to do a separate traversal to make sure we don't + * double count fields. + */ + private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage { + val totalUsage = UsageBuilder() + val seenObjects = hashSetOf<Int>() + + row.publicLayout?.let { computeViewHierarchyUse(it, totalUsage, seenObjects) } + row.privateLayout?.let { child -> + for (view in listOf(child.expandedChild, child.contractedChild, child.headsUpChild)) { + (view as? ViewGroup)?.let { v -> + computeViewHierarchyUse(v, totalUsage, seenObjects) + } + } + } + return totalUsage.build(ViewType.TOTAL) + } + + private fun getViewUsage( + type: ViewType, + rootView: View?, + seenObjects: HashSet<Int> = hashSetOf() + ): NotificationViewUsage { + val usageBuilder = UsageBuilder() + (rootView as? ViewGroup)?.let { computeViewHierarchyUse(it, usageBuilder, seenObjects) } + return usageBuilder.build(type) + } + + private fun computeViewHierarchyUse( + rootView: ViewGroup, + builder: UsageBuilder, + seenObjects: HashSet<Int> = hashSetOf(), + ) { + for (child in rootView.children) { + if (child is ViewGroup) { + computeViewHierarchyUse(child, builder, seenObjects) + } else { + computeViewUse(child, builder, seenObjects) + } + } + } + + private fun computeViewUse(view: View, builder: UsageBuilder, seenObjects: HashSet<Int>) { + if (view !is ImageView) return + val drawable = view.drawable ?: return + val drawableRef = System.identityHashCode(drawable) + if (seenObjects.contains(drawableRef)) return + val drawableUse = computeDrawableUse(drawable, seenObjects) + // TODO(b/235451049): We need to make sure we traverse large icon before small icon - + // sometimes the large icons are assigned to small icon views and we want to + // attribute them to large view in those cases. + when (view.id) { + R.id.left_icon, + R.id.icon, + R.id.conversation_icon -> builder.addSmallIcon(drawableUse) + R.id.right_icon -> builder.addLargeIcon(drawableUse) + R.id.big_picture -> builder.addStyle(drawableUse) + // Elements that are part of platform with resources + R.id.phishing_alert, + R.id.feedback, + R.id.alerted_icon, + R.id.expand_button_icon, + R.id.remote_input_send -> builder.addSystem(drawableUse) + // Custom view ImageViews + else -> { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Custom view: ${identifierForView(view)}") + } + builder.addCustomViews(drawableUse) + } + } + + if (isDrawableSoftwareBitmap(drawable)) { + builder.addSoftwareBitmapPenalty(drawableUse) + } + + seenObjects.add(drawableRef) + } + + private fun computeDrawableUse(drawable: Drawable, seenObjects: HashSet<Int>): Int = + when (drawable) { + is BitmapDrawable -> { + val ref = System.identityHashCode(drawable.bitmap) + if (seenObjects.contains(ref)) { + 0 + } else { + seenObjects.add(ref) + drawable.bitmap.allocationByteCount + } + } + else -> 0 + } + + private fun isDrawableSoftwareBitmap(drawable: Drawable) = + drawable is BitmapDrawable && drawable.bitmap.config != Bitmap.Config.HARDWARE + + private fun identifierForView(view: View) = + if (view.id == View.NO_ID) { + "no-id" + } else { + view.resources.getResourceName(view.id) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java index 9faef1b43bc1..5ca13c95309f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java @@ -45,11 +45,21 @@ public interface NotificationPanelLogger { void logPanelShown(boolean isLockscreen, @Nullable List<NotificationEntry> visibleNotifications); + /** + * Log a NOTIFICATION_PANEL_REPORTED statsd event, with + * {@link NotificationPanelEvent#NOTIFICATION_DRAG} as the eventID. + * + * @param draggedNotification the notification that is being dragged + */ + void logNotificationDrag(NotificationEntry draggedNotification); + enum NotificationPanelEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "Notification panel shown from status bar.") NOTIFICATION_PANEL_OPEN_STATUS_BAR(200), @UiEvent(doc = "Notification panel shown from lockscreen.") - NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201); + NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201), + @UiEvent(doc = "Notification was dragged") + NOTIFICATION_DRAG(1226); private final int mId; NotificationPanelEvent(int id) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java index 75a60194f2fa..9a632282ae16 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java @@ -16,12 +16,15 @@ package com.android.systemui.statusbar.notification.logging; +import static com.android.systemui.statusbar.notification.logging.NotificationPanelLogger.NotificationPanelEvent.NOTIFICATION_DRAG; + import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.nano.Notifications; import com.google.protobuf.nano.MessageNano; +import java.util.Collections; import java.util.List; /** @@ -38,4 +41,14 @@ public class NotificationPanelLoggerImpl implements NotificationPanelLogger { /* int num_notifications*/ proto.notifications.length, /* byte[] notifications*/ MessageNano.toByteArray(proto)); } + + @Override + public void logNotificationDrag(NotificationEntry draggedNotification) { + final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto( + Collections.singletonList(draggedNotification)); + SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED, + /* int event_id */ NOTIFICATION_DRAG.getId(), + /* int num_notifications*/ proto.notifications.length, + /* byte[] notifications*/ MessageNano.toByteArray(proto)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt index fe03b2ad6a32..10197a38527e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.logging -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationRenderLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.stack.NotificationSection diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 755e3e1a208e..d29298a2f637 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -613,22 +613,21 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView protected void resetAllContentAlphas() {} @Override - protected void applyRoundness() { + public void applyRoundness() { super.applyRoundness(); - applyBackgroundRoundness(getCurrentBackgroundRadiusTop(), - getCurrentBackgroundRadiusBottom()); + applyBackgroundRoundness(getTopCornerRadius(), getBottomCornerRadius()); } @Override - public float getCurrentBackgroundRadiusTop() { + public float getTopCornerRadius() { float fraction = getInterpolatedAppearAnimationFraction(); - return MathUtils.lerp(0, super.getCurrentBackgroundRadiusTop(), fraction); + return MathUtils.lerp(0, super.getTopCornerRadius(), fraction); } @Override - public float getCurrentBackgroundRadiusBottom() { + public float getBottomCornerRadius() { float fraction = getInterpolatedAppearAnimationFraction(); - return MathUtils.lerp(0, super.getCurrentBackgroundRadiusBottom(), fraction); + return MathUtils.lerp(0, super.getBottomCornerRadius(), fraction); } private void applyBackgroundRoundness(float topRadius, float bottomRadius) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java index 7c41800d880d..d626c18e46f5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java @@ -21,6 +21,7 @@ import android.util.ArrayMap; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -64,7 +65,7 @@ public abstract class BindStage<Params> extends BindRequester { * Get the stage parameters for the entry. Clients should use this to modify how the stage * handles the notification content. */ - public final Params getStageParams(@NonNull NotificationEntry entry) { + public final @NonNull Params getStageParams(@NonNull NotificationEntry entry) { Params params = mContentParams.get(entry); if (params == null) { // TODO: This should throw an exception but there are some cases of re-entrant calls @@ -79,6 +80,17 @@ public abstract class BindStage<Params> extends BindRequester { return params; } + // TODO(b/253081345): Remove this method. + /** + * Get the stage parameters for the entry, or null if there are no stage parameters for the + * entry. + * + * @see #getStageParams(NotificationEntry) + */ + public final @Nullable Params tryGetStageParams(@NonNull NotificationEntry entry) { + return mContentParams.get(entry); + } + /** * Create a params entry for the notification for this stage. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 6138265c641d..9e7717caf69c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -93,6 +93,7 @@ import com.android.systemui.statusbar.notification.LaunchAnimationParameters; import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; @@ -154,7 +155,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView void onLayout(); } - /** Listens for changes to the expansion state of this row. */ + /** + * Listens for changes to the expansion state of this row. + */ public interface OnExpansionChangedListener { void onExpansionChanged(boolean isExpanded); } @@ -183,22 +186,34 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private int mNotificationLaunchHeight; private boolean mMustStayOnScreen; - /** Does this row contain layouts that can adapt to row expansion */ + /** + * Does this row contain layouts that can adapt to row expansion + */ private boolean mExpandable; - /** Has the user actively changed the expansion state of this row */ + /** + * Has the user actively changed the expansion state of this row + */ private boolean mHasUserChangedExpansion; - /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */ + /** + * If {@link #mHasUserChangedExpansion}, has the user expanded this row + */ private boolean mUserExpanded; - /** Whether the blocking helper is showing on this notification (even if dismissed) */ + /** + * Whether the blocking helper is showing on this notification (even if dismissed) + */ private boolean mIsBlockingHelperShowing; /** * Has this notification been expanded while it was pinned */ private boolean mExpandedWhenPinned; - /** Is the user touching this row */ + /** + * Is the user touching this row + */ private boolean mUserLocked; - /** Are we showing the "public" version */ + /** + * Are we showing the "public" version + */ private boolean mShowingPublic; private boolean mSensitive; private boolean mSensitiveHiddenInGeneral; @@ -351,11 +366,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mWasChildInGroupWhenRemoved; private NotificationInlineImageResolver mImageResolver; private NotificationMediaManager mMediaManager; - @Nullable private OnExpansionChangedListener mExpansionChangedListener; - @Nullable private Runnable mOnIntrinsicHeightReachedRunnable; + @Nullable + private OnExpansionChangedListener mExpansionChangedListener; + @Nullable + private Runnable mOnIntrinsicHeightReachedRunnable; private float mTopRoundnessDuringLaunchAnimation; private float mBottomRoundnessDuringLaunchAnimation; + private boolean mIsNotificationGroupCornerEnabled; /** * Returns whether the given {@code statusBarNotification} is a system notification. @@ -574,14 +592,18 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - /** Called when the notification's ranking was changed (but nothing else changed). */ + /** + * Called when the notification's ranking was changed (but nothing else changed). + */ public void onNotificationRankingUpdated() { if (mMenuRow != null) { mMenuRow.onNotificationUpdated(mEntry.getSbn()); } } - /** Call when bubble state has changed and the button on the notification should be updated. */ + /** + * Call when bubble state has changed and the button on the notification should be updated. + */ public void updateBubbleButton() { for (NotificationContentView l : mLayouts) { l.updateBubbleButton(mEntry); @@ -620,6 +642,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Sets a supplier that can determine whether the keyguard is secure or not. + * * @param secureStateProvider A function that returns true if keyguard is secure. */ public void setSecureStateProvider(BooleanSupplier secureStateProvider) { @@ -781,7 +804,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.setUntruncatedChildCount(childCount); } - /** Called after children have been attached to set the expansion states */ + /** + * Called after children have been attached to set the expansion states + */ public void resetChildSystemExpandedStates() { if (isSummaryWithChildren()) { mChildrenContainer.updateExpansionStates(); @@ -791,7 +816,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Add a child notification to this view. * - * @param row the row to add + * @param row the row to add * @param childIndex the index to add it at, if -1 it will be added at the end */ public void addChildNotification(ExpandableNotificationRow row, int childIndex) { @@ -809,10 +834,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } onAttachedChildrenCountChanged(); row.setIsChildInGroup(false, null); - row.setBottomRoundness(0.0f, false /* animate */); + row.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue); } - /** Returns the child notification at [index], or null if no such child. */ + /** + * Returns the child notification at [index], or null if no such child. + */ @Nullable public ExpandableNotificationRow getChildNotificationAt(int index) { if (mChildrenContainer == null @@ -834,7 +861,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * @param isChildInGroup Is this notification now in a group - * @param parent the new parent notification + * @param parent the new parent notification */ public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) { if (mExpandAnimationRunning && !isChildInGroup && mNotificationParent != null) { @@ -898,7 +925,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mChildrenContainer == null ? null : mChildrenContainer.getAttachedChildren(); } - /** Updates states of all children. */ + /** + * Updates states of all children. + */ public void updateChildrenStates(AmbientState ambientState) { if (mIsSummaryWithChildren) { ExpandableViewState parentState = getViewState(); @@ -906,21 +935,27 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - /** Applies children states. */ + /** + * Applies children states. + */ public void applyChildrenState() { if (mIsSummaryWithChildren) { mChildrenContainer.applyState(); } } - /** Prepares expansion changed. */ + /** + * Prepares expansion changed. + */ public void prepareExpansionChanged() { if (mIsSummaryWithChildren) { mChildrenContainer.prepareExpansionChanged(); } } - /** Starts child animations. */ + /** + * Starts child animations. + */ public void startChildAnimation(AnimationProperties properties) { if (mIsSummaryWithChildren) { mChildrenContainer.startAnimationToState(properties); @@ -984,7 +1019,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren) { return mChildrenContainer.getIntrinsicHeight(); } - if(mExpandedWhenPinned) { + if (mExpandedWhenPinned) { return Math.max(getMaxExpandHeight(), getHeadsUpHeight()); } else if (atLeastMinHeight) { return Math.max(getCollapsedHeight(), getHeadsUpHeight()); @@ -1079,18 +1114,22 @@ public class ExpandableNotificationRow extends ActivatableNotificationView updateClickAndFocus(); } - /** The click listener for the bubble button. */ + /** + * The click listener for the bubble button. + */ public View.OnClickListener getBubbleClickListener() { return v -> { if (mBubblesManagerOptional.isPresent()) { mBubblesManagerOptional.get() - .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */); + .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */); } mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */); }; } - /** The click listener for the snooze button. */ + /** + * The click listener for the snooze button. + */ public View.OnClickListener getSnoozeClickListener(MenuItem item) { return v -> { // Dismiss a snoozed notification if one is still left behind @@ -1252,7 +1291,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void setContentBackground(int customBackgroundColor, boolean animate, - NotificationContentView notificationContentView) { + NotificationContentView notificationContentView) { if (getShowingLayout() == notificationContentView) { setTintColor(customBackgroundColor, animate); } @@ -1477,6 +1516,20 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } + /** + * Sets the alpha on the content, while leaving the background of the row itself as is. + * + * @param alpha alpha value to apply to the notification content + */ + public void setContentAlpha(float alpha) { + for (NotificationContentView l : mLayouts) { + l.setAlpha(alpha); + } + if (mChildrenContainer != null) { + mChildrenContainer.setAlpha(alpha); + } + } + public void setIsLowPriority(boolean isLowPriority) { mIsLowPriority = isLowPriority; mPrivateLayout.setIsLowPriority(isLowPriority); @@ -1623,7 +1676,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView setTargetPoint(null); } - /** Shows the given feedback icon, or hides the icon if null. */ + /** + * Shows the given feedback icon, or hides the icon if null. + */ public void setFeedbackIcon(@Nullable FeedbackIcon icon) { if (mIsSummaryWithChildren) { mChildrenContainer.setFeedbackIcon(icon); @@ -1632,7 +1687,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mPublicLayout.setFeedbackIcon(icon); } - /** Sets the last time the notification being displayed audibly alerted the user. */ + /** + * Sets the last time the notification being displayed audibly alerted the user. + */ public void setLastAudiblyAlertedMs(long lastAudiblyAlertedMs) { long timeSinceAlertedAudibly = System.currentTimeMillis() - lastAudiblyAlertedMs; boolean alertedRecently = timeSinceAlertedAudibly < RECENTLY_ALERTED_THRESHOLD_MS; @@ -1686,7 +1743,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView Trace.endSection(); } - /** Generates and appends "(MessagingStyle)" type tag to passed string for tracing. */ + /** + * Generates and appends "(MessagingStyle)" type tag to passed string for tracing. + */ @NonNull private String appendTraceStyleTag(@NonNull String traceTag) { if (!Trace.isEnabled()) { @@ -1707,7 +1766,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView super.onFinishInflate(); mPublicLayout = findViewById(R.id.expandedPublic); mPrivateLayout = findViewById(R.id.expanded); - mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout}; + mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout}; for (NotificationContentView l : mLayouts) { l.setExpandClickListener(mExpandClickListener); @@ -1726,6 +1785,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.setIsLowPriority(mIsLowPriority); mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this); mChildrenContainer.onNotificationUpdated(); + mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled); mTranslateableViews.add(mChildrenContainer); }); @@ -1782,6 +1842,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Perform a smart action which triggers a longpress (expose guts). * Based on the semanticAction passed, may update the state of the guts view. + * * @param semanticAction associated with this smart action click */ public void doSmartActionClick(int x, int y, int semanticAction) { @@ -1925,9 +1986,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Set the dismiss behavior of the view. + * * @param usingRowTranslationX {@code true} if the view should translate using regular - * translationX, otherwise the contents will be - * translated. + * translationX, otherwise the contents will be + * translated. */ @Override public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) { @@ -1941,6 +2003,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (previousTranslation != 0) { setTranslation(previousTranslation); } + if (mChildrenContainer != null) { + List<ExpandableNotificationRow> notificationChildren = + mChildrenContainer.getAttachedChildren(); + for (int i = 0; i < notificationChildren.size(); i++) { + ExpandableNotificationRow child = notificationChildren.get(i); + child.setDismissUsingRowTranslationX(usingRowTranslationX); + } + } } } @@ -1995,7 +2065,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public Animator getTranslateViewAnimator(final float leftTarget, - AnimatorUpdateListener listener) { + AnimatorUpdateListener listener) { if (mTranslateAnim != null) { mTranslateAnim.cancel(); } @@ -2101,7 +2171,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING)); float startTop = params.getStartNotificationTop(); top = (int) Math.min(MathUtils.lerp(startTop, - params.getTop(), expandProgress), + params.getTop(), expandProgress), startTop); } else { top = params.getTop(); @@ -2137,29 +2207,30 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } setTranslationY(top); - mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / mOutlineRadius; - mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / mOutlineRadius; + final float maxRadius = getMaxRadius(); + mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / maxRadius; + mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / maxRadius; invalidateOutline(); mBackgroundNormal.setExpandAnimationSize(params.getWidth(), actualHeight); } @Override - public float getCurrentTopRoundness() { + public float getTopRoundness() { if (mExpandAnimationRunning) { return mTopRoundnessDuringLaunchAnimation; } - return super.getCurrentTopRoundness(); + return super.getTopRoundness(); } @Override - public float getCurrentBottomRoundness() { + public float getBottomRoundness() { if (mExpandAnimationRunning) { return mBottomRoundnessDuringLaunchAnimation; } - return super.getCurrentBottomRoundness(); + return super.getBottomRoundness(); } public void setExpandAnimationRunning(boolean expandAnimationRunning) { @@ -2270,7 +2341,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Set this notification to be expanded by the user * - * @param userExpanded whether the user wants this notification to be expanded + * @param userExpanded whether the user wants this notification to be expanded * @param allowChildExpansion whether a call to this method allows expanding children */ public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) { @@ -2420,7 +2491,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * @return {@code true} if the notification can show it's heads up layout. This is mostly true - * except for legacy use cases. + * except for legacy use cases. */ public boolean canShowHeadsUp() { if (mOnKeyguard && !isDozing() && !isBypassEnabled()) { @@ -2611,7 +2682,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, - long duration) { + long duration) { if (getVisibility() == GONE) { // If we are GONE, the hideSensitive parameter will not be calculated and always be // false, which is incorrect, let's wait until a real call comes in later. @@ -2644,9 +2715,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void animateShowingPublic(long delay, long duration, boolean showingPublic) { View[] privateViews = mIsSummaryWithChildren - ? new View[] {mChildrenContainer} - : new View[] {mPrivateLayout}; - View[] publicViews = new View[] {mPublicLayout}; + ? new View[]{mChildrenContainer} + : new View[]{mPrivateLayout}; + View[] publicViews = new View[]{mPublicLayout}; View[] hiddenChildren = showingPublic ? privateViews : publicViews; View[] shownChildren = showingPublic ? publicViews : privateViews; for (final View hiddenView : hiddenChildren) { @@ -2679,8 +2750,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * @return Whether this view is allowed to be dismissed. Only valid for visible notifications as - * otherwise some state might not be updated. To request about the general clearability - * see {@link NotificationEntry#isDismissable()}. + * otherwise some state might not be updated. To request about the general clearability + * see {@link NotificationEntry#isDismissable()}. */ public boolean canViewBeDismissed() { return mEntry.isDismissable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); @@ -2763,8 +2834,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } @Override - public long performRemoveAnimation(long duration, long delay, float translationDirection, - boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable, + public long performRemoveAnimation( + long duration, + long delay, + float translationDirection, + boolean isHeadsUpAnimation, + float endLocation, + Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener) { if (mMenuRow != null && mMenuRow.isMenuVisible()) { Animator anim = getTranslateViewAnimator(0f, null /* listener */); @@ -2814,7 +2890,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - /** Gets the last value set with {@link #setNotificationFaded(boolean)} */ + /** + * Gets the last value set with {@link #setNotificationFaded(boolean)} + */ @Override public boolean isNotificationFaded() { return mIsFaded; @@ -2829,7 +2907,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * notifications return false from {@link #hasOverlappingRendering()} and delegate the * layerType to child views which really need it in order to render correctly, such as icon * views or the conversation face pile. - * + * <p> * Another compounding factor for notifications is that we change clipping on each frame of the * animation, so the hardware layer isn't able to do any caching at the top level, but the * individual elements we render with hardware layers (e.g. icons) cache wonderfully because we @@ -2855,7 +2933,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - /** Private helper for iterating over the layouts and children containers to set faded state */ + /** + * Private helper for iterating over the layouts and children containers to set faded state + */ private void setNotificationFadedOnChildren(boolean faded) { delegateNotificationFaded(mChildrenContainer, faded); for (NotificationContentView layout : mLayouts) { @@ -2883,7 +2963,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * Because RemoteInputView is designed to be an opaque view that overlaps the Actions row, the * row should require overlapping rendering to ensure that the overlapped view doesn't bleed * through when alpha fading. - * + * <p> * Note that this currently works for top-level notifications which squish their height down * while collapsing the shade, but does not work for children inside groups, because the * accordion affect does not apply to those views, so super.hasOverlappingRendering() will @@ -2962,7 +3042,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mGuts.getIntrinsicHeight(); } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp && mHeadsUpManager.isTrackingHeadsUp()) { - return getPinnedHeadsUpHeight(false /* atLeastMinHeight */); + return getPinnedHeadsUpHeight(false /* atLeastMinHeight */); } else if (mIsSummaryWithChildren && !isGroupExpanded() && !shouldShowPublic()) { return mChildrenContainer.getMinHeight(); } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp) { @@ -3204,8 +3284,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView MenuItem snoozeMenu = provider.getSnoozeMenuItem(getContext()); if (snoozeMenu != null) { AccessibilityAction action = new AccessibilityAction(R.id.action_snooze, - getContext().getResources() - .getString(R.string.notification_menu_snooze_action)); + getContext().getResources() + .getString(R.string.notification_menu_snooze_action)); info.addAction(action); } } @@ -3266,17 +3346,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView NotificationContentView contentView = (NotificationContentView) child; if (isClippingNeeded()) { return true; - } else if (!hasNoRounding() - && contentView.shouldClipToRounding(getCurrentTopRoundness() != 0.0f, - getCurrentBottomRoundness() != 0.0f)) { + } else if (hasRoundedCorner() + && contentView.shouldClipToRounding(getTopRoundness() != 0.0f, + getBottomRoundness() != 0.0f)) { return true; } } else if (child == mChildrenContainer) { - if (isClippingNeeded() || !hasNoRounding()) { + if (isClippingNeeded() || hasRoundedCorner()) { return true; } } else if (child instanceof NotificationGuts) { - return !hasNoRounding(); + return hasRoundedCorner(); } return super.childNeedsClipping(child); } @@ -3302,14 +3382,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } @Override - protected void applyRoundness() { + public void applyRoundness() { super.applyRoundness(); applyChildrenRoundness(); } private void applyChildrenRoundness() { if (mIsSummaryWithChildren) { - mChildrenContainer.setCurrentBottomRoundness(getCurrentBottomRoundness()); + mChildrenContainer.requestBottomRoundness( + getBottomRoundness(), + /* animate = */ false, + SourceType.DefaultValue); } } @@ -3321,10 +3404,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return super.getCustomClipPath(child); } - private boolean hasNoRounding() { - return getCurrentBottomRoundness() == 0.0f && getCurrentTopRoundness() == 0.0f; - } - public boolean isMediaRow() { return mEntry.getSbn().getNotification().isMediaNotification(); } @@ -3362,7 +3441,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void handleFixedTranslationZ(ExpandableNotificationRow row) { if (row.hasExpandingChild()) { - zTranslation = row.getTranslationZ(); + setZTranslation(row.getTranslationZ()); clipTopAmount = row.getClipTopAmount(); } } @@ -3420,6 +3499,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public interface LongPressListener { /** * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates + * * @return whether the longpress was handled */ boolean onLongPress(View v, int x, int y, MenuItem item); @@ -3441,6 +3521,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public interface CoordinateOnClickListener { /** * Equivalent to {@link View.OnClickListener#onClick(View)} with coordinates + * * @return whether the click was handled */ boolean onClick(View v, int x, int y, MenuItem item); @@ -3497,7 +3578,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void setTargetPoint(Point p) { mTargetPoint = p; } + public Point getTargetPoint() { return mTargetPoint; } + + /** + * Enable the support for rounded corner in notification group + * @param enabled true if is supported + */ + public void enableNotificationGroupCorner(boolean enabled) { + mIsNotificationGroupCornerEnabled = enabled; + if (mChildrenContainer != null) { + mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index a493a676e3d8..842526ee0371 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -231,6 +231,8 @@ public class ExpandableNotificationRowController implements NotifViewController mStatusBarStateController.removeCallback(mStatusBarStateListener); } }); + mView.enableNotificationGroupCorner( + mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER)); } private final StatusBarStateController.StateListener mStatusBarStateListener = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java index 4939a9c22cf8..64f87cabaf74 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java @@ -45,12 +45,17 @@ import android.widget.Toast; import androidx.annotation.VisibleForTesting; +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.InstanceIdSequence; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger; import com.android.systemui.statusbar.policy.HeadsUpManager; +import java.util.Collections; + import javax.inject.Inject; /** @@ -63,14 +68,17 @@ public class ExpandableNotificationRowDragController { private final Context mContext; private final HeadsUpManager mHeadsUpManager; private final ShadeController mShadeController; + private NotificationPanelLogger mNotificationPanelLogger; @Inject public ExpandableNotificationRowDragController(Context context, HeadsUpManager headsUpManager, - ShadeController shadeController) { + ShadeController shadeController, + NotificationPanelLogger notificationPanelLogger) { mContext = context; mHeadsUpManager = headsUpManager; mShadeController = shadeController; + mNotificationPanelLogger = notificationPanelLogger; init(); } @@ -120,12 +128,16 @@ public class ExpandableNotificationRowDragController { dragIntent.putExtra(ClipDescription.EXTRA_PENDING_INTENT, contentIntent); dragIntent.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle()); ClipData.Item item = new ClipData.Item(dragIntent); + InstanceId instanceId = new InstanceIdSequence(Integer.MAX_VALUE).newInstanceId(); + item.getIntent().putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, instanceId); ClipData dragData = new ClipData(clipDescription, item); View.DragShadowBuilder myShadow = new View.DragShadowBuilder(snapshot); view.setOnDragListener(getDraggedViewDragListener()); boolean result = view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION); if (result) { + // Log notification drag only if it succeeds + mNotificationPanelLogger.logNotificationDrag(enr.getEntry()); view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); if (enr.isPinned()) { mHeadsUpManager.releaseAllImmediately(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java index d58fe3b3c4a3..4fde5d06f816 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java @@ -28,46 +28,21 @@ import android.view.View; import android.view.ViewOutlineProvider; import com.android.systemui.R; -import com.android.systemui.statusbar.notification.AnimatableProperty; -import com.android.systemui.statusbar.notification.PropertyAnimator; -import com.android.systemui.statusbar.notification.stack.AnimationProperties; -import com.android.systemui.statusbar.notification.stack.StackStateAnimator; +import com.android.systemui.statusbar.notification.RoundableState; +import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer; /** * Like {@link ExpandableView}, but setting an outline for the height and clipping. */ public abstract class ExpandableOutlineView extends ExpandableView { - private static final AnimatableProperty TOP_ROUNDNESS = AnimatableProperty.from( - "topRoundness", - ExpandableOutlineView::setTopRoundnessInternal, - ExpandableOutlineView::getCurrentTopRoundness, - R.id.top_roundess_animator_tag, - R.id.top_roundess_animator_end_tag, - R.id.top_roundess_animator_start_tag); - private static final AnimatableProperty BOTTOM_ROUNDNESS = AnimatableProperty.from( - "bottomRoundness", - ExpandableOutlineView::setBottomRoundnessInternal, - ExpandableOutlineView::getCurrentBottomRoundness, - R.id.bottom_roundess_animator_tag, - R.id.bottom_roundess_animator_end_tag, - R.id.bottom_roundess_animator_start_tag); - private static final AnimationProperties ROUNDNESS_PROPERTIES = - new AnimationProperties().setDuration( - StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS); + private RoundableState mRoundableState; private static final Path EMPTY_PATH = new Path(); - private final Rect mOutlineRect = new Rect(); - private final Path mClipPath = new Path(); private boolean mCustomOutline; private float mOutlineAlpha = -1f; - protected float mOutlineRadius; private boolean mAlwaysRoundBothCorners; private Path mTmpPath = new Path(); - private float mCurrentBottomRoundness; - private float mCurrentTopRoundness; - private float mBottomRoundness; - private float mTopRoundness; private int mBackgroundTop; /** @@ -80,8 +55,7 @@ public abstract class ExpandableOutlineView extends ExpandableView { private final ViewOutlineProvider mProvider = new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { - if (!mCustomOutline && getCurrentTopRoundness() == 0.0f - && getCurrentBottomRoundness() == 0.0f && !mAlwaysRoundBothCorners) { + if (!mCustomOutline && !hasRoundedCorner() && !mAlwaysRoundBothCorners) { // Only when translating just the contents, does the outline need to be shifted. int translation = !mDismissUsingRowTranslationX ? (int) getTranslation() : 0; int left = Math.max(translation, 0); @@ -99,14 +73,18 @@ public abstract class ExpandableOutlineView extends ExpandableView { } }; + @Override + public RoundableState getRoundableState() { + return mRoundableState; + } + protected Path getClipPath(boolean ignoreTranslation) { int left; int top; int right; int bottom; int height; - float topRoundness = mAlwaysRoundBothCorners - ? mOutlineRadius : getCurrentBackgroundRadiusTop(); + float topRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getTopCornerRadius(); if (!mCustomOutline) { // The outline just needs to be shifted if we're translating the contents. Otherwise // it's already in the right place. @@ -130,12 +108,11 @@ public abstract class ExpandableOutlineView extends ExpandableView { if (height == 0) { return EMPTY_PATH; } - float bottomRoundness = mAlwaysRoundBothCorners - ? mOutlineRadius : getCurrentBackgroundRadiusBottom(); + float bottomRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius(); if (topRoundness + bottomRoundness > height) { float overShoot = topRoundness + bottomRoundness - height; - float currentTopRoundness = getCurrentTopRoundness(); - float currentBottomRoundness = getCurrentBottomRoundness(); + float currentTopRoundness = getTopRoundness(); + float currentBottomRoundness = getBottomRoundness(); topRoundness -= overShoot * currentTopRoundness / (currentTopRoundness + currentBottomRoundness); bottomRoundness -= overShoot * currentBottomRoundness @@ -145,8 +122,18 @@ public abstract class ExpandableOutlineView extends ExpandableView { return mTmpPath; } - public void getRoundedRectPath(int left, int top, int right, int bottom, - float topRoundness, float bottomRoundness, Path outPath) { + /** + * Add a round rect in {@code outPath} + * @param outPath destination path + */ + public void getRoundedRectPath( + int left, + int top, + int right, + int bottom, + float topRoundness, + float bottomRoundness, + Path outPath) { outPath.reset(); mTmpCornerRadii[0] = topRoundness; mTmpCornerRadii[1] = topRoundness; @@ -168,15 +155,28 @@ public abstract class ExpandableOutlineView extends ExpandableView { @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { canvas.save(); + Path clipPath = null; + Path childClipPath = null; if (childNeedsClipping(child)) { - Path clipPath = getCustomClipPath(child); + clipPath = getCustomClipPath(child); if (clipPath == null) { clipPath = getClipPath(false /* ignoreTranslation */); } - if (clipPath != null) { - canvas.clipPath(clipPath); + // If the notification uses "RowTranslationX" as dismiss behavior, we should clip the + // children instead. + if (mDismissUsingRowTranslationX && child instanceof NotificationChildrenContainer) { + childClipPath = clipPath; + clipPath = null; } } + + if (child instanceof NotificationChildrenContainer) { + ((NotificationChildrenContainer) child).setChildClipPath(childClipPath); + } + if (clipPath != null) { + canvas.clipPath(clipPath); + } + boolean result = super.drawChild(canvas, child, drawingTime); canvas.restore(); return result; @@ -207,73 +207,21 @@ public abstract class ExpandableOutlineView extends ExpandableView { private void initDimens() { Resources res = getResources(); - mOutlineRadius = res.getDimension(R.dimen.notification_shadow_radius); mAlwaysRoundBothCorners = res.getBoolean(R.bool.config_clipNotificationsToOutline); - if (!mAlwaysRoundBothCorners) { - mOutlineRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius); + float maxRadius; + if (mAlwaysRoundBothCorners) { + maxRadius = res.getDimension(R.dimen.notification_shadow_radius); + } else { + maxRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius); } + mRoundableState = new RoundableState(this, this, maxRadius); setClipToOutline(mAlwaysRoundBothCorners); } @Override - public boolean setTopRoundness(float topRoundness, boolean animate) { - if (mTopRoundness != topRoundness) { - float diff = Math.abs(topRoundness - mTopRoundness); - mTopRoundness = topRoundness; - boolean shouldAnimate = animate; - if (PropertyAnimator.isAnimating(this, TOP_ROUNDNESS) && diff > 0.5f) { - // Fail safe: - // when we've been animating previously and we're now getting an update in the - // other direction, make sure to animate it too, otherwise, the localized updating - // may make the start larger than 1.0. - shouldAnimate = true; - } - PropertyAnimator.setProperty(this, TOP_ROUNDNESS, topRoundness, - ROUNDNESS_PROPERTIES, shouldAnimate); - return true; - } - return false; - } - - protected void applyRoundness() { + public void applyRoundness() { invalidateOutline(); - invalidate(); - } - - public float getCurrentBackgroundRadiusTop() { - return getCurrentTopRoundness() * mOutlineRadius; - } - - public float getCurrentTopRoundness() { - return mCurrentTopRoundness; - } - - public float getCurrentBottomRoundness() { - return mCurrentBottomRoundness; - } - - public float getCurrentBackgroundRadiusBottom() { - return getCurrentBottomRoundness() * mOutlineRadius; - } - - @Override - public boolean setBottomRoundness(float bottomRoundness, boolean animate) { - if (mBottomRoundness != bottomRoundness) { - float diff = Math.abs(bottomRoundness - mBottomRoundness); - mBottomRoundness = bottomRoundness; - boolean shouldAnimate = animate; - if (PropertyAnimator.isAnimating(this, BOTTOM_ROUNDNESS) && diff > 0.5f) { - // Fail safe: - // when we've been animating previously and we're now getting an update in the - // other direction, make sure to animate it too, otherwise, the localized updating - // may make the start larger than 1.0. - shouldAnimate = true; - } - PropertyAnimator.setProperty(this, BOTTOM_ROUNDNESS, bottomRoundness, - ROUNDNESS_PROPERTIES, shouldAnimate); - return true; - } - return false; + super.applyRoundness(); } protected void setBackgroundTop(int backgroundTop) { @@ -283,16 +231,6 @@ public abstract class ExpandableOutlineView extends ExpandableView { } } - private void setTopRoundnessInternal(float topRoundness) { - mCurrentTopRoundness = topRoundness; - applyRoundness(); - } - - private void setBottomRoundnessInternal(float bottomRoundness) { - mCurrentBottomRoundness = bottomRoundness; - applyRoundness(); - } - public void onDensityOrFontScaleChanged() { initDimens(); applyRoundness(); @@ -348,9 +286,10 @@ public abstract class ExpandableOutlineView extends ExpandableView { /** * Set the dismiss behavior of the view. + * * @param usingRowTranslationX {@code true} if the view should translate using regular - * translationX, otherwise the contents will be - * translated. + * translationX, otherwise the contents will be + * translated. */ public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) { mDismissUsingRowTranslationX = usingRowTranslationX; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index 1e09b8a37645..955d7c18f870 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -36,6 +36,8 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.statusbar.StatusBarIconView; +import com.android.systemui.statusbar.notification.Roundable; +import com.android.systemui.statusbar.notification.RoundableState; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.util.DumpUtilsKt; @@ -47,9 +49,10 @@ import java.util.List; /** * An abstract view for expandable views. */ -public abstract class ExpandableView extends FrameLayout implements Dumpable { +public abstract class ExpandableView extends FrameLayout implements Dumpable, Roundable { private static final String TAG = "ExpandableView"; + private RoundableState mRoundableState = null; protected OnHeightChangedListener mOnHeightChangedListener; private int mActualHeight; protected int mClipTopAmount; @@ -78,6 +81,14 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { initDimens(); } + @Override + public RoundableState getRoundableState() { + if (mRoundableState == null) { + mRoundableState = new RoundableState(this, this, 0f); + } + return mRoundableState; + } + private void initDimens() { mContentShift = getResources().getDimensionPixelSize( R.dimen.shelf_transform_content_shift); @@ -440,8 +451,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { int top = getClipTopAmount(); int bottom = Math.max(Math.max(getActualHeight() + getExtraBottomPadding() - mClipBottomAmount, top), mMinimumHeightForClipping); - int halfExtraWidth = (int) (mExtraWidthForClipping / 2.0f); - mClipRect.set(-halfExtraWidth, top, getWidth() + halfExtraWidth, bottom); + mClipRect.set(Integer.MIN_VALUE, top, Integer.MAX_VALUE, bottom); setClipBounds(mClipRect); } else { setClipBounds(null); @@ -455,7 +465,6 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { public void setExtraWidthForClipping(float extraWidthForClipping) { mExtraWidthForClipping = extraWidthForClipping; - updateClipping(); } public float getHeaderVisibleAmount() { @@ -621,12 +630,12 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { // initialize with the default values of the view mViewState.height = getIntrinsicHeight(); mViewState.gone = getVisibility() == View.GONE; - mViewState.alpha = 1f; + mViewState.setAlpha(1f); mViewState.notGoneIndex = -1; - mViewState.xTranslation = getTranslationX(); + mViewState.setXTranslation(getTranslationX()); mViewState.hidden = false; - mViewState.scaleX = getScaleX(); - mViewState.scaleY = getScaleY(); + mViewState.setScaleX(getScaleX()); + mViewState.setScaleY(getScaleY()); mViewState.inShelf = false; mViewState.headsUpIsVisible = false; @@ -844,22 +853,6 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { return mFirstInSection; } - /** - * Set the topRoundness of this view. - * @return Whether the roundness was changed. - */ - public boolean setTopRoundness(float topRoundness, boolean animate) { - return false; - } - - /** - * Set the bottom roundness of this view. - * @return Whether the roundness was changed. - */ - public boolean setBottomRoundness(float bottomRoundness, boolean animate) { - return false; - } - public int getHeadsUpHeightWithoutHeader() { return getHeight(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt index ab91926d466a..46fef3f973a7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.row -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 4c693045bc88..c534860d12c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -40,7 +40,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.ImageMessageConsumer; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.media.MediaFeatureFlag; +import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index df81c0ed3a61..277ad8e54016 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -1374,13 +1374,8 @@ public class NotificationContentView extends FrameLayout implements Notification if (bubbleButton == null || actionContainer == null) { return; } - boolean isPersonWithShortcut = - mPeopleIdentifier.getPeopleNotificationType(entry) - >= PeopleNotificationIdentifier.TYPE_FULL_PERSON; - boolean showButton = BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser()) - && isPersonWithShortcut - && entry.getBubbleMetadata() != null; - if (showButton) { + + if (shouldShowBubbleButton(entry)) { // explicitly resolve drawable resource using SystemUI's theme Drawable d = mContext.getDrawable(entry.isBubble() ? R.drawable.bubble_ic_stop_bubble @@ -1410,6 +1405,16 @@ public class NotificationContentView extends FrameLayout implements Notification } } + @VisibleForTesting + boolean shouldShowBubbleButton(NotificationEntry entry) { + boolean isPersonWithShortcut = + mPeopleIdentifier.getPeopleNotificationType(entry) + >= PeopleNotificationIdentifier.TYPE_FULL_PERSON; + return BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser()) + && isPersonWithShortcut + && entry.getBubbleMetadata() != null; + } + private void applySnoozeAction(View layout) { if (layout == null || mContainingNotification == null) { return; @@ -1986,6 +1991,25 @@ public class NotificationContentView extends FrameLayout implements Notification public void setRemoteInputVisible(boolean remoteInputVisible) { mRemoteInputVisible = remoteInputVisible; setClipChildren(!remoteInputVisible); + setActionsImportanceForAccessibility( + remoteInputVisible ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); + } + + private void setActionsImportanceForAccessibility(int mode) { + if (mExpandedChild != null) { + setActionsImportanceForAccessibility(mode, mExpandedChild); + } + if (mHeadsUpChild != null) { + setActionsImportanceForAccessibility(mode, mHeadsUpChild); + } + } + + private void setActionsImportanceForAccessibility(int mode, View child) { + View actionsCandidate = child.findViewById(com.android.internal.R.id.actions); + if (actionsCandidate != null) { + actionsCandidate.setImportantForAccessibility(mode); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt index f9923b2254d7..8a5d29a1ae2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.row -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index 7a654365e0ae..f13e48d55ae4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -35,12 +35,15 @@ import androidx.annotation.Nullable; import com.android.internal.widget.CachingIconView; import com.android.internal.widget.NotificationExpandButton; +import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.ViewTransformationHelper; import com.android.systemui.statusbar.notification.CustomInterpolatorTransformation; import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.ImageTransformState; +import com.android.systemui.statusbar.notification.Roundable; +import com.android.systemui.statusbar.notification.RoundableState; import com.android.systemui.statusbar.notification.TransformState; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -49,13 +52,12 @@ import java.util.Stack; /** * Wraps a notification view which may or may not include a header. */ -public class NotificationHeaderViewWrapper extends NotificationViewWrapper { +public class NotificationHeaderViewWrapper extends NotificationViewWrapper implements Roundable { + private final RoundableState mRoundableState; private static final Interpolator LOW_PRIORITY_HEADER_CLOSE = new PathInterpolator(0.4f, 0f, 0.7f, 1f); - protected final ViewTransformationHelper mTransformationHelper; - private CachingIconView mIcon; private NotificationExpandButton mExpandButton; private View mAltExpandTarget; @@ -67,12 +69,16 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { private ImageView mWorkProfileImage; private View mAudiblyAlertedIcon; private View mFeedbackIcon; - private boolean mIsLowPriority; private boolean mTransformLowPriorityTitle; protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) { super(ctx, view, row); + mRoundableState = new RoundableState( + mView, + this, + ctx.getResources().getDimension(R.dimen.notification_corner_radius) + ); mTransformationHelper = new ViewTransformationHelper(); // we want to avoid that the header clashes with the other text when transforming @@ -81,7 +87,8 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { new CustomInterpolatorTransformation(TRANSFORMING_VIEW_TITLE) { @Override - public Interpolator getCustomInterpolator(int interpolationType, + public Interpolator getCustomInterpolator( + int interpolationType, boolean isFrom) { boolean isLowPriority = mView instanceof NotificationHeaderView; if (interpolationType == TRANSFORM_Y) { @@ -99,11 +106,17 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { protected boolean hasCustomTransformation() { return mIsLowPriority && mTransformLowPriorityTitle; } - }, TRANSFORMING_VIEW_TITLE); + }, + TRANSFORMING_VIEW_TITLE); resolveHeaderViews(); addFeedbackOnClickListener(row); } + @Override + public RoundableState getRoundableState() { + return mRoundableState; + } + protected void resolveHeaderViews() { mIcon = mView.findViewById(com.android.internal.R.id.icon); mHeaderText = mView.findViewById(com.android.internal.R.id.header_text); @@ -128,7 +141,9 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { } } - /** Shows the given feedback icon, or hides the icon if null. */ + /** + * Shows the given feedback icon, or hides the icon if null. + */ @Override public void setFeedbackIcon(@Nullable FeedbackIcon icon) { if (mFeedbackIcon != null) { @@ -193,7 +208,7 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { // its animation && child.getId() != com.android.internal.R.id.conversation_icon_badge_ring) { ((ImageView) child).setCropToPadding(true); - } else if (child instanceof ViewGroup){ + } else if (child instanceof ViewGroup) { ViewGroup group = (ViewGroup) child; for (int i = 0; i < group.getChildCount(); i++) { stack.push(group.getChildAt(i)); @@ -215,7 +230,9 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { } @Override - public void updateExpandability(boolean expandable, View.OnClickListener onClickListener, + public void updateExpandability( + boolean expandable, + View.OnClickListener onClickListener, boolean requestLayout) { mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE); mExpandButton.setOnClickListener(expandable ? onClickListener : null); 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 ce465bcfb42d..b2628e40e77e 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 @@ -44,7 +44,8 @@ import java.io.PrintWriter; import javax.inject.Inject; /** - * A global state to track all input states for the algorithm. + * Global state to track all input states for + * {@link com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm}. */ @SysUISingleton public class AmbientState implements Dumpable { @@ -141,6 +142,11 @@ public class AmbientState implements Dumpable { */ private boolean mIsFlingRequiredAfterLockScreenSwipeUp = false; + /** + * Whether the shade is currently closing. + */ + private boolean mIsClosing; + @VisibleForTesting public boolean isFlingRequiredAfterLockScreenSwipeUp() { return mIsFlingRequiredAfterLockScreenSwipeUp; @@ -716,6 +722,20 @@ public class AmbientState implements Dumpable { && mStatusBarKeyguardViewManager.isBouncerInTransit(); } + /** + * @param isClosing Whether the shade is currently closing. + */ + public void setIsClosing(boolean isClosing) { + mIsClosing = isClosing; + } + + /** + * @return Whether the shade is currently closing. + */ + public boolean isClosing() { + return mIsClosing; + } + @Override public void dump(PrintWriter pw, String[] args) { pw.println("mTopPadding=" + mTopPadding); @@ -760,5 +780,6 @@ public class AmbientState implements Dumpable { + mIsFlingRequiredAfterLockScreenSwipeUp); pw.println("mZDistanceBetweenElements=" + mZDistanceBetweenElements); pw.println("mBaseZHeight=" + mBaseZHeight); + pw.println("mIsClosing=" + mIsClosing); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index d77e03fd043d..26f0ad9eca87 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -21,6 +21,9 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.Path.Direction; import android.graphics.drawable.ColorDrawable; import android.service.notification.StatusBarNotification; import android.util.AttributeSet; @@ -33,6 +36,7 @@ import android.view.ViewGroup; import android.widget.RemoteViews; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; @@ -43,10 +47,14 @@ import com.android.systemui.statusbar.NotificationGroupingUtil; import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.Roundable; +import com.android.systemui.statusbar.notification.RoundableState; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.HybridGroupManager; import com.android.systemui.statusbar.notification.row.HybridNotificationView; +import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import java.util.ArrayList; @@ -56,7 +64,7 @@ import java.util.List; * A container containing child notifications */ public class NotificationChildrenContainer extends ViewGroup - implements NotificationFadeAware { + implements NotificationFadeAware, Roundable { private static final String TAG = "NotificationChildrenContainer"; @@ -100,9 +108,9 @@ public class NotificationChildrenContainer extends ViewGroup private boolean mEnableShadowOnChildNotifications; private NotificationHeaderView mNotificationHeader; - private NotificationViewWrapper mNotificationHeaderWrapper; + private NotificationHeaderViewWrapper mNotificationHeaderWrapper; private NotificationHeaderView mNotificationHeaderLowPriority; - private NotificationViewWrapper mNotificationHeaderWrapperLowPriority; + private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority; private NotificationGroupingUtil mGroupingUtil; private ViewState mHeaderViewState; private int mClipBottomAmount; @@ -110,7 +118,8 @@ public class NotificationChildrenContainer extends ViewGroup private OnClickListener mHeaderClickListener; private ViewGroup mCurrentHeader; private boolean mIsConversation; - + private Path mChildClipPath = null; + private final Path mHeaderPath = new Path(); private boolean mShowGroupCountInExpander; private boolean mShowDividersWhenExpanded; private boolean mHideDividersDuringExpand; @@ -119,6 +128,8 @@ public class NotificationChildrenContainer extends ViewGroup private float mHeaderVisibleAmount = 1.0f; private int mUntruncatedChildCount; private boolean mContainingNotificationIsFaded = false; + private RoundableState mRoundableState; + private boolean mIsNotificationGroupCornerEnabled; public NotificationChildrenContainer(Context context) { this(context, null); @@ -132,10 +143,14 @@ public class NotificationChildrenContainer extends ViewGroup this(context, attrs, defStyleAttr, 0); } - public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr, + public NotificationChildrenContainer( + Context context, + AttributeSet attrs, + int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mHybridGroupManager = new HybridGroupManager(getContext()); + mRoundableState = new RoundableState(this, this, 0f); initDimens(); setClipChildren(false); } @@ -167,6 +182,12 @@ public class NotificationChildrenContainer extends ViewGroup mHybridGroupManager.initDimens(); } + @NonNull + @Override + public RoundableState getRoundableState() { + return mRoundableState; + } + @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = @@ -271,7 +292,7 @@ public class NotificationChildrenContainer extends ViewGroup /** * Add a child notification to this view. * - * @param row the row to add + * @param row the row to add * @param childIndex the index to add it at, if -1 it will be added at the end */ public void addNotification(ExpandableNotificationRow row, int childIndex) { @@ -347,8 +368,11 @@ public class NotificationChildrenContainer extends ViewGroup mNotificationHeader.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); mNotificationHeader.setOnClickListener(mHeaderClickListener); - mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(), - mNotificationHeader, mContainingNotification); + mNotificationHeaderWrapper = + (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( + getContext(), + mNotificationHeader, + mContainingNotification); addView(mNotificationHeader, 0); invalidate(); } else { @@ -381,8 +405,11 @@ public class NotificationChildrenContainer extends ViewGroup mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener); - mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(), - mNotificationHeaderLowPriority, mContainingNotification); + mNotificationHeaderWrapperLowPriority = + (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( + getContext(), + mNotificationHeaderLowPriority, + mContainingNotification); addView(mNotificationHeaderLowPriority, 0); invalidate(); } else { @@ -461,7 +488,9 @@ public class NotificationChildrenContainer extends ViewGroup return mAttachedChildren; } - /** To be called any time the rows have been updated */ + /** + * To be called any time the rows have been updated + */ public void updateExpansionStates() { if (mChildrenExpanded || mUserLocked) { // we don't modify it the group is expanded or if we are expanding it @@ -475,7 +504,6 @@ public class NotificationChildrenContainer extends ViewGroup } /** - * * @return the intrinsic size of this children container, i.e the natural fully expanded state */ public int getIntrinsicHeight() { @@ -485,7 +513,7 @@ public class NotificationChildrenContainer extends ViewGroup /** * @return the intrinsic height with a number of children given - * in @param maxAllowedVisibleChildren + * in @param maxAllowedVisibleChildren */ private int getIntrinsicHeight(float maxAllowedVisibleChildren) { if (showingAsLowPriority()) { @@ -539,7 +567,8 @@ public class NotificationChildrenContainer extends ViewGroup /** * Update the state of all its children based on a linear layout algorithm. - * @param parentState the state of the parent + * + * @param parentState the state of the parent * @param ambientState the ambient state containing ambient information */ public void updateState(ExpandableViewState parentState, AmbientState ambientState) { @@ -583,24 +612,26 @@ public class NotificationChildrenContainer extends ViewGroup ExpandableViewState childState = child.getViewState(); int intrinsicHeight = child.getIntrinsicHeight(); childState.height = intrinsicHeight; - childState.yTranslation = yPosition + launchTransitionCompensation; + childState.setYTranslation(yPosition + launchTransitionCompensation); childState.hidden = false; // When the group is expanded, the children cast the shadows rather than the parent // so use the parent's elevation here. - childState.zTranslation = - (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications) - ? parentState.zTranslation - : 0; + if (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications) { + childState.setZTranslation(parentState.getZTranslation()); + } else { + childState.setZTranslation(0); + } childState.dimmed = parentState.dimmed; childState.hideSensitive = parentState.hideSensitive; childState.belowSpeedBump = parentState.belowSpeedBump; childState.clipTopAmount = 0; - childState.alpha = 0; + childState.setAlpha(0); if (i < firstOverflowIndex) { - childState.alpha = showingAsLowPriority() ? expandFactor : 1.0f; + childState.setAlpha(showingAsLowPriority() ? expandFactor : 1.0f); } else if (expandFactor == 1.0f && i <= lastVisibleIndex) { - childState.alpha = (mActualHeight - childState.yTranslation) / childState.height; - childState.alpha = Math.max(0.0f, Math.min(1.0f, childState.alpha)); + childState.setAlpha( + (mActualHeight - childState.getYTranslation()) / childState.height); + childState.setAlpha(Math.max(0.0f, Math.min(1.0f, childState.getAlpha()))); } childState.location = parentState.location; childState.inShelf = parentState.inShelf; @@ -621,13 +652,16 @@ public class NotificationChildrenContainer extends ViewGroup if (mirrorView.getVisibility() == GONE) { mirrorView = alignView; } - mGroupOverFlowState.alpha = mirrorView.getAlpha(); - mGroupOverFlowState.yTranslation += NotificationUtils.getRelativeYOffset( + mGroupOverFlowState.setAlpha(mirrorView.getAlpha()); + float yTranslation = mGroupOverFlowState.getYTranslation() + + NotificationUtils.getRelativeYOffset( mirrorView, overflowView); + mGroupOverFlowState.setYTranslation(yTranslation); } } else { - mGroupOverFlowState.yTranslation += mNotificationHeaderMargin; - mGroupOverFlowState.alpha = 0.0f; + mGroupOverFlowState.setYTranslation( + mGroupOverFlowState.getYTranslation() + mNotificationHeaderMargin); + mGroupOverFlowState.setAlpha(0.0f); } } if (mNotificationHeader != null) { @@ -635,11 +669,11 @@ public class NotificationChildrenContainer extends ViewGroup mHeaderViewState = new ViewState(); } mHeaderViewState.initFrom(mNotificationHeader); - mHeaderViewState.zTranslation = childrenExpandedAndNotAnimating - ? parentState.zTranslation - : 0; - mHeaderViewState.yTranslation = mCurrentHeaderTranslation; - mHeaderViewState.alpha = mHeaderVisibleAmount; + mHeaderViewState.setZTranslation(childrenExpandedAndNotAnimating + ? parentState.getZTranslation() + : 0); + mHeaderViewState.setYTranslation(mCurrentHeaderTranslation); + mHeaderViewState.setAlpha(mHeaderVisibleAmount); // The hiding is done automatically by the alpha, otherwise we'll pick it up again // in the next frame with the initFrom call above and have an invisible header mHeaderViewState.hidden = false; @@ -650,14 +684,17 @@ public class NotificationChildrenContainer extends ViewGroup * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its * height, children in the group after this are gone. * - * @param child the child who's height to adjust. + * @param child the child who's height to adjust. * @param parentHeight the height of the parent. - * @param childState the state to update. - * @param yPosition the yPosition of the view. + * @param childState the state to update. + * @param yPosition the yPosition of the view. * @return true if children after this one should be hidden. */ - private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child, - int parentHeight, ExpandableViewState childState, int yPosition) { + private boolean updateChildStateForExpandedGroup( + ExpandableNotificationRow child, + int parentHeight, + ExpandableViewState childState, + int yPosition) { final int top = yPosition + child.getClipTopAmount(); final int intrinsicHeight = child.getIntrinsicHeight(); final int bottom = top + intrinsicHeight; @@ -685,13 +722,15 @@ public class NotificationChildrenContainer extends ViewGroup if (mIsLowPriority || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded()) || (mContainingNotification.isHeadsUpState() - && mContainingNotification.canShowHeadsUp())) { + && mContainingNotification.canShowHeadsUp())) { return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED; } return NUMBER_OF_CHILDREN_WHEN_COLLAPSED; } - /** Applies state to children. */ + /** + * Applies state to children. + */ public void applyState() { int childCount = mAttachedChildren.size(); ViewState tmpState = new ViewState(); @@ -711,14 +750,14 @@ public class NotificationChildrenContainer extends ViewGroup // layout the divider View divider = mDividers.get(i); tmpState.initFrom(divider); - tmpState.yTranslation = viewState.yTranslation - mDividerHeight; - float alpha = mChildrenExpanded && viewState.alpha != 0 ? mDividerAlpha : 0; - if (mUserLocked && !showingAsLowPriority() && viewState.alpha != 0) { + tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight); + float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0; + if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) { alpha = NotificationUtils.interpolate(0, mDividerAlpha, - Math.min(viewState.alpha, expandFraction)); + Math.min(viewState.getAlpha(), expandFraction)); } tmpState.hidden = !dividersVisible; - tmpState.alpha = alpha; + tmpState.setAlpha(alpha); tmpState.applyToView(divider); // There is no fake shadow to be drawn on the children child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); @@ -763,17 +802,73 @@ public class NotificationChildrenContainer extends ViewGroup } } + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean isCanvasChanged = false; + + Path clipPath = mChildClipPath; + if (clipPath != null) { + final float translation; + if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow notificationRow = (ExpandableNotificationRow) child; + translation = notificationRow.getTranslation(); + } else { + translation = child.getTranslationX(); + } + + isCanvasChanged = true; + canvas.save(); + if (mIsNotificationGroupCornerEnabled && translation != 0f) { + clipPath.offset(translation, 0f); + canvas.clipPath(clipPath); + clipPath.offset(-translation, 0f); + } else { + canvas.clipPath(clipPath); + } + } + + if (child instanceof NotificationHeaderView + && mNotificationHeaderWrapper.hasRoundedCorner()) { + float[] radii = mNotificationHeaderWrapper.getUpdatedRadii(); + mHeaderPath.reset(); + mHeaderPath.addRoundRect( + child.getLeft(), + child.getTop(), + child.getRight(), + child.getBottom(), + radii, + Direction.CW + ); + if (!isCanvasChanged) { + isCanvasChanged = true; + canvas.save(); + } + canvas.clipPath(mHeaderPath); + } + + if (isCanvasChanged) { + boolean result = super.drawChild(canvas, child, drawingTime); + canvas.restore(); + return result; + } else { + // If there have been no changes to the canvas we can proceed as usual + return super.drawChild(canvas, child, drawingTime); + } + } + + /** * This is called when the children expansion has changed and positions the children properly * for an appear animation. - * */ public void prepareExpansionChanged() { // TODO: do something that makes sense, like placing the invisible views correctly return; } - /** Animate to a given state. */ + /** + * Animate to a given state. + */ public void startAnimationToState(AnimationProperties properties) { int childCount = mAttachedChildren.size(); ViewState tmpState = new ViewState(); @@ -790,24 +885,24 @@ public class NotificationChildrenContainer extends ViewGroup // layout the divider View divider = mDividers.get(i); tmpState.initFrom(divider); - tmpState.yTranslation = viewState.yTranslation - mDividerHeight; - float alpha = mChildrenExpanded && viewState.alpha != 0 ? mDividerAlpha : 0; - if (mUserLocked && !showingAsLowPriority() && viewState.alpha != 0) { + tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight); + float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0; + if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) { alpha = NotificationUtils.interpolate(0, mDividerAlpha, - Math.min(viewState.alpha, expandFraction)); + Math.min(viewState.getAlpha(), expandFraction)); } tmpState.hidden = !dividersVisible; - tmpState.alpha = alpha; + tmpState.setAlpha(alpha); tmpState.animateTo(divider, properties); // There is no fake shadow to be drawn on the children child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); } if (mOverflowNumber != null) { if (mNeverAppliedGroupState) { - float alpha = mGroupOverFlowState.alpha; - mGroupOverFlowState.alpha = 0; + float alpha = mGroupOverFlowState.getAlpha(); + mGroupOverFlowState.setAlpha(0); mGroupOverFlowState.applyToView(mOverflowNumber); - mGroupOverFlowState.alpha = alpha; + mGroupOverFlowState.setAlpha(alpha); mNeverAppliedGroupState = false; } mGroupOverFlowState.animateTo(mOverflowNumber, properties); @@ -949,7 +1044,7 @@ public class NotificationChildrenContainer extends ViewGroup child.setAlpha(start); ViewState viewState = new ViewState(); viewState.initFrom(child); - viewState.alpha = target; + viewState.setAlpha(target); ALPHA_FADE_IN.setDelay(i * 50); viewState.animateTo(child, ALPHA_FADE_IN); } @@ -1097,7 +1192,8 @@ public class NotificationChildrenContainer extends ViewGroup * Get the minimum Height for this group. * * @param maxAllowedVisibleChildren the number of children that should be visible - * @param likeHighPriority if the height should be calculated as if it were not low priority + * @param likeHighPriority if the height should be calculated as if it were not low + * priority */ private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) { return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation); @@ -1107,10 +1203,13 @@ public class NotificationChildrenContainer extends ViewGroup * Get the minimum Height for this group. * * @param maxAllowedVisibleChildren the number of children that should be visible - * @param likeHighPriority if the height should be calculated as if it were not low priority - * @param headerTranslation the translation amount of the header + * @param likeHighPriority if the height should be calculated as if it were not low + * priority + * @param headerTranslation the translation amount of the header */ - private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority, + private int getMinHeight( + int maxAllowedVisibleChildren, + boolean likeHighPriority, int headerTranslation) { if (!likeHighPriority && showingAsLowPriority()) { if (mNotificationHeaderLowPriority == null) { @@ -1269,16 +1368,19 @@ public class NotificationChildrenContainer extends ViewGroup return mUserLocked; } - public void setCurrentBottomRoundness(float currentBottomRoundness) { + @Override + public void applyRoundness() { + Roundable.super.applyRoundness(); boolean last = true; for (int i = mAttachedChildren.size() - 1; i >= 0; i--) { ExpandableNotificationRow child = mAttachedChildren.get(i); if (child.getVisibility() == View.GONE) { continue; } - float bottomRoundness = last ? currentBottomRoundness : 0.0f; - child.setBottomRoundness(bottomRoundness, isShown() /* animate */); - child.setTopRoundness(0.0f, false /* animate */); + child.requestBottomRoundness( + last ? getBottomRoundness() : 0f, + /* animate = */ isShown(), + SourceType.DefaultValue); last = false; } } @@ -1288,7 +1390,9 @@ public class NotificationChildrenContainer extends ViewGroup mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader); } - /** Shows the given feedback icon, or hides the icon if null. */ + /** + * Shows the given feedback icon, or hides the icon if null. + */ public void setFeedbackIcon(@Nullable FeedbackIcon icon) { if (mNotificationHeaderWrapper != null) { mNotificationHeaderWrapper.setFeedbackIcon(icon); @@ -1320,4 +1424,26 @@ public class NotificationChildrenContainer extends ViewGroup child.setNotificationFaded(faded); } } + + /** + * Allow to define a path the clip the children in #drawChild() + * + * @param childClipPath path used to clip the children + */ + public void setChildClipPath(@Nullable Path childClipPath) { + mChildClipPath = childClipPath; + invalidate(); + } + + public NotificationHeaderViewWrapper getNotificationHeaderWrapper() { + return mNotificationHeaderWrapper; + } + + /** + * Enable the support for rounded corner in notification group + * @param enabled true if is supported + */ + public void enableNotificationGroupCorner(boolean enabled) { + mIsNotificationGroupCornerEnabled = enabled; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java index 2015c87aac2b..6810055ad3bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java @@ -26,6 +26,8 @@ import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; +import com.android.systemui.statusbar.notification.Roundable; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -59,8 +61,8 @@ public class NotificationRoundnessManager implements Dumpable { private boolean mIsClearAllInProgress; private ExpandableView mSwipedView = null; - private ExpandableView mViewBeforeSwipedView = null; - private ExpandableView mViewAfterSwipedView = null; + private Roundable mViewBeforeSwipedView = null; + private Roundable mViewAfterSwipedView = null; @Inject NotificationRoundnessManager( @@ -101,11 +103,12 @@ public class NotificationRoundnessManager implements Dumpable { public boolean isViewAffectedBySwipe(ExpandableView expandableView) { return expandableView != null && (expandableView == mSwipedView - || expandableView == mViewBeforeSwipedView - || expandableView == mViewAfterSwipedView); + || expandableView == mViewBeforeSwipedView + || expandableView == mViewAfterSwipedView); } - boolean updateViewWithoutCallback(ExpandableView view, + boolean updateViewWithoutCallback( + ExpandableView view, boolean animate) { if (view == null || view == mViewBeforeSwipedView @@ -113,11 +116,15 @@ public class NotificationRoundnessManager implements Dumpable { return false; } - final float topRoundness = getRoundnessFraction(view, true /* top */); - final float bottomRoundness = getRoundnessFraction(view, false /* top */); + final boolean isTopChanged = view.requestTopRoundness( + getRoundnessDefaultValue(view, true /* top */), + animate, + SourceType.DefaultValue); - final boolean topChanged = view.setTopRoundness(topRoundness, animate); - final boolean bottomChanged = view.setBottomRoundness(bottomRoundness, animate); + final boolean isBottomChanged = view.requestBottomRoundness( + getRoundnessDefaultValue(view, /* top = */ false), + animate, + SourceType.DefaultValue); final boolean isFirstInSection = isFirstInSection(view); final boolean isLastInSection = isLastInSection(view); @@ -126,9 +133,9 @@ public class NotificationRoundnessManager implements Dumpable { view.setLastInSection(isLastInSection); mNotifLogger.onCornersUpdated(view, isFirstInSection, - isLastInSection, topChanged, bottomChanged); + isLastInSection, isTopChanged, isBottomChanged); - return (isFirstInSection || isLastInSection) && (topChanged || bottomChanged); + return (isFirstInSection || isLastInSection) && (isTopChanged || isBottomChanged); } private boolean isFirstInSection(ExpandableView view) { @@ -150,42 +157,46 @@ public class NotificationRoundnessManager implements Dumpable { } void setViewsAffectedBySwipe( - ExpandableView viewBefore, + Roundable viewBefore, ExpandableView viewSwiped, - ExpandableView viewAfter) { + Roundable viewAfter) { final boolean animate = true; + final SourceType source = SourceType.OnDismissAnimation; + + // This method requires you to change the roundness of the current View targets and reset + // the roundness of the old View targets (if any) to 0f. + // To avoid conflicts, it generates a set of old Views and removes the current Views + // from this set. + HashSet<Roundable> oldViews = new HashSet<>(); + if (mViewBeforeSwipedView != null) oldViews.add(mViewBeforeSwipedView); + if (mSwipedView != null) oldViews.add(mSwipedView); + if (mViewAfterSwipedView != null) oldViews.add(mViewAfterSwipedView); - ExpandableView oldViewBefore = mViewBeforeSwipedView; mViewBeforeSwipedView = viewBefore; - if (oldViewBefore != null) { - final float bottomRoundness = getRoundnessFraction(oldViewBefore, false /* top */); - oldViewBefore.setBottomRoundness(bottomRoundness, animate); - } if (viewBefore != null) { - viewBefore.setBottomRoundness(1f, animate); + oldViews.remove(viewBefore); + viewBefore.requestTopRoundness(0f, animate, source); + viewBefore.requestBottomRoundness(1f, animate, source); } - ExpandableView oldSwipedview = mSwipedView; mSwipedView = viewSwiped; - if (oldSwipedview != null) { - final float bottomRoundness = getRoundnessFraction(oldSwipedview, false /* top */); - final float topRoundness = getRoundnessFraction(oldSwipedview, true /* top */); - oldSwipedview.setTopRoundness(topRoundness, animate); - oldSwipedview.setBottomRoundness(bottomRoundness, animate); - } if (viewSwiped != null) { - viewSwiped.setTopRoundness(1f, animate); - viewSwiped.setBottomRoundness(1f, animate); + oldViews.remove(viewSwiped); + viewSwiped.requestTopRoundness(1f, animate, source); + viewSwiped.requestBottomRoundness(1f, animate, source); } - ExpandableView oldViewAfter = mViewAfterSwipedView; mViewAfterSwipedView = viewAfter; - if (oldViewAfter != null) { - final float topRoundness = getRoundnessFraction(oldViewAfter, true /* top */); - oldViewAfter.setTopRoundness(topRoundness, animate); - } if (viewAfter != null) { - viewAfter.setTopRoundness(1f, animate); + oldViews.remove(viewAfter); + viewAfter.requestTopRoundness(1f, animate, source); + viewAfter.requestBottomRoundness(0f, animate, source); + } + + // After setting the current Views, reset the views that are still present in the set. + for (Roundable oldView : oldViews) { + oldView.requestTopRoundness(0f, animate, source); + oldView.requestBottomRoundness(0f, animate, source); } } @@ -193,7 +204,7 @@ public class NotificationRoundnessManager implements Dumpable { mIsClearAllInProgress = isClearingAll; } - private float getRoundnessFraction(ExpandableView view, boolean top) { + private float getRoundnessDefaultValue(Roundable view, boolean top) { if (view == null) { return 0f; } @@ -207,28 +218,35 @@ public class NotificationRoundnessManager implements Dumpable { && mIsClearAllInProgress) { return 1.0f; } - if ((view.isPinned() - || (view.isHeadsUpAnimatingAway()) && !mExpanded)) { - return 1.0f; - } - if (isFirstInSection(view) && top) { - return 1.0f; - } - if (isLastInSection(view) && !top) { - return 1.0f; - } + if (view instanceof ExpandableView) { + ExpandableView expandableView = (ExpandableView) view; + if ((expandableView.isPinned() + || (expandableView.isHeadsUpAnimatingAway()) && !mExpanded)) { + return 1.0f; + } + if (isFirstInSection(expandableView) && top) { + return 1.0f; + } + if (isLastInSection(expandableView) && !top) { + return 1.0f; + } - if (view == mTrackedHeadsUp) { - // If we're pushing up on a headsup the appear fraction is < 0 and it needs to still be - // rounded. - return MathUtils.saturate(1.0f - mAppearFraction); - } - if (view.showingPulsing() && mRoundForPulsingViews) { - return 1.0f; + if (view == mTrackedHeadsUp) { + // If we're pushing up on a headsup the appear fraction is < 0 and it needs to + // still be rounded. + return MathUtils.saturate(1.0f - mAppearFraction); + } + if (expandableView.showingPulsing() && mRoundForPulsingViews) { + return 1.0f; + } + if (expandableView.isChildInGroup()) { + return 0f; + } + final Resources resources = expandableView.getResources(); + return resources.getDimension(R.dimen.notification_corner_radius_small) + / resources.getDimension(R.dimen.notification_corner_radius); } - final Resources resources = view.getResources(); - return resources.getDimension(R.dimen.notification_corner_radius_small) - / resources.getDimension(R.dimen.notification_corner_radius); + return 0f; } public void setExpanded(float expandedHeight, float appearFraction) { @@ -258,8 +276,10 @@ public class NotificationRoundnessManager implements Dumpable { mNotifLogger.onSectionCornersUpdated(sections, anyChanged); } - private boolean handleRemovedOldViews(NotificationSection[] sections, - ExpandableView[] oldViews, boolean first) { + private boolean handleRemovedOldViews( + NotificationSection[] sections, + ExpandableView[] oldViews, + boolean first) { boolean anyChanged = false; for (ExpandableView oldView : oldViews) { if (oldView != null) { @@ -289,8 +309,10 @@ public class NotificationRoundnessManager implements Dumpable { return anyChanged; } - private boolean handleAddedNewViews(NotificationSection[] sections, - ExpandableView[] oldViews, boolean first) { + private boolean handleAddedNewViews( + NotificationSection[] sections, + ExpandableView[] oldViews, + boolean first) { boolean anyChanged = false; for (NotificationSection section : sections) { ExpandableView newView = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java index bc172ce537f3..0b435fe9dcc6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java @@ -35,12 +35,12 @@ import com.android.systemui.statusbar.notification.row.ExpandableView; * bounds change. */ public class NotificationSection { - private @PriorityBucket int mBucket; - private View mOwningView; - private Rect mBounds = new Rect(); - private Rect mCurrentBounds = new Rect(-1, -1, -1, -1); - private Rect mStartAnimationRect = new Rect(); - private Rect mEndAnimationRect = new Rect(); + private @PriorityBucket final int mBucket; + private final View mOwningView; + private final Rect mBounds = new Rect(); + private final Rect mCurrentBounds = new Rect(-1, -1, -1, -1); + private final Rect mStartAnimationRect = new Rect(); + private final Rect mEndAnimationRect = new Rect(); private ObjectAnimator mTopAnimator = null; private ObjectAnimator mBottomAnimator = null; private ExpandableView mFirstVisibleChild; @@ -277,7 +277,6 @@ public class NotificationSection { } } } - top = Math.max(minTopPosition, top); ExpandableView lastView = getLastVisibleChild(); if (lastView != null) { float finalTranslationY = ViewState.getFinalTranslationY(lastView); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt index cb7dfe87f7fb..b61c55edadcd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt @@ -17,9 +17,9 @@ package com.android.systemui.statusbar.notification.stack import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationSectionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import javax.inject.Inject private const val TAG = "NotifSections" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt index 91a28139c775..a1b77acb9a5e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt @@ -19,11 +19,10 @@ import android.annotation.ColorInt import android.util.Log import android.view.View import com.android.internal.annotations.VisibleForTesting -import com.android.systemui.media.KeyguardMediaController +import com.android.systemui.media.controls.ui.KeyguardMediaController import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager import com.android.systemui.statusbar.notification.collection.render.MediaContainerController import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController -import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager import com.android.systemui.statusbar.notification.dagger.AlertingHeader import com.android.systemui.statusbar.notification.dagger.IncomingHeader import com.android.systemui.statusbar.notification.dagger.PeopleHeader 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 5fbaa515d5d7..df705c5afeef 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 @@ -135,7 +135,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private static final boolean SPEW = Log.isLoggable(TAG, Log.VERBOSE); // Delay in milli-seconds before shade closes for clear all. - private final int DELAY_BEFORE_SHADE_CLOSE = 200; + private static final int DELAY_BEFORE_SHADE_CLOSE = 200; private boolean mShadeNeedsToClose = false; private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f; @@ -152,7 +152,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private static final int DISTANCE_BETWEEN_ADJACENT_SECTIONS_PX = 1; private boolean mKeyguardBypassEnabled; - private ExpandHelper mExpandHelper; + private final ExpandHelper mExpandHelper; private NotificationSwipeHelper mSwipeHelper; private int mCurrentStackHeight = Integer.MAX_VALUE; private final Paint mBackgroundPaint = new Paint(); @@ -165,12 +165,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private VelocityTracker mVelocityTracker; private OverScroller mScroller; - /** Last Y position reported by {@link #mScroller}, used to calculate scroll delta. */ - private int mLastScrollerY; - /** - * True if the max position was set to a known position on the last call to {@link #mScroller}. - */ - private boolean mIsScrollerBoundSet; + private Runnable mFinishScrollingCallback; private int mTouchSlop; private float mSlopMultiplier; @@ -194,7 +189,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private int mContentHeight; private float mIntrinsicContentHeight; - private int mCollapsedSize; private int mPaddingBetweenElements; private int mMaxTopPadding; private int mTopPadding; @@ -210,15 +204,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private final StackScrollAlgorithm mStackScrollAlgorithm; private final AmbientState mAmbientState; - private GroupMembershipManager mGroupMembershipManager; - private GroupExpansionManager mGroupExpansionManager; - private HashSet<ExpandableView> mChildrenToAddAnimated = new HashSet<>(); - private ArrayList<View> mAddedHeadsUpChildren = new ArrayList<>(); - private ArrayList<ExpandableView> mChildrenToRemoveAnimated = new ArrayList<>(); - private ArrayList<ExpandableView> mChildrenChangingPositions = new ArrayList<>(); - private HashSet<View> mFromMoreCardAdditions = new HashSet<>(); - private ArrayList<AnimationEvent> mAnimationEvents = new ArrayList<>(); - private ArrayList<View> mSwipedOutViews = new ArrayList<>(); + private final GroupMembershipManager mGroupMembershipManager; + private final GroupExpansionManager mGroupExpansionManager; + private final HashSet<ExpandableView> mChildrenToAddAnimated = new HashSet<>(); + private final ArrayList<View> mAddedHeadsUpChildren = new ArrayList<>(); + private final ArrayList<ExpandableView> mChildrenToRemoveAnimated = new ArrayList<>(); + private final ArrayList<ExpandableView> mChildrenChangingPositions = new ArrayList<>(); + private final HashSet<View> mFromMoreCardAdditions = new HashSet<>(); + private final ArrayList<AnimationEvent> mAnimationEvents = new ArrayList<>(); + private final ArrayList<View> mSwipedOutViews = new ArrayList<>(); private NotificationStackSizeCalculator mNotificationStackSizeCalculator; private final StackStateAnimator mStateAnimator = new StackStateAnimator(this); private boolean mAnimationsEnabled; @@ -261,7 +255,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private boolean mClearAllInProgress; private FooterClearAllListener mFooterClearAllListener; private boolean mFlingAfterUpEvent; - /** * Was the scroller scrolled to the top when the down motion was observed? */ @@ -296,7 +289,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private boolean mDisallowDismissInThisMotion; private boolean mDisallowScrollingInThisMotion; private long mGoToFullShadeDelay; - private ViewTreeObserver.OnPreDrawListener mChildrenUpdater + private final ViewTreeObserver.OnPreDrawListener mChildrenUpdater = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { @@ -309,17 +302,16 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable }; private NotificationStackScrollLogger mLogger; private CentralSurfaces mCentralSurfaces; - private int[] mTempInt2 = new int[2]; + private final int[] mTempInt2 = new int[2]; private boolean mGenerateChildOrderChangedEvent; - private HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>(); - private HashSet<ExpandableView> mClearTransientViewsWhenFinished = new HashSet<>(); - private HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations + private final HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>(); + private final HashSet<ExpandableView> mClearTransientViewsWhenFinished = new HashSet<>(); + private final HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations = new HashSet<>(); - private boolean mTrackingHeadsUp; private boolean mForceNoOverlappingRendering; private final ArrayList<Pair<ExpandableNotificationRow, Boolean>> mTmpList = new ArrayList<>(); private boolean mAnimationRunning; - private ViewTreeObserver.OnPreDrawListener mRunningAnimationUpdater + private final ViewTreeObserver.OnPreDrawListener mRunningAnimationUpdater = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { @@ -327,21 +319,21 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return true; } }; - private NotificationSection[] mSections; + private final NotificationSection[] mSections; private boolean mAnimateNextBackgroundTop; private boolean mAnimateNextBackgroundBottom; private boolean mAnimateNextSectionBoundsChange; private int mBgColor; private float mDimAmount; private ValueAnimator mDimAnimator; - private ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>(); + private final ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>(); private final Animator.AnimatorListener mDimEndListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mDimAnimator = null; } }; - private ValueAnimator.AnimatorUpdateListener mDimUpdateListener + private final ValueAnimator.AnimatorUpdateListener mDimUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override @@ -351,29 +343,23 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable }; protected ViewGroup mQsHeader; // Rect of QsHeader. Kept as a field just to avoid creating a new one each time. - private Rect mQsHeaderBound = new Rect(); + private final Rect mQsHeaderBound = new Rect(); private boolean mContinuousShadowUpdate; private boolean mContinuousBackgroundUpdate; - private ViewTreeObserver.OnPreDrawListener mShadowUpdater + private final ViewTreeObserver.OnPreDrawListener mShadowUpdater = () -> { updateViewShadows(); return true; }; - private ViewTreeObserver.OnPreDrawListener mBackgroundUpdater = () -> { + private final ViewTreeObserver.OnPreDrawListener mBackgroundUpdater = () -> { updateBackground(); return true; }; - private Comparator<ExpandableView> mViewPositionComparator = (view, otherView) -> { + private final Comparator<ExpandableView> mViewPositionComparator = (view, otherView) -> { float endY = view.getTranslationY() + view.getActualHeight(); float otherEndY = otherView.getTranslationY() + otherView.getActualHeight(); - if (endY < otherEndY) { - return -1; - } else if (endY > otherEndY) { - return 1; - } else { - // The two notifications end at the same location - return 0; - } + // Return zero when the two notifications end at the same location + return Float.compare(endY, otherEndY); }; private final ViewOutlineProvider mOutlineProvider = new ViewOutlineProvider() { @Override @@ -435,16 +421,14 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private int mUpcomingStatusBarState; private int mCachedBackgroundColor; private boolean mHeadsUpGoingAwayAnimationsAllowed = true; - private Runnable mReflingAndAnimateScroll = () -> { - animateScroll(); - }; + private final Runnable mReflingAndAnimateScroll = this::animateScroll; private int mCornerRadius; private int mMinimumPaddings; private int mQsTilePadding; private boolean mSkinnyNotifsInLandscape; private int mSidePaddings; private final Rect mBackgroundAnimationRect = new Rect(); - private ArrayList<BiConsumer<Float, Float>> mExpandedHeightListeners = new ArrayList<>(); + private final ArrayList<BiConsumer<Float, Float>> mExpandedHeightListeners = new ArrayList<>(); private int mHeadsUpInset; /** @@ -479,8 +463,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private int mWaterfallTopInset; private NotificationStackScrollLayoutController mController; - private boolean mKeyguardMediaControllorVisible; - /** * The clip path used to clip the view in a rounded way. */ @@ -501,7 +483,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private int mRoundedRectClippingTop; private int mRoundedRectClippingBottom; private int mRoundedRectClippingRight; - private float[] mBgCornerRadii = new float[8]; + private final float[] mBgCornerRadii = new float[8]; /** * Whether stackY should be animated in case the view is getting shorter than the scroll @@ -527,7 +509,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /** * Corner radii of the launched notification if it's clipped */ - private float[] mLaunchedNotificationRadii = new float[8]; + private final float[] mLaunchedNotificationRadii = new float[8]; /** * The notification that is being launched currently. @@ -779,7 +761,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable y = getLayoutHeight(); drawDebugInfo(canvas, y, Color.YELLOW, /* label= */ "getLayoutHeight() = " + y); - y = (int) mMaxLayoutHeight; + y = mMaxLayoutHeight; drawDebugInfo(canvas, y, Color.MAGENTA, /* label= */ "mMaxLayoutHeight = " + y); // The space between mTopPadding and mKeyguardBottomPadding determines the available space @@ -997,7 +979,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mOverflingDistance = configuration.getScaledOverflingDistance(); Resources res = context.getResources(); - mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height); mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height); mStackScrollAlgorithm.initView(context); mAmbientState.reload(context); @@ -1133,6 +1114,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable updateAlgorithmLayoutMinHeight(); updateOwnTranslationZ(); + // Give The Algorithm information regarding the QS height so it can layout notifications + // properly. Needed for some devices that grows notifications down-to-top + mStackScrollAlgorithm.updateQSFrameTop(mQsHeader == null ? 0 : mQsHeader.getHeight()); + // Once the layout has finished, we don't need to animate any scrolling clampings anymore. mAnimateStackYForContentHeightChange = false; } @@ -1203,7 +1188,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return; } for (int i = 0; i < getChildCount(); i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (mChildrenToAddAnimated.contains(child)) { final int startingPosition = getPositionInLinearLayout(child); final int childHeight = getIntrinsicHeight(child) + mPaddingBetweenElements; @@ -1256,12 +1241,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private void clampScrollPosition() { int scrollRange = getScrollRange(); if (scrollRange < mOwnScrollY && !mAmbientState.isClearAllInProgress()) { - boolean animateStackY = false; - if (scrollRange < getScrollAmountToScrollBoundary() - && mAnimateStackYForContentHeightChange) { - // if the scroll boundary updates the position of the stack, - animateStackY = true; - } + // if the scroll boundary updates the position of the stack, + boolean animateStackY = scrollRange < getScrollAmountToScrollBoundary() + && mAnimateStackYForContentHeightChange; setOwnScrollY(scrollRange, animateStackY); } } @@ -1318,7 +1300,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable + mAmbientState.getOverExpansion() - getCurrentOverScrollAmount(false /* top */); float fraction = mAmbientState.getExpansionFraction(); - if (mAmbientState.isBouncerInTransit()) { + // If we are on quick settings, we need to quickly hide it to show the bouncer to avoid an + // overlap. Otherwise, we maintain the normal fraction for smoothness. + if (mAmbientState.isBouncerInTransit() && mQsExpansionFraction > 0f) { fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction); } final float stackY = MathUtils.lerp(0, endTopPosition, fraction); @@ -1504,7 +1488,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } if (mAmbientState.isHiddenAtAll()) { - clipToOutline = false; invalidateOutline(); if (isFullyHidden()) { setClipBounds(null); @@ -1675,7 +1658,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable // find the view under the pointer, accounting for GONE views final int count = getChildCount(); for (int childIdx = 0; childIdx < count; childIdx++) { - ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx); + ExpandableView slidingChild = getChildAtIndex(childIdx); if (slidingChild.getVisibility() != VISIBLE || (ignoreDecors && slidingChild instanceof StackScrollerDecorView)) { continue; @@ -1708,6 +1691,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return null; } + private ExpandableView getChildAtIndex(int index) { + return (ExpandableView) getChildAt(index); + } + public ExpandableView getChildAtRawPosition(float touchX, float touchY) { getLocationOnScreen(mTempInt2); return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]); @@ -1782,7 +1769,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) - private Runnable mReclamp = new Runnable() { + private final Runnable mReclamp = new Runnable() { @Override public void run() { int range = getScrollRange(); @@ -2293,7 +2280,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable int childCount = getChildCount(); int count = 0; for (int i = 0; i < childCount; i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != View.GONE && !child.willBeGone() && child != mShelf) { count++; } @@ -2513,7 +2500,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private ExpandableView getLastChildWithBackground() { int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView) && child != mShelf) { return child; @@ -2526,7 +2513,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private ExpandableView getFirstChildWithBackground() { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView) && child != mShelf) { return child; @@ -2540,7 +2527,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable ArrayList<ExpandableView> children = new ArrayList<>(); int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView) && child != mShelf) { @@ -2899,7 +2886,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } int position = 0; for (int i = 0; i < getChildCount(); i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); boolean notGone = child.getVisibility() != View.GONE; if (notGone && !child.hasNoContentHeight()) { if (position != 0) { @@ -2953,7 +2940,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } mAmbientState.setLastVisibleBackgroundChild(lastChild); // TODO: Refactor SectionManager and put the RoundnessManager there. - mController.getNoticationRoundessManager().updateRoundedChildren(mSections); + mController.getNotificationRoundnessManager().updateRoundedChildren(mSections); mAnimateBottomOnLayout = false; invalidate(); } @@ -3084,11 +3071,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable int currentIndex = indexOfChild(child); if (currentIndex == -1) { - boolean isTransient = false; - if (child instanceof ExpandableNotificationRow - && child.getTransientContainer() != null) { - isTransient = true; - } + boolean isTransient = child instanceof ExpandableNotificationRow + && child.getTransientContainer() != null; Log.e(TAG, "Attempting to re-position " + (isTransient ? "transient" : "") + " view {" @@ -3149,7 +3133,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private void generateHeadsUpAnimationEvents() { for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) { ExpandableNotificationRow row = eventPair.first; - String key = row.getEntry().getKey(); boolean isHeadsUp = eventPair.second; if (isHeadsUp != row.isHeadsUp()) { // For cases where we have a heads up showing and appearing again we shouldn't @@ -3212,10 +3195,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.COORDINATOR) private boolean shouldHunAppearFromBottom(ExpandableViewState viewState) { - if (viewState.yTranslation + viewState.height < mAmbientState.getMaxHeadsUpTranslation()) { - return false; - } - return true; + return viewState.getYTranslation() + viewState.height + >= mAmbientState.getMaxHeadsUpTranslation(); } @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) @@ -3991,7 +3972,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) private void clearUserLockedViews() { for (int i = 0; i < getChildCount(); i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) child; row.setUserLocked(false); @@ -4004,7 +3985,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable // lets make sure nothing is transient anymore clearTemporaryViewsInGroup(this); for (int i = 0; i < getChildCount(); i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) child; clearTemporaryViewsInGroup(row.getChildrenContainer()); @@ -4042,8 +4023,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable setOwnScrollY(0); } + @VisibleForTesting @ShadeViewRefactor(RefactorComponent.COORDINATOR) - private void setIsExpanded(boolean isExpanded) { + void setIsExpanded(boolean isExpanded) { boolean changed = isExpanded != mIsExpanded; mIsExpanded = isExpanded; mStackScrollAlgorithm.setIsExpanded(isExpanded); @@ -4252,7 +4234,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (hideSensitive != mAmbientState.isHideSensitive()) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { - ExpandableView v = (ExpandableView) getChildAt(i); + ExpandableView v = getChildAtIndex(i); v.setHideSensitiveForIntrinsicHeight(hideSensitive); } mAmbientState.setHideSensitive(hideSensitive); @@ -4287,7 +4269,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private void applyCurrentState() { int numChildren = getChildCount(); for (int i = 0; i < numChildren; i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); child.applyViewState(); } @@ -4307,7 +4289,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable // Lefts first sort by Z difference for (int i = 0; i < getChildCount(); i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != GONE) { mTmpSortedChildren.add(child); } @@ -4534,7 +4516,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable public void setClearAllInProgress(boolean clearAllInProgress) { mClearAllInProgress = clearAllInProgress; mAmbientState.setClearAllInProgress(clearAllInProgress); - mController.getNoticationRoundessManager().setClearAllInProgress(clearAllInProgress); + mController.getNotificationRoundnessManager().setClearAllInProgress(clearAllInProgress); } boolean getClearAllInProgress() { @@ -4577,7 +4559,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable final int count = getChildCount(); float max = 0; for (int childIdx = 0; childIdx < count; childIdx++) { - ExpandableView child = (ExpandableView) getChildAt(childIdx); + ExpandableView child = getChildAtIndex(childIdx); if (child.getVisibility() == GONE) { continue; } @@ -4608,7 +4590,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable public boolean isBelowLastNotification(float touchX, float touchY) { int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); if (child.getVisibility() != View.GONE) { float childTop = child.getY(); if (childTop > touchY) { @@ -4790,7 +4772,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void setTrackingHeadsUp(ExpandableNotificationRow row) { mAmbientState.setTrackedHeadsUpRow(row); - mTrackingHeadsUp = row != null; } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) @@ -4865,13 +4846,21 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } + @VisibleForTesting @ShadeViewRefactor(RefactorComponent.COORDINATOR) - private void setOwnScrollY(int ownScrollY) { + void setOwnScrollY(int ownScrollY) { setOwnScrollY(ownScrollY, false /* animateScrollChangeListener */); } @ShadeViewRefactor(RefactorComponent.COORDINATOR) private void setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener) { + // Avoid Flicking during clear all + // when the shade finishes closing, onExpansionStopped will call + // resetScrollPosition to setOwnScrollY to 0 + if (mAmbientState.isClosing()) { + return; + } + if (ownScrollY != mOwnScrollY) { // We still want to call the normal scrolled changed for accessibility reasons onScrollChanged(mScrollX, ownScrollY, mScrollX, mOwnScrollY); @@ -5067,7 +5056,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable pw.println(); for (int i = 0; i < childCount; i++) { - ExpandableView child = (ExpandableView) getChildAt(i); + ExpandableView child = getChildAtIndex(i); child.dump(pw, args); pw.println(); } @@ -5356,7 +5345,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable float wakeUplocation = -1f; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { - ExpandableView view = (ExpandableView) getChildAt(i); + ExpandableView view = getChildAtIndex(i); if (view.getVisibility() == View.GONE) { continue; } @@ -5395,7 +5384,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable public void setController( NotificationStackScrollLayoutController notificationStackScrollLayoutController) { mController = notificationStackScrollLayoutController; - mController.getNoticationRoundessManager().setAnimatedChildren(mChildrenToAddAnimated); + mController.getNotificationRoundnessManager().setAnimatedChildren(mChildrenToAddAnimated); } void addSwipedOutView(View v) { @@ -5406,31 +5395,22 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (!(viewSwiped instanceof ExpandableNotificationRow)) { return; } - final int indexOfSwipedView = indexOfChild(viewSwiped); - if (indexOfSwipedView < 0) { - return; - } mSectionsManager.updateFirstAndLastViewsForAllSections( - mSections, getChildrenWithBackground()); - View viewBefore = null; - if (indexOfSwipedView > 0) { - viewBefore = getChildAt(indexOfSwipedView - 1); - if (mSectionsManager.beginsSection(viewSwiped, viewBefore)) { - viewBefore = null; - } - } - View viewAfter = null; - if (indexOfSwipedView < getChildCount()) { - viewAfter = getChildAt(indexOfSwipedView + 1); - if (mSectionsManager.beginsSection(viewAfter, viewSwiped)) { - viewAfter = null; - } - } - mController.getNoticationRoundessManager() + mSections, + getChildrenWithBackground() + ); + + RoundableTargets targets = mController.getNotificationTargetsHelper().findRoundableTargets( + (ExpandableNotificationRow) viewSwiped, + this, + mSectionsManager + ); + + mController.getNotificationRoundnessManager() .setViewsAffectedBySwipe( - (ExpandableView) viewBefore, - (ExpandableView) viewSwiped, - (ExpandableView) viewAfter); + targets.getBefore(), + targets.getSwiped(), + targets.getAfter()); updateFirstAndLastBackgroundViews(); requestDisallowInterceptTouchEvent(true); @@ -5441,7 +5421,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable void onSwipeEnd() { updateFirstAndLastBackgroundViews(); - mController.getNoticationRoundessManager() + mController.getNotificationRoundnessManager() .setViewsAffectedBySwipe(null, null, null); // Round bottom corners for notification right before shelf. mShelf.updateAppearance(); @@ -6176,7 +6156,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) - private ExpandHelper.Callback mExpandHelperCallback = new ExpandHelper.Callback() { + private final ExpandHelper.Callback mExpandHelperCallback = new ExpandHelper.Callback() { @Override public ExpandableView getChildAtPosition(float touchX, float touchY) { return NotificationStackScrollLayout.this.getChildAtPosition(touchX, touchY); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 843a9ff2acb5..e13378269ba6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -56,13 +56,14 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.ExpandHelper; import com.android.systemui.Gefingerpoken; -import com.android.systemui.R; import com.android.systemui.SwipeHelper; import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.KeyguardMediaController; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener; @@ -178,6 +179,8 @@ public class NotificationStackScrollLayoutController { @Nullable private Boolean mHistoryEnabled; private int mBarState; private HeadsUpAppearanceController mHeadsUpAppearanceController; + private final FeatureFlags mFeatureFlags; + private final NotificationTargetsHelper mNotificationTargetsHelper; private View mLongPressedView; @@ -639,7 +642,9 @@ public class NotificationStackScrollLayoutController { InteractionJankMonitor jankMonitor, StackStateLogger stackLogger, NotificationStackScrollLogger logger, - NotificationStackSizeCalculator notificationStackSizeCalculator) { + NotificationStackSizeCalculator notificationStackSizeCalculator, + FeatureFlags featureFlags, + NotificationTargetsHelper notificationTargetsHelper) { mStackStateLogger = stackLogger; mLogger = logger; mAllowLongPress = allowLongPress; @@ -675,6 +680,8 @@ public class NotificationStackScrollLayoutController { mUiEventLogger = uiEventLogger; mRemoteInputManager = remoteInputManager; mShadeController = shadeController; + mFeatureFlags = featureFlags; + mNotificationTargetsHelper = notificationTargetsHelper; updateResources(); } @@ -739,9 +746,7 @@ public class NotificationStackScrollLayoutController { mLockscreenUserManager.addUserChangedListener(mLockscreenUserChangeListener); - mFadeNotificationsOnDismiss = // TODO: this should probably be injected directly - mResources.getBoolean(R.bool.config_fadeNotificationsOnDismiss); - + mFadeNotificationsOnDismiss = mFeatureFlags.isEnabled(Flags.NOTIFICATION_DISMISSAL_FADE); mNotificationRoundnessManager.setOnRoundingChangedCallback(mView::invalidate); mView.addOnExpandedHeightChangedListener(mNotificationRoundnessManager::setExpanded); @@ -1378,7 +1383,7 @@ public class NotificationStackScrollLayoutController { return mView.calculateGapHeight(previousView, child, count); } - NotificationRoundnessManager getNoticationRoundessManager() { + NotificationRoundnessManager getNotificationRoundnessManager() { return mNotificationRoundnessManager; } @@ -1535,6 +1540,10 @@ public class NotificationStackScrollLayoutController { mNotificationActivityStarter = activityStarter; } + public NotificationTargetsHelper getNotificationTargetsHelper() { + return mNotificationTargetsHelper; + } + /** * Enum for UiEvent logged from this class */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt index 5f79c0e3913a..4c52db7f8732 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.stack -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java index 2d2fbe588728..ee57411cb495 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java @@ -185,6 +185,13 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc return false; } + @Override + protected void updateSwipeProgressAlpha(View animView, float alpha) { + if (animView instanceof ExpandableNotificationRow) { + ((ExpandableNotificationRow) animView).setContentAlpha(alpha); + } + } + @VisibleForTesting protected void handleMenuRowSwipe(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt new file mode 100644 index 000000000000..991a14bb9c2a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt @@ -0,0 +1,100 @@ +package com.android.systemui.statusbar.notification.stack + +import androidx.core.view.children +import androidx.core.view.isVisible +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.statusbar.notification.Roundable +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.ExpandableView +import javax.inject.Inject + +/** + * Utility class that helps us find the targets of an animation, often used to find the notification + * ([Roundable]) above and below the current one (see [findRoundableTargets]). + */ +@SysUISingleton +class NotificationTargetsHelper +@Inject +constructor( + featureFlags: FeatureFlags, +) { + private val isNotificationGroupCornerEnabled = + featureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER) + + /** + * This method looks for views that can be rounded (and implement [Roundable]) during a + * notification swipe. + * @return The [Roundable] targets above/below the [viewSwiped] (if available). The + * [RoundableTargets.before] and [RoundableTargets.after] parameters can be `null` if there is + * no above/below notification or the notification is not part of the same section. + */ + fun findRoundableTargets( + viewSwiped: ExpandableNotificationRow, + stackScrollLayout: NotificationStackScrollLayout, + sectionsManager: NotificationSectionsManager, + ): RoundableTargets { + val viewBefore: Roundable? + val viewAfter: Roundable? + + val notificationParent = viewSwiped.notificationParent + val childrenContainer = notificationParent?.childrenContainer + val visibleStackChildren = + stackScrollLayout.children + .filterIsInstance<ExpandableView>() + .filter { it.isVisible } + .toList() + if (notificationParent != null && childrenContainer != null) { + // We are inside a notification group + + if (!isNotificationGroupCornerEnabled) { + return RoundableTargets(null, null, null) + } + + val visibleGroupChildren = childrenContainer.attachedChildren.filter { it.isVisible } + val indexOfParentSwipedView = visibleGroupChildren.indexOf(viewSwiped) + + viewBefore = + visibleGroupChildren.getOrNull(indexOfParentSwipedView - 1) + ?: childrenContainer.notificationHeaderWrapper + + viewAfter = + visibleGroupChildren.getOrNull(indexOfParentSwipedView + 1) + ?: visibleStackChildren.indexOf(notificationParent).let { + visibleStackChildren.getOrNull(it + 1) + } + } else { + // Assumption: we are inside the NotificationStackScrollLayout + + val indexOfSwipedView = visibleStackChildren.indexOf(viewSwiped) + + viewBefore = + visibleStackChildren.getOrNull(indexOfSwipedView - 1)?.takeIf { + !sectionsManager.beginsSection(viewSwiped, it) + } + + viewAfter = + visibleStackChildren.getOrNull(indexOfSwipedView + 1)?.takeIf { + !sectionsManager.beginsSection(it, viewSwiped) + } + } + + return RoundableTargets( + before = viewBefore, + swiped = viewSwiped, + after = viewAfter, + ) + } +} + +/** + * This object contains targets above/below the [swiped] (if available). The [before] and [after] + * parameters can be `null` if there is no above/below notification or the notification is not part + * of the same section. + */ +data class RoundableTargets( + val before: Roundable?, + val swiped: ExpandableNotificationRow?, + val after: Roundable?, +) 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 eeed07014c11..eea1d9118fb7 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 @@ -31,6 +31,7 @@ import com.android.systemui.R; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; +import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; @@ -134,23 +135,23 @@ public class StackScrollAlgorithm { if (isHunGoingToShade) { // Keep 100% opacity for heads up notification going to shade. - viewState.alpha = 1f; + viewState.setAlpha(1f); } else if (ambientState.isOnKeyguard()) { // Adjust alpha for wakeup to lockscreen. - viewState.alpha = 1f - ambientState.getHideAmount(); + viewState.setAlpha(1f - ambientState.getHideAmount()); } else if (ambientState.isExpansionChanging()) { // Adjust alpha for shade open & close. float expansion = ambientState.getExpansionFraction(); - viewState.alpha = ambientState.isBouncerInTransit() + viewState.setAlpha(ambientState.isBouncerInTransit() ? BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(expansion) - : ShadeInterpolation.getContentAlpha(expansion); + : ShadeInterpolation.getContentAlpha(expansion)); } // For EmptyShadeView if on keyguard, we need to control the alpha to create // a nice transition when the user is dragging down the notification panel. if (view instanceof EmptyShadeView && ambientState.isOnKeyguard()) { final float fractionToShade = ambientState.getFractionToShade(); - viewState.alpha = ShadeInterpolation.getContentAlpha(fractionToShade); + viewState.setAlpha(ShadeInterpolation.getContentAlpha(fractionToShade)); } NotificationShelf shelf = ambientState.getShelf(); @@ -166,10 +167,10 @@ public class StackScrollAlgorithm { continue; } - final float shelfTop = shelfState.yTranslation; - final float viewTop = viewState.yTranslation; + final float shelfTop = shelfState.getYTranslation(); + final float viewTop = viewState.getYTranslation(); if (viewTop >= shelfTop) { - viewState.alpha = 0; + viewState.setAlpha(0); } } } @@ -277,7 +278,7 @@ public class StackScrollAlgorithm { if (!child.mustStayOnScreen() || state.headsUpIsVisible) { clipStart = Math.max(drawStart, clipStart); } - float newYTranslation = state.yTranslation; + float newYTranslation = state.getYTranslation(); float newHeight = state.height; float newNotificationEnd = newYTranslation + newHeight; boolean isHeadsUp = (child instanceof ExpandableNotificationRow) && child.isPinned(); @@ -322,7 +323,8 @@ public class StackScrollAlgorithm { childViewState.hideSensitive = hideSensitive; boolean isActivatedChild = activatedChild == child; if (dimmed && isActivatedChild) { - childViewState.zTranslation += 2.0f * ambientState.getZDistanceBetweenElements(); + childViewState.setZTranslation(childViewState.getZTranslation() + + 2.0f * ambientState.getZDistanceBetweenElements()); } } } @@ -416,12 +418,19 @@ public class StackScrollAlgorithm { } /** + * Update the position of QS Frame. + */ + public void updateQSFrameTop(int qsHeight) { + // Intentionally empty for sub-classes in other device form factors to override + } + + /** * Determine the positions for the views. This is the main part of the algorithm. * * @param algorithmState The state in which the current pass of the algorithm is currently in * @param ambientState The current ambient state */ - private void updatePositionsForState(StackScrollAlgorithmState algorithmState, + protected void updatePositionsForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState) { if (!ambientState.isOnKeyguard() || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) { @@ -447,7 +456,7 @@ public class StackScrollAlgorithm { * @return Fraction to apply to view height and gap between views. * Does not include shelf height even if shelf is showing. */ - private float getExpansionFractionWithoutShelf( + protected float getExpansionFractionWithoutShelf( StackScrollAlgorithmState algorithmState, AmbientState ambientState) { @@ -527,12 +536,12 @@ public class StackScrollAlgorithm { // Must set viewState.yTranslation _before_ use. // Incoming views have yTranslation=0 by default. - viewState.yTranslation = algorithmState.mCurrentYPosition; + viewState.setYTranslation(algorithmState.mCurrentYPosition); + float viewEnd = viewState.getYTranslation() + viewState.height + ambientState.getStackY(); maybeUpdateHeadsUpIsVisible(viewState, ambientState.isShadeExpanded(), - view.mustStayOnScreen(), /* topVisible */ viewState.yTranslation >= 0, - /* viewEnd */ viewState.yTranslation + viewState.height + ambientState.getStackY(), - /* hunMax */ ambientState.getMaxHeadsUpTranslation() + view.mustStayOnScreen(), /* topVisible */ viewState.getYTranslation() >= 0, + viewEnd, /* hunMax */ ambientState.getMaxHeadsUpTranslation() ); if (view instanceof FooterView) { final boolean shadeClosed = !ambientState.isShadeExpanded(); @@ -552,7 +561,7 @@ public class StackScrollAlgorithm { if (view instanceof EmptyShadeView) { float fullHeight = ambientState.getLayoutMaxHeight() + mMarginBottom - ambientState.getStackY(); - viewState.yTranslation = (fullHeight - getMaxAllowedChildHeight(view)) / 2f; + viewState.setYTranslation((fullHeight - getMaxAllowedChildHeight(view)) / 2f); } else if (view != ambientState.getTrackedHeadsUpRow()) { if (ambientState.isExpansionChanging()) { // We later update shelf state, then hide views below the shelf. @@ -591,13 +600,13 @@ public class StackScrollAlgorithm { + mPaddingBetweenElements; setLocation(view.getViewState(), algorithmState.mCurrentYPosition, i); - viewState.yTranslation += ambientState.getStackY(); + viewState.setYTranslation(viewState.getYTranslation() + ambientState.getStackY()); } @VisibleForTesting void updateViewWithShelf(ExpandableView view, ExpandableViewState viewState, float shelfStart) { - viewState.yTranslation = Math.min(viewState.yTranslation, shelfStart); - if (viewState.yTranslation >= shelfStart) { + viewState.setYTranslation(Math.min(viewState.getYTranslation(), shelfStart)); + if (viewState.getYTranslation() >= shelfStart) { viewState.hidden = !view.isExpandAnimationRunning() && !view.hasExpandingChild(); viewState.inShelf = true; @@ -690,9 +699,9 @@ public class StackScrollAlgorithm { if (trackedHeadsUpRow != null) { ExpandableViewState childState = trackedHeadsUpRow.getViewState(); if (childState != null) { - float endPosition = childState.yTranslation - ambientState.getStackTranslation(); - childState.yTranslation = MathUtils.lerp( - headsUpTranslation, endPosition, ambientState.getAppearFraction()); + float endPos = childState.getYTranslation() - ambientState.getStackTranslation(); + childState.setYTranslation(MathUtils.lerp( + headsUpTranslation, endPos, ambientState.getAppearFraction())); } } @@ -712,7 +721,7 @@ public class StackScrollAlgorithm { childState.location = ExpandableViewState.LOCATION_FIRST_HUN; } boolean isTopEntry = topHeadsUpEntry == row; - float unmodifiedEndLocation = childState.yTranslation + childState.height; + float unmodifiedEndLocation = childState.getYTranslation() + childState.height; if (mIsExpanded) { if (row.mustStayOnScreen() && !childState.headsUpIsVisible && !row.showingPulsing()) { @@ -727,13 +736,14 @@ public class StackScrollAlgorithm { } } if (row.isPinned()) { - childState.yTranslation = Math.max(childState.yTranslation, headsUpTranslation); + childState.setYTranslation( + Math.max(childState.getYTranslation(), headsUpTranslation)); childState.height = Math.max(row.getIntrinsicHeight(), childState.height); childState.hidden = false; ExpandableViewState topState = topHeadsUpEntry == null ? null : topHeadsUpEntry.getViewState(); if (topState != null && !isTopEntry && (!mIsExpanded - || unmodifiedEndLocation > topState.yTranslation + topState.height)) { + || unmodifiedEndLocation > topState.getYTranslation() + topState.height)) { // Ensure that a headsUp doesn't vertically extend further than the heads-up at // the top most z-position childState.height = row.getIntrinsicHeight(); @@ -745,11 +755,12 @@ public class StackScrollAlgorithm { // heads up show full of row's content and any scroll y indicate that the // translationY need to move up the HUN. if (!mIsExpanded && isTopEntry && ambientState.getScrollY() > 0) { - childState.yTranslation -= ambientState.getScrollY(); + childState.setYTranslation( + childState.getYTranslation() - ambientState.getScrollY()); } } if (row.isHeadsUpAnimatingAway()) { - childState.yTranslation = Math.max(childState.yTranslation, mHeadsUpInset); + childState.setYTranslation(Math.max(childState.getYTranslation(), mHeadsUpInset)); childState.hidden = false; } } @@ -765,13 +776,13 @@ public class StackScrollAlgorithm { ExpandableViewState viewState) { final float newTranslation = Math.max(quickQsOffsetHeight + stackTranslation, - viewState.yTranslation); + viewState.getYTranslation()); // Transition from collapsed pinned state to fully expanded state // when the pinned HUN approaches its actual location (when scrolling back to top). - final float distToRealY = newTranslation - viewState.yTranslation; + final float distToRealY = newTranslation - viewState.getYTranslation(); viewState.height = (int) Math.max(viewState.height - distToRealY, collapsedHeight); - viewState.yTranslation = newTranslation; + viewState.setYTranslation(newTranslation); } // Pin HUN to bottom of expanded QS @@ -784,17 +795,17 @@ public class StackScrollAlgorithm { maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition); final float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight(); - final float newTranslation = Math.min(childState.yTranslation, bottomPosition); + final float newTranslation = Math.min(childState.getYTranslation(), bottomPosition); childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation - newTranslation); - childState.yTranslation = newTranslation; + childState.setYTranslation(newTranslation); // Animate pinned HUN bottom corners to and from original roundness. final float originalCornerRadius = row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius); final float roundness = computeCornerRoundnessForPinnedHun(mHostView.getHeight(), ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius); - row.setBottomRoundness(roundness, /* animate= */ false); + row.requestBottomRoundness(roundness, /* animate = */ false, SourceType.OnScroll); } @VisibleForTesting @@ -859,17 +870,17 @@ public class StackScrollAlgorithm { float baseZ = ambientState.getBaseZHeight(); if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible && !ambientState.isDozingAndNotPulsing(child) - && childViewState.yTranslation < ambientState.getTopPadding() + && childViewState.getYTranslation() < ambientState.getTopPadding() + ambientState.getStackTranslation()) { if (childrenOnTop != 0.0f) { childrenOnTop++; } else { float overlap = ambientState.getTopPadding() - + ambientState.getStackTranslation() - childViewState.yTranslation; + + ambientState.getStackTranslation() - childViewState.getYTranslation(); childrenOnTop += Math.min(1.0f, overlap / childViewState.height); } - childViewState.zTranslation = baseZ - + childrenOnTop * zDistanceBetweenElements; + childViewState.setZTranslation(baseZ + + childrenOnTop * zDistanceBetweenElements); } else if (shouldElevateHun) { // In case this is a new view that has never been measured before, we don't want to // elevate if we are currently expanded more then the notification @@ -878,25 +889,28 @@ public class StackScrollAlgorithm { float shelfStart = ambientState.getInnerHeight() - shelfHeight + ambientState.getTopPadding() + ambientState.getStackTranslation(); - float notificationEnd = childViewState.yTranslation + child.getIntrinsicHeight() + float notificationEnd = childViewState.getYTranslation() + child.getIntrinsicHeight() + mPaddingBetweenElements; if (shelfStart > notificationEnd) { - childViewState.zTranslation = baseZ; + childViewState.setZTranslation(baseZ); } else { float factor = (notificationEnd - shelfStart) / shelfHeight; + if (Float.isNaN(factor)) { // Avoid problems when the above is 0/0. + factor = 1.0f; + } factor = Math.min(factor, 1.0f); - childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements; + childViewState.setZTranslation(baseZ + factor * zDistanceBetweenElements); } } else { - childViewState.zTranslation = baseZ; + childViewState.setZTranslation(baseZ); } // We need to scrim the notification more from its surrounding content when we are pinned, // and we therefore elevate it higher. // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when // expanding after which we have a normal elevation again. - childViewState.zTranslation += (1.0f - child.getHeaderVisibleAmount()) - * mPinnedZTranslationExtra; + childViewState.setZTranslation(childViewState.getZTranslation() + + (1.0f - child.getHeaderVisibleAmount()) * mPinnedZTranslationExtra); return childrenOnTop; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index 174bf4c27de8..ee72943bef57 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -169,9 +169,9 @@ public class StackStateAnimator { adaptDurationWhenGoingToFullShade(child, viewState, wasAdded, animationStaggerCount); mAnimationProperties.delay = 0; if (wasAdded || mAnimationFilter.hasDelays - && (viewState.yTranslation != child.getTranslationY() - || viewState.zTranslation != child.getTranslationZ() - || viewState.alpha != child.getAlpha() + && (viewState.getYTranslation() != child.getTranslationY() + || viewState.getZTranslation() != child.getTranslationZ() + || viewState.getAlpha() != child.getAlpha() || viewState.height != child.getActualHeight() || viewState.clipTopAmount != child.getClipTopAmount())) { mAnimationProperties.delay = mCurrentAdditionalDelay @@ -191,7 +191,7 @@ public class StackStateAnimator { mAnimationProperties.duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 + (long) (100 * longerDurationFactor); } - child.setTranslationY(viewState.yTranslation + startOffset); + child.setTranslationY(viewState.getYTranslation() + startOffset); } } @@ -400,7 +400,7 @@ public class StackStateAnimator { // travelled ExpandableViewState viewState = ((ExpandableView) event.viewAfterChangingView).getViewState(); - translationDirection = ((viewState.yTranslation + translationDirection = ((viewState.getYTranslation() - (ownPosition + actualHeight / 2.0f)) * 2 / actualHeight); translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f); @@ -433,7 +433,7 @@ public class StackStateAnimator { ExpandableViewState viewState = changingView.getViewState(); mTmpState.copyFrom(viewState); if (event.headsUpFromBottom) { - mTmpState.yTranslation = mHeadsUpAppearHeightBottom; + mTmpState.setYTranslation(mHeadsUpAppearHeightBottom); } else { Runnable onAnimationEnd = null; if (loggable) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt index cb4a0884fea4..f5de678a8536 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt @@ -1,8 +1,8 @@ package com.android.systemui.statusbar.notification.stack -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java index 786de29a5819..d07da381a186 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; +import android.util.Log; import android.util.Property; import android.view.View; import android.view.animation.Interpolator; @@ -42,7 +43,7 @@ import java.lang.reflect.Modifier; * A state of a view. This can be used to apply a set of view properties to a view with * {@link com.android.systemui.statusbar.notification.stack.StackScrollState} or start * animations with {@link com.android.systemui.statusbar.notification.stack.StackStateAnimator}. -*/ + */ public class ViewState implements Dumpable { /** @@ -51,6 +52,7 @@ public class ViewState implements Dumpable { */ protected static final AnimationProperties NO_NEW_ANIMATIONS = new AnimationProperties() { AnimationFilter mAnimationFilter = new AnimationFilter(); + @Override public AnimationFilter getAnimationFilter() { return mAnimationFilter; @@ -68,6 +70,7 @@ public class ViewState implements Dumpable { private static final int TAG_START_TRANSLATION_Y = R.id.translation_y_animator_start_value_tag; private static final int TAG_START_TRANSLATION_Z = R.id.translation_z_animator_start_value_tag; private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag; + private static final String LOG_TAG = "StackViewState"; private static final AnimatableProperty SCALE_X_PROPERTY = new AnimatableProperty() { @@ -117,35 +120,127 @@ public class ViewState implements Dumpable { } }; - public float alpha; - public float xTranslation; - public float yTranslation; - public float zTranslation; public boolean gone; public boolean hidden; - public float scaleX = 1.0f; - public float scaleY = 1.0f; + + private float mAlpha; + private float mXTranslation; + private float mYTranslation; + private float mZTranslation; + private float mScaleX = 1.0f; + private float mScaleY = 1.0f; + + public float getAlpha() { + return mAlpha; + } + + /** + * @param alpha View transparency. + */ + public void setAlpha(float alpha) { + if (isValidFloat(alpha, "alpha")) { + this.mAlpha = alpha; + } + } + + public float getXTranslation() { + return mXTranslation; + } + + /** + * @param xTranslation x-axis translation value for the animation. + */ + public void setXTranslation(float xTranslation) { + if (isValidFloat(xTranslation, "xTranslation")) { + this.mXTranslation = xTranslation; + } + } + + public float getYTranslation() { + return mYTranslation; + } + + /** + * @param yTranslation y-axis translation value for the animation. + */ + public void setYTranslation(float yTranslation) { + if (isValidFloat(yTranslation, "yTranslation")) { + this.mYTranslation = yTranslation; + } + } + + public float getZTranslation() { + return mZTranslation; + } + + + /** + * @param zTranslation z-axis translation value for the animation. + */ + public void setZTranslation(float zTranslation) { + if (isValidFloat(zTranslation, "zTranslation")) { + this.mZTranslation = zTranslation; + } + } + + public float getScaleX() { + return mScaleX; + } + + /** + * @param scaleX x-axis scale property for the animation. + */ + public void setScaleX(float scaleX) { + if (isValidFloat(scaleX, "scaleX")) { + this.mScaleX = scaleX; + } + } + + public float getScaleY() { + return mScaleY; + } + + /** + * @param scaleY y-axis scale property for the animation. + */ + public void setScaleY(float scaleY) { + if (isValidFloat(scaleY, "scaleY")) { + this.mScaleY = scaleY; + } + } + + /** + * Checks if {@code value} is a valid float value. If it is not, logs it (using {@code name}) + * and returns false. + */ + private boolean isValidFloat(float value, String name) { + if (Float.isNaN(value)) { + Log.wtf(LOG_TAG, "Cannot set property " + name + " to NaN"); + return false; + } + return true; + } public void copyFrom(ViewState viewState) { - alpha = viewState.alpha; - xTranslation = viewState.xTranslation; - yTranslation = viewState.yTranslation; - zTranslation = viewState.zTranslation; + mAlpha = viewState.mAlpha; + mXTranslation = viewState.mXTranslation; + mYTranslation = viewState.mYTranslation; + mZTranslation = viewState.mZTranslation; gone = viewState.gone; hidden = viewState.hidden; - scaleX = viewState.scaleX; - scaleY = viewState.scaleY; + mScaleX = viewState.mScaleX; + mScaleY = viewState.mScaleY; } public void initFrom(View view) { - alpha = view.getAlpha(); - xTranslation = view.getTranslationX(); - yTranslation = view.getTranslationY(); - zTranslation = view.getTranslationZ(); + mAlpha = view.getAlpha(); + mXTranslation = view.getTranslationX(); + mYTranslation = view.getTranslationY(); + mZTranslation = view.getTranslationZ(); gone = view.getVisibility() == View.GONE; hidden = view.getVisibility() == View.INVISIBLE; - scaleX = view.getScaleX(); - scaleY = view.getScaleY(); + mScaleX = view.getScaleX(); + mScaleY = view.getScaleY(); } /** @@ -161,51 +256,51 @@ public class ViewState implements Dumpable { boolean animatingX = isAnimating(view, TAG_ANIMATOR_TRANSLATION_X); if (animatingX) { updateAnimationX(view); - } else if (view.getTranslationX() != this.xTranslation){ - view.setTranslationX(this.xTranslation); + } else if (view.getTranslationX() != this.mXTranslation) { + view.setTranslationX(this.mXTranslation); } // apply yTranslation boolean animatingY = isAnimating(view, TAG_ANIMATOR_TRANSLATION_Y); if (animatingY) { updateAnimationY(view); - } else if (view.getTranslationY() != this.yTranslation) { - view.setTranslationY(this.yTranslation); + } else if (view.getTranslationY() != this.mYTranslation) { + view.setTranslationY(this.mYTranslation); } // apply zTranslation boolean animatingZ = isAnimating(view, TAG_ANIMATOR_TRANSLATION_Z); if (animatingZ) { updateAnimationZ(view); - } else if (view.getTranslationZ() != this.zTranslation) { - view.setTranslationZ(this.zTranslation); + } else if (view.getTranslationZ() != this.mZTranslation) { + view.setTranslationZ(this.mZTranslation); } // apply scaleX boolean animatingScaleX = isAnimating(view, SCALE_X_PROPERTY); if (animatingScaleX) { - updateAnimation(view, SCALE_X_PROPERTY, scaleX); - } else if (view.getScaleX() != scaleX) { - view.setScaleX(scaleX); + updateAnimation(view, SCALE_X_PROPERTY, mScaleX); + } else if (view.getScaleX() != mScaleX) { + view.setScaleX(mScaleX); } // apply scaleY boolean animatingScaleY = isAnimating(view, SCALE_Y_PROPERTY); if (animatingScaleY) { - updateAnimation(view, SCALE_Y_PROPERTY, scaleY); - } else if (view.getScaleY() != scaleY) { - view.setScaleY(scaleY); + updateAnimation(view, SCALE_Y_PROPERTY, mScaleY); + } else if (view.getScaleY() != mScaleY) { + view.setScaleY(mScaleY); } int oldVisibility = view.getVisibility(); - boolean becomesInvisible = this.alpha == 0.0f + boolean becomesInvisible = this.mAlpha == 0.0f || (this.hidden && (!isAnimating(view) || oldVisibility != View.VISIBLE)); boolean animatingAlpha = isAnimating(view, TAG_ANIMATOR_ALPHA); if (animatingAlpha) { updateAlphaAnimation(view); - } else if (view.getAlpha() != this.alpha) { + } else if (view.getAlpha() != this.mAlpha) { // apply layer type - boolean becomesFullyVisible = this.alpha == 1.0f; + boolean becomesFullyVisible = this.mAlpha == 1.0f; boolean becomesFaded = !becomesInvisible && !becomesFullyVisible; if (FadeOptimizedNotification.FADE_LAYER_OPTIMIZATION_ENABLED && view instanceof FadeOptimizedNotification) { @@ -229,7 +324,7 @@ public class ViewState implements Dumpable { } // apply alpha - view.setAlpha(this.alpha); + view.setAlpha(this.mAlpha); } // apply visibility @@ -274,54 +369,55 @@ public class ViewState implements Dumpable { /** * Start an animation to this viewstate - * @param child the view to animate + * + * @param child the view to animate * @param animationProperties the properties of the animation */ public void animateTo(View child, AnimationProperties animationProperties) { boolean wasVisible = child.getVisibility() == View.VISIBLE; - final float alpha = this.alpha; + final float alpha = this.mAlpha; if (!wasVisible && (alpha != 0 || child.getAlpha() != 0) && !this.gone && !this.hidden) { child.setVisibility(View.VISIBLE); } float childAlpha = child.getAlpha(); - boolean alphaChanging = this.alpha != childAlpha; + boolean alphaChanging = this.mAlpha != childAlpha; if (child instanceof ExpandableView) { // We don't want views to change visibility when they are animating to GONE alphaChanging &= !((ExpandableView) child).willBeGone(); } // start translationX animation - if (child.getTranslationX() != this.xTranslation) { + if (child.getTranslationX() != this.mXTranslation) { startXTranslationAnimation(child, animationProperties); } else { abortAnimation(child, TAG_ANIMATOR_TRANSLATION_X); } // start translationY animation - if (child.getTranslationY() != this.yTranslation) { + if (child.getTranslationY() != this.mYTranslation) { startYTranslationAnimation(child, animationProperties); } else { abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Y); } // start translationZ animation - if (child.getTranslationZ() != this.zTranslation) { + if (child.getTranslationZ() != this.mZTranslation) { startZTranslationAnimation(child, animationProperties); } else { abortAnimation(child, TAG_ANIMATOR_TRANSLATION_Z); } // start scaleX animation - if (child.getScaleX() != scaleX) { - PropertyAnimator.startAnimation(child, SCALE_X_PROPERTY, scaleX, animationProperties); + if (child.getScaleX() != mScaleX) { + PropertyAnimator.startAnimation(child, SCALE_X_PROPERTY, mScaleX, animationProperties); } else { abortAnimation(child, SCALE_X_PROPERTY.getAnimatorTag()); } // start scaleX animation - if (child.getScaleY() != scaleY) { - PropertyAnimator.startAnimation(child, SCALE_Y_PROPERTY, scaleY, animationProperties); + if (child.getScaleY() != mScaleY) { + PropertyAnimator.startAnimation(child, SCALE_Y_PROPERTY, mScaleY, animationProperties); } else { abortAnimation(child, SCALE_Y_PROPERTY.getAnimatorTag()); } @@ -329,7 +425,7 @@ public class ViewState implements Dumpable { // start alpha animation if (alphaChanging) { startAlphaAnimation(child, animationProperties); - } else { + } else { abortAnimation(child, TAG_ANIMATOR_ALPHA); } } @@ -339,9 +435,9 @@ public class ViewState implements Dumpable { } private void startAlphaAnimation(final View child, AnimationProperties properties) { - Float previousStartValue = getChildTag(child,TAG_START_ALPHA); - Float previousEndValue = getChildTag(child,TAG_END_ALPHA); - final float newEndValue = this.alpha; + Float previousStartValue = getChildTag(child, TAG_START_ALPHA); + Float previousEndValue = getChildTag(child, TAG_END_ALPHA); + final float newEndValue = this.mAlpha; if (previousEndValue != null && previousEndValue == newEndValue) { return; } @@ -426,9 +522,9 @@ public class ViewState implements Dumpable { } private void startZTranslationAnimation(final View child, AnimationProperties properties) { - Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z); - Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z); - float newEndValue = this.zTranslation; + Float previousStartValue = getChildTag(child, TAG_START_TRANSLATION_Z); + Float previousEndValue = getChildTag(child, TAG_END_TRANSLATION_Z); + float newEndValue = this.mZTranslation; if (previousEndValue != null && previousEndValue == newEndValue) { return; } @@ -487,9 +583,9 @@ public class ViewState implements Dumpable { } private void startXTranslationAnimation(final View child, AnimationProperties properties) { - Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_X); - Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_X); - float newEndValue = this.xTranslation; + Float previousStartValue = getChildTag(child, TAG_START_TRANSLATION_X); + Float previousEndValue = getChildTag(child, TAG_END_TRANSLATION_X); + float newEndValue = this.mXTranslation; if (previousEndValue != null && previousEndValue == newEndValue) { return; } @@ -519,7 +615,7 @@ public class ViewState implements Dumpable { child.getTranslationX(), newEndValue); Interpolator customInterpolator = properties.getCustomInterpolator(child, View.TRANSLATION_X); - Interpolator interpolator = customInterpolator != null ? customInterpolator + Interpolator interpolator = customInterpolator != null ? customInterpolator : Interpolators.FAST_OUT_SLOW_IN; animator.setInterpolator(interpolator); long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); @@ -553,9 +649,9 @@ public class ViewState implements Dumpable { } private void startYTranslationAnimation(final View child, AnimationProperties properties) { - Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y); - Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y); - float newEndValue = this.yTranslation; + Float previousStartValue = getChildTag(child, TAG_START_TRANSLATION_Y); + Float previousEndValue = getChildTag(child, TAG_END_TRANSLATION_Y); + float newEndValue = this.mYTranslation; if (previousEndValue != null && previousEndValue == newEndValue) { return; } @@ -585,7 +681,7 @@ public class ViewState implements Dumpable { child.getTranslationY(), newEndValue); Interpolator customInterpolator = properties.getCustomInterpolator(child, View.TRANSLATION_Y); - Interpolator interpolator = customInterpolator != null ? customInterpolator + Interpolator interpolator = customInterpolator != null ? customInterpolator : Interpolators.FAST_OUT_SLOW_IN; animator.setInterpolator(interpolator); long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); @@ -644,7 +740,7 @@ public class ViewState implements Dumpable { /** * Cancel the previous animator and get the duration of the new animation. * - * @param duration the new duration + * @param duration the new duration * @param previousAnimator the animator which was running before * @return the new duration */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index fe431377f854..a2798f47be65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -163,7 +163,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private PowerManager.WakeLock mWakeLock; private final com.android.systemui.shade.ShadeController mShadeController; private final KeyguardUpdateMonitor mUpdateMonitor; - private final DozeParameters mDozeParameters; private final KeyguardStateController mKeyguardStateController; private final NotificationShadeWindowController mNotificationShadeWindowController; private final SessionTracker mSessionTracker; @@ -278,7 +277,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp KeyguardStateController keyguardStateController, Handler handler, KeyguardUpdateMonitor keyguardUpdateMonitor, @Main Resources resources, - KeyguardBypassController keyguardBypassController, DozeParameters dozeParameters, + KeyguardBypassController keyguardBypassController, MetricsLogger metricsLogger, DumpManager dumpManager, PowerManager powerManager, NotificationMediaManager notificationMediaManager, @@ -294,7 +293,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mPowerManager = powerManager; mShadeController = shadeController; mUpdateMonitor = keyguardUpdateMonitor; - mDozeParameters = dozeParameters; mUpdateMonitor.registerCallback(this); mMediaManager = notificationMediaManager; mLatencyTracker = latencyTracker; @@ -552,7 +550,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp boolean deviceDreaming = mUpdateMonitor.isDreaming(); if (!mUpdateMonitor.isDeviceInteractive()) { - if (!mKeyguardViewController.isShowing() + if (!mKeyguardStateController.isShowing() && !mScreenOffAnimationController.isKeyguardShowDelayed()) { if (mKeyguardStateController.isUnlocked()) { return MODE_WAKE_AND_UNLOCK; @@ -569,7 +567,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp if (unlockingAllowed && deviceDreaming) { return MODE_WAKE_AND_UNLOCK_FROM_DREAM; } - if (mKeyguardViewController.isShowing()) { + if (mKeyguardStateController.isShowing()) { if (mKeyguardViewController.bouncerIsOrWillBeShowing() && unlockingAllowed) { return MODE_DISMISS_BOUNCER; } else if (unlockingAllowed) { @@ -588,7 +586,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp boolean bypass = mKeyguardBypassController.getBypassEnabled() || mAuthController.isUdfpsFingerDown(); if (!mUpdateMonitor.isDeviceInteractive()) { - if (!mKeyguardViewController.isShowing()) { + if (!mKeyguardStateController.isShowing()) { return bypass ? MODE_WAKE_AND_UNLOCK : MODE_ONLY_WAKE; } else if (!unlockingAllowed) { return bypass ? MODE_SHOW_BOUNCER : MODE_NONE; @@ -612,7 +610,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp if (unlockingAllowed && mKeyguardStateController.isOccluded()) { return MODE_UNLOCK_COLLAPSING; } - if (mKeyguardViewController.isShowing()) { + if (mKeyguardStateController.isShowing()) { if ((mKeyguardViewController.bouncerIsOrWillBeShowing() || mKeyguardBypassController.getAltBouncerShowing()) && unlockingAllowed) { return MODE_DISMISS_BOUNCER; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index f37243adfa9e..169c90780926 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -262,8 +262,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn @Override void startActivity(Intent intent, boolean dismissShade, Callback callback); - void setQsExpanded(boolean expanded); - boolean isWakeUpComingFromTouch(); boolean isFalsingThresholdNeeded(); @@ -416,6 +414,9 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void endAffordanceLaunch(); + /** Should the keyguard be hidden immediately in response to a back press/gesture. */ + boolean shouldKeyguardHideImmediately(); + boolean onBackPressed(); boolean onSpacePressed(); @@ -452,6 +453,9 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn void collapseShade(); + /** Collapse the shade, but conditional on a flag specific to the trigger of a bugreport. */ + void collapseShadeForBugreport(); + int getWakefulnessState(); boolean isScreenFullyOff(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 52a45d6785ca..1e95dad6b115 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -363,7 +363,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba mKeyguardUpdateMonitor.onCameraLaunched(); } - if (!mStatusBarKeyguardViewManager.isShowing()) { + if (!mKeyguardStateController.isShowing()) { final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext); mCentralSurfaces.startActivityDismissingKeyguard(cameraIntent, false /* onlyProvisioned */, true /* dismissShade */, @@ -420,7 +420,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba // TODO(b/169087248) Possibly add haptics here for emergency action. Currently disabled for // app-side haptic experimentation. - if (!mStatusBarKeyguardViewManager.isShowing()) { + if (!mKeyguardStateController.isShowing()) { mCentralSurfaces.startActivityDismissingKeyguard(emergencyIntent, false /* onlyProvisioned */, true /* dismissShade */, true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index a77a6000b73b..bbbb66ea4988 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -91,7 +91,6 @@ import android.util.EventLog; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; -import android.util.Slog; import android.view.Display; import android.view.IRemoteAnimationRunner; import android.view.IWindowManager; @@ -183,6 +182,8 @@ import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeExpansionChangeEvent; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.statusbar.AutoHideUiElement; import com.android.systemui.statusbar.BackDropView; @@ -221,8 +222,6 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.phone.dagger.StatusBarPhoneModule; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -270,8 +269,7 @@ import dagger.Lazy; * </b> */ @SysUISingleton -public class CentralSurfacesImpl extends CoreStartable implements - CentralSurfaces { +public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private static final String BANNER_ACTION_CANCEL = "com.android.systemui.statusbar.banner_action_cancel"; @@ -307,6 +305,7 @@ public class CentralSurfacesImpl extends CoreStartable implements ONLY_CORE_APPS = onlyCoreApps; } + private final Context mContext; private final LockscreenShadeTransitionController mLockscreenShadeTransitionController; private CentralSurfacesCommandQueueCallbacks mCommandQueueCallbacks; private float mTransitionToFullShadeProgress = 0f; @@ -476,7 +475,6 @@ public class CentralSurfacesImpl extends CoreStartable implements private final KeyguardStateController mKeyguardStateController; private final HeadsUpManagerPhone mHeadsUpManager; private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager; - private final DynamicPrivacyController mDynamicPrivacyController; private final FalsingCollector mFalsingCollector; private final FalsingManager mFalsingManager; private final BroadcastDispatcher mBroadcastDispatcher; @@ -514,7 +512,7 @@ public class CentralSurfacesImpl extends CoreStartable implements private final NotificationGutsManager mGutsManager; private final NotificationLogger mNotificationLogger; - private final PanelExpansionStateManager mPanelExpansionStateManager; + private final ShadeExpansionStateManager mShadeExpansionStateManager; private final KeyguardViewMediator mKeyguardViewMediator; protected final NotificationInterruptStateProvider mNotificationInterruptStateProvider; private final BrightnessSliderController.Factory mBrightnessSliderFactory; @@ -700,7 +698,7 @@ public class CentralSurfacesImpl extends CoreStartable implements NotificationGutsManager notificationGutsManager, NotificationLogger notificationLogger, NotificationInterruptStateProvider notificationInterruptStateProvider, - PanelExpansionStateManager panelExpansionStateManager, + ShadeExpansionStateManager shadeExpansionStateManager, KeyguardViewMediator keyguardViewMediator, DisplayMetrics displayMetrics, MetricsLogger metricsLogger, @@ -765,7 +763,7 @@ public class CentralSurfacesImpl extends CoreStartable implements DeviceStateManager deviceStateManager, WiredChargingRippleController wiredChargingRippleController, IDreamManager dreamManager) { - super(context); + mContext = context; mNotificationsController = notificationsController; mFragmentService = fragmentService; mLightBarController = lightBarController; @@ -779,14 +777,13 @@ public class CentralSurfacesImpl extends CoreStartable implements mHeadsUpManager = headsUpManagerPhone; mKeyguardIndicationController = keyguardIndicationController; mStatusBarTouchableRegionManager = statusBarTouchableRegionManager; - mDynamicPrivacyController = dynamicPrivacyController; mFalsingCollector = falsingCollector; mFalsingManager = falsingManager; mBroadcastDispatcher = broadcastDispatcher; mGutsManager = notificationGutsManager; mNotificationLogger = notificationLogger; mNotificationInterruptStateProvider = notificationInterruptStateProvider; - mPanelExpansionStateManager = panelExpansionStateManager; + mShadeExpansionStateManager = shadeExpansionStateManager; mKeyguardViewMediator = keyguardViewMediator; mDisplayMetrics = displayMetrics; mMetricsLogger = metricsLogger; @@ -851,7 +848,7 @@ public class CentralSurfacesImpl extends CoreStartable implements mScreenOffAnimationController = screenOffAnimationController; - mPanelExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged); + mShadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged); mBubbleExpandListener = (isExpanding, key) -> mContext.getMainExecutor().execute(this::updateScrimController); @@ -888,6 +885,11 @@ public class CentralSurfacesImpl extends CoreStartable implements mBubblesOptional.get().setExpandListener(mBubbleExpandListener); } + // Do not restart System UI when the bugreport flag changes. + mFeatureFlags.addListener(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, event -> { + event.requestNoRestart(); + }); + mStatusBarSignalPolicy.init(); mKeyguardIndicationController.init(); @@ -1139,9 +1141,8 @@ public class CentralSurfacesImpl extends CoreStartable implements // TODO: Deal with the ugliness that comes from having some of the status bar broken out // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot. - mNotificationLogger.setUpWithContainer(mNotifListContainer); mNotificationIconAreaController.setupShelf(mNotificationShelfController); - mPanelExpansionStateManager.addExpansionListener(mWakeUpCoordinator); + mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator); mUserSwitcherController.init(mNotificationShadeWindowView); // Allow plugins to reference DarkIconDispatcher and StatusBarStateController @@ -1377,7 +1378,7 @@ public class CentralSurfacesImpl extends CoreStartable implements } } - private void onPanelExpansionChanged(PanelExpansionChangeEvent event) { + private void onPanelExpansionChanged(ShadeExpansionChangeEvent event) { float fraction = event.getFraction(); boolean tracking = event.getTracking(); dispatchPanelExpansionForKeyguardDismiss(fraction, tracking); @@ -1424,6 +1425,7 @@ public class CentralSurfacesImpl extends CoreStartable implements mStackScrollerController.setNotificationActivityStarter(mNotificationActivityStarter); mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter); mNotificationsController.initialize( + this, mPresenter, mNotifListContainer, mStackScrollerController.getNotifStackController(), @@ -1561,7 +1563,7 @@ public class CentralSurfacesImpl extends CoreStartable implements mKeyguardViewMediator.registerCentralSurfaces( /* statusBar= */ this, mNotificationPanelViewController, - mPanelExpansionStateManager, + mShadeExpansionStateManager, mBiometricUnlockController, mStackScroller, mKeyguardBypassController); @@ -1570,7 +1572,6 @@ public class CentralSurfacesImpl extends CoreStartable implements .setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager); mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); mRemoteInputManager.addControllerCallback(mStatusBarKeyguardViewManager); - mDynamicPrivacyController.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager); mLightBarController.setBiometricUnlockController(mBiometricUnlockController); mMediaManager.setBiometricUnlockController(mBiometricUnlockController); @@ -1794,18 +1795,6 @@ public class CentralSurfacesImpl extends CoreStartable implements } @Override - public void setQsExpanded(boolean expanded) { - mNotificationShadeWindowController.setQsExpanded(expanded); - mNotificationPanelViewController.setStatusAccessibilityImportance(expanded - ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); - mNotificationPanelViewController.updateSystemUiStateFlags(); - if (getNavigationBarView() != null) { - getNavigationBarView().onStatusBarPanelStateChanged(); - } - } - - @Override public boolean isWakeUpComingFromTouch() { return mWakeUpComingFromTouch; } @@ -2082,7 +2071,7 @@ public class CentralSurfacesImpl extends CoreStartable implements // Trimming will happen later if Keyguard is showing - doing it here might cause a jank in // the bouncer appear animation. - if (!mStatusBarKeyguardViewManager.isShowing()) { + if (!mKeyguardStateController.isShowing()) { WindowManagerGlobal.getInstance().trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN); } } @@ -2519,8 +2508,8 @@ public class CentralSurfacesImpl extends CoreStartable implements }; // Do not deferKeyguard when occluded because, when keyguard is occluded, // we do not launch the activity until keyguard is done. - boolean occluded = mStatusBarKeyguardViewManager.isShowing() - && mStatusBarKeyguardViewManager.isOccluded(); + boolean occluded = mKeyguardStateController.isShowing() + && mKeyguardStateController.isOccluded(); boolean deferred = !occluded; executeRunnableDismissingKeyguard(runnable, cancelRunnable, dismissShadeDirectly, willLaunchResolverActivity, deferred /* deferred */, animate); @@ -2590,8 +2579,8 @@ public class CentralSurfacesImpl extends CoreStartable implements @Override public boolean onDismiss() { if (runnable != null) { - if (mStatusBarKeyguardViewManager.isShowing() - && mStatusBarKeyguardViewManager.isOccluded()) { + if (mKeyguardStateController.isShowing() + && mKeyguardStateController.isOccluded()) { mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable); } else { mMainExecutor.execute(runnable); @@ -2685,7 +2674,7 @@ public class CentralSurfacesImpl extends CoreStartable implements private void executeWhenUnlocked(OnDismissAction action, boolean requiresShadeOpen, boolean afterKeyguardGone) { - if (mStatusBarKeyguardViewManager.isShowing() && requiresShadeOpen) { + if (mKeyguardStateController.isShowing() && requiresShadeOpen) { mStatusBarStateController.setLeaveOpenOnKeyguardHide(true); } dismissKeyguardThenExecute(action, null /* cancelAction */, @@ -2709,7 +2698,7 @@ public class CentralSurfacesImpl extends CoreStartable implements mBiometricUnlockController.startWakeAndUnlock( BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING); } - if (mStatusBarKeyguardViewManager.isShowing()) { + if (mKeyguardStateController.isShowing()) { mStatusBarKeyguardViewManager.dismissWithAction(action, cancelAction, afterKeyguardGone); } else { @@ -2845,8 +2834,8 @@ public class CentralSurfacesImpl extends CoreStartable implements } private void logStateToEventlog() { - boolean isShowing = mStatusBarKeyguardViewManager.isShowing(); - boolean isOccluded = mStatusBarKeyguardViewManager.isOccluded(); + boolean isShowing = mKeyguardStateController.isShowing(); + boolean isOccluded = mKeyguardStateController.isOccluded(); boolean isBouncerShowing = mStatusBarKeyguardViewManager.isBouncerShowing(); boolean isSecure = mKeyguardStateController.isMethodSecure(); boolean unlocked = mKeyguardStateController.canDismissLockScreen(); @@ -2991,7 +2980,10 @@ public class CentralSurfacesImpl extends CoreStartable implements // * When phone is unlocked: we still don't want to execute hiding of the keyguard // as the animation could prepare 'fake AOD' interface (without actually // transitioning to keyguard state) and this might reset the view states - if (!mScreenOffAnimationController.isKeyguardHideDelayed()) { + if (!mScreenOffAnimationController.isKeyguardHideDelayed() + // If we're animating occluded, there's an activity launching over the keyguard + // UI. Wait to hide it until after the animation concludes. + && !mKeyguardViewMediator.isOccludeAnimationPlaying()) { return hideKeyguardImpl(forceStateChange); } } @@ -3242,18 +3234,17 @@ public class CentralSurfacesImpl extends CoreStartable implements Trace.traceCounter(Trace.TRACE_TAG_APP, "dozing", mDozing ? 1 : 0); Trace.beginSection("CentralSurfaces#updateDozingState"); - boolean visibleNotOccluded = mStatusBarKeyguardViewManager.isShowing() - && !mStatusBarKeyguardViewManager.isOccluded(); + boolean keyguardVisible = mKeyguardStateController.isVisible(); // If we're dozing and we'll be animating the screen off, the keyguard isn't currently // visible but will be shortly for the animation, so we should proceed as if it's visible. - boolean visibleNotOccludedOrWillBe = - visibleNotOccluded || (mDozing && mDozeParameters.shouldDelayKeyguardShow()); + boolean keyguardVisibleOrWillBe = + keyguardVisible || (mDozing && mDozeParameters.shouldDelayKeyguardShow()); boolean wakeAndUnlock = mBiometricUnlockController.getMode() == BiometricUnlockController.MODE_WAKE_AND_UNLOCK; boolean animate = (!mDozing && mDozeServiceHost.shouldAnimateWakeup() && !wakeAndUnlock) || (mDozing && mDozeParameters.shouldControlScreenOff() - && visibleNotOccludedOrWillBe); + && keyguardVisibleOrWillBe); mNotificationPanelViewController.setDozing(mDozing, animate); updateQsExpansionEnabled(); @@ -3312,19 +3303,23 @@ public class CentralSurfacesImpl extends CoreStartable implements mNotificationPanelViewController.onAffordanceLaunchEnded(); } + /** + * Returns whether the keyguard should hide immediately (as opposed to via an animation). + * Non-scrimmed bouncers have a special animation tied to the notification panel expansion. + * @return whether the keyguard should be immediately hidden. + */ @Override - public boolean onBackPressed() { + public boolean shouldKeyguardHideImmediately() { final boolean isScrimmedBouncer = mScrimController.getState() == ScrimState.BOUNCER_SCRIMMED; final boolean isBouncerOverDream = isBouncerShowingOverDream(); + return (isScrimmedBouncer || isBouncerOverDream); + } - if (mStatusBarKeyguardViewManager.onBackPressed( - isScrimmedBouncer || isBouncerOverDream /* hideImmediately */)) { - if (isScrimmedBouncer || isBouncerOverDream) { - mStatusBarStateController.setLeaveOpenOnKeyguardHide(false); - } else { - mNotificationPanelViewController.expandWithoutQs(); - } + @Override + public boolean onBackPressed() { + if (mStatusBarKeyguardViewManager.canHandleBackPressed()) { + mStatusBarKeyguardViewManager.onBackPressed(); return true; } if (mNotificationPanelViewController.isQsCustomizing()) { @@ -3339,7 +3334,7 @@ public class CentralSurfacesImpl extends CoreStartable implements return true; } if (mState != StatusBarState.KEYGUARD && mState != StatusBarState.SHADE_LOCKED - && !isBouncerOverDream) { + && !isBouncerShowingOverDream()) { if (mNotificationPanelViewController.canPanelBeCollapsed()) { mShadeController.animateCollapsePanels(); } @@ -3551,7 +3546,7 @@ public class CentralSurfacesImpl extends CoreStartable implements public void setBouncerShowingOverDream(boolean bouncerShowingOverDream) { mBouncerShowingOverDream = bouncerShowingOverDream; } - + /** * Propagate the bouncer state to status bar components. * @@ -3582,6 +3577,13 @@ public class CentralSurfacesImpl extends CoreStartable implements } } + @Override + public void collapseShadeForBugreport() { + if (!mFeatureFlags.isEnabled(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT)) { + collapseShade(); + } + } + @VisibleForTesting final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() { @Override @@ -3813,8 +3815,7 @@ public class CentralSurfacesImpl extends CoreStartable implements if (mDevicePolicyManager.getCameraDisabled(null, mLockscreenUserManager.getCurrentUserId())) { return false; - } else if (mStatusBarKeyguardViewManager == null - || (isKeyguardShowing() && isKeyguardSecure())) { + } else if (isKeyguardShowing() && isKeyguardSecure()) { // Check if the admin has disabled the camera specifically for the keyguard return (mDevicePolicyManager.getKeyguardDisabledFeatures(null, mLockscreenUserManager.getCurrentUserId()) @@ -3931,11 +3932,7 @@ public class CentralSurfacesImpl extends CoreStartable implements @Override public boolean isKeyguardShowing() { - if (mStatusBarKeyguardViewManager == null) { - Slog.i(TAG, "isKeyguardShowing() called before startKeyguard(), returning true"); - return true; - } - return mStatusBarKeyguardViewManager.isShowing(); + return mKeyguardStateController.isShowing(); } @Override @@ -4226,14 +4223,6 @@ public class CentralSurfacesImpl extends CoreStartable implements @Override public boolean isKeyguardSecure() { - if (mStatusBarKeyguardViewManager == null) { - // startKeyguard() hasn't been called yet, so we don't know. - // Make sure anything that needs to know isKeyguardSecure() checks and re-checks this - // value onVisibilityChanged(). - Slog.w(TAG, "isKeyguardSecure() called before startKeyguard(), returning false", - new Throwable()); - return false; - } return mStatusBarKeyguardViewManager.isSecure(); } @Override @@ -4293,11 +4282,6 @@ public class CentralSurfacesImpl extends CoreStartable implements .Callback() { @Override public void onFinished() { - if (mStatusBarKeyguardViewManager == null) { - Log.w(TAG, "Tried to notify keyguard visibility when " - + "mStatusBarKeyguardViewManager was null"); - return; - } if (mKeyguardStateController.isKeyguardFadingAway()) { mStatusBarKeyguardViewManager.onKeyguardFadedAway(); } @@ -4454,10 +4438,11 @@ public class CentralSurfacesImpl extends CoreStartable implements Trace.beginSection("CentralSurfaces#updateDozing"); mDozing = isDozing; - // Collapse the notification panel if open boolean dozingAnimated = mDozeServiceHost.getDozingRequested() && mDozeParameters.shouldControlScreenOff(); - mNotificationPanelViewController.resetViews(dozingAnimated); + // resetting views is already done when going into doze, there's no need to + // reset them again when we're waking up + mNotificationPanelViewController.resetViews(dozingAnimated && isDozing); updateQsExpansionEnabled(); mKeyguardViewMediator.setDozing(mDozing); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index 54d39fd673f6..de7b152adaab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -93,13 +93,13 @@ public class DozeParameters implements private boolean mControlScreenOffAnimation; private boolean mIsQuickPickupEnabled; - private boolean mKeyguardShowing; + private boolean mKeyguardVisible; @VisibleForTesting final KeyguardUpdateMonitorCallback mKeyguardVisibilityCallback = new KeyguardUpdateMonitorCallback() { @Override - public void onKeyguardVisibilityChanged(boolean showing) { - mKeyguardShowing = showing; + public void onKeyguardVisibilityChanged(boolean visible) { + mKeyguardVisible = visible; updateControlScreenOff(); } @@ -293,7 +293,7 @@ public class DozeParameters implements public void updateControlScreenOff() { if (!getDisplayNeedsBlanking()) { final boolean controlScreenOff = - getAlwaysOn() && (mKeyguardShowing || shouldControlUnlockedScreenOff()); + getAlwaysOn() && (mKeyguardVisible || shouldControlUnlockedScreenOff()); setControlScreenOffAnimation(controlScreenOff); } } @@ -348,7 +348,7 @@ public class DozeParameters implements } private boolean willAnimateFromLockScreenToAod() { - return getAlwaysOn() && mKeyguardShowing; + return getAlwaysOn() && mKeyguardVisible; } private boolean getBoolean(String propName, int resId) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java index 7de4668abe28..00673169d036 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.phone; import android.annotation.NonNull; import android.os.Handler; -import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; @@ -34,9 +33,6 @@ import javax.inject.Inject; */ @SysUISingleton public class DozeScrimController implements StateListener { - private static final String TAG = "DozeScrimController"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private final DozeLog mDozeLog; private final DozeParameters mDozeParameters; private final Handler mHandler = new Handler(); @@ -44,28 +40,26 @@ public class DozeScrimController implements StateListener { private boolean mDozing; private DozeHost.PulseCallback mPulseCallback; private int mPulseReason; - private boolean mFullyPulsing; private final ScrimController.Callback mScrimCallback = new ScrimController.Callback() { @Override public void onDisplayBlanked() { - if (DEBUG) { - Log.d(TAG, "Pulse in, mDozing=" + mDozing + " mPulseReason=" - + DozeLog.reasonToString(mPulseReason)); - } if (!mDozing) { + mDozeLog.tracePulseDropped("onDisplayBlanked - not dozing"); return; } - // Signal that the pulse is ready to turn the screen on and draw. - pulseStarted(); + if (mPulseCallback != null) { + // Signal that the pulse is ready to turn the screen on and draw. + mDozeLog.tracePulseStart(mPulseReason); + mPulseCallback.onPulseStarted(); + } } @Override public void onFinished() { - if (DEBUG) { - Log.d(TAG, "Pulse in finished, mDozing=" + mDozing); - } + mDozeLog.tracePulseEvent("scrimCallback-onFinished", mDozing, mPulseReason); + if (!mDozing) { return; } @@ -78,7 +72,6 @@ public class DozeScrimController implements StateListener { mHandler.postDelayed(mPulseOutExtended, mDozeParameters.getPulseVisibleDurationExtended()); } - mFullyPulsing = true; } /** @@ -118,19 +111,14 @@ public class DozeScrimController implements StateListener { } if (!mDozing || mPulseCallback != null) { - if (DEBUG) { - Log.d(TAG, "Pulse suppressed. Dozing: " + mDozeParameters + " had callback? " - + (mPulseCallback != null)); - } // Pulse suppressed. callback.onPulseFinished(); if (!mDozing) { - mDozeLog.tracePulseDropped("device isn't dozing"); + mDozeLog.tracePulseDropped("pulse - device isn't dozing"); } else { - mDozeLog.tracePulseDropped("already has pulse callback mPulseCallback=" + mDozeLog.tracePulseDropped("pulse - already has pulse callback mPulseCallback=" + mPulseCallback); } - return; } @@ -141,9 +129,7 @@ public class DozeScrimController implements StateListener { } public void pulseOutNow() { - if (mPulseCallback != null && mFullyPulsing) { - mPulseOut.run(); - } + mPulseOut.run(); } public boolean isPulsing() { @@ -168,24 +154,16 @@ public class DozeScrimController implements StateListener { private void cancelPulsing() { if (mPulseCallback != null) { - if (DEBUG) Log.d(TAG, "Cancel pulsing"); - mFullyPulsing = false; + mDozeLog.tracePulseEvent("cancel", mDozing, mPulseReason); mHandler.removeCallbacks(mPulseOut); mHandler.removeCallbacks(mPulseOutExtended); pulseFinished(); } } - private void pulseStarted() { - mDozeLog.tracePulseStart(mPulseReason); - if (mPulseCallback != null) { - mPulseCallback.onPulseStarted(); - } - } - private void pulseFinished() { - mDozeLog.tracePulseFinish(); if (mPulseCallback != null) { + mDozeLog.tracePulseFinish(); mPulseCallback.onPulseFinished(); mPulseCallback = null; } @@ -202,10 +180,9 @@ public class DozeScrimController implements StateListener { private final Runnable mPulseOut = new Runnable() { @Override public void run() { - mFullyPulsing = false; mHandler.removeCallbacks(mPulseOut); mHandler.removeCallbacks(mPulseOutExtended); - if (DEBUG) Log.d(TAG, "Pulse out, mDozing=" + mDozing); + mDozeLog.tracePulseEvent("out", mDozing, mPulseReason); if (!mDozing) return; pulseFinished(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index 24ce5e98bdd0..5196e10df450 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -36,7 +36,6 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.doze.DozeHost; import com.android.systemui.doze.DozeLog; import com.android.systemui.doze.DozeReceiver; -import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.NotificationShadeWindowViewController; @@ -48,6 +47,7 @@ import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; +import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import com.android.systemui.util.Assert; import java.util.ArrayList; @@ -80,7 +80,6 @@ public final class DozeServiceHost implements DozeHost { private final BatteryController mBatteryController; private final ScrimController mScrimController; private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy; - private final KeyguardViewMediator mKeyguardViewMediator; private final Lazy<AssistManager> mAssistManagerLazy; private final DozeScrimController mDozeScrimController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @@ -95,6 +94,7 @@ public final class DozeServiceHost implements DozeHost { private View mAmbientIndicationContainer; private CentralSurfaces mCentralSurfaces; private boolean mAlwaysOnSuppressed; + private boolean mPulsePending; @Inject public DozeServiceHost(DozeLog dozeLog, PowerManager powerManager, @@ -104,7 +104,6 @@ public final class DozeServiceHost implements DozeHost { HeadsUpManagerPhone headsUpManagerPhone, BatteryController batteryController, ScrimController scrimController, Lazy<BiometricUnlockController> biometricUnlockControllerLazy, - KeyguardViewMediator keyguardViewMediator, Lazy<AssistManager> assistManagerLazy, DozeScrimController dozeScrimController, KeyguardUpdateMonitor keyguardUpdateMonitor, PulseExpansionHandler pulseExpansionHandler, @@ -122,7 +121,6 @@ public final class DozeServiceHost implements DozeHost { mBatteryController = batteryController; mScrimController = scrimController; mBiometricUnlockControllerLazy = biometricUnlockControllerLazy; - mKeyguardViewMediator = keyguardViewMediator; mAssistManagerLazy = assistManagerLazy; mDozeScrimController = dozeScrimController; mKeyguardUpdateMonitor = keyguardUpdateMonitor; @@ -131,6 +129,7 @@ public final class DozeServiceHost implements DozeHost { mNotificationWakeUpCoordinator = notificationWakeUpCoordinator; mAuthController = authController; mNotificationIconAreaController = notificationIconAreaController; + mHeadsUpManagerPhone.addListener(mOnHeadsUpChangedListener); } // TODO: we should try to not pass status bar in here if we can avoid it. @@ -246,7 +245,7 @@ public final class DozeServiceHost implements DozeHost { mDozeScrimController.pulse(new PulseCallback() { @Override public void onPulseStarted() { - callback.onPulseStarted(); + callback.onPulseStarted(); // requestState(DozeMachine.State.DOZE_PULSING) mCentralSurfaces.updateNotificationPanelTouchState(); setPulsing(true); } @@ -254,7 +253,7 @@ public final class DozeServiceHost implements DozeHost { @Override public void onPulseFinished() { mPulsing = false; - callback.onPulseFinished(); + callback.onPulseFinished(); // requestState(DozeMachine.State.DOZE_PULSE_DONE) mCentralSurfaces.updateNotificationPanelTouchState(); mScrimController.setWakeLockScreenSensorActive(false); setPulsing(false); @@ -338,9 +337,8 @@ public final class DozeServiceHost implements DozeHost { @Override public void stopPulsing() { - if (mDozeScrimController.isPulsing()) { - mDozeScrimController.pulseOutNow(); - } + setPulsePending(false); // prevent any pending pulses from continuing + mDozeScrimController.pulseOutNow(); } @Override @@ -451,6 +449,16 @@ public final class DozeServiceHost implements DozeHost { } } + @Override + public boolean isPulsePending() { + return mPulsePending; + } + + @Override + public void setPulsePending(boolean isPulsePending) { + mPulsePending = isPulsePending; + } + /** * Whether always-on-display is being suppressed. This does not affect wakeup gestures like * pickup and tap. @@ -458,4 +466,22 @@ public final class DozeServiceHost implements DozeHost { public boolean isAlwaysOnSuppressed() { return mAlwaysOnSuppressed; } + + final OnHeadsUpChangedListener mOnHeadsUpChangedListener = new OnHeadsUpChangedListener() { + @Override + public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { + if (mStatusBarStateController.isDozing() && isHeadsUp) { + entry.setPulseSuppressed(false); + fireNotificationPulse(entry); + if (isPulsing()) { + mDozeScrimController.cancelPendingPulseTimeout(); + } + } + if (!isHeadsUp && !mHeadsUpManagerPhone.hasNotifications()) { + // There are no longer any notifications to show. We should end the + // pulse now. + stopPulsing(); + } + } + }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index d61c51e76e86..9bb4132490d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -91,6 +91,11 @@ public class KeyguardBouncer { mBouncerPromptReason = mCallback.getBouncerPromptReason(); } } + + @Override + public void onNonStrongBiometricAllowedChanged(int userId) { + mBouncerPromptReason = mCallback.getBouncerPromptReason(); + } }; private final Runnable mRemoveViewRunnable = this::removeView; private final KeyguardBypassController mKeyguardBypassController; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt index b987f6815000..b965ac97cc1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt @@ -26,6 +26,7 @@ import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm @@ -95,14 +96,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr var bouncerShowing: Boolean = false var altBouncerShowing: Boolean = false var launchingAffordance: Boolean = false - var qSExpanded = false - set(value) { - val changed = field != value - field = value - if (changed && !value) { - maybePerformPendingUnlock() - } - } + var qsExpanded = false @Inject constructor( @@ -111,6 +105,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr statusBarStateController: StatusBarStateController, lockscreenUserManager: NotificationLockscreenUserManager, keyguardStateController: KeyguardStateController, + shadeExpansionStateManager: ShadeExpansionStateManager, dumpManager: DumpManager ) { this.mKeyguardStateController = keyguardStateController @@ -132,6 +127,14 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr } }) + shadeExpansionStateManager.addQsExpansionListener { isQsExpanded -> + val changed = qsExpanded != isQsExpanded + qsExpanded = isQsExpanded + if (changed && !isQsExpanded) { + maybePerformPendingUnlock() + } + } + val dismissByDefault = if (context.resources.getBoolean( com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) 1 else 0 tunerService.addTunable(object : TunerService.Tunable { @@ -160,7 +163,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr ): Boolean { if (biometricSourceType == BiometricSourceType.FACE && bypassEnabled) { val can = canBypass() - if (!can && (isPulseExpanding || qSExpanded)) { + if (!can && (isPulseExpanding || qsExpanded)) { pendingUnlock = PendingUnlock(biometricSourceType, isStrongBiometric) } return can @@ -189,7 +192,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr altBouncerShowing -> true statusBarStateController.state != StatusBarState.KEYGUARD -> false launchingAffordance -> false - isPulseExpanding || qSExpanded -> false + isPulseExpanding || qsExpanded -> false else -> true } } @@ -214,7 +217,7 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr pw.println(" altBouncerShowing: $altBouncerShowing") pw.println(" isPulseExpanding: $isPulseExpanding") pw.println(" launchingAffordance: $launchingAffordance") - pw.println(" qSExpanded: $qSExpanded") + pw.println(" qSExpanded: $qsExpanded") pw.println(" hasFaceFeature: $hasFaceFeature") } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt index b58dbe263eff..5e26cf062b58 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt @@ -47,7 +47,7 @@ class KeyguardLiftController @Inject constructor( private val asyncSensorManager: AsyncSensorManager, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val dumpManager: DumpManager -) : Dumpable, CoreStartable(context) { +) : Dumpable, CoreStartable { private val pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) private var isListening = false @@ -88,7 +88,7 @@ class KeyguardLiftController @Inject constructor( updateListeningState() } - override fun onKeyguardVisibilityChanged(showing: Boolean) { + override fun onKeyguardVisibilityChanged(visible: Boolean) { updateListeningState() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 0026b71a5304..14cebf4b9f4b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -40,6 +40,7 @@ import androidx.annotation.VisibleForTesting; import com.android.keyguard.CarrierTextController; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.battery.BatteryMeterViewController; @@ -116,6 +117,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat private final CommandQueue mCommandQueue; private final Executor mMainExecutor; private final Object mLock = new Object(); + private final KeyguardLogger mLogger; private final ConfigurationController.ConfigurationListener mConfigurationListener = new ConfigurationController.ConfigurationListener() { @@ -185,8 +187,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat } @Override - public void onKeyguardVisibilityChanged(boolean showing) { - if (showing) { + public void onKeyguardVisibilityChanged(boolean visible) { + if (visible) { updateUserSwitcher(); } } @@ -279,7 +281,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat StatusBarUserInfoTracker statusBarUserInfoTracker, SecureSettings secureSettings, CommandQueue commandQueue, - @Main Executor mainExecutor + @Main Executor mainExecutor, + KeyguardLogger logger ) { super(view); mCarrierTextController = carrierTextController; @@ -304,6 +307,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mSecureSettings = secureSettings; mCommandQueue = commandQueue; mMainExecutor = mainExecutor; + mLogger = logger; mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled(); mKeyguardStateController.addCallback( @@ -430,6 +434,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar in. */ public void animateKeyguardStatusBarIn() { + mLogger.d("animating status bar in"); if (mDisableStateTracker.isDisabled()) { // If our view is disabled, don't allow us to animate in. return; @@ -445,6 +450,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar out. */ public void animateKeyguardStatusBarOut(long startDelay, long duration) { + mLogger.d("animating status bar out"); ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f); anim.addUpdateListener(mAnimatorUpdateListener); anim.setStartDelay(startDelay); @@ -481,6 +487,9 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat newAlpha = Math.min(getKeyguardContentsAlpha(), alphaQsExpansion) * mKeyguardStatusBarAnimateAlpha * (1.0f - mKeyguardHeadsUpShowingAmount); + if (newAlpha != mView.getAlpha() && (newAlpha == 0 || newAlpha == 1)) { + mLogger.logStatusBarCalculatedAlpha(newAlpha); + } } boolean hideForBypass = @@ -503,6 +512,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat if (mDisableStateTracker.isDisabled()) { visibility = View.INVISIBLE; } + if (visibility != mView.getVisibility()) { + mLogger.logStatusBarAlphaVisibility(visibility, alpha, + StatusBarState.toString(mStatusBarState)); + } mView.setAlpha(alpha); mView.setVisibility(visibility); } @@ -596,6 +609,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat pw.println("KeyguardStatusBarView:"); pw.println(" mBatteryListening: " + mBatteryListening); pw.println(" mExplicitAlpha: " + mExplicitAlpha); + pw.println(" alpha: " + mView.getAlpha()); + pw.println(" visibility: " + mView.getVisibility()); mView.dump(pw, args); } @@ -605,6 +620,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat * @param alpha a value between 0 and 1. -1 if the value is to be reset/ignored. */ public void setAlpha(float alpha) { + if (mExplicitAlpha != alpha && (mExplicitAlpha == -1 || alpha == -1)) { + // logged if value changed to ignored or from ignored + mLogger.logStatusBarExplicitAlpha(alpha); + } mExplicitAlpha = alpha; updateViewState(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt index 02b235493715..4839fe6a7bef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt @@ -19,9 +19,9 @@ package com.android.systemui.statusbar.phone import android.util.DisplayMetrics import android.view.View import com.android.internal.logging.nano.MetricsProto.MetricsEvent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.LSShadeTransitionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java index 00c3e8fac0b4..5e2a7c8ca540 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java @@ -26,6 +26,7 @@ import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.animation.Expandable; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ActivityStarter; @@ -67,7 +68,7 @@ public class MultiUserSwitchController extends ViewController<MultiUserSwitch> { ActivityLaunchAnimator.Controller.fromView(v, null), true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM); } else { - mUserSwitchDialogController.showDialog(v); + mUserSwitchDialogController.showDialog(v.getContext(), Expandable.fromView(v)); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 5a70d89908f8..976710351a44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -211,11 +211,11 @@ public class NotificationIconContainer extends ViewGroup { canvas.drawLine(end, 0, end, height, paint); paint.setColor(Color.GREEN); - int lastIcon = (int) mLastVisibleIconState.xTranslation; + int lastIcon = (int) mLastVisibleIconState.getXTranslation(); canvas.drawLine(lastIcon, 0, lastIcon, height, paint); if (mFirstVisibleIconState != null) { - int firstIcon = (int) mFirstVisibleIconState.xTranslation; + int firstIcon = (int) mFirstVisibleIconState.getXTranslation(); canvas.drawLine(firstIcon, 0, firstIcon, height, paint); } @@ -413,7 +413,7 @@ public class NotificationIconContainer extends ViewGroup { View view = getChildAt(i); ViewState iconState = mIconStates.get(view); iconState.initFrom(view); - iconState.alpha = mIsolatedIcon == null || view == mIsolatedIcon ? 1.0f : 0.0f; + iconState.setAlpha(mIsolatedIcon == null || view == mIsolatedIcon ? 1.0f : 0.0f); iconState.hidden = false; } } @@ -467,7 +467,7 @@ public class NotificationIconContainer extends ViewGroup { // We only modify the xTranslation if it's fully inside of the container // since during the transition to the shelf, the translations are controlled // from the outside - iconState.xTranslation = translationX; + iconState.setXTranslation(translationX); } if (mFirstVisibleIconState == null) { mFirstVisibleIconState = iconState; @@ -501,7 +501,7 @@ public class NotificationIconContainer extends ViewGroup { View view = getChildAt(i); IconState iconState = mIconStates.get(view); int dotWidth = mStaticDotDiameter + mDotPadding; - iconState.xTranslation = translationX; + iconState.setXTranslation(translationX); if (mNumDots < MAX_DOTS) { if (mNumDots == 0 && iconState.iconAppearAmount < 0.8f) { iconState.visibleState = StatusBarIconView.STATE_ICON; @@ -525,7 +525,8 @@ public class NotificationIconContainer extends ViewGroup { for (int i = 0; i < childCount; i++) { View view = getChildAt(i); IconState iconState = mIconStates.get(view); - iconState.xTranslation = getWidth() - iconState.xTranslation - view.getWidth(); + iconState.setXTranslation( + getWidth() - iconState.getXTranslation() - view.getWidth()); } } if (mIsolatedIcon != null) { @@ -533,8 +534,8 @@ public class NotificationIconContainer extends ViewGroup { if (iconState != null) { // Most of the time the icon isn't yet added when this is called but only happening // later - iconState.xTranslation = mIsolatedIconLocation.left - mAbsolutePosition[0] - - (1 - mIsolatedIcon.getIconScale()) * mIsolatedIcon.getWidth() / 2.0f; + iconState.setXTranslation(mIsolatedIconLocation.left - mAbsolutePosition[0] + - (1 - mIsolatedIcon.getIconScale()) * mIsolatedIcon.getWidth() / 2.0f); iconState.visibleState = StatusBarIconView.STATE_ICON; } } @@ -609,8 +610,10 @@ public class NotificationIconContainer extends ViewGroup { return 0; } - int translation = (int) (isLayoutRtl() ? getWidth() - mLastVisibleIconState.xTranslation - : mLastVisibleIconState.xTranslation + mIconSize); + int translation = (int) (isLayoutRtl() + ? getWidth() - mLastVisibleIconState.getXTranslation() + : mLastVisibleIconState.getXTranslation() + mIconSize); + // There's a chance that last translation goes beyond the edge maybe return Math.min(getWidth(), translation); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 4d1c3617ac1a..cf3a48cf5000 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -53,6 +53,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.scrim.ScrimView; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.statusbar.notification.stack.ViewState; @@ -204,6 +205,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private final ScreenOffAnimationController mScreenOffAnimationController; private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + private KeyguardViewMediator mKeyguardViewMediator; private GradientColors mColors; private boolean mNeedsDrawableColorUpdate; @@ -249,6 +251,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private Callback mCallback; private boolean mWallpaperSupportsAmbientMode; private boolean mScreenOn; + private boolean mTransparentScrimBackground; // Scrim blanking callbacks private Runnable mPendingFrameCallback; @@ -272,7 +275,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump @Main Executor mainExecutor, ScreenOffAnimationController screenOffAnimationController, KeyguardUnlockAnimationController keyguardUnlockAnimationController, - StatusBarKeyguardViewManager statusBarKeyguardViewManager) { + StatusBarKeyguardViewManager statusBarKeyguardViewManager, + KeyguardViewMediator keyguardViewMediator) { mScrimStateListener = lightBarController::setScrimState; mDefaultScrimAlpha = BUSY_SCRIM_ALPHA; @@ -311,6 +315,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } }); mColors = new GradientColors(); + + mKeyguardViewMediator = keyguardViewMediator; } /** @@ -341,6 +347,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mScrimBehind.setDefaultFocusHighlightEnabled(false); mNotificationsScrim.setDefaultFocusHighlightEnabled(false); mScrimInFront.setDefaultFocusHighlightEnabled(false); + mTransparentScrimBackground = notificationsScrim.getResources() + .getBoolean(R.bool.notification_scrim_transparent); updateScrims(); mKeyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback); } @@ -777,13 +785,16 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump float behindFraction = getInterpolatedFraction(); behindFraction = (float) Math.pow(behindFraction, 0.8f); if (mClipsQsScrim) { - mBehindAlpha = 1; - mNotificationsAlpha = behindFraction * mDefaultScrimAlpha; + mBehindAlpha = mTransparentScrimBackground ? 0 : 1; + mNotificationsAlpha = + mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha; } else { - mBehindAlpha = behindFraction * mDefaultScrimAlpha; + mBehindAlpha = + mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha; // Delay fade-in of notification scrim a bit further, to coincide with the // view fade in. Otherwise the empty panel can be quite jarring. - mNotificationsAlpha = MathUtils.constrainedMap(0f, 1f, 0.3f, 0.75f, + mNotificationsAlpha = mTransparentScrimBackground + ? 0 : MathUtils.constrainedMap(0f, 1f, 0.3f, 0.75f, mPanelExpansionFraction); } mBehindTint = mState.getBehindTint(); @@ -801,6 +812,13 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mBehindTint, interpolatedFraction); } + + // If we're unlocked but still playing the occlude animation, remain at the keyguard + // alpha temporarily. + if (mKeyguardViewMediator.isOccludeAnimationPlaying() + || mState.mLaunchingAffordanceWithPreview) { + mNotificationsAlpha = KEYGUARD_SCRIM_ALPHA; + } } else if (mState == ScrimState.AUTH_SCRIMMED_SHADE) { float behindFraction = getInterpolatedFraction(); behindFraction = (float) Math.pow(behindFraction, 0.8f); @@ -1534,7 +1552,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private class KeyguardVisibilityCallback extends KeyguardUpdateMonitorCallback { @Override - public void onKeyguardVisibilityChanged(boolean showing) { + public void onKeyguardVisibilityChanged(boolean visible) { mNeedsDrawableColorUpdate = true; scheduleUpdate(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java index ae201e3ea8cf..5512bedb5dd4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java @@ -21,8 +21,6 @@ import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import com.android.systemui.statusbar.window.StatusBarWindowController; @@ -41,9 +39,6 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener private final HeadsUpManagerPhone mHeadsUpManager; private final StatusBarStateController mStatusBarStateController; private final NotificationRemoteInputManager mNotificationRemoteInputManager; - private final NotificationsController mNotificationsController; - private final DozeServiceHost mDozeServiceHost; - private final DozeScrimController mDozeScrimController; @Inject StatusBarHeadsUpChangeListener( @@ -53,10 +48,7 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener KeyguardBypassController keyguardBypassController, HeadsUpManagerPhone headsUpManager, StatusBarStateController statusBarStateController, - NotificationRemoteInputManager notificationRemoteInputManager, - NotificationsController notificationsController, - DozeServiceHost dozeServiceHost, - DozeScrimController dozeScrimController) { + NotificationRemoteInputManager notificationRemoteInputManager) { mNotificationShadeWindowController = notificationShadeWindowController; mStatusBarWindowController = statusBarWindowController; @@ -65,9 +57,6 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener mHeadsUpManager = headsUpManager; mStatusBarStateController = statusBarStateController; mNotificationRemoteInputManager = notificationRemoteInputManager; - mNotificationsController = notificationsController; - mDozeServiceHost = dozeServiceHost; - mDozeScrimController = dozeScrimController; } @Override @@ -117,20 +106,4 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener } } } - - @Override - public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { - if (mStatusBarStateController.isDozing() && isHeadsUp) { - entry.setPulseSuppressed(false); - mDozeServiceHost.fireNotificationPulse(entry); - if (mDozeServiceHost.isPulsing()) { - mDozeScrimController.cancelPendingPulseTimeout(); - } - } - if (!isHeadsUp && !mHeadsUpManager.hasNotifications()) { - // There are no longer any notifications to show. We should end the - //pulse now. - mDozeScrimController.pulseOutNow(); - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index d6d021ff2819..86f6ff850409 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE; +import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI; import android.annotation.Nullable; @@ -38,7 +39,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.demomode.DemoModeCommandReceiver; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; -import com.android.systemui.statusbar.BaseStatusBarWifiView; +import com.android.systemui.statusbar.BaseStatusBarFrameLayout; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.StatusBarMobileView; import com.android.systemui.statusbar.StatusBarWifiView; @@ -48,6 +49,10 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorI import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder; +import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel; import com.android.systemui.util.Assert; @@ -84,6 +89,12 @@ public interface StatusBarIconController { void setMobileIcons(String slot, List<MobileIconState> states); /** + * This method completely replaces {@link #setMobileIcons} with the information from the new + * mobile data pipeline. Icons will automatically keep their state up to date, so we don't have + * to worry about funneling MobileIconState objects through anymore. + */ + void setNewMobileIconSubIds(List<Integer> subIds); + /** * Display the no calling & SMS icons. */ void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states); @@ -141,12 +152,14 @@ public interface StatusBarIconController { StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, + MobileUiAdapter mobileUiAdapter, MobileContextProvider mobileContextProvider, DarkIconDispatcher darkIconDispatcher) { super(linearLayout, location, statusBarPipelineFlags, wifiViewModel, + mobileUiAdapter, mobileContextProvider); mIconHPadding = mContext.getResources().getDimensionPixelSize( R.dimen.status_bar_icon_padding); @@ -207,6 +220,7 @@ public interface StatusBarIconController { private final StatusBarPipelineFlags mStatusBarPipelineFlags; private final WifiViewModel mWifiViewModel; private final MobileContextProvider mMobileContextProvider; + private final MobileUiAdapter mMobileUiAdapter; private final DarkIconDispatcher mDarkIconDispatcher; @Inject @@ -214,10 +228,12 @@ public interface StatusBarIconController { StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, MobileContextProvider mobileContextProvider, + MobileUiAdapter mobileUiAdapter, DarkIconDispatcher darkIconDispatcher) { mStatusBarPipelineFlags = statusBarPipelineFlags; mWifiViewModel = wifiViewModel; mMobileContextProvider = mobileContextProvider; + mMobileUiAdapter = mobileUiAdapter; mDarkIconDispatcher = darkIconDispatcher; } @@ -227,6 +243,7 @@ public interface StatusBarIconController { location, mStatusBarPipelineFlags, mWifiViewModel, + mMobileUiAdapter, mMobileContextProvider, mDarkIconDispatcher); } @@ -244,11 +261,14 @@ public interface StatusBarIconController { StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, - MobileContextProvider mobileContextProvider) { + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider + ) { super(group, location, statusBarPipelineFlags, wifiViewModel, + mobileUiAdapter, mobileContextProvider); } @@ -284,14 +304,18 @@ public interface StatusBarIconController { private final StatusBarPipelineFlags mStatusBarPipelineFlags; private final WifiViewModel mWifiViewModel; private final MobileContextProvider mMobileContextProvider; + private final MobileUiAdapter mMobileUiAdapter; @Inject public Factory( StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, - MobileContextProvider mobileContextProvider) { + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider + ) { mStatusBarPipelineFlags = statusBarPipelineFlags; mWifiViewModel = wifiViewModel; + mMobileUiAdapter = mobileUiAdapter; mMobileContextProvider = mobileContextProvider; } @@ -301,6 +325,7 @@ public interface StatusBarIconController { location, mStatusBarPipelineFlags, mWifiViewModel, + mMobileUiAdapter, mMobileContextProvider); } } @@ -315,6 +340,8 @@ public interface StatusBarIconController { private final StatusBarPipelineFlags mStatusBarPipelineFlags; private final WifiViewModel mWifiViewModel; private final MobileContextProvider mMobileContextProvider; + private final MobileIconsViewModel mMobileIconsViewModel; + protected final Context mContext; protected final int mIconSize; // Whether or not these icons show up in dumpsys @@ -333,7 +360,9 @@ public interface StatusBarIconController { StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, - MobileContextProvider mobileContextProvider) { + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider + ) { mGroup = group; mLocation = location; mStatusBarPipelineFlags = statusBarPipelineFlags; @@ -342,6 +371,14 @@ public interface StatusBarIconController { mContext = group.getContext(); mIconSize = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_icon_size); + + if (statusBarPipelineFlags.useNewMobileIcons()) { + // This starts the flow for the new pipeline, and will notify us of changes + mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel(); + MobileIconsBinder.bind(mGroup, mMobileIconsViewModel); + } else { + mMobileIconsViewModel = null; + } } public boolean isDemoable() { @@ -394,6 +431,9 @@ public interface StatusBarIconController { case TYPE_MOBILE: return addMobileIcon(index, slot, holder.getMobileState()); + + case TYPE_MOBILE_NEW: + return addNewMobileIcon(index, slot, holder.getTag()); } return null; @@ -410,8 +450,8 @@ public interface StatusBarIconController { @VisibleForTesting protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) { - final BaseStatusBarWifiView view; - if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + final BaseStatusBarFrameLayout view; + if (mStatusBarPipelineFlags.useNewWifiIcon()) { view = onCreateModernStatusBarWifiView(slot); // When [ModernStatusBarWifiView] is created, it will automatically apply the // correct view state so we don't need to call applyWifiState. @@ -429,17 +469,47 @@ public interface StatusBarIconController { } @VisibleForTesting - protected StatusBarMobileView addMobileIcon(int index, String slot, MobileIconState state) { + protected StatusIconDisplayable addMobileIcon( + int index, + String slot, + MobileIconState state + ) { + if (mStatusBarPipelineFlags.useNewMobileIcons()) { + throw new IllegalStateException("Attempting to add a mobile icon while the new " + + "icons are enabled is not supported"); + } + // Use the `subId` field as a key to query for the correct context - StatusBarMobileView view = onCreateStatusBarMobileView(state.subId, slot); - view.applyMobileState(state); - mGroup.addView(view, index, onCreateLayoutParams()); + StatusBarMobileView mobileView = onCreateStatusBarMobileView(state.subId, slot); + mobileView.applyMobileState(state); + mGroup.addView(mobileView, index, onCreateLayoutParams()); if (mIsInDemoMode) { Context mobileContext = mMobileContextProvider .getMobileContextForSub(state.subId, mContext); mDemoStatusIcons.addMobileView(state, mobileContext); } + return mobileView; + } + + protected StatusIconDisplayable addNewMobileIcon( + int index, + String slot, + int subId + ) { + if (!mStatusBarPipelineFlags.useNewMobileIcons()) { + throw new IllegalStateException("Attempting to add a mobile icon using the new" + + "pipeline, but the enabled flag is false."); + } + + BaseStatusBarFrameLayout view = onCreateModernStatusBarMobileView(slot, subId); + mGroup.addView(view, index, onCreateLayoutParams()); + + if (mIsInDemoMode) { + // TODO (b/249790009): demo mode should be handled at the data layer in the + // new pipeline + } + return view; } @@ -464,6 +534,15 @@ public interface StatusBarIconController { return view; } + private ModernStatusBarMobileView onCreateModernStatusBarMobileView( + String slot, int subId) { + return ModernStatusBarMobileView + .constructAndBind( + mContext, + slot, + mMobileIconsViewModel.viewModelForSub(subId)); + } + protected LinearLayout.LayoutParams onCreateLayoutParams() { return new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize); } @@ -519,6 +598,10 @@ public interface StatusBarIconController { return; case TYPE_MOBILE: onSetMobileIcon(viewIndex, holder.getMobileState()); + return; + case TYPE_MOBILE_NEW: + // Nothing, the icon updates itself now + return; default: break; } @@ -542,9 +625,13 @@ public interface StatusBarIconController { } public void onSetMobileIcon(int viewIndex, MobileIconState state) { - StatusBarMobileView view = (StatusBarMobileView) mGroup.getChildAt(viewIndex); - if (view != null) { - view.applyMobileState(state); + View view = mGroup.getChildAt(viewIndex); + if (view instanceof StatusBarMobileView) { + ((StatusBarMobileView) view).applyMobileState(state); + } else { + // ModernStatusBarMobileView automatically updates via the ViewModel + throw new IllegalStateException("Cannot update ModernStatusBarMobileView outside of" + + "the new pipeline"); } if (mIsInDemoMode) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java index 7c31366ba4f0..31e960ad7d69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -40,6 +40,7 @@ import com.android.systemui.statusbar.StatusIconDisplayable; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.tuner.TunerService; @@ -66,8 +67,8 @@ public class StatusBarIconControllerImpl implements Tunable, private final StatusBarIconList mStatusBarIconList; private final ArrayList<IconManager> mIconGroups = new ArrayList<>(); private final ArraySet<String> mIconHideList = new ArraySet<>(); - - private Context mContext; + private final StatusBarPipelineFlags mStatusBarPipelineFlags; + private final Context mContext; /** */ @Inject @@ -78,9 +79,12 @@ public class StatusBarIconControllerImpl implements Tunable, ConfigurationController configurationController, TunerService tunerService, DumpManager dumpManager, - StatusBarIconList statusBarIconList) { + StatusBarIconList statusBarIconList, + StatusBarPipelineFlags statusBarPipelineFlags + ) { mStatusBarIconList = statusBarIconList; mContext = context; + mStatusBarPipelineFlags = statusBarPipelineFlags; configurationController.addCallback(this); commandQueue.addCallback(this); @@ -220,6 +224,11 @@ public class StatusBarIconControllerImpl implements Tunable, */ @Override public void setMobileIcons(String slot, List<MobileIconState> iconStates) { + if (mStatusBarPipelineFlags.useNewMobileIcons()) { + Log.d(TAG, "ignoring old pipeline callbacks, because the new " + + "icons are enabled"); + return; + } Slot mobileSlot = mStatusBarIconList.getSlot(slot); // Reverse the sort order to show icons with left to right([Slot1][Slot2]..). @@ -227,7 +236,6 @@ public class StatusBarIconControllerImpl implements Tunable, Collections.reverse(iconStates); for (MobileIconState state : iconStates) { - StatusBarIconHolder holder = mobileSlot.getHolderForTag(state.subId); if (holder == null) { holder = StatusBarIconHolder.fromMobileIconState(state); @@ -239,6 +247,28 @@ public class StatusBarIconControllerImpl implements Tunable, } } + @Override + public void setNewMobileIconSubIds(List<Integer> subIds) { + if (!mStatusBarPipelineFlags.useNewMobileIcons()) { + Log.d(TAG, "ignoring new pipeline callback, " + + "since the new icons are disabled"); + return; + } + Slot mobileSlot = mStatusBarIconList.getSlot("mobile"); + + Collections.reverse(subIds); + + for (Integer subId : subIds) { + StatusBarIconHolder holder = mobileSlot.getHolderForTag(subId); + if (holder == null) { + holder = StatusBarIconHolder.fromSubIdForModernMobileIcon(subId); + setIcon("mobile", holder); + } else { + // Don't have to do anything in the new world + } + } + } + /** * Accept a list of CallIndicatorIconStates, and show the call strength icons. * @param slot statusbar slot for the call strength icons @@ -384,8 +414,6 @@ public class StatusBarIconControllerImpl implements Tunable, } } - - private void handleSet(String slotName, StatusBarIconHolder holder) { int viewIndex = mStatusBarIconList.getViewIndex(slotName, holder.getTag()); mIconGroups.forEach(l -> l.onSetIconHolder(viewIndex, holder)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java index af342dd31a76..68a203e30f98 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import android.annotation.IntDef; import android.annotation.Nullable; import android.content.Context; import android.graphics.drawable.Icon; @@ -25,6 +26,10 @@ import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Wraps {@link com.android.internal.statusbar.StatusBarIcon} so we can still have a uniform list @@ -33,15 +38,35 @@ public class StatusBarIconHolder { public static final int TYPE_ICON = 0; public static final int TYPE_WIFI = 1; public static final int TYPE_MOBILE = 2; + /** + * TODO (b/249790733): address this once the new pipeline is in place + * This type exists so that the new pipeline (see {@link MobileIconViewModel}) can be used + * to inform the old view system about changes to the data set (the list of mobile icons). The + * design of the new pipeline should allow for removal of this icon holder type, and obsolete + * the need for this entire class. + * + * @deprecated This field only exists so the new status bar pipeline can interface with the + * view holder system. + */ + @Deprecated + public static final int TYPE_MOBILE_NEW = 3; + + @IntDef({ + TYPE_ICON, + TYPE_WIFI, + TYPE_MOBILE, + TYPE_MOBILE_NEW + }) + @Retention(RetentionPolicy.SOURCE) + @interface IconType {} private StatusBarIcon mIcon; private WifiIconState mWifiState; private MobileIconState mMobileState; - private int mType = TYPE_ICON; + private @IconType int mType = TYPE_ICON; private int mTag = 0; private StatusBarIconHolder() { - } public static StatusBarIconHolder fromIcon(StatusBarIcon icon) { @@ -80,6 +105,18 @@ public class StatusBarIconHolder { } /** + * ONLY for use with the new connectivity pipeline, where we only need a subscriptionID to + * determine icon ordering and building the correct view model + */ + public static StatusBarIconHolder fromSubIdForModernMobileIcon(int subId) { + StatusBarIconHolder holder = new StatusBarIconHolder(); + holder.mType = TYPE_MOBILE_NEW; + holder.mTag = subId; + + return holder; + } + + /** * Creates a new StatusBarIconHolder from a CallIndicatorIconState. */ public static StatusBarIconHolder fromCallIndicatorState( @@ -95,7 +132,7 @@ public class StatusBarIconHolder { return holder; } - public int getType() { + public @IconType int getType() { return mType; } @@ -134,8 +171,12 @@ public class StatusBarIconHolder { return mWifiState.visible; case TYPE_MOBILE: return mMobileState.visible; + case TYPE_MOBILE_NEW: + //TODO (b/249790733), the new pipeline can control visibility via the ViewModel + return true; - default: return true; + default: + return true; } } @@ -156,6 +197,10 @@ public class StatusBarIconHolder { case TYPE_MOBILE: mMobileState.visible = visible; break; + + case TYPE_MOBILE_NEW: + //TODO (b/249790733), the new pipeline can control visibility via the ViewModel + break; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 9d5392af3127..ccb5d8800ddb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -31,12 +31,15 @@ import android.hardware.biometrics.BiometricSourceType; import android.os.Bundle; import android.os.SystemClock; import android.os.Trace; +import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewRootImpl; import android.view.WindowManagerGlobal; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -57,7 +60,6 @@ import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.data.BouncerView; -import com.android.systemui.keyguard.data.BouncerViewDelegate; import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor; import com.android.systemui.keyguard.domain.interactor.BouncerInteractor; import com.android.systemui.navigationbar.NavigationBarView; @@ -65,6 +67,9 @@ import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeExpansionChangeEvent; +import com.android.systemui.shade.ShadeExpansionListener; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.NotificationMediaManager; @@ -74,9 +79,6 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.ViewGroupFadeHelper; import com.android.systemui.statusbar.phone.KeyguardBouncer.BouncerExpansionCallback; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.unfold.FoldAodAnimationController; @@ -100,7 +102,7 @@ import dagger.Lazy; @SysUISingleton public class StatusBarKeyguardViewManager implements RemoteInputController.Callback, StatusBarStateController.StateListener, ConfigurationController.ConfigurationListener, - PanelExpansionListener, NavigationModeController.ModeChangedListener, + ShadeExpansionListener, NavigationModeController.ModeChangedListener, KeyguardViewController, FoldAodAnimationController.FoldAodAnimationStatus { // When hiding the Keyguard with timing supplied from WindowManager, better be early than late. @@ -119,6 +121,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private static final long KEYGUARD_DISMISS_DURATION_LOCKED = 2000; private static String TAG = "StatusBarKeyguardViewManager"; + private static final boolean DEBUG = false; protected final Context mContext; private final ConfigurationController mConfigurationController; @@ -132,7 +135,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private KeyguardMessageAreaController<AuthKeyguardMessageArea> mKeyguardMessageAreaController; private final BouncerCallbackInteractor mBouncerCallbackInteractor; private final BouncerInteractor mBouncerInteractor; - private final BouncerViewDelegate mBouncerViewDelegate; + private final BouncerView mBouncerView; private final Lazy<com.android.systemui.shade.ShadeController> mShadeController; private final BouncerExpansionCallback mExpansionCallback = new BouncerExpansionCallback() { @@ -184,8 +187,25 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (mAlternateAuthInterceptor != null) { mAlternateAuthInterceptor.onBouncerVisibilityChanged(); } + + /* Register predictive back callback when keyguard becomes visible, and unregister + when it's hidden. */ + if (isVisible) { + registerBackCallback(); + } else { + unregisterBackCallback(); + } + } + }; + + private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { + if (DEBUG) { + Log.d(TAG, "onBackInvokedCallback() called, invoking onBackPressed()"); } + onBackPressed(); }; + private boolean mIsBackCallbackRegistered = false; + private final DockManager.DockEventListener mDockEventListener = new DockManager.DockEventListener() { @Override @@ -204,12 +224,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb protected CentralSurfaces mCentralSurfaces; private NotificationPanelViewController mNotificationPanelViewController; private BiometricUnlockController mBiometricUnlockController; + private boolean mCentralSurfacesRegistered; private View mNotificationContainer; @Nullable protected KeyguardBouncer mBouncer; - protected boolean mShowing; - protected boolean mOccluded; protected boolean mRemoteInputActive; private boolean mGlobalActionsVisible = false; private boolean mLastGlobalActionsVisible = false; @@ -256,10 +275,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb new KeyguardUpdateMonitorCallback() { @Override public void onEmergencyCallAction() { - // Since we won't get a setOccluded call we have to reset the view manually such that // the bouncer goes away. - if (mOccluded) { + if (mKeyguardStateController.isOccluded()) { reset(true /* hideBouncerWhenShowing */); } } @@ -308,7 +326,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mKeyguardSecurityModel = keyguardSecurityModel; mBouncerCallbackInteractor = bouncerCallbackInteractor; mBouncerInteractor = bouncerInteractor; - mBouncerViewDelegate = bouncerView.getDelegate(); + mBouncerView = bouncerView; mFoldAodAnimationController = sysUIUnfoldComponent .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null); mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER); @@ -317,7 +335,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void registerCentralSurfaces(CentralSurfaces centralSurfaces, NotificationPanelViewController notificationPanelViewController, - PanelExpansionStateManager panelExpansionStateManager, + ShadeExpansionStateManager shadeExpansionStateManager, BiometricUnlockController biometricUnlockController, View notificationContainer, KeyguardBypassController bypassController) { @@ -331,13 +349,14 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback); } mNotificationPanelViewController = notificationPanelViewController; - if (panelExpansionStateManager != null) { - panelExpansionStateManager.addExpansionListener(this); + if (shadeExpansionStateManager != null) { + shadeExpansionStateManager.addExpansionListener(this); } mBypassController = bypassController; mNotificationContainer = notificationContainer; mKeyguardMessageAreaController = mKeyguardMessageAreaFactory.create( centralSurfaces.getKeyguardMessageArea()); + mCentralSurfacesRegistered = true; registerListeners(); } @@ -378,13 +397,53 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } } + /** Register a callback, to be invoked by the Predictive Back system. */ + private void registerBackCallback() { + if (!mIsBackCallbackRegistered) { + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot != null) { + viewRoot.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_OVERLAY, mOnBackInvokedCallback); + mIsBackCallbackRegistered = true; + } else { + if (DEBUG) { + Log.d(TAG, "view root was null, could not register back callback"); + } + } + } else { + if (DEBUG) { + Log.d(TAG, "prevented registering back callback twice"); + } + } + } + + /** Unregister the callback formerly registered with the Predictive Back system. */ + private void unregisterBackCallback() { + if (mIsBackCallbackRegistered) { + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot != null) { + viewRoot.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback( + mOnBackInvokedCallback); + mIsBackCallbackRegistered = false; + } else { + if (DEBUG) { + Log.d(TAG, "view root was null, could not unregister back callback"); + } + } + } else { + if (DEBUG) { + Log.d(TAG, "prevented unregistering back callback twice"); + } + } + } + @Override public void onDensityOrFontScaleChanged() { hideBouncer(true /* destroyView */); } @Override - public void onPanelExpansionChanged(PanelExpansionChangeEvent event) { + public void onPanelExpansionChanged(ShadeExpansionChangeEvent event) { float fraction = event.getFraction(); boolean tracking = event.getTracking(); // Avoid having the shade and the bouncer open at the same time over a dream. @@ -405,8 +464,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } else if (mNotificationPanelViewController.isUnlockHintRunning()) { if (mBouncer != null) { mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN); + } else { + mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN); } - mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN); } else if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) { // Don't expand to the bouncer. Instead transition back to the lock screen (see // CentralSurfaces#showBouncerOrLockScreenIfKeyguard) @@ -414,17 +474,19 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } else if (bouncerNeedsScrimming()) { if (mBouncer != null) { mBouncer.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE); + } else { + mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE); } - mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE); - } else if (mShowing && !hideBouncerOverDream) { + } else if (mKeyguardStateController.isShowing() && !hideBouncerOverDream) { if (!isWakeAndUnlocking() && !(mBiometricUnlockController.getMode() == MODE_DISMISS_BOUNCER) && !mCentralSurfaces.isInLaunchTransition() && !isUnlockCollapsing()) { if (mBouncer != null) { mBouncer.setExpansion(fraction); + } else { + mBouncerInteractor.setExpansion(fraction); } - mBouncerInteractor.setExpansion(fraction); } if (fraction != KeyguardBouncer.EXPANSION_HIDDEN && tracking && !mKeyguardStateController.canDismissLockScreen() @@ -432,16 +494,18 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb && !bouncerIsAnimatingAway()) { if (mBouncer != null) { mBouncer.show(false /* resetSecuritySelection */, false /* scrimmed */); + } else { + mBouncerInteractor.show(/* isScrimmed= */false); } - mBouncerInteractor.show(/* isScrimmed= */false); } - } else if (!mShowing && isBouncerInTransit()) { + } else if (!mKeyguardStateController.isShowing() && isBouncerInTransit()) { // Keyguard is not visible anymore, but expansion animation was still running. // We need to hide the bouncer, otherwise it will be stuck in transit. if (mBouncer != null) { mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN); + } else { + mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN); } - mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN); } else if (mPulsing && fraction == KeyguardBouncer.EXPANSION_VISIBLE) { // Panel expanded while pulsing but didn't translate the bouncer (because we are // unlocked.) Let's simply wake-up to dismiss the lock screen. @@ -467,9 +531,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void show(Bundle options) { Trace.beginSection("StatusBarKeyguardViewManager#show"); - mShowing = true; mNotificationShadeWindowController.setKeyguardShowing(true); - mKeyguardStateController.notifyKeyguardState(mShowing, + mKeyguardStateController.notifyKeyguardState(true, mKeyguardStateController.isOccluded()); reset(true /* hideBouncerWhenShowing */); SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED, @@ -487,8 +550,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mCentralSurfaces.hideKeyguard(); if (mBouncer != null) { mBouncer.show(true /* resetSecuritySelection */); + } else { + mBouncerInteractor.show(true); } - mBouncerInteractor.show(true); } else { mCentralSurfaces.showKeyguard(); if (hideBouncerWhenShowing) { @@ -529,9 +593,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb void hideBouncer(boolean destroyView) { if (mBouncer != null) { mBouncer.hide(destroyView); + } else { + mBouncerInteractor.hide(); } - mBouncerInteractor.hide(); - if (mShowing) { + if (mKeyguardStateController.isShowing()) { // If we were showing the bouncer and then aborting, we need to also clear out any // potential actions unless we actually unlocked. cancelPostAuthActions(); @@ -548,11 +613,12 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public void showBouncer(boolean scrimmed) { resetAlternateAuth(false); - if (mShowing && !isBouncerShowing()) { + if (mKeyguardStateController.isShowing() && !isBouncerShowing()) { if (mBouncer != null) { mBouncer.show(false /* resetSecuritySelection */, scrimmed); + } else { + mBouncerInteractor.show(scrimmed); } - mBouncerInteractor.show(scrimmed); } updateStates(); } @@ -564,7 +630,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public void dismissWithAction(OnDismissAction r, Runnable cancelAction, boolean afterKeyguardGone, String message) { - if (mShowing) { + if (mKeyguardStateController.isShowing()) { try { Trace.beginSection("StatusBarKeyguardViewManager#dismissWithAction"); cancelPendingWakeupAction(); @@ -588,9 +654,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (mBouncer != null) { mBouncer.setDismissAction(mAfterKeyguardGoneAction, mKeyguardGoneCancelAction); + } else { + mBouncerInteractor.setDismissAction(mAfterKeyguardGoneAction, + mKeyguardGoneCancelAction); } - mBouncerInteractor.setDismissAction(mAfterKeyguardGoneAction, - mKeyguardGoneCancelAction); mAfterKeyguardGoneAction = null; mKeyguardGoneCancelAction = null; } @@ -603,17 +670,21 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (afterKeyguardGone) { // we'll handle the dismiss action after keyguard is gone, so just show the // bouncer - mBouncerInteractor.show(/* isScrimmed= */true); - if (mBouncer != null) mBouncer.show(false /* resetSecuritySelection */); + if (mBouncer != null) { + mBouncer.show(false /* resetSecuritySelection */); + } else { + mBouncerInteractor.show(/* isScrimmed= */true); + } } else { // after authentication success, run dismiss action with the option to defer // hiding the keyguard based on the return value of the OnDismissAction - mBouncerInteractor.setDismissAction( - mAfterKeyguardGoneAction, mKeyguardGoneCancelAction); - mBouncerInteractor.show(/* isScrimmed= */true); if (mBouncer != null) { mBouncer.showWithDismissAction(mAfterKeyguardGoneAction, mKeyguardGoneCancelAction); + } else { + mBouncerInteractor.setDismissAction( + mAfterKeyguardGoneAction, mKeyguardGoneCancelAction); + mBouncerInteractor.show(/* isScrimmed= */true); } // bouncer will handle the dismiss action, so we no longer need to track it here mAfterKeyguardGoneAction = null; @@ -645,11 +716,12 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void reset(boolean hideBouncerWhenShowing) { - if (mShowing) { + if (mKeyguardStateController.isShowing()) { + final boolean isOccluded = mKeyguardStateController.isOccluded(); // Hide quick settings. - mNotificationPanelViewController.resetViews(/* animate= */ !mOccluded); + mNotificationPanelViewController.resetViews(/* animate= */ !isOccluded); // Hide bouncer and quick-quick settings. - if (mOccluded && !mDozing) { + if (isOccluded && !mDozing) { mCentralSurfaces.hideKeyguard(); if (hideBouncerWhenShowing || needsFullscreenBouncer()) { hideBouncer(false /* destroyView */); @@ -717,8 +789,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public void onFinishedGoingToSleep() { if (mBouncer != null) { mBouncer.onScreenTurnedOff(); + } else { + mBouncerInteractor.onScreenTurnedOff(); } - mBouncerInteractor.onScreenTurnedOff(); } @Override @@ -730,7 +803,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private void setDozing(boolean dozing) { if (mDozing != dozing) { mDozing = dozing; - if (dozing || mBouncer.needsFullscreenBouncer() || mOccluded) { + if (dozing || needsFullscreenBouncer() + || mKeyguardStateController.isOccluded()) { reset(dozing /* hideBouncerWhenShowing */); } updateStates(); @@ -763,18 +837,23 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void setOccluded(boolean occluded, boolean animate) { - final boolean isOccluding = !mOccluded && occluded; - final boolean isUnOccluding = mOccluded && !occluded; - setOccludedAndUpdateStates(occluded); + final boolean wasOccluded = mKeyguardStateController.isOccluded(); + final boolean isOccluding = !wasOccluded && occluded; + final boolean isUnOccluding = wasOccluded && !occluded; + mKeyguardStateController.notifyKeyguardState( + mKeyguardStateController.isShowing(), occluded); + updateStates(); + final boolean isShowing = mKeyguardStateController.isShowing(); + final boolean isOccluded = mKeyguardStateController.isOccluded(); - if (mShowing && isOccluding) { + if (isShowing && isOccluding) { SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED, SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__OCCLUDED); if (mCentralSurfaces.isInLaunchTransition()) { final Runnable endRunnable = new Runnable() { @Override public void run() { - mNotificationShadeWindowController.setKeyguardOccluded(mOccluded); + mNotificationShadeWindowController.setKeyguardOccluded(isOccluded); reset(true /* hideBouncerWhenShowing */); } }; @@ -789,19 +868,19 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb // When isLaunchingActivityOverLockscreen() is true, we know for sure that the post // collapse runnables will be run. mShadeController.get().addPostCollapseAction(() -> { - mNotificationShadeWindowController.setKeyguardOccluded(mOccluded); + mNotificationShadeWindowController.setKeyguardOccluded(isOccluded); reset(true /* hideBouncerWhenShowing */); }); return; } - } else if (mShowing && isUnOccluding) { + } else if (isShowing && isUnOccluding) { SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED, SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__SHOWN); } - if (mShowing) { - mMediaManager.updateMediaMetaData(false, animate && !mOccluded); + if (isShowing) { + mMediaManager.updateMediaMetaData(false, animate && !isOccluded); } - mNotificationShadeWindowController.setKeyguardOccluded(mOccluded); + mNotificationShadeWindowController.setKeyguardOccluded(isOccluded); // setDozing(false) will call reset once we stop dozing. Also, if we're going away, there's // no need to reset the keyguard views as we'll be gone shortly. Resetting now could cause @@ -811,27 +890,19 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb // by a FLAG_DISMISS_KEYGUARD_ACTIVITY. reset(isOccluding /* hideBouncerWhenShowing*/); } - if (animate && !mOccluded && mShowing && !bouncerIsShowing()) { + if (animate && !isOccluded && isShowing && !bouncerIsShowing()) { mCentralSurfaces.animateKeyguardUnoccluding(); } } - private void setOccludedAndUpdateStates(boolean occluded) { - mOccluded = occluded; - updateStates(); - } - - public boolean isOccluded() { - return mOccluded; - } - @Override public void startPreHideAnimation(Runnable finishRunnable) { if (bouncerIsShowing()) { if (mBouncer != null) { mBouncer.startPreHideAnimation(finishRunnable); + } else { + mBouncerInteractor.startDisappearAnimation(finishRunnable); } - mBouncerInteractor.startDisappearAnimation(finishRunnable); mCentralSurfaces.onBouncerPreHideAnimation(); // We update the state (which will show the keyguard) only if an animation will run on @@ -854,8 +925,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void hide(long startTime, long fadeoutDuration) { Trace.beginSection("StatusBarKeyguardViewManager#hide"); - mShowing = false; - mKeyguardStateController.notifyKeyguardState(mShowing, + mKeyguardStateController.notifyKeyguardState(false, mKeyguardStateController.isOccluded()); launchPendingWakeupAction(); @@ -1004,33 +1074,43 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb KeyguardUpdateMonitor.getCurrentUser()) != KeyguardSecurityModel.SecurityMode.None; } - @Override - public boolean isShowing() { - return mShowing; + /** + * Returns whether a back invocation can be handled, which depends on whether the keyguard + * is currently showing (which itself is derived from multiple states). + * + * @return whether a back press can be handled right now. + */ + public boolean canHandleBackPressed() { + return bouncerIsShowing(); } /** * Notifies this manager that the back button has been pressed. - * - * @param hideImmediately Hide bouncer when {@code true}, keep it around otherwise. - * Non-scrimmed bouncers have a special animation tied to the expansion - * of the notification panel. - * @return whether the back press has been handled */ - public boolean onBackPressed(boolean hideImmediately) { - if (bouncerIsShowing()) { - mCentralSurfaces.endAffordanceLaunch(); - // The second condition is for SIM card locked bouncer - if (bouncerIsScrimmed() - && !needsFullscreenBouncer()) { - hideBouncer(false); - updateStates(); + public void onBackPressed() { + if (!canHandleBackPressed()) { + return; + } + + mCentralSurfaces.endAffordanceLaunch(); + // The second condition is for SIM card locked bouncer + if (bouncerIsScrimmed() && !needsFullscreenBouncer()) { + hideBouncer(false); + updateStates(); + } else { + /* Non-scrimmed bouncers have a special animation tied to the expansion + * of the notification panel. We decide whether to kick this animation off + * by computing the hideImmediately boolean. + */ + boolean hideImmediately = mCentralSurfaces.shouldKeyguardHideImmediately(); + reset(hideImmediately); + if (hideImmediately) { + mStatusBarStateController.setLeaveOpenOnKeyguardHide(false); } else { - reset(hideImmediately); + mNotificationPanelViewController.expandWithoutQs(); } - return true; } - return false; + return; } @Override @@ -1044,8 +1124,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } public boolean isFullscreenBouncer() { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.isFullScreenBouncer(); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().isFullScreenBouncer(); } return mBouncer != null && mBouncer.isFullscreenBouncer(); } @@ -1089,8 +1169,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb }; protected void updateStates() { - boolean showing = mShowing; - boolean occluded = mOccluded; + if (!mCentralSurfacesRegistered) { + return; + } + boolean showing = mKeyguardStateController.isShowing(); + boolean occluded = mKeyguardStateController.isOccluded(); boolean bouncerShowing = bouncerIsShowing(); boolean bouncerIsOrWillBeShowing = bouncerIsOrWillBeShowing(); boolean bouncerDismissible = !isFullscreenBouncer(); @@ -1102,13 +1185,15 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (bouncerDismissible || !showing || remoteInputActive) { if (mBouncer != null) { mBouncer.setBackButtonEnabled(true); + } else { + mBouncerInteractor.setBackButtonEnabled(true); } - mBouncerInteractor.setBackButtonEnabled(true); } else { if (mBouncer != null) { mBouncer.setBackButtonEnabled(false); + } else { + mBouncerInteractor.setBackButtonEnabled(false); } - mBouncerInteractor.setBackButtonEnabled(false); } } @@ -1122,13 +1207,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mNotificationShadeWindowController.setBouncerShowing(bouncerShowing); mCentralSurfaces.setBouncerShowing(bouncerShowing); } - - if (occluded != mLastOccluded || mFirstUpdate) { - mKeyguardStateController.notifyKeyguardState(showing, occluded); - } - if ((showing && !occluded) != (mLastShowing && !mLastOccluded) || mFirstUpdate) { - mKeyguardUpdateManager.onKeyguardVisibilityChanged(showing && !occluded); - } if (bouncerIsOrWillBeShowing != mLastBouncerIsOrWillBeShowing || mFirstUpdate || bouncerShowing != mLastBouncerShowing) { mKeyguardUpdateManager.sendKeyguardBouncerChanged(bouncerIsOrWillBeShowing, @@ -1179,12 +1257,12 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public boolean isNavBarVisible() { boolean isWakeAndUnlockPulsing = mBiometricUnlockController != null && mBiometricUnlockController.getMode() == MODE_WAKE_AND_UNLOCK_PULSING; - boolean keyguardShowing = mShowing && !mOccluded; + boolean keyguardVisible = mKeyguardStateController.isVisible(); boolean hideWhileDozing = mDozing && !isWakeAndUnlockPulsing; - boolean keyguardWithGestureNav = (keyguardShowing && !mDozing && !mScreenOffAnimationPlaying + boolean keyguardWithGestureNav = (keyguardVisible && !mDozing && !mScreenOffAnimationPlaying || mPulsing && !mIsDocked) && mGesturalNav; - return (!keyguardShowing && !hideWhileDozing && !mScreenOffAnimationPlaying + return (!keyguardVisible && !hideWhileDozing && !mScreenOffAnimationPlaying || bouncerIsShowing() || mRemoteInputActive || keyguardWithGestureNav @@ -1206,15 +1284,15 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } public boolean shouldDismissOnMenuPressed() { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.shouldDismissOnMenuPressed(); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().shouldDismissOnMenuPressed(); } return mBouncer != null && mBouncer.shouldDismissOnMenuPressed(); } public boolean interceptMediaKey(KeyEvent event) { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.interceptMediaKey(event); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().interceptMediaKey(event); } return mBouncer != null && mBouncer.interceptMediaKey(event); } @@ -1223,8 +1301,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * @return true if the pre IME back event should be handled */ public boolean dispatchBackKeyEventPreIme() { - if (mBouncerViewDelegate != null) { - return mBouncerViewDelegate.dispatchBackKeyEventPreIme(); + if (mBouncerView.getDelegate() != null) { + return mBouncerView.getDelegate().dispatchBackKeyEventPreIme(); } return mBouncer != null && mBouncer.dispatchBackKeyEventPreIme(); } @@ -1274,8 +1352,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public void notifyKeyguardAuthenticated(boolean strongAuth) { if (mBouncer != null) { mBouncer.notifyKeyguardAuthenticated(strongAuth); + } else { + mBouncerInteractor.notifyKeyguardAuthenticated(strongAuth); } - mBouncerInteractor.notifyKeyguardAuthenticated(strongAuth); if (mAlternateAuthInterceptor != null && isShowingAlternateAuthOrAnimating()) { resetAlternateAuth(false); @@ -1292,21 +1371,30 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } else { if (mBouncer != null) { mBouncer.showMessage(message, colorState); + } else { + mBouncerInteractor.showMessage(message, colorState); } - mBouncerInteractor.showMessage(message, colorState); } } @Override public ViewRootImpl getViewRootImpl() { - return mNotificationShadeWindowController.getNotificationShadeView().getViewRootImpl(); + ViewGroup viewGroup = mNotificationShadeWindowController.getNotificationShadeView(); + if (viewGroup != null) { + return viewGroup.getViewRootImpl(); + } else { + if (DEBUG) { + Log.d(TAG, "ViewGroup was null, cannot get ViewRootImpl"); + } + return null; + } } public void launchPendingWakeupAction() { DismissWithActionRequest request = mPendingWakeupAction; mPendingWakeupAction = null; if (request != null) { - if (mShowing) { + if (mKeyguardStateController.isShowing()) { dismissWithAction(request.dismissAction, request.cancelAction, request.afterKeyguardGone, request.message); } else if (request.dismissAction != null) { @@ -1325,10 +1413,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public boolean bouncerNeedsScrimming() { // When a dream overlay is active, scrimming will cause any expansion to immediately expand. - return (mOccluded && !mDreamOverlayStateController.isOverlayActive()) + return (mKeyguardStateController.isOccluded() + && !mDreamOverlayStateController.isOverlayActive()) || bouncerWillDismissWithAction() - || (bouncerIsShowing() - && bouncerIsScrimmed()) + || (bouncerIsShowing() && bouncerIsScrimmed()) || isFullscreenBouncer(); } @@ -1340,14 +1428,13 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public void updateResources() { if (mBouncer != null) { mBouncer.updateResources(); + } else { + mBouncerInteractor.updateResources(); } - mBouncerInteractor.updateResources(); } public void dump(PrintWriter pw) { pw.println("StatusBarKeyguardViewManager:"); - pw.println(" mShowing: " + mShowing); - pw.println(" mOccluded: " + mOccluded); pw.println(" mRemoteInputActive: " + mRemoteInputActive); pw.println(" mDozing: " + mDozing); pw.println(" mAfterKeyguardGoneAction: " + mAfterKeyguardGoneAction); @@ -1426,9 +1513,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public void updateKeyguardPosition(float x) { if (mBouncer != null) { mBouncer.updateKeyguardPosition(x); + } else { + mBouncerInteractor.setKeyguardPosition(x); } - - mBouncerInteractor.setKeyguardPosition(x); } private static class DismissWithActionRequest { @@ -1470,9 +1557,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public boolean isBouncerInTransit() { if (mBouncer != null) { return mBouncer.inTransit(); + } else { + return mBouncerInteractor.isInTransit(); } - - return mBouncerInteractor.isInTransit(); } /** @@ -1481,9 +1568,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public boolean bouncerIsShowing() { if (mBouncer != null) { return mBouncer.isShowing(); + } else { + return mBouncerInteractor.isFullyShowing(); } - - return mBouncerInteractor.isFullyShowing(); } /** @@ -1492,9 +1579,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public boolean bouncerIsScrimmed() { if (mBouncer != null) { return mBouncer.isScrimmed(); + } else { + return mBouncerInteractor.isScrimmed(); } - - return mBouncerInteractor.isScrimmed(); } /** @@ -1503,9 +1590,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public boolean bouncerIsAnimatingAway() { if (mBouncer != null) { return mBouncer.isAnimatingAway(); + } else { + return mBouncerInteractor.isAnimatingAway(); } - return mBouncerInteractor.isAnimatingAway(); } /** @@ -1514,9 +1602,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public boolean bouncerWillDismissWithAction() { if (mBouncer != null) { return mBouncer.willDismissWithAction(); + } else { + return mBouncerInteractor.willDismissWithAction(); } - - return mBouncerInteractor.willDismissWithAction(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt index b9a1413ff791..81edff45c505 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt @@ -17,12 +17,12 @@ package com.android.systemui.statusbar.phone import android.app.PendingIntent -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogLevel.ERROR -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogLevel.ERROR +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.WARNING import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index 64ca270558e8..a1e0c5067ef3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -179,7 +179,6 @@ class StatusBarNotificationPresenter implements NotificationPresenter, mNotifShadeEventSource.setNotifRemovedByUserCallback(this::maybeEndAmbientPulse); notificationInterruptStateProvider.addSuppressor(mInterruptSuppressor); mLockscreenUserManager.setUpWithPresenter(this); - mMediaManager.setUpWithPresenter(this); mGutsManager.setUpWithPresenter( this, mNotifListContainer, mOnSettingsClickListener); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java index d464acb7fe76..26c17674ab10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java @@ -337,7 +337,7 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { // without cutting off the child view. translationX -= getViewTotalWidth(child); childState.visibleState = STATE_ICON; - childState.xTranslation = translationX; + childState.setXTranslation(translationX); mLayoutStates.add(0, childState); // Shift translationX over by mIconSpacing for the next view. @@ -354,13 +354,13 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { for (int i = totalVisible - 1; i >= 0; i--) { StatusIconState state = mLayoutStates.get(i); // Allow room for underflow if we found we need it in onMeasure - if (mNeedsUnderflow && (state.xTranslation < (contentStart + mUnderflowWidth))|| - (mShouldRestrictIcons && visible >= maxVisible)) { + if (mNeedsUnderflow && (state.getXTranslation() < (contentStart + mUnderflowWidth)) + || (mShouldRestrictIcons && (visible >= maxVisible))) { firstUnderflowIndex = i; break; } mUnderflowStart = (int) Math.max( - contentStart, state.xTranslation - mUnderflowWidth - mIconSpacing); + contentStart, state.getXTranslation() - mUnderflowWidth - mIconSpacing); visible++; } @@ -371,7 +371,7 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { for (int i = firstUnderflowIndex; i >= 0; i--) { StatusIconState state = mLayoutStates.get(i); if (totalDots < MAX_DOTS) { - state.xTranslation = dotOffset; + state.setXTranslation(dotOffset); state.visibleState = STATE_DOT; dotOffset -= dotWidth; totalDots++; @@ -386,7 +386,7 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { for (int i = 0; i < childCount; i++) { View child = getChildAt(i); StatusIconState state = getViewStateFromChild(child); - state.xTranslation = width - state.xTranslation - child.getWidth(); + state.setXTranslation(width - state.getXTranslation() - child.getWidth()); } } } @@ -410,7 +410,7 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { } vs.initFrom(child); - vs.alpha = 1.0f; + vs.setAlpha(1.0f); vs.hidden = false; } } @@ -442,7 +442,7 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { parentWidth = ((View) view.getParent()).getWidth(); } - float currentDistanceToEnd = parentWidth - xTranslation; + float currentDistanceToEnd = parentWidth - getXTranslation(); if (!(view instanceof StatusIconDisplayable)) { return; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt index a0415f2f3d7c..6cd8c78dd52f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt @@ -22,8 +22,6 @@ import android.view.WindowInsetsController.Behavior import com.android.internal.statusbar.LetterboxDetails import com.android.internal.view.AppearanceRegion import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope @@ -42,7 +40,6 @@ class SystemBarAttributesListener @Inject internal constructor( private val centralSurfaces: CentralSurfaces, - private val featureFlags: FeatureFlags, private val letterboxAppearanceCalculator: LetterboxAppearanceCalculator, private val statusBarStateController: SysuiStatusBarStateController, private val lightBarController: LightBarController, @@ -127,15 +124,11 @@ internal constructor( } private fun shouldUseLetterboxAppearance(letterboxDetails: Array<LetterboxDetails>) = - isLetterboxAppearanceFlagEnabled() && letterboxDetails.isNotEmpty() - - private fun isLetterboxAppearanceFlagEnabled() = - featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE) + letterboxDetails.isNotEmpty() private fun dump(printWriter: PrintWriter, strings: Array<String>) { printWriter.println("lastSystemBarAttributesParams: $lastSystemBarAttributesParams") printWriter.println("lastLetterboxAppearance: $lastLetterboxAppearance") - printWriter.println("letterbox appearance flag: ${isLetterboxAppearanceFlagEnabled()}") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java index 627cfb718d91..dc902666874d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java @@ -41,6 +41,7 @@ import com.android.systemui.shade.NotificationPanelView; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationsQuickSettingsContainer; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.NotificationShelfController; @@ -64,7 +65,6 @@ import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment; import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragmentLogger; import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -284,7 +284,7 @@ public abstract class StatusBarViewModule { SystemStatusAnimationScheduler animationScheduler, StatusBarLocationPublisher locationPublisher, NotificationIconAreaController notificationIconAreaController, - PanelExpansionStateManager panelExpansionStateManager, + ShadeExpansionStateManager shadeExpansionStateManager, FeatureFlags featureFlags, StatusBarIconController statusBarIconController, StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory, @@ -306,7 +306,7 @@ public abstract class StatusBarViewModule { animationScheduler, locationPublisher, notificationIconAreaController, - panelExpansionStateManager, + shadeExpansionStateManager, featureFlags, statusBarIconController, darkIconManagerFactory, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index e1215ee95238..9f3fd727be24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -53,6 +53,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationPanelViewController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.DisableFlagsLogger.DisableState; import com.android.systemui.statusbar.OperatorNameView; @@ -74,7 +75,6 @@ import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentCom import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent.Startable; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.EncryptionHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.CarrierConfigTracker; @@ -127,7 +127,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final StatusBarLocationPublisher mLocationPublisher; private final FeatureFlags mFeatureFlags; private final NotificationIconAreaController mNotificationIconAreaController; - private final PanelExpansionStateManager mPanelExpansionStateManager; + private final ShadeExpansionStateManager mShadeExpansionStateManager; private final StatusBarIconController mStatusBarIconController; private final CarrierConfigTracker mCarrierConfigTracker; private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; @@ -184,7 +184,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue SystemStatusAnimationScheduler animationScheduler, StatusBarLocationPublisher locationPublisher, NotificationIconAreaController notificationIconAreaController, - PanelExpansionStateManager panelExpansionStateManager, + ShadeExpansionStateManager shadeExpansionStateManager, FeatureFlags featureFlags, StatusBarIconController statusBarIconController, StatusBarIconController.DarkIconManager.Factory darkIconManagerFactory, @@ -206,7 +206,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mAnimationScheduler = animationScheduler; mLocationPublisher = locationPublisher; mNotificationIconAreaController = notificationIconAreaController; - mPanelExpansionStateManager = panelExpansionStateManager; + mShadeExpansionStateManager = shadeExpansionStateManager; mFeatureFlags = featureFlags; mStatusBarIconController = statusBarIconController; mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager; @@ -490,7 +490,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } private boolean shouldHideNotificationIcons() { - if (!mPanelExpansionStateManager.isClosed() + if (!mShadeExpansionStateManager.isClosed() && mNotificationPanelViewController.hideStatusBarIconsWhenExpanded()) { return true; } @@ -536,7 +536,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue * don't set the clock GONE otherwise it'll mess up the animation. */ private int clockHiddenMode() { - if (!mPanelExpansionStateManager.isClosed() && !mKeyguardStateController.isShowing() + if (!mShadeExpansionStateManager.isClosed() && !mKeyguardStateController.isShowing() && !mStatusBarStateController.isDozing()) { return View.INVISIBLE; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt index 9ae378f34fc0..0e6b7f253bcf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.phone.fragment -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.CollapsedSbFragmentLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.DisableFlagsLogger import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt index 0d52f46e571f..e498ae451400 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.phone.userswitcher import android.content.Intent import android.os.UserHandle import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter @@ -75,7 +76,7 @@ class StatusBarUserSwitcherControllerImpl @Inject constructor( null /* ActivityLaunchAnimator.Controller */, true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM) } else { - userSwitcherDialogController.showDialog(view) + userSwitcherDialogController.showDialog(view.context, Expandable.fromView(view)) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt index 9b8b6434827e..06cd12dd1a0d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt @@ -24,29 +24,19 @@ import javax.inject.Inject /** All flagging methods related to the new status bar pipeline (see b/238425913). */ @SysUISingleton class StatusBarPipelineFlags @Inject constructor(private val featureFlags: FeatureFlags) { - /** - * Returns true if we should run the new pipeline backend. - * - * The new pipeline backend hooks up to all our external callbacks, logs those callback inputs, - * and logs the output state. - */ - fun isNewPipelineBackendEnabled(): Boolean = - featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_BACKEND) + /** True if we should display the mobile icons using the new status bar data pipeline. */ + fun useNewMobileIcons(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS) - /** - * Returns true if we should run the new pipeline frontend *and* backend. - * - * The new pipeline frontend will use the outputted state from the new backend and will make the - * correct changes to the UI. - */ - fun isNewPipelineFrontendEnabled(): Boolean = - isNewPipelineBackendEnabled() && - featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_FRONTEND) + /** True if we should display the wifi icon using the new status bar data pipeline. */ + fun useNewWifiIcon(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON) + + // TODO(b/238425913): Add flags to only run the mobile backend or wifi backend so we get the + // logging without getting the UI effects. /** - * Returns true if we should apply some coloring to icons that were rendered with the new + * Returns true if we should apply some coloring to the wifi icon that was rendered with the new * pipeline to help with debugging. */ - // For now, just always apply the debug coloring if we've enabled frontend rendering. - fun useNewPipelineDebugColoring(): Boolean = isNewPipelineFrontendEnabled() + // For now, just always apply the debug coloring if we've enabled the new icon. + fun useWifiDebugColoring(): Boolean = useNewWifiIcon() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt new file mode 100644 index 000000000000..7aa5ee1389f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.data.repository + +import android.os.Handler +import android.os.UserHandle +import android.provider.Settings.Global +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.SettingObserver +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange +import com.android.systemui.util.settings.GlobalSettings +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn + +/** + * Provides data related to airplane mode. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. It is + * only used to help [com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel] + * determine what parts of the wifi icon view should be shown. + * + * TODO(b/238425913): Consider migrating the status bar airplane mode icon to use this repo. + */ +interface AirplaneModeRepository { + /** Observable for whether the device is currently in airplane mode. */ + val isAirplaneMode: StateFlow<Boolean> +} + +@SysUISingleton +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class AirplaneModeRepositoryImpl +@Inject +constructor( + @Background private val bgHandler: Handler, + private val globalSettings: GlobalSettings, + logger: ConnectivityPipelineLogger, + @Application scope: CoroutineScope, +) : AirplaneModeRepository { + // TODO(b/254848912): Replace this with a generic SettingObserver coroutine once we have it. + override val isAirplaneMode: StateFlow<Boolean> = + conflatedCallbackFlow { + val observer = + object : + SettingObserver( + globalSettings, + bgHandler, + Global.AIRPLANE_MODE_ON, + UserHandle.USER_ALL + ) { + override fun handleValueChanged(value: Int, observedChange: Boolean) { + trySend(value == 1) + } + } + + observer.isListening = true + trySend(observer.value == 1) + awaitClose { observer.isListening = false } + } + .distinctUntilChanged() + .logInputChange(logger, "isAirplaneMode") + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + // When the observer starts listening, the flow will emit the current value so the + // initialValue here is irrelevant. + initialValue = false, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt new file mode 100644 index 000000000000..3e9b2c2ae809 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * The business logic layer for airplane mode. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See + * [AirplaneModeRepository] for more details. + */ +@SysUISingleton +class AirplaneModeInteractor +@Inject +constructor( + airplaneModeRepository: AirplaneModeRepository, + connectivityRepository: ConnectivityRepository, +) { + /** True if the device is currently in airplane mode. */ + val isAirplaneMode: Flow<Boolean> = airplaneModeRepository.isAirplaneMode + + /** True if we're configured to force-hide the airplane mode icon and false otherwise. */ + val isForceHidden: Flow<Boolean> = + connectivityRepository.forceHiddenSlots.map { it.contains(ConnectivitySlot.AIRPLANE) } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt new file mode 100644 index 000000000000..fe30c0169021 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn + +/** + * Models the UI state for the status bar airplane mode icon. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See + * [com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository] for + * more details. + */ +@SysUISingleton +class AirplaneModeViewModel +@Inject +constructor( + interactor: AirplaneModeInteractor, + logger: ConnectivityPipelineLogger, + @Application private val scope: CoroutineScope, +) { + /** True if the airplane mode icon is currently visible in the status bar. */ + val isAirplaneModeIconVisible: StateFlow<Boolean> = + combine(interactor.isAirplaneMode, interactor.isForceHidden) { + isAirplaneMode, + isAirplaneIconForceHidden -> + isAirplaneMode && !isAirplaneIconForceHidden + } + .distinctUntilChanged() + .logOutputChange(logger, "isAirplaneModeIconVisible") + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 9a7c3fae780c..fcd1b8abefe4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -16,6 +16,16 @@ package com.android.systemui.statusbar.pipeline.dagger +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl +import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository @@ -26,8 +36,25 @@ import dagger.Module @Module abstract class StatusBarPipelineModule { @Binds + abstract fun airplaneModeRepository(impl: AirplaneModeRepositoryImpl): AirplaneModeRepository + + @Binds abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository + + @Binds + abstract fun mobileConnectionsRepository( + impl: MobileConnectionsRepositoryImpl + ): MobileConnectionsRepository + + @Binds + abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository + + @Binds + abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy + + @Binds + abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt new file mode 100644 index 000000000000..eaba0e93e750 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.model + +import android.annotation.IntRange +import android.telephony.Annotation.DataActivityType +import android.telephony.CellSignalStrength +import android.telephony.TelephonyCallback.CarrierNetworkListener +import android.telephony.TelephonyCallback.DataActivityListener +import android.telephony.TelephonyCallback.DataConnectionStateListener +import android.telephony.TelephonyCallback.DisplayInfoListener +import android.telephony.TelephonyCallback.ServiceStateListener +import android.telephony.TelephonyCallback.SignalStrengthsListener +import android.telephony.TelephonyDisplayInfo +import android.telephony.TelephonyManager +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN + +/** + * Data class containing all of the relevant information for a particular line of service, known as + * a Subscription in the telephony world. These models are the result of a single telephony listener + * which has many callbacks which each modify some particular field on this object. + * + * The design goal here is to de-normalize fields from the system into our model fields below. So + * any new field that needs to be tracked should be copied into this data class rather than + * threading complex system objects through the pipeline. + */ +data class MobileSubscriptionModel( + /** From [ServiceStateListener.onServiceStateChanged] */ + val isEmergencyOnly: Boolean = false, + + /** From [SignalStrengthsListener.onSignalStrengthsChanged] */ + val isGsm: Boolean = false, + @IntRange(from = 0, to = 4) + val cdmaLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN, + @IntRange(from = 0, to = 4) + val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN, + + /** Comes directly from [DataConnectionStateListener.onDataConnectionStateChanged] */ + val dataConnectionState: Int? = null, + + /** From [DataActivityListener.onDataActivity]. See [TelephonyManager] for the values */ + @DataActivityType val dataActivityDirection: Int? = null, + + /** From [CarrierNetworkListener.onCarrierNetworkChange] */ + val carrierNetworkChangeActive: Boolean? = null, + + /** + * From [DisplayInfoListener.onDisplayInfoChanged]. + * + * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or + * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon + */ + val resolvedNetworkType: ResolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN), +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt new file mode 100644 index 000000000000..f385806c1b22 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.model + +import android.telephony.Annotation.NetworkType +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy + +/** + * A SysUI type to represent the [NetworkType] that we pull out of [TelephonyDisplayInfo]. Depending + * on whether or not the display info contains an override type, we may have to call different + * methods on [MobileMappingsProxy] to generate an icon lookup key. + */ +sealed interface ResolvedNetworkType { + @NetworkType val type: Int +} + +data class DefaultNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType + +data class OverrideNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt new file mode 100644 index 000000000000..45284cf0332b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository + +import android.telephony.CellSignalStrength +import android.telephony.CellSignalStrengthCdma +import android.telephony.ServiceState +import android.telephony.SignalStrength +import android.telephony.SubscriptionInfo +import android.telephony.TelephonyCallback +import android.telephony.TelephonyDisplayInfo +import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE +import android.telephony.TelephonyManager +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import java.lang.IllegalStateException +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +/** + * Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a + * repository for each individual, tracked subscription via [MobileConnectionsRepository], and this + * repository is responsible for setting up a [TelephonyManager] object tied to its subscriptionId + * + * There should only ever be one [MobileConnectionRepository] per subscription, since + * [TelephonyManager] limits the number of callbacks that can be registered per process. + * + * This repository should have all of the relevant information for a single line of service, which + * eventually becomes a single icon in the status bar. + */ +interface MobileConnectionRepository { + /** + * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single + * listener + model. + */ + val subscriptionModelFlow: Flow<MobileSubscriptionModel> +} + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class MobileConnectionRepositoryImpl( + private val subId: Int, + telephonyManager: TelephonyManager, + bgDispatcher: CoroutineDispatcher, + logger: ConnectivityPipelineLogger, + scope: CoroutineScope, +) : MobileConnectionRepository { + init { + if (telephonyManager.subscriptionId != subId) { + throw IllegalStateException( + "TelephonyManager should be created with subId($subId). " + + "Found ${telephonyManager.subscriptionId} instead." + ) + } + } + + override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run { + var state = MobileSubscriptionModel() + conflatedCallbackFlow { + // TODO (b/240569788): log all of these into the connectivity logger + val callback = + object : + TelephonyCallback(), + TelephonyCallback.ServiceStateListener, + TelephonyCallback.SignalStrengthsListener, + TelephonyCallback.DataConnectionStateListener, + TelephonyCallback.DataActivityListener, + TelephonyCallback.CarrierNetworkListener, + TelephonyCallback.DisplayInfoListener { + override fun onServiceStateChanged(serviceState: ServiceState) { + state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly) + trySend(state) + } + + override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { + val cdmaLevel = + signalStrength + .getCellSignalStrengths(CellSignalStrengthCdma::class.java) + .let { strengths -> + if (!strengths.isEmpty()) { + strengths[0].level + } else { + CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN + } + } + + val primaryLevel = signalStrength.level + + state = + state.copy( + cdmaLevel = cdmaLevel, + primaryLevel = primaryLevel, + isGsm = signalStrength.isGsm, + ) + trySend(state) + } + + override fun onDataConnectionStateChanged( + dataState: Int, + networkType: Int + ) { + state = state.copy(dataConnectionState = dataState) + trySend(state) + } + + override fun onDataActivity(direction: Int) { + state = state.copy(dataActivityDirection = direction) + trySend(state) + } + + override fun onCarrierNetworkChange(active: Boolean) { + state = state.copy(carrierNetworkChangeActive = active) + trySend(state) + } + + override fun onDisplayInfoChanged( + telephonyDisplayInfo: TelephonyDisplayInfo + ) { + val networkType = + if ( + telephonyDisplayInfo.overrideNetworkType == + OVERRIDE_NETWORK_TYPE_NONE + ) { + DefaultNetworkType(telephonyDisplayInfo.networkType) + } else { + OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType) + } + state = state.copy(resolvedNetworkType = networkType) + trySend(state) + } + } + telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) + awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } + } + .onEach { logger.logOutputChange("mobileSubscriptionModel", it.toString()) } + .stateIn(scope, SharingStarted.WhileSubscribed(), state) + } + + class Factory + @Inject + constructor( + private val telephonyManager: TelephonyManager, + private val logger: ConnectivityPipelineLogger, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + ) { + fun build(subId: Int): MobileConnectionRepository { + return MobileConnectionRepositoryImpl( + subId, + telephonyManager.createForSubscriptionId(subId), + bgDispatcher, + logger, + scope, + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt new file mode 100644 index 000000000000..0e2428ae393a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository + +import android.content.Context +import android.content.IntentFilter +import android.telephony.CarrierConfigManager +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyCallback +import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener +import android.telephony.TelephonyManager +import androidx.annotation.VisibleForTesting +import com.android.settingslib.mobile.MobileMappings +import com.android.settingslib.mobile.MobileMappings.Config +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +/** + * Repo for monitoring the complete active subscription info list, to be consumed and filtered based + * on various policy + */ +interface MobileConnectionsRepository { + /** Observable list of current mobile subscriptions */ + val subscriptionsFlow: Flow<List<SubscriptionInfo>> + + /** Observable for the subscriptionId of the current mobile data connection */ + val activeMobileDataSubscriptionId: Flow<Int> + + /** Observable for [MobileMappings.Config] tracking the defaults */ + val defaultDataSubRatConfig: StateFlow<Config> + + /** Get or create a repository for the line of service for the given subscription ID */ + fun getRepoForSubId(subId: Int): MobileConnectionRepository +} + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class MobileConnectionsRepositoryImpl +@Inject +constructor( + private val subscriptionManager: SubscriptionManager, + private val telephonyManager: TelephonyManager, + private val logger: ConnectivityPipelineLogger, + broadcastDispatcher: BroadcastDispatcher, + private val context: Context, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, + private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory +) : MobileConnectionsRepository { + private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf() + + /** + * State flow that emits the set of mobile data subscriptions, each represented by its own + * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each + * info object, but for now we keep track of the infos themselves. + */ + override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> = + conflatedCallbackFlow { + val callback = + object : SubscriptionManager.OnSubscriptionsChangedListener() { + override fun onSubscriptionsChanged() { + trySend(Unit) + } + } + + subscriptionManager.addOnSubscriptionsChangedListener( + bgDispatcher.asExecutor(), + callback, + ) + + awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } + } + .mapLatest { fetchSubscriptionsList() } + .onEach { infos -> dropUnusedReposFromCache(infos) } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) + + /** StateFlow that keeps track of the current active mobile data subscription */ + override val activeMobileDataSubscriptionId: StateFlow<Int> = + conflatedCallbackFlow { + val callback = + object : TelephonyCallback(), ActiveDataSubscriptionIdListener { + override fun onActiveDataSubscriptionIdChanged(subId: Int) { + trySend(subId) + } + } + + telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) + awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } + } + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + SubscriptionManager.INVALID_SUBSCRIPTION_ID + ) + + private val defaultDataSubChangedEvent = + broadcastDispatcher.broadcastFlow( + IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + ) + + private val carrierConfigChangedEvent = + broadcastDispatcher.broadcastFlow( + IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED) + ) + + /** + * [Config] is an object that tracks relevant configuration flags for a given subscription ID. + * In the case of [MobileMappings], it's hard-coded to check the default data subscription's + * config, so this will apply to every icon that we care about. + * + * Relevant bits in the config are things like + * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL] + * + * This flow will produce whenever the default data subscription or the carrier config changes. + */ + override val defaultDataSubRatConfig: StateFlow<Config> = + combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ -> + Config.readConfig(context) + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + initialValue = Config.readConfig(context) + ) + + override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + if (!isValidSubId(subId)) { + throw IllegalArgumentException( + "subscriptionId $subId is not in the list of valid subscriptions" + ) + } + + return subIdRepositoryCache[subId] + ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it } + } + + private fun isValidSubId(subId: Int): Boolean { + subscriptionsFlow.value.forEach { + if (it.subscriptionId == subId) { + return true + } + } + + return false + } + + @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache + + private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { + return mobileConnectionRepositoryFactory.build(subId) + } + + private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) { + // Remove any connection repository from the cache that isn't in the new set of IDs. They + // will get garbage collected once their subscribers go away + val currentValidSubscriptionIds = newInfos.map { it.subscriptionId } + + subIdRepositoryCache.keys.forEach { + if (!currentValidSubscriptionIds.contains(it)) { + subIdRepositoryCache.remove(it) + } + } + } + + private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> = + withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt new file mode 100644 index 000000000000..77de849691db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository + +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +/** + * Repository to observe the state of [DeviceProvisionedController.isUserSetup]. This information + * can change some policy related to display + */ +interface UserSetupRepository { + /** Observable tracking [DeviceProvisionedController.isUserSetup] */ + val isUserSetupFlow: Flow<Boolean> +} + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class UserSetupRepositoryImpl +@Inject +constructor( + private val deviceProvisionedController: DeviceProvisionedController, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application scope: CoroutineScope, +) : UserSetupRepository { + /** State flow that tracks [DeviceProvisionedController.isUserSetup] */ + override val isUserSetupFlow: StateFlow<Boolean> = + conflatedCallbackFlow { + val callback = + object : DeviceProvisionedController.DeviceProvisionedListener { + override fun onUserSetupChanged() { + trySend(Unit) + } + } + + deviceProvisionedController.addCallback(callback) + + awaitClose { deviceProvisionedController.removeCallback(callback) } + } + .onStart { emit(Unit) } + .mapLatest { fetchUserSetupState() } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) + + private suspend fun fetchUserSetupState(): Boolean = + withContext(bgDispatcher) { deviceProvisionedController.isCurrentUserSetup } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt new file mode 100644 index 000000000000..15f4acc1127c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.domain.interactor + +import android.telephony.CarrierConfigManager +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import com.android.systemui.util.CarrierConfigTracker +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +interface MobileIconInteractor { + /** Observable for RAT type (network type) indicator */ + val networkTypeIconGroup: Flow<MobileIconGroup> + + /** True if this line of service is emergency-only */ + val isEmergencyOnly: Flow<Boolean> + + /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */ + val level: Flow<Int> + + /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */ + val numberOfLevels: Flow<Int> + + /** True when we want to draw an icon that makes room for the exclamation mark */ + val cutOut: Flow<Boolean> +} + +/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ +class MobileIconInteractorImpl( + defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>, + defaultMobileIconGroup: Flow<MobileIconGroup>, + mobileMappingsProxy: MobileMappingsProxy, + connectionRepository: MobileConnectionRepository, +) : MobileIconInteractor { + private val mobileStatusInfo = connectionRepository.subscriptionModelFlow + + /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ + override val networkTypeIconGroup: Flow<MobileIconGroup> = + combine( + mobileStatusInfo, + defaultMobileIconMapping, + defaultMobileIconGroup, + ) { info, mapping, defaultGroup -> + val lookupKey = + when (val resolved = info.resolvedNetworkType) { + is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type) + is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type) + } + mapping[lookupKey] ?: defaultGroup + } + + override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly } + + override val level: Flow<Int> = + mobileStatusInfo.map { mobileModel -> + // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi] + if (mobileModel.isGsm) { + mobileModel.primaryLevel + } else { + mobileModel.cdmaLevel + } + } + + /** + * This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL] + * once it's wired up inside of [CarrierConfigTracker] + */ + override val numberOfLevels: Flow<Int> = flowOf(4) + + /** Whether or not to draw the mobile triangle as "cut out", i.e., with the exclamation mark */ + // TODO: find a better name for this? + override val cutOut: Flow<Boolean> = flowOf(false) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt new file mode 100644 index 000000000000..cd411a4a2afe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.domain.interactor + +import android.telephony.CarrierConfigManager +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import com.android.systemui.util.CarrierConfigTracker +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * Business layer logic for the set of mobile subscription icons. + * + * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]). + * The list of subscriptions is filtered based on the opportunistic flags on the infos. + * + * It provides the default mapping between the telephony display info and the icon group that + * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual + * icon + */ +interface MobileIconsInteractor { + val filteredSubscriptions: Flow<List<SubscriptionInfo>> + val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> + val defaultMobileIconGroup: Flow<MobileIconGroup> + val isUserSetup: Flow<Boolean> + fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor +} + +@SysUISingleton +class MobileIconsInteractorImpl +@Inject +constructor( + private val mobileSubscriptionRepo: MobileConnectionsRepository, + private val carrierConfigTracker: CarrierConfigTracker, + private val mobileMappingsProxy: MobileMappingsProxy, + userSetupRepo: UserSetupRepository, + @Application private val scope: CoroutineScope, +) : MobileIconsInteractor { + private val activeMobileDataSubscriptionId = + mobileSubscriptionRepo.activeMobileDataSubscriptionId + + private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> = + mobileSubscriptionRepo.subscriptionsFlow + + /** + * Generally, SystemUI wants to show iconography for each subscription that is listed by + * [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only + * show a single representation of the pair of subscriptions. The docs define opportunistic as: + * + * "A subscription is opportunistic (if) the network it connects to has limited coverage" + * https://developer.android.com/reference/android/telephony/SubscriptionManager#setOpportunistic(boolean,%20int) + * + * In the case of opportunistic networks (typically CBRS), we will filter out one of the + * subscriptions based on + * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN], + * and by checking which subscription is opportunistic, or which one is active. + */ + override val filteredSubscriptions: Flow<List<SubscriptionInfo>> = + combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId + -> + // Based on the old logic, + if (unfilteredSubs.size != 2) { + return@combine unfilteredSubs + } + + val info1 = unfilteredSubs[0] + val info2 = unfilteredSubs[1] + // If both subscriptions are primary, show both + if (!info1.isOpportunistic && !info2.isOpportunistic) { + return@combine unfilteredSubs + } + + // NOTE: at this point, we are now returning a single SubscriptionInfo + + // If carrier required, always show the icon of the primary subscription. + // Otherwise, show whichever subscription is currently active for internet. + if (carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) { + // return the non-opportunistic info + return@combine if (info1.isOpportunistic) listOf(info2) else listOf(info1) + } else { + return@combine if (info1.subscriptionId == activeId) { + listOf(info1) + } else { + listOf(info2) + } + } + } + + /** + * Mapping from network type to [MobileIconGroup] using the config generated for the default + * subscription Id. This mapping is the same for every subscription. + */ + override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> = + mobileSubscriptionRepo.defaultDataSubRatConfig + .map { mobileMappingsProxy.mapIconSets(it) } + .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf()) + + /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */ + override val defaultMobileIconGroup: StateFlow<MobileIconGroup> = + mobileSubscriptionRepo.defaultDataSubRatConfig + .map { mobileMappingsProxy.getDefaultIcons(it) } + .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G) + + override val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow + + /** Vends out new [MobileIconInteractor] for a particular subId */ + override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = + MobileIconInteractorImpl( + defaultMobileIconMapping, + defaultMobileIconGroup, + mobileMappingsProxy, + mobileSubscriptionRepo.getRepoForSubId(subId), + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt new file mode 100644 index 000000000000..380017cd3418 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.phone.StatusBarIconController +import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +/** + * This class is intended to provide a context to collect on the + * [MobileIconsInteractor.filteredSubscriptions] data source and supply a state flow that can + * control [StatusBarIconController] to keep the old UI in sync with the new data source. + * + * It also provides a mechanism to create a top-level view model for each IconManager to know about + * the list of available mobile lines of service for which we want to show icons. + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class MobileUiAdapter +@Inject +constructor( + interactor: MobileIconsInteractor, + private val iconController: StatusBarIconController, + private val iconsViewModelFactory: MobileIconsViewModel.Factory, + @Application scope: CoroutineScope, +) { + private val mobileSubIds: Flow<List<Int>> = + interactor.filteredSubscriptions.mapLatest { infos -> + infos.map { subscriptionInfo -> subscriptionInfo.subscriptionId } + } + + /** + * We expose the list of tracked subscriptions as a flow of a list of ints, where each int is + * the subscriptionId of the relevant subscriptions. These act as a key into the layouts which + * house the mobile infos. + * + * NOTE: this should go away as the view presenter learns more about this data pipeline + */ + private val mobileSubIdsState: StateFlow<List<Int>> = + mobileSubIds + .onEach { + // Notify the icon controller here so that it knows to add icons + iconController.setNewMobileIconSubIds(it) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + + /** + * Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's + * lifecycle. This will start collecting on [mobileSubIdsState] and link our new pipeline with + * the old view system. + */ + fun createMobileIconsViewModel(): MobileIconsViewModel = + iconsViewModelFactory.create(mobileSubIdsState) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt new file mode 100644 index 000000000000..67ea139271fc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.binder + +import android.content.res.ColorStateList +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.widget.ImageView +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.settingslib.graph.SignalDrawable +import com.android.systemui.R +import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +object MobileIconBinder { + /** Binds the view to the view-model, continuing to update the former based on the latter */ + @JvmStatic + fun bind( + view: ViewGroup, + viewModel: MobileIconViewModel, + ) { + val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type) + val iconView = view.requireViewById<ImageView>(R.id.mobile_signal) + val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) } + + view.isVisible = true + iconView.isVisible = true + + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + // Set the icon for the triangle + launch { + viewModel.iconId.distinctUntilChanged().collect { iconId -> + mobileDrawable.level = iconId + } + } + + // Set the network type icon + launch { + viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId -> + dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) } + networkTypeView.visibility = if (dataTypeId != null) VISIBLE else GONE + } + } + + // Set the tint + launch { + viewModel.tint.collect { tint -> + val tintList = ColorStateList.valueOf(tint) + iconView.imageTintList = tintList + networkTypeView.imageTintList = tintList + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt new file mode 100644 index 000000000000..e7d5ee264efe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.binder + +import android.view.View +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +object MobileIconsBinder { + /** + * Start this ViewModel collecting on the list of mobile subscriptions in the scope of [view] + * which is passed in and managed by [IconManager]. Once the subscription list flow starts + * collecting, [MobileUiAdapter] will send updates to the icon manager. + */ + @JvmStatic + fun bind(view: View, viewModel: MobileIconsViewModel) { + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.subscriptionIdsFlow.collect { + // TODO(b/249790733): This is an empty collect, because [MobileUiAdapter] + // sets up a side-effect in this flow to trigger the methods on + // [StatusBarIconController] which allows for this pipeline to be a data + // source for the mobile icons. + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt new file mode 100644 index 000000000000..ec4fa9ca8128 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.view + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.LayoutInflater +import com.android.systemui.R +import com.android.systemui.statusbar.BaseStatusBarFrameLayout +import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import java.util.ArrayList + +class ModernStatusBarMobileView( + context: Context, + attrs: AttributeSet?, +) : BaseStatusBarFrameLayout(context, attrs) { + + private lateinit var slot: String + override fun getSlot() = slot + + override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) { + // TODO + } + + override fun setStaticDrawableColor(color: Int) { + // TODO + } + + override fun setDecorColor(color: Int) { + // TODO + } + + override fun setVisibleState(state: Int, animate: Boolean) { + // TODO + } + + override fun getVisibleState(): Int { + return STATE_ICON + } + + override fun isIconVisible(): Boolean { + return true + } + + companion object { + + /** + * Inflates a new instance of [ModernStatusBarMobileView], binds it to [viewModel], and + * returns it. + */ + @JvmStatic + fun constructAndBind( + context: Context, + slot: String, + viewModel: MobileIconViewModel, + ): ModernStatusBarMobileView { + return (LayoutInflater.from(context) + .inflate(R.layout.status_bar_mobile_signal_group_new, null) + as ModernStatusBarMobileView) + .also { + it.slot = slot + MobileIconBinder.bind(it, viewModel) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt new file mode 100644 index 000000000000..cc8f6dd08585 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import android.graphics.Color +import com.android.settingslib.graph.SignalDrawable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +/** + * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over + * a single line of service via [MobileIconInteractor] and update the UI based on that + * subscription's information. + * + * There will be exactly one [MobileIconViewModel] per filtered subscription offered from + * [MobileIconsInteractor.filteredSubscriptions] + * + * TODO: figure out where carrier merged and VCN models go (probably here?) + */ +class MobileIconViewModel +constructor( + val subscriptionId: Int, + iconInteractor: MobileIconInteractor, + logger: ConnectivityPipelineLogger, +) { + /** An int consumable by [SignalDrawable] for display */ + var iconId: Flow<Int> = + combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) { + level, + numberOfLevels, + cutOut -> + SignalDrawable.getState(level, numberOfLevels, cutOut) + } + .distinctUntilChanged() + .logOutputChange(logger, "iconId($subscriptionId)") + + /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ + var networkTypeIcon: Flow<Icon?> = + iconInteractor.networkTypeIconGroup.map { + val desc = + if (it.dataContentDescription != 0) + ContentDescription.Resource(it.dataContentDescription) + else null + Icon.Resource(it.dataType, desc) + } + + var tint: Flow<Int> = flowOf(Color.CYAN) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt new file mode 100644 index 000000000000..24c1db995d50 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(InternalCoroutinesApi::class) + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import javax.inject.Inject +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.flow.Flow + +/** + * View model for describing the system's current mobile cellular connections. The result is a list + * of [MobileIconViewModel]s which describe the individual icons and can be bound to + * [ModernStatusBarMobileView] + */ +class MobileIconsViewModel +@Inject +constructor( + val subscriptionIdsFlow: Flow<List<Int>>, + private val interactor: MobileIconsInteractor, + private val logger: ConnectivityPipelineLogger, +) { + /** TODO: do we need to cache these? */ + fun viewModelForSub(subId: Int): MobileIconViewModel = + MobileIconViewModel( + subId, + interactor.createMobileConnectionInteractorForSubId(subId), + logger + ) + + class Factory + @Inject + constructor( + private val interactor: MobileIconsInteractor, + private val logger: ConnectivityPipelineLogger, + ) { + fun create(subscriptionIdsFlow: Flow<List<Int>>): MobileIconsViewModel { + return MobileIconsViewModel( + subscriptionIdsFlow, + interactor, + logger, + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt new file mode 100644 index 000000000000..60bd0383f8c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.util + +import android.telephony.Annotation.NetworkType +import android.telephony.TelephonyDisplayInfo +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.MobileMappings +import com.android.settingslib.mobile.MobileMappings.Config +import javax.inject.Inject + +/** + * [MobileMappings] owns the logic on creating the map from [TelephonyDisplayInfo] to + * [MobileIconGroup]. It creates that hash map and also manages the creation of lookup keys. This + * interface allows us to proxy those calls to the static java methods in SettingsLib and also fake + * them out in tests + */ +interface MobileMappingsProxy { + fun mapIconSets(config: Config): Map<String, MobileIconGroup> + fun getDefaultIcons(config: Config): MobileIconGroup + fun toIconKey(@NetworkType networkType: Int): String + fun toIconKeyOverride(@NetworkType networkType: Int): String +} + +/** Injectable wrapper class for [MobileMappings] */ +class MobileMappingsProxyImpl @Inject constructor() : MobileMappingsProxy { + override fun mapIconSets(config: Config): Map<String, MobileIconGroup> = + MobileMappings.mapIconSets(config) + + override fun getDefaultIcons(config: Config): MobileIconGroup = + MobileMappings.getDefaultIcons(config) + + override fun toIconKey(@NetworkType networkType: Int): String = + MobileMappings.toIconKey(networkType) + + override fun toIconKeyOverride(networkType: Int): String = + MobileMappings.toDisplayIconKey(networkType) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt index dbb1aa54d8ee..d3cf32fb44ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt @@ -18,10 +18,10 @@ package com.android.systemui.statusbar.pipeline.shared import android.net.Network import android.net.NetworkCapabilities -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.StatusBarConnectivityLog +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString import javax.inject.Inject import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt index c96faabcda6c..062c3d1a4b10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt @@ -21,7 +21,9 @@ import androidx.annotation.VisibleForTesting /** Provides information about the current wifi network. */ sealed class WifiNetworkModel { /** A model representing that we have no active wifi network. */ - object Inactive : WifiNetworkModel() + object Inactive : WifiNetworkModel() { + override fun toString() = "WifiNetwork.Inactive" + } /** * A model representing that our wifi network is actually a carrier merged network, meaning it's @@ -29,7 +31,9 @@ sealed class WifiNetworkModel { * * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information. */ - object CarrierMerged : WifiNetworkModel() + object CarrierMerged : WifiNetworkModel() { + override fun toString() = "WifiNetwork.CarrierMerged" + } /** Provides information about an active wifi network. */ data class Active( @@ -72,6 +76,24 @@ sealed class WifiNetworkModel { } } + override fun toString(): String { + // Only include the passpoint-related values in the string if we have them. (Most + // networks won't have them so they'll be mostly clutter.) + val passpointString = + if (isPasspointAccessPoint || + isOnlineSignUpForPasspointAccessPoint || + passpointProviderFriendlyName != null) { + ", isPasspointAp=$isPasspointAccessPoint, " + + "isOnlineSignUpForPasspointAp=$isOnlineSignUpForPasspointAccessPoint, " + + "passpointName=$passpointProviderFriendlyName" + } else { + "" + } + + return "WifiNetworkModel.Active(networkId=$networkId, isValidated=$isValidated, " + + "level=$level, ssid=$ssid$passpointString)" + } + companion object { @VisibleForTesting internal const val MIN_VALID_LEVEL = 0 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt index 681cf7254ae7..93448c1dee0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt @@ -39,7 +39,6 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel import java.util.concurrent.Executor @@ -64,6 +63,9 @@ interface WifiRepository { /** Observable for the current wifi enabled status. */ val isWifiEnabled: StateFlow<Boolean> + /** Observable for the current wifi default status. */ + val isWifiDefault: StateFlow<Boolean> + /** Observable for the current wifi network. */ val wifiNetwork: StateFlow<WifiNetworkModel> @@ -103,7 +105,7 @@ class WifiRepositoryImpl @Inject constructor( merge(wifiNetworkChangeEvents, wifiStateChangeEvents) .mapLatest { wifiManager.isWifiEnabled } .distinctUntilChanged() - .logOutputChange(logger, "enabled") + .logInputChange(logger, "enabled") .stateIn( scope = scope, started = SharingStarted.WhileSubscribed(), @@ -111,6 +113,39 @@ class WifiRepositoryImpl @Inject constructor( ) } + override val isWifiDefault: StateFlow<Boolean> = conflatedCallbackFlow { + // Note: This callback doesn't do any logging because we already log every network change + // in the [wifiNetwork] callback. + val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + // This method will always be called immediately after the network becomes the + // default, in addition to any time the capabilities change while the network is + // the default. + // If this network contains valid wifi info, then wifi is the default network. + val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities) + trySend(wifiInfo != null) + } + + override fun onLost(network: Network) { + // The system no longer has a default network, so wifi is definitely not default. + trySend(false) + } + } + + connectivityManager.registerDefaultNetworkCallback(callback) + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } + .distinctUntilChanged() + .logInputChange(logger, "isWifiDefault") + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + initialValue = false + ) + override val wifiNetwork: StateFlow<WifiNetworkModel> = conflatedCallbackFlow { var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt index 04b17ed2924a..3a3e611de96a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt @@ -59,6 +59,9 @@ class WifiInteractor @Inject constructor( /** Our current enabled status. */ val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled + /** Our current default status. */ + val isDefault: Flow<Boolean> = wifiRepository.isWifiDefault + /** Our current wifi network. See [WifiNetworkModel]. */ val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt index 273be63eb8a2..25537b948517 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt @@ -91,6 +91,7 @@ object WifiViewBinder { val activityInView = view.requireViewById<ImageView>(R.id.wifi_in) val activityOutView = view.requireViewById<ImageView>(R.id.wifi_out) val activityContainerView = view.requireViewById<View>(R.id.inout_container) + val airplaneSpacer = view.requireViewById<View>(R.id.wifi_airplane_spacer) view.isVisible = true iconView.isVisible = true @@ -142,6 +143,12 @@ object WifiViewBinder { activityContainerView.isVisible = visible } } + + launch { + viewModel.isAirplaneSpacerVisible.distinctUntilChanged().collect { visible -> + airplaneSpacer.isVisible = visible + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt index 6c616ac7c3b8..0cd9bd7d97b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt @@ -22,7 +22,7 @@ import android.util.AttributeSet import android.view.Gravity import android.view.LayoutInflater import com.android.systemui.R -import com.android.systemui.statusbar.BaseStatusBarWifiView +import com.android.systemui.statusbar.BaseStatusBarFrameLayout import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN @@ -37,7 +37,7 @@ import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel class ModernStatusBarWifiView( context: Context, attrs: AttributeSet? -) : BaseStatusBarWifiView(context, attrs) { +) : BaseStatusBarFrameLayout(context, attrs) { private lateinit var slot: String private lateinit var binding: WifiViewBinder.Binding diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt index 40f948f9ee6c..95ab251422b2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt @@ -32,6 +32,7 @@ class HomeWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -40,4 +41,5 @@ class HomeWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt index 9642ac42972e..86535d63f84f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt @@ -29,6 +29,7 @@ class KeyguardWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -37,4 +38,5 @@ class KeyguardWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt index e23f8c7e97e0..7cbdf5dbdf2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt @@ -44,11 +44,14 @@ abstract class LocationBasedWifiViewModel( /** True if the activity container view should be visible. */ val isActivityContainerVisible: Flow<Boolean>, + + /** True if the airplane spacer view should be visible. */ + val isAirplaneSpacerVisible: Flow<Boolean>, ) { /** The color that should be used to tint the icon. */ val tint: Flow<Int> = flowOf( - if (statusBarPipelineFlags.useNewPipelineDebugColoring()) { + if (statusBarPipelineFlags.useWifiDebugColoring()) { debugTint } else { DEFAULT_TINT diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt index 0ddf90e21872..fd54c5f5062e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt @@ -29,6 +29,7 @@ class QsWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -37,4 +38,5 @@ class QsWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index ebbd77b72014..89b96b7bc75d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange @@ -66,6 +67,7 @@ import kotlinx.coroutines.flow.stateIn class WifiViewModel @Inject constructor( + airplaneModeViewModel: AirplaneModeViewModel, connectivityConstants: ConnectivityConstants, private val context: Context, logger: ConnectivityPipelineLogger, @@ -124,9 +126,10 @@ constructor( private val wifiIcon: StateFlow<Icon.Resource?> = combine( interactor.isEnabled, + interactor.isDefault, interactor.isForceHidden, interactor.wifiNetwork, - ) { isEnabled, isForceHidden, wifiNetwork -> + ) { isEnabled, isDefault, isForceHidden, wifiNetwork -> if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) { return@combine null } @@ -135,6 +138,7 @@ constructor( val icon = Icon.Resource(iconResId, wifiNetwork.contentDescription()) return@combine when { + isDefault -> icon wifiConstants.alwaysShowIconIfEnabled -> icon !connectivityConstants.hasDataCapabilities -> icon wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon @@ -175,6 +179,12 @@ constructor( } .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) + // TODO(b/238425913): It isn't ideal for the wifi icon to need to know about whether the + // airplane icon is visible. Instead, we should have a parent StatusBarSystemIconsViewModel + // that appropriately knows about both icons and sets the padding appropriately. + private val isAirplaneSpacerVisible: Flow<Boolean> = + airplaneModeViewModel.isAirplaneModeIconVisible + /** A view model for the status bar on the home screen. */ val home: HomeWifiViewModel = HomeWifiViewModel( @@ -183,6 +193,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) /** A view model for the status bar on keyguard. */ @@ -193,6 +204,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) /** A view model for the status bar in quick settings. */ @@ -203,6 +215,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AospPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AospPolicyModule.java new file mode 100644 index 000000000000..ba5fa1df4b15 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AospPolicyModule.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import android.content.Context; +import android.os.Handler; +import android.os.PowerManager; + +import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.demomode.DemoModeController; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.power.EnhancedEstimates; + +import dagger.Module; +import dagger.Provides; + +/** + * com.android.systemui.statusbar.policy related providers that others may want to override. + */ +@Module +public class AospPolicyModule { + @Provides + @SysUISingleton + static BatteryController provideBatteryController( + Context context, + EnhancedEstimates enhancedEstimates, + PowerManager powerManager, + BroadcastDispatcher broadcastDispatcher, + DemoModeController demoModeController, + DumpManager dumpManager, + @Main Handler mainHandler, + @Background Handler bgHandler) { + BatteryController bC = new BatteryControllerImpl( + context, + enhancedEstimates, + powerManager, + broadcastDispatcher, + demoModeController, + dumpManager, + mainHandler, + bgHandler); + bC.init(); + return bC; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt index 5b2d69564585..cf4106c508cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt @@ -35,19 +35,15 @@ protected constructor( protected val controller: UserSwitcherController, ) : BaseAdapter() { - protected open val users: ArrayList<UserRecord> - get() = controller.users + protected open val users: List<UserRecord> + get() = controller.users.filter { !controller.isKeyguardShowing || !it.isRestricted } init { controller.addAdapter(WeakReference(this)) } override fun getCount(): Int { - return if (controller.isKeyguardShowing) { - users.count { !it.isRestricted } - } else { - users.size - } + return users.size } override fun getItem(position: Int): UserRecord { @@ -65,7 +61,7 @@ protected constructor( * animation to and from the parent dialog. */ @JvmOverloads - fun onUserListItemClicked( + open fun onUserListItemClicked( record: UserRecord, dialogShower: DialogShower? = null, ) { @@ -112,6 +108,7 @@ protected constructor( item.isGuest, item.isAddSupervisedUser, isTablet, + item.isManageUsers, ) return checkNotNull(context.getDrawable(iconRes)) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java index f2ee85886dca..21a83004ba84 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/EmergencyCryptkeeperText.java @@ -107,7 +107,7 @@ public class EmergencyCryptkeeperText extends TextView { boolean allSimsMissing = true; CharSequence displayText = null; - List<SubscriptionInfo> subs = mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(false); + List<SubscriptionInfo> subs = mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(); final int N = subs.size(); for (int i = 0; i < N; i++) { int subId = subs.get(i).getSubscriptionId(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt index d7c81af53d8b..df1e80b78c9b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt @@ -16,10 +16,10 @@ package com.android.systemui.statusbar.policy -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel.INFO -import com.android.systemui.log.LogLevel.VERBOSE import com.android.systemui.log.dagger.NotificationHeadsUpLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.INFO +import com.android.systemui.plugins.log.LogLevel.VERBOSE import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java index dc73d1f007c6..f63d65246d9b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java @@ -36,6 +36,7 @@ import com.android.keyguard.KeyguardVisibilityHelper; import com.android.keyguard.dagger.KeyguardUserSwitcherScope; import com.android.settingslib.drawable.CircleFramedDrawable; import com.android.systemui.R; +import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -190,7 +191,8 @@ public class KeyguardQsUserSwitchController extends ViewController<FrameLayout> mUiEventLogger.log( LockscreenGestureLogger.LockscreenUiEvent.LOCKSCREEN_SWITCH_USER_TAP); - mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground); + mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground.getContext(), + Expandable.fromView(mUserAvatarViewWithBackground)); }); mUserAvatarView.setAccessibilityDelegate(new View.AccessibilityDelegate() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java index 250d9d46de66..1ae1eae00651 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java @@ -35,9 +35,16 @@ public interface KeyguardStateController extends CallbackController<Callback> { } /** - * If the lock screen is visible. - * The keyguard is also visible when the device is asleep or in always on mode, except when - * the screen timed out and the user can unlock by quickly pressing power. + * If the keyguard is visible. This is unrelated to being locked or not. + */ + default boolean isVisible() { + return isShowing() && !isOccluded(); + } + + /** + * If the keyguard is showing. This includes when it's occluded by an activity, and when + * the device is asleep or in always on mode, except when the screen timed out and the user + * can unlock by quickly pressing power. * * This is unrelated to being locked or not. * diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java index f4d08e01d5c3..cc6fdccba789 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java @@ -181,6 +181,7 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum if (mShowing == showing && mOccluded == occluded) return; mShowing = showing; mOccluded = occluded; + mKeyguardUpdateMonitor.setKeyguardShowing(showing, occluded); Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events", "Keyguard showing: " + showing + " occluded: " + occluded); notifyKeyguardChanged(); @@ -387,6 +388,8 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum @Override public void dump(PrintWriter pw, String[] args) { pw.println("KeyguardStateController:"); + pw.println(" mShowing: " + mShowing); + pw.println(" mOccluded: " + mOccluded); pw.println(" mSecure: " + mSecure); pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); pw.println(" mTrustManaged: " + mTrustManaged); @@ -435,7 +438,7 @@ public class KeyguardStateControllerImpl implements KeyguardStateController, Dum } @Override - public void onKeyguardVisibilityChanged(boolean showing) { + public void onKeyguardVisibilityChanged(boolean visible) { update(false /* updateAlways */); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java index 0995a00533a8..c1506541229d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java @@ -53,6 +53,7 @@ import com.android.systemui.user.data.source.UserRecord; import com.android.systemui.util.ViewController; import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; @@ -91,11 +92,11 @@ public class KeyguardUserSwitcherController extends ViewController<KeyguardUserS private final KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { @Override - public void onKeyguardVisibilityChanged(boolean showing) { - if (DEBUG) Log.d(TAG, String.format("onKeyguardVisibilityChanged %b", showing)); + public void onKeyguardVisibilityChanged(boolean visible) { + if (DEBUG) Log.d(TAG, String.format("onKeyguardVisibilityChanged %b", visible)); // Any time the keyguard is hidden, try to close the user switcher menu to // restore keyguard to the default state - if (!showing) { + if (!visible) { closeSwitcherIfOpenAndNotSimple(false); } } @@ -456,7 +457,7 @@ public class KeyguardUserSwitcherController extends ViewController<KeyguardUserS } void refreshUserOrder() { - ArrayList<UserRecord> users = super.getUsers(); + List<UserRecord> users = super.getUsers(); mUsersOrdered = new ArrayList<>(users.size()); for (int i = 0; i < users.size(); i++) { UserRecord record = users.get(i); @@ -505,7 +506,7 @@ public class KeyguardUserSwitcherController extends ViewController<KeyguardUserS v.bind(name, drawable, item.info.id); } v.setActivated(item.isCurrent); - v.setDisabledByAdmin(getController().isDisabledByAdmin(item)); + v.setDisabledByAdmin(item.isDisabledByAdmin()); v.setEnabled(item.isSwitchToEnabled); UserSwitcherController.setSelectableAlpha(v); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 5a33603d81ce..dd400b3fc0ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -47,6 +47,7 @@ import android.view.OnReceiveContentListener; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; +import android.view.ViewRootImpl; import android.view.WindowInsets; import android.view.WindowInsetsAnimation; import android.view.WindowInsetsController; @@ -61,6 +62,8 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -88,6 +91,7 @@ import java.util.function.Consumer; */ public class RemoteInputView extends LinearLayout implements View.OnClickListener { + private static final boolean DEBUG = false; private static final String TAG = "RemoteInput"; // A marker object that let's us easily find views of this class. @@ -124,6 +128,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene // TODO(b/193539698): remove this; views shouldn't have access to their controller, and places // that need the controller shouldn't have access to the view private RemoteInputViewController mViewController; + private ViewRootImpl mTestableViewRootImpl; /** * Enum for logged notification remote input UiEvents. @@ -221,8 +226,10 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mEditText.setTextColor(textColor); mEditText.setHintTextColor(hintColor); - mEditText.getTextCursorDrawable().setColorFilter( - accentColor.getDefaultColor(), PorterDuff.Mode.SRC_IN); + if (mEditText.getTextCursorDrawable() != null) { + mEditText.getTextCursorDrawable().setColorFilter( + accentColor.getDefaultColor(), PorterDuff.Mode.SRC_IN); + } mContentBackground.setColor(editBgColor); mContentBackground.setStroke(stroke, accentColor); mDelete.setImageTintList(ColorStateList.valueOf(deleteFgColor)); @@ -428,10 +435,20 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } } + @VisibleForTesting + protected void setViewRootImpl(ViewRootImpl viewRoot) { + mTestableViewRootImpl = viewRoot; + } + + @VisibleForTesting + protected void setEditTextReferenceToSelf() { + mEditText.mRemoteInputView = this; + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - mEditText.mRemoteInputView = this; + setEditTextReferenceToSelf(); mEditText.setOnEditorActionListener(mEditorActionHandler); mEditText.addTextChangedListener(mTextWatcher); if (mEntry.getRow().isChangingPosition()) { @@ -455,7 +472,50 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } @Override + public ViewRootImpl getViewRootImpl() { + if (mTestableViewRootImpl != null) { + return mTestableViewRootImpl; + } + return super.getViewRootImpl(); + } + + private void registerBackCallback() { + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot == null) { + if (DEBUG) { + Log.d(TAG, "ViewRoot was null, NOT registering Predictive Back callback"); + } + return; + } + if (DEBUG) { + Log.d(TAG, "registering Predictive Back callback"); + } + viewRoot.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_OVERLAY, mEditText.mOnBackInvokedCallback); + } + + private void unregisterBackCallback() { + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot == null) { + if (DEBUG) { + Log.d(TAG, "ViewRoot was null, NOT unregistering Predictive Back callback"); + } + return; + } + if (DEBUG) { + Log.d(TAG, "unregistering Predictive Back callback"); + } + viewRoot.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback( + mEditText.mOnBackInvokedCallback); + } + + @Override public void onVisibilityAggregated(boolean isVisible) { + if (isVisible) { + registerBackCallback(); + } else { + unregisterBackCallback(); + } super.onVisibilityAggregated(isVisible); mEditText.setEnabled(isVisible && !mSending); } @@ -820,10 +880,21 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene return super.onKeyDown(keyCode, event); } + private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { + if (DEBUG) { + Log.d(TAG, "Predictive Back Callback dispatched"); + } + respondToKeycodeBack(); + }; + + private void respondToKeycodeBack() { + defocusIfNeeded(true /* animate */); + } + @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { - defocusIfNeeded(true /* animate */); + respondToKeycodeBack(); return true; } return super.onKeyUp(keyCode, event); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt index 843c2329092c..146b222c94ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.policy import android.annotation.UserIdInt import android.content.Intent import android.view.View -import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin import com.android.systemui.Dumpable import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower import com.android.systemui.user.data.source.UserRecord @@ -130,12 +129,6 @@ interface UserSwitcherController : Dumpable { /** Whether keyguard is showing. */ val isKeyguardShowing: Boolean - /** Returns the [EnforcedAdmin] for the given record, or `null` if there isn't one. */ - fun getEnforcedAdmin(record: UserRecord): EnforcedAdmin? - - /** Returns `true` if the given record is disabled by the admin; `false` otherwise. */ - fun isDisabledByAdmin(record: UserRecord): Boolean - /** Starts an activity with the given [Intent]. */ fun startActivity(intent: Intent) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt index 12834f68c3b7..935fc7f10198 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt @@ -17,13 +17,20 @@ package com.android.systemui.statusbar.policy +import android.content.Context import android.content.Intent import android.view.View -import com.android.settingslib.RestrictedLockUtils +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.domain.interactor.GuestUserInteractor +import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import dagger.Lazy import java.io.PrintWriter import java.lang.ref.WeakReference @@ -31,58 +38,76 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow /** Implementation of [UserSwitcherController]. */ +@SysUISingleton class UserSwitcherControllerImpl @Inject constructor( - private val flags: FeatureFlags, + @Application private val applicationContext: Context, + flags: FeatureFlags, @Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>, + private val userInteractorLazy: Lazy<UserInteractor>, + private val guestUserInteractorLazy: Lazy<GuestUserInteractor>, + private val keyguardInteractorLazy: Lazy<KeyguardInteractor>, + private val activityStarter: ActivityStarter, ) : UserSwitcherController { - private val isNewImpl: Boolean - get() = flags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + private val useInteractor: Boolean = + flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) && + !flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) private val _oldImpl: UserSwitcherControllerOldImpl get() = oldImpl.get() + private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() } + private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() } + private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() } - private fun notYetImplemented(): Nothing { - error("Not yet implemented!") + private val callbackCompatMap = + mutableMapOf<UserSwitcherController.UserSwitchCallback, UserInteractor.UserCallback>() + + private fun notSupported(): Nothing { + error("Not supported in the new implementation!") } override val users: ArrayList<UserRecord> get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.userRecords.value } else { _oldImpl.users } override val isSimpleUserSwitcher: Boolean get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.isSimpleUserSwitcher } else { _oldImpl.isSimpleUserSwitcher } override fun init(view: View) { - if (isNewImpl) { - notYetImplemented() - } else { + if (!useInteractor) { _oldImpl.init(view) } } override val currentUserRecord: UserRecord? get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.selectedUserRecord.value } else { _oldImpl.currentUserRecord } override val currentUserName: String? get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + currentUserRecord?.let { + LegacyUserUiHelper.getUserRecordName( + context = applicationContext, + record = it, + isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, + isGuestUserResetting = userInteractor.isGuestUserResetting, + ) + } } else { _oldImpl.currentUserName } @@ -91,8 +116,8 @@ constructor( userId: Int, dialogShower: UserSwitchDialogController.DialogShower? ) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.selectUser(userId, dialogShower) } else { _oldImpl.onUserSelected(userId, dialogShower) } @@ -100,24 +125,24 @@ constructor( override val isAddUsersFromLockScreenEnabled: Flow<Boolean> get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + notSupported() } else { _oldImpl.isAddUsersFromLockScreenEnabled } override val isGuestUserAutoCreated: Boolean get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.isGuestUserAutoCreated } else { _oldImpl.isGuestUserAutoCreated } override val isGuestUserResetting: Boolean get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.isGuestUserResetting } else { _oldImpl.isGuestUserResetting } @@ -125,40 +150,48 @@ constructor( override fun createAndSwitchToGuestUser( dialogShower: UserSwitchDialogController.DialogShower?, ) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + notSupported() } else { _oldImpl.createAndSwitchToGuestUser(dialogShower) } } override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + notSupported() } else { _oldImpl.showAddUserDialog(dialogShower) } } override fun startSupervisedUserActivity() { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + notSupported() } else { _oldImpl.startSupervisedUserActivity() } } override fun onDensityOrFontScaleChanged() { - if (isNewImpl) { - notYetImplemented() - } else { + if (!useInteractor) { _oldImpl.onDensityOrFontScaleChanged() } } override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.addCallback( + object : UserInteractor.UserCallback { + override fun isEvictable(): Boolean { + return adapter.get() == null + } + + override fun onUserStateChanged() { + adapter.get()?.notifyDataSetChanged() + } + } + ) } else { _oldImpl.addAdapter(adapter) } @@ -168,16 +201,19 @@ constructor( record: UserRecord, dialogShower: UserSwitchDialogController.DialogShower?, ) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.onRecordSelected(record, dialogShower) } else { _oldImpl.onUserListItemClicked(record, dialogShower) } } override fun removeGuestUser(guestUserId: Int, targetUserId: Int) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.removeGuestUser( + guestUserId = guestUserId, + targetUserId = targetUserId, + ) } else { _oldImpl.removeGuestUser(guestUserId, targetUserId) } @@ -188,16 +224,16 @@ constructor( targetUserId: Int, forceRemoveGuestOnExit: Boolean ) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) } else { _oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) } } override fun schedulePostBootGuestCreation() { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + guestUserInteractor.onDeviceBootCompleted() } else { _oldImpl.schedulePostBootGuestCreation() } @@ -205,63 +241,57 @@ constructor( override val isKeyguardShowing: Boolean get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + keyguardInteractor.isKeyguardShowing() } else { _oldImpl.isKeyguardShowing } - override fun getEnforcedAdmin(record: UserRecord): RestrictedLockUtils.EnforcedAdmin? { - return if (isNewImpl) { - notYetImplemented() - } else { - _oldImpl.getEnforcedAdmin(record) - } - } - - override fun isDisabledByAdmin(record: UserRecord): Boolean { - return if (isNewImpl) { - notYetImplemented() - } else { - _oldImpl.isDisabledByAdmin(record) - } - } - override fun startActivity(intent: Intent) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + activityStarter.startActivity(intent, /* dismissShade= */ true) } else { _oldImpl.startActivity(intent) } } override fun refreshUsers(forcePictureLoadForId: Int) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.refreshUsers() } else { _oldImpl.refreshUsers(forcePictureLoadForId) } } override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + val interactorCallback = + object : UserInteractor.UserCallback { + override fun onUserStateChanged() { + callback.onUserSwitched() + } + } + callbackCompatMap[callback] = interactorCallback + userInteractor.addCallback(interactorCallback) } else { _oldImpl.addUserSwitchCallback(callback) } } override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + val interactorCallback = callbackCompatMap.remove(callback) + if (interactorCallback != null) { + userInteractor.removeCallback(interactorCallback) + } } else { _oldImpl.removeUserSwitchCallback(callback) } } override fun dump(pw: PrintWriter, args: Array<out String>) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.dump(pw) } else { _oldImpl.dump(pw, args) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java index d365aa6f952d..c294c370a601 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java @@ -17,17 +17,13 @@ package com.android.systemui.statusbar.policy; import static android.os.UserManager.SWITCHABILITY_STATUS_OK; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; - import android.annotation.UserIdInt; -import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Dialog; import android.app.IActivityManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.UserInfo; @@ -40,7 +36,6 @@ import android.os.UserManager; import android.provider.Settings; import android.telephony.TelephonyCallback; import android.text.TextUtils; -import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -49,17 +44,15 @@ import android.view.WindowManagerGlobal; import android.widget.Toast; import androidx.annotation.Nullable; -import androidx.collection.SimpleArrayMap; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.util.LatencyTracker; -import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.keyguard.KeyguardUpdateMonitor; import com.android.settingslib.users.UserCreatingDialog; import com.android.systemui.GuestResetOrExitSessionReceiver; import com.android.systemui.GuestResumeSessionReceiver; -import com.android.systemui.R; import com.android.systemui.SystemUISecondaryUserService; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; @@ -75,10 +68,12 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower; import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.telephony.TelephonyListenerManager; -import com.android.systemui.user.CreateUserActivity; import com.android.systemui.user.data.source.UserRecord; +import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper; +import com.android.systemui.user.shared.model.UserActionModel; +import com.android.systemui.user.ui.dialog.AddUserDialog; +import com.android.systemui.user.ui.dialog.ExitGuestDialog; import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.settings.SecureSettings; @@ -139,9 +134,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { private final InteractionJankMonitor mInteractionJankMonitor; private final LatencyTracker mLatencyTracker; private final DialogLaunchAnimator mDialogLaunchAnimator; - private final SimpleArrayMap<UserRecord, EnforcedAdmin> mEnforcedAdminByUserRecord = - new SimpleArrayMap<>(); - private final ArraySet<UserRecord> mDisabledByAdmin = new ArraySet<>(); private ArrayList<UserRecord> mUsers = new ArrayList<>(); @VisibleForTesting @@ -334,7 +326,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { for (UserInfo info : infos) { boolean isCurrent = currentId == info.id; - boolean switchToEnabled = canSwitchUsers || isCurrent; if (!mUserSwitcherEnabled && !info.isPrimary()) { continue; } @@ -343,25 +334,22 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { if (info.isGuest()) { // Tapping guest icon triggers remove and a user switch therefore // the icon shouldn't be enabled even if the user is current - guestRecord = new UserRecord(info, null /* picture */, - true /* isGuest */, isCurrent, false /* isAddUser */, - false /* isRestricted */, canSwitchUsers, - false /* isAddSupervisedUser */); + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + mUserManager, + null /* picture */, + info, + isCurrent, + canSwitchUsers); } else if (info.supportsSwitchToByUser()) { - Bitmap picture = bitmaps.get(info.id); - if (picture == null) { - picture = mUserManager.getUserIcon(info.id); - - if (picture != null) { - int avatarSize = mContext.getResources() - .getDimensionPixelSize(R.dimen.max_avatar_size); - picture = Bitmap.createScaledBitmap( - picture, avatarSize, avatarSize, true); - } - } - records.add(new UserRecord(info, picture, false /* isGuest */, - isCurrent, false /* isAddUser */, false /* isRestricted */, - switchToEnabled, false /* isAddSupervisedUser */)); + records.add( + LegacyUserDataHelper.createRecord( + mContext, + mUserManager, + bitmaps.get(info.id), + info, + isCurrent, + canSwitchUsers)); } } } @@ -372,18 +360,20 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { // we will just use it as an indicator for "Resetting guest...". // Otherwise, default to canSwitchUsers. boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers; - guestRecord = new UserRecord(null /* info */, null /* picture */, - true /* isGuest */, false /* isCurrent */, - false /* isAddUser */, false /* isRestricted */, - isSwitchToGuestEnabled, false /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(guestRecord); + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ENTER_GUEST_MODE, + false /* isRestricted */, + isSwitchToGuestEnabled); records.add(guestRecord); } else if (canCreateGuest(guestRecord != null)) { - guestRecord = new UserRecord(null /* info */, null /* picture */, - true /* isGuest */, false /* isCurrent */, - false /* isAddUser */, createIsRestricted(), canSwitchUsers, - false /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(guestRecord); + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ENTER_GUEST_MODE, + false /* isRestricted */, + canSwitchUsers); records.add(guestRecord); } } else { @@ -391,20 +381,33 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { } if (canCreateUser()) { - UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, - false /* isGuest */, false /* isCurrent */, true /* isAddUser */, - createIsRestricted(), canSwitchUsers, - false /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(addUserRecord); - records.add(addUserRecord); + final UserRecord userRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ADD_USER, + createIsRestricted(), + canSwitchUsers); + records.add(userRecord); } if (canCreateSupervisedUser()) { - UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, - false /* isGuest */, false /* isCurrent */, false /* isAddUser */, - createIsRestricted(), canSwitchUsers, true /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(addUserRecord); - records.add(addUserRecord); + final UserRecord userRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ADD_SUPERVISED_USER, + createIsRestricted(), + canSwitchUsers); + records.add(userRecord); + } + + if (canManageUsers()) { + records.add(LegacyUserDataHelper.createRecord( + mContext, + KeyguardUpdateMonitor.getCurrentUser(), + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + /* isRestricted= */ false, + /* isSwitchToEnabled= */ true + )); } mUiExecutor.execute(() -> { @@ -446,6 +449,14 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY); } + @VisibleForTesting + boolean canManageUsers() { + UserInfo currentUser = mUserTracker.getUserInfo(); + return mUserSwitcherEnabled + && ((currentUser != null && currentUser.isAdmin()) + || mAddUsersFromLockScreen.getValue()); + } + private boolean createIsRestricted() { return !mAddUsersFromLockScreen.getValue(); } @@ -533,6 +544,8 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { showAddUserDialog(dialogShower); } else if (record.isAddSupervisedUser) { startSupervisedUserActivity(); + } else if (record.isManageUsers) { + startActivity(new Intent(Settings.ACTION_USER_SETTINGS)); } else { onUserListItemClicked(record.info.id, record, dialogShower); } @@ -591,12 +604,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower); } - private void showExitGuestDialog(int id, boolean isGuestEphemeral, - int targetId, DialogShower dialogShower) { + private void showExitGuestDialog( + int id, + boolean isGuestEphemeral, + int targetId, + DialogShower dialogShower) { if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) { mExitGuestDialog.cancel(); } - mExitGuestDialog = new ExitGuestDialog(mContext, id, isGuestEphemeral, targetId); + mExitGuestDialog = new ExitGuestDialog( + mContext, + id, + isGuestEphemeral, + targetId, + mKeyguardStateController.isShowing(), + mFalsingManager, + mDialogLaunchAnimator, + this::exitGuestUser); if (dialogShower != null) { dialogShower.showDialog(mExitGuestDialog, new DialogCuj( InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, @@ -622,7 +646,15 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { if (mAddUserDialog != null && mAddUserDialog.isShowing()) { mAddUserDialog.cancel(); } - mAddUserDialog = new AddUserDialog(mContext); + final UserInfo currentUser = mUserTracker.getUserInfo(); + mAddUserDialog = new AddUserDialog( + mContext, + currentUser.getUserHandle(), + mKeyguardStateController.isShowing(), + /* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(), + mFalsingManager, + mBroadcastSender, + mDialogLaunchAnimator); if (dialogShower != null) { dialogShower.showDialog(mAddUserDialog, new DialogCuj( @@ -964,30 +996,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { return mKeyguardStateController.isShowing(); } - @Override - @Nullable - public EnforcedAdmin getEnforcedAdmin(UserRecord record) { - return mEnforcedAdminByUserRecord.get(record); - } - - @Override - public boolean isDisabledByAdmin(UserRecord record) { - return mDisabledByAdmin.contains(record); - } - - private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) { - EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, - UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId()); - if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, - UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId())) { - mDisabledByAdmin.add(record); - mEnforcedAdminByUserRecord.put(record, admin); - } else { - mDisabledByAdmin.remove(record); - mEnforcedAdminByUserRecord.put(record, null); - } - } - private boolean shouldUseSimpleUserSwitcher() { int defaultSimpleUserSwitcher = mContext.getResources().getBoolean( com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0; @@ -997,7 +1005,7 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { @Override public void startActivity(Intent intent) { - mActivityStarter.startActivity(intent, true); + mActivityStarter.startActivity(intent, /* dismissShade= */ true); } @Override @@ -1052,133 +1060,4 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { } } }; - - - private final class ExitGuestDialog extends SystemUIDialog implements - DialogInterface.OnClickListener { - - private final int mGuestId; - private final int mTargetId; - private final boolean mIsGuestEphemeral; - - ExitGuestDialog(Context context, int guestId, boolean isGuestEphemeral, - int targetId) { - super(context); - if (isGuestEphemeral) { - setTitle(context.getString( - com.android.settingslib.R.string.guest_exit_dialog_title)); - setMessage(context.getString( - com.android.settingslib.R.string.guest_exit_dialog_message)); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_dialog_button), this); - } else { - setTitle(context.getString( - com.android.settingslib - .R.string.guest_exit_dialog_title_non_ephemeral)); - setMessage(context.getString( - com.android.settingslib - .R.string.guest_exit_dialog_message_non_ephemeral)); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_NEGATIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_clear_data_button), - this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_save_data_button), - this); - } - SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing()); - setCanceledOnTouchOutside(false); - mGuestId = guestId; - mTargetId = targetId; - mIsGuestEphemeral = isGuestEphemeral; - } - - @Override - public void onClick(DialogInterface dialog, int which) { - int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY - : FalsingManager.HIGH_PENALTY; - if (mFalsingManager.isFalseTap(penalty)) { - return; - } - if (mIsGuestEphemeral) { - if (which == DialogInterface.BUTTON_POSITIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Ephemeral guest: exit guest, guest is removed by the system - // on exit, since its marked ephemeral - exitGuestUser(mGuestId, mTargetId, false); - } else if (which == DialogInterface.BUTTON_NEGATIVE) { - // Cancel clicked, do nothing - cancel(); - } - } else { - if (which == DialogInterface.BUTTON_POSITIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Non-ephemeral guest: exit guest, guest is not removed by the system - // on exit, since its marked non-ephemeral - exitGuestUser(mGuestId, mTargetId, false); - } else if (which == DialogInterface.BUTTON_NEGATIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Non-ephemeral guest: remove guest and then exit - exitGuestUser(mGuestId, mTargetId, true); - } else if (which == DialogInterface.BUTTON_NEUTRAL) { - // Cancel clicked, do nothing - cancel(); - } - } - } - } - - @VisibleForTesting - final class AddUserDialog extends SystemUIDialog implements - DialogInterface.OnClickListener { - - AddUserDialog(Context context) { - super(context); - - setTitle(com.android.settingslib.R.string.user_add_user_title); - String message = context.getString( - com.android.settingslib.R.string.user_add_user_message_short); - UserInfo currentUser = mUserTracker.getUserInfo(); - if (currentUser != null && currentUser.isGuest() && currentUser.isEphemeral()) { - message += context.getString(R.string.user_add_user_message_guest_remove); - } - setMessage(message); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString(android.R.string.ok), this); - SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing()); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY - : FalsingManager.MODERATE_PENALTY; - if (mFalsingManager.isFalseTap(penalty)) { - return; - } - if (which == BUTTON_NEUTRAL) { - cancel(); - } else { - mDialogLaunchAnimator.dismissStack(this); - if (ActivityManager.isUserAMonkey()) { - return; - } - // Use broadcast instead of ShadeController, as this dialog may have started in - // another process and normal dagger bindings are not available - mBroadcastSender.sendBroadcastAsUser( - new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), UserHandle.CURRENT); - getContext().startActivityAsUser( - CreateUserActivity.createIntentForStart(getContext()), - mUserTracker.getUserHandle()); - } - } - } - } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java index 09298b60ff51..b1b8341d9584 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java @@ -37,19 +37,20 @@ import dagger.Lazy; * Serves as a collection of UI components, rather than showing its own UI. */ @SysUISingleton -public class TvStatusBar extends CoreStartable implements CommandQueue.Callbacks { +public class TvStatusBar implements CoreStartable, CommandQueue.Callbacks { private static final String ACTION_SHOW_PIP_MENU = "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU"; private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"; + private final Context mContext; private final CommandQueue mCommandQueue; private final Lazy<AssistManager> mAssistManagerLazy; @Inject public TvStatusBar(Context context, CommandQueue commandQueue, Lazy<AssistManager> assistManagerLazy) { - super(context); + mContext = context; mCommandQueue = commandQueue; mAssistManagerLazy = assistManagerLazy; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt b/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt index c1997446c126..b938c9002d90 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/VpnStatusObserver.kt @@ -35,9 +35,9 @@ import javax.inject.Inject */ @SysUISingleton class VpnStatusObserver @Inject constructor( - context: Context, + private val context: Context, private val securityController: SecurityController -) : CoreStartable(context), +) : CoreStartable, SecurityController.SecurityControllerCallback { private var vpnConnected = false @@ -102,7 +102,7 @@ class VpnStatusObserver @Inject constructor( .apply { vpnName?.let { setContentText( - mContext.getString( + context.getString( R.string.notification_disclosure_vpn_text, it ) ) @@ -111,23 +111,23 @@ class VpnStatusObserver @Inject constructor( .build() private fun createVpnConnectedNotificationBuilder() = - Notification.Builder(mContext, NOTIFICATION_CHANNEL_TV_VPN) + Notification.Builder(context, NOTIFICATION_CHANNEL_TV_VPN) .setSmallIcon(vpnIconId) .setVisibility(Notification.VISIBILITY_PUBLIC) .setCategory(Notification.CATEGORY_SYSTEM) .extend(Notification.TvExtender()) .setOngoing(true) - .setContentTitle(mContext.getString(R.string.notification_vpn_connected)) - .setContentIntent(VpnConfig.getIntentForStatusPanel(mContext)) + .setContentTitle(context.getString(R.string.notification_vpn_connected)) + .setContentIntent(VpnConfig.getIntentForStatusPanel(context)) private fun createVpnDisconnectedNotification() = - Notification.Builder(mContext, NOTIFICATION_CHANNEL_TV_VPN) + Notification.Builder(context, NOTIFICATION_CHANNEL_TV_VPN) .setSmallIcon(vpnIconId) .setVisibility(Notification.VISIBILITY_PUBLIC) .setCategory(Notification.CATEGORY_SYSTEM) .extend(Notification.TvExtender()) .setTimeoutAfter(VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS) - .setContentTitle(mContext.getString(R.string.notification_vpn_disconnected)) + .setContentTitle(context.getString(R.string.notification_vpn_disconnected)) .build() companion object { @@ -137,4 +137,4 @@ class VpnStatusObserver @Inject constructor( private const val TAG = "TvVpnNotification" private const val VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS = 5_000L } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java index 8026ba517820..b92725bd7cf7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationHandler.java @@ -18,13 +18,13 @@ package com.android.systemui.statusbar.tv.notifications; import android.annotation.Nullable; import android.app.Notification; -import android.content.Context; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.util.Log; import android.util.SparseArray; import com.android.systemui.CoreStartable; +import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.statusbar.NotificationListener; import javax.inject.Inject; @@ -32,7 +32,8 @@ import javax.inject.Inject; /** * Keeps track of the notifications on TV. */ -public class TvNotificationHandler extends CoreStartable implements +@SysUISingleton +public class TvNotificationHandler implements CoreStartable, NotificationListener.NotificationHandler { private static final String TAG = "TvNotificationHandler"; private final NotificationListener mNotificationListener; @@ -41,8 +42,7 @@ public class TvNotificationHandler extends CoreStartable implements private Listener mUpdateListener; @Inject - public TvNotificationHandler(Context context, NotificationListener notificationListener) { - super(context); + public TvNotificationHandler(NotificationListener notificationListener) { mNotificationListener = notificationListener; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java index 892fedcc8ce2..dbbd0b8613de 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/notifications/TvNotificationPanel.java @@ -35,14 +35,15 @@ import javax.inject.Inject; * Offers control methods for the notification panel handler on TV devices. */ @SysUISingleton -public class TvNotificationPanel extends CoreStartable implements CommandQueue.Callbacks { +public class TvNotificationPanel implements CoreStartable, CommandQueue.Callbacks { private static final String TAG = "TvNotificationPanel"; + private final Context mContext; private final CommandQueue mCommandQueue; private final String mNotificationHandlerPackage; @Inject public TvNotificationPanel(Context context, CommandQueue commandQueue) { - super(context); + mContext = context; mCommandQueue = commandQueue; mNotificationHandlerPackage = mContext.getResources().getString( com.android.internal.R.string.config_notificationHandlerPackage); diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt new file mode 100644 index 000000000000..9c38dc0f8852 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.telephony.data.repository + +import android.telephony.Annotation +import android.telephony.TelephonyCallback +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.telephony.TelephonyListenerManager +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Defines interface for classes that encapsulate _some_ telephony-related state. */ +interface TelephonyRepository { + /** The state of the current call. */ + @Annotation.CallState val callState: Flow<Int> +} + +/** + * NOTE: This repository tracks only telephony-related state regarding the default mobile + * subscription. `TelephonyListenerManager` does not create new instances of `TelephonyManager` on a + * per-subscription basis and thus will always be tracking telephony information regarding + * `SubscriptionManager.getDefaultSubscriptionId`. See `TelephonyManager` and `SubscriptionManager` + * for more documentation. + */ +@SysUISingleton +class TelephonyRepositoryImpl +@Inject +constructor( + private val manager: TelephonyListenerManager, +) : TelephonyRepository { + @Annotation.CallState + override val callState: Flow<Int> = conflatedCallbackFlow { + val listener = TelephonyCallback.CallStateListener { state -> trySend(state) } + + manager.addCallStateListener(listener) + + awaitClose { manager.removeCallStateListener(listener) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt new file mode 100644 index 000000000000..630fbf2d1a07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.telephony.data.repository + +import dagger.Binds +import dagger.Module + +@Module +interface TelephonyRepositoryModule { + @Binds fun repository(impl: TelephonyRepositoryImpl): TelephonyRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt new file mode 100644 index 000000000000..86ca33df24dd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.telephony.domain.interactor + +import android.telephony.Annotation +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.telephony.data.repository.TelephonyRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Hosts business logic related to telephony. */ +@SysUISingleton +class TelephonyInteractor +@Inject +constructor( + repository: TelephonyRepository, +) { + @Annotation.CallState val callState: Flow<Int> = repository.callState +} diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index a52e2aff52c1..f0a50de02b3a 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -17,20 +17,21 @@ package com.android.systemui.temporarydisplay import android.annotation.LayoutRes -import android.annotation.SuppressLint import android.content.Context import android.graphics.PixelFormat +import android.graphics.Rect import android.graphics.drawable.Drawable import android.os.PowerManager import android.os.SystemClock import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT -import androidx.annotation.CallSuper +import com.android.systemui.CoreStartable import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor @@ -60,17 +61,17 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora @LayoutRes private val viewLayoutRes: Int, private val windowTitle: String, private val wakeReason: String, -) { +) : CoreStartable { /** * Window layout params that will be used as a starting point for the [windowLayoutParams] of * all subclasses. */ - @SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply { width = WindowManager.LayoutParams.WRAP_CONTENT height = WindowManager.LayoutParams.WRAP_CONTENT - type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY - flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR + flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL title = windowTitle format = PixelFormat.TRANSLUCENT setTrustedOverlay() @@ -84,11 +85,8 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora */ internal abstract val windowLayoutParams: WindowManager.LayoutParams - /** The view currently being displayed. Null if the view is not being displayed. */ - private var view: ViewGroup? = null - - /** The info currently being displayed. Null if the view is not being displayed. */ - internal var info: T? = null + /** A container for all the display-related objects. Null if the view is not being displayed. */ + private var displayInfo: DisplayInfo? = null /** A [Runnable] that, when run, will cancel the pending timeout of the view. */ private var cancelViewTimeout: Runnable? = null @@ -100,10 +98,11 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora * display the correct information in the view. */ fun displayView(newInfo: T) { - val currentView = view + val currentDisplayInfo = displayInfo - if (currentView != null) { - updateView(newInfo, currentView) + if (currentDisplayInfo != null) { + currentDisplayInfo.info = newInfo + updateView(currentDisplayInfo.info, currentDisplayInfo.view) } else { // The view is new, so set up all our callbacks and inflate the view configurationController.addCallback(displayScaleListener) @@ -130,7 +129,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora ) cancelViewTimeout?.run() cancelViewTimeout = mainExecutor.executeDelayed( - { removeView(TemporaryDisplayRemovalReason.REASON_TIMEOUT) }, + { removeView(REMOVAL_REASON_TIMEOUT) }, timeout.toLong() ) } @@ -140,19 +139,24 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora val newView = LayoutInflater .from(context) .inflate(viewLayoutRes, null) as ViewGroup - view = newView - updateView(newInfo, newView) + val newViewController = TouchableRegionViewController(newView, this::getTouchableRegion) + newViewController.init() + + // We don't need to hold on to the view controller since we never set anything additional + // on it -- it will be automatically cleaned up when the view is detached. + val newDisplayInfo = DisplayInfo(newView, newInfo) + displayInfo = newDisplayInfo + updateView(newDisplayInfo.info, newDisplayInfo.view) windowManager.addView(newView, windowLayoutParams) animateViewIn(newView) } /** Removes then re-inflates the view. */ private fun reinflateView() { - val currentInfo = info - if (view == null || currentInfo == null) { return } + val currentViewInfo = displayInfo ?: return - windowManager.removeView(view) - inflateAndUpdateView(currentInfo) + windowManager.removeView(currentViewInfo.view) + inflateAndUpdateView(currentViewInfo.info) } private val displayScaleListener = object : ConfigurationController.ConfigurationListener { @@ -167,13 +171,18 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora * @param removalReason a short string describing why the view was removed (timeout, state * change, etc.) */ - open fun removeView(removalReason: String) { - if (view == null) { return } + fun removeView(removalReason: String) { + val currentDisplayInfo = displayInfo ?: return + + val currentView = currentDisplayInfo.view + animateViewOut(currentView) { windowManager.removeView(currentView) } + logger.logChipRemoval(removalReason) configurationController.removeCallback(displayScaleListener) - windowManager.removeView(view) - view = null - info = null + // Re-set to null immediately (instead as part of the animation end runnable) so + // that if a new view event comes in while this view is animating out, we still display the + // new view appropriately. + displayInfo = null // No need to time the view out since it's already gone cancelViewTimeout?.run() } @@ -181,23 +190,42 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora /** * A method implemented by subclasses to update [currentView] based on [newInfo]. */ - @CallSuper - open fun updateView(newInfo: T, currentView: ViewGroup) { - info = newInfo - } + abstract fun updateView(newInfo: T, currentView: ViewGroup) + + /** + * Fills [outRect] with the touchable region of this view. This will be used by WindowManager + * to decide which touch events go to the view. + */ + abstract fun getTouchableRegion(view: View, outRect: Rect) /** * A method that can be implemented by subclasses to do custom animations for when the view * appears. */ - open fun animateViewIn(view: ViewGroup) {} -} + internal open fun animateViewIn(view: ViewGroup) {} -object TemporaryDisplayRemovalReason { - const val REASON_TIMEOUT = "TIMEOUT" - const val REASON_SCREEN_TAP = "SCREEN_TAP" + /** + * A method that can be implemented by subclasses to do custom animations for when the view + * disappears. + * + * @param onAnimationEnd an action that *must* be run once the animation finishes successfully. + */ + internal open fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { + onAnimationEnd.run() + } + + /** A container for all the display-related state objects. */ + private inner class DisplayInfo( + /** The view currently being displayed. */ + val view: ViewGroup, + + /** The info currently being displayed. */ + var info: T, + ) } +private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT" + private data class IconInfo( val iconName: String, val icon: Drawable, diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt index 606a11a84686..a7185cb18c40 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt @@ -16,8 +16,8 @@ package com.android.systemui.temporarydisplay -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel /** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */ open class TemporaryViewLogger( diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TouchableRegionViewController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TouchableRegionViewController.kt new file mode 100644 index 000000000000..60241a9684d9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TouchableRegionViewController.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay + +import android.graphics.Rect +import android.view.View +import android.view.ViewTreeObserver +import com.android.systemui.util.ViewController + +/** + * A view controller that will notify the [ViewTreeObserver] about the touchable region for this + * view. This will be used by WindowManager to decide which touch events go to the view and which + * pass through to the window below. + * + * @param touchableRegionSetter a function that, given the view and an out rect, fills the rect with + * the touchable region of this view. + */ +class TouchableRegionViewController( + view: View, + touchableRegionSetter: (View, Rect) -> Unit, +) : ViewController<View>(view) { + + private val tempRect = Rect() + + private val internalInsetsListener = + ViewTreeObserver.OnComputeInternalInsetsListener { inoutInfo -> + inoutInfo.setTouchableInsets( + ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION + ) + + tempRect.setEmpty() + touchableRegionSetter.invoke(mView, tempRect) + inoutInfo.touchableRegion.set(tempRect) + } + + public override fun onViewAttached() { + mView.viewTreeObserver.addOnComputeInternalInsetsListener(internalInsetsListener) + } + + public override fun onViewDetached() { + mView.viewTreeObserver.removeOnComputeInternalInsetsListener(internalInsetsListener) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt new file mode 100644 index 000000000000..b8930a45cd33 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay.chipbar + +import android.content.Context +import android.graphics.Rect +import android.os.PowerManager +import android.view.Gravity +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.view.accessibility.AccessibilityManager +import android.widget.TextView +import com.android.internal.widget.CachingIconView +import com.android.systemui.Gefingerpoken +import com.android.systemui.R +import com.android.systemui.animation.Interpolators +import com.android.systemui.animation.ViewHierarchyAnimator +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Text.Companion.loadText +import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.common.ui.binder.TextViewBinder +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.media.taptotransfer.common.MediaTttUtils +import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.temporarydisplay.TemporaryViewDisplayController +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.view.ViewUtil +import javax.inject.Inject + +/** + * A coordinator for showing/hiding the chipbar. + * + * The chipbar is a UI element that displays on top of all content. It appears at the top of the + * screen and consists of an icon, one line of text, and an optional end icon or action. It will + * auto-dismiss after some amount of seconds. The user is *not* able to manually dismiss the + * chipbar. + * + * It should be only be used for critical and temporary information that the user *must* be aware + * of. In general, prefer using heads-up notifications, since they are dismissable and will remain + * in the list of notifications until the user dismisses them. + * + * Only one chipbar may be shown at a time. + * TODO(b/245610654): Should we just display whichever chipbar was most recently requested, or do we + * need to maintain a priority ordering? + * + * TODO(b/245610654): Remove all media-related items from this class so it's just for generic + * chipbars. + */ +@SysUISingleton +open class ChipbarCoordinator @Inject constructor( + context: Context, + @MediaTttSenderLogger logger: MediaTttLogger, + windowManager: WindowManager, + @Main mainExecutor: DelayableExecutor, + accessibilityManager: AccessibilityManager, + configurationController: ConfigurationController, + powerManager: PowerManager, + private val falsingManager: FalsingManager, + private val falsingCollector: FalsingCollector, + private val viewUtil: ViewUtil, + private val vibratorHelper: VibratorHelper, +) : TemporaryViewDisplayController<ChipbarInfo, MediaTttLogger>( + context, + logger, + windowManager, + mainExecutor, + accessibilityManager, + configurationController, + powerManager, + R.layout.chipbar, + MediaTttUtils.WINDOW_TITLE, + MediaTttUtils.WAKE_REASON, +) { + + private lateinit var parent: ChipbarRootView + + override val windowLayoutParams = commonWindowLayoutParams.apply { + gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) + } + + override fun start() {} + + override fun updateView( + newInfo: ChipbarInfo, + currentView: ViewGroup + ) { + // TODO(b/245610654): Adding logging here. + + // Detect falsing touches on the chip. + parent = currentView.requireViewById(R.id.chipbar_root_view) + parent.touchHandler = object : Gefingerpoken { + override fun onTouchEvent(ev: MotionEvent?): Boolean { + falsingCollector.onTouchEvent(ev) + return false + } + } + + // ---- Start icon ---- + val iconView = currentView.requireViewById<CachingIconView>(R.id.start_icon) + IconViewBinder.bind(newInfo.startIcon, iconView) + + // ---- Text ---- + val textView = currentView.requireViewById<TextView>(R.id.text) + TextViewBinder.bind(textView, newInfo.text) + // Updates text view bounds to make sure it perfectly fits the new text + // (If the new text is smaller than the previous text) see b/253228632. + textView.requestLayout() + + // ---- End item ---- + // Loading + currentView.requireViewById<View>(R.id.loading).visibility = + (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue() + + // Error + currentView.requireViewById<View>(R.id.error).visibility = + (newInfo.endItem == ChipbarEndItem.Error).visibleIfTrue() + + // Button + val buttonView = currentView.requireViewById<TextView>(R.id.end_button) + if (newInfo.endItem is ChipbarEndItem.Button) { + TextViewBinder.bind(buttonView, newInfo.endItem.text) + + val onClickListener = View.OnClickListener { clickedView -> + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener + newInfo.endItem.onClickListener.onClick(clickedView) + } + + buttonView.setOnClickListener(onClickListener) + buttonView.visibility = View.VISIBLE + } else { + buttonView.visibility = View.GONE + } + + // ---- Overall accessibility ---- + currentView.requireViewById<ViewGroup>( + R.id.chipbar_inner + ).contentDescription = + "${newInfo.startIcon.contentDescription.loadContentDescription(context)} " + + "${newInfo.text.loadText(context)}" + + // ---- Haptics ---- + newInfo.vibrationEffect?.let { + vibratorHelper.vibrate(it) + } + } + + override fun animateViewIn(view: ViewGroup) { + val chipInnerView = view.requireViewById<ViewGroup>(R.id.chipbar_inner) + ViewHierarchyAnimator.animateAddition( + chipInnerView, + ViewHierarchyAnimator.Hotspot.TOP, + Interpolators.EMPHASIZED_DECELERATE, + duration = ANIMATION_DURATION, + includeMargins = true, + includeFadeIn = true, + // We can only request focus once the animation finishes. + onAnimationEnd = { chipInnerView.requestAccessibilityFocus() }, + ) + } + + override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { + ViewHierarchyAnimator.animateRemoval( + view.requireViewById<ViewGroup>(R.id.chipbar_inner), + ViewHierarchyAnimator.Hotspot.TOP, + Interpolators.EMPHASIZED_ACCELERATE, + ANIMATION_DURATION, + includeMargins = true, + onAnimationEnd, + ) + } + + override fun getTouchableRegion(view: View, outRect: Rect) { + viewUtil.setRectToViewWindowLocation(view, outRect) + } + + private fun Boolean.visibleIfTrue(): Int { + return if (this) { + View.VISIBLE + } else { + View.GONE + } + } +} + +private const val ANIMATION_DURATION = 500L diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt new file mode 100644 index 000000000000..57fde87114d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay.chipbar + +import android.os.VibrationEffect +import android.view.View +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.temporarydisplay.TemporaryViewInfo + +/** + * A container for all the state needed to display a chipbar via [ChipbarCoordinator]. + * + * @property startIcon the icon to display at the start of the chipbar (on the left in LTR locales; + * on the right in RTL locales). + * @property text the text to display. + * @property endItem an optional end item to display at the end of the chipbar (on the right in LTR + * locales; on the left in RTL locales). + * @property vibrationEffect an optional vibration effect when the chipbar is displayed + */ +data class ChipbarInfo( + val startIcon: Icon, + val text: Text, + val endItem: ChipbarEndItem?, + val vibrationEffect: VibrationEffect? = null, +) : TemporaryViewInfo + +/** The possible items to display at the end of the chipbar. */ +sealed class ChipbarEndItem { + /** A loading icon should be displayed. */ + object Loading : ChipbarEndItem() + + /** An error icon should be displayed. */ + object Error : ChipbarEndItem() + + /** + * A button with the provided [text] and [onClickListener] functionality should be displayed. + */ + data class Button(val text: Text, val onClickListener: View.OnClickListener) : ChipbarEndItem() + + // TODO(b/245610654): Add support for a generic icon. +} diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarRootView.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarRootView.kt new file mode 100644 index 000000000000..edec420bc408 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarRootView.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay.chipbar + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.FrameLayout +import com.android.systemui.Gefingerpoken + +/** A simple subclass that allows for observing touch events on chipbar. */ +class ChipbarRootView( + context: Context, + attrs: AttributeSet? +) : FrameLayout(context, attrs) { + + /** Assign this field to observe touch events. */ + var touchHandler: Gefingerpoken? = null + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + touchHandler?.onTouchEvent(ev) + return super.dispatchTouchEvent(ev) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index adef1823d491..3ecb15b9d79c 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -79,6 +79,7 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -100,7 +101,7 @@ import javax.inject.Inject; * associated work profiles */ @SysUISingleton -public class ThemeOverlayController extends CoreStartable implements Dumpable { +public class ThemeOverlayController implements CoreStartable, Dumpable { protected static final String TAG = "ThemeOverlayController"; private static final boolean DEBUG = true; @@ -114,6 +115,8 @@ public class ThemeOverlayController extends CoreStartable implements Dumpable { private final SecureSettings mSecureSettings; private final Executor mMainExecutor; private final Handler mBgHandler; + private final boolean mIsMonochromaticEnabled; + private final Context mContext; private final boolean mIsMonetEnabled; private final UserTracker mUserTracker; private final DeviceProvisionedController mDeviceProvisionedController; @@ -171,6 +174,10 @@ public class ThemeOverlayController extends CoreStartable implements Dumpable { @Override public void onColorsChanged(WallpaperColors wallpaperColors, int which, int userId) { + WallpaperColors currentColors = mCurrentColors.get(userId); + if (wallpaperColors != null && wallpaperColors.equals(currentColors)) { + return; + } boolean currentUser = userId == mUserTracker.getUserId(); if (currentUser && !mAcceptColorEvents && mWakefulnessLifecycle.getWakefulness() != WAKEFULNESS_ASLEEP) { @@ -357,8 +364,8 @@ public class ThemeOverlayController extends CoreStartable implements Dumpable { UserManager userManager, DeviceProvisionedController deviceProvisionedController, UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags, @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) { - super(context); - + mContext = context; + mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEMES); mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET); mDeviceProvisionedController = deviceProvisionedController; mBroadcastDispatcher = broadcastDispatcher; @@ -661,8 +668,13 @@ public class ThemeOverlayController extends CoreStartable implements Dumpable { // Allow-list of Style objects that can be created from a setting string, i.e. can be // used as a system-wide theme. // - Content intentionally excluded, intended for media player, not system-wide - List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT, - Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT); + List<Style> validStyles = new ArrayList<>(Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, + Style.TONAL_SPOT, Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT)); + + if (mIsMonochromaticEnabled) { + validStyles.add(Style.MONOCHROMATIC); + } + Style style = mThemeStyle; final String overlayPackageJson = mSecureSettings.getStringForUser( Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt index 51541bd3032e..fda511433143 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt @@ -16,11 +16,11 @@ package com.android.systemui.toast -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogLevel.DEBUG -import com.android.systemui.log.LogMessage import com.android.systemui.log.dagger.ToastLog +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogLevel.DEBUG +import com.android.systemui.plugins.log.LogMessage import javax.inject.Inject private const val TAG = "ToastLog" diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java index 9eb34a42a0a9..ed14c8ad150c 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java @@ -50,13 +50,14 @@ import javax.inject.Inject; * Controls display of text toasts. */ @SysUISingleton -public class ToastUI extends CoreStartable implements CommandQueue.Callbacks { +public class ToastUI implements CoreStartable, CommandQueue.Callbacks { // values from NotificationManagerService#LONG_DELAY and NotificationManagerService#SHORT_DELAY private static final int TOAST_LONG_TIME = 3500; // 3.5 seconds private static final int TOAST_SHORT_TIME = 2000; // 2 seconds private static final String TAG = "ToastUI"; + private final Context mContext; private final CommandQueue mCommandQueue; private final INotificationManager mNotificationManager; private final IAccessibilityManager mIAccessibilityManager; @@ -90,7 +91,7 @@ public class ToastUI extends CoreStartable implements CommandQueue.Callbacks { @Nullable IAccessibilityManager accessibilityManager, ToastFactory toastFactory, ToastLogger toastLogger ) { - super(context); + mContext = context; mCommandQueue = commandQueue; mNotificationManager = notificationManager; mIAccessibilityManager = accessibilityManager; @@ -179,7 +180,7 @@ public class ToastUI extends CoreStartable implements CommandQueue.Callbacks { } @Override - protected void onConfigurationChanged(Configuration newConfig) { + public void onConfigurationChanged(Configuration newConfig) { if (newConfig.orientation != mOrientation) { mOrientation = newConfig.orientation; if (mToast != null) { diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java index 00ed3d635fa1..10a09dd169e8 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java @@ -22,25 +22,20 @@ import static com.android.systemui.Dependency.LEAK_REPORT_EMAIL_NAME; import android.content.Context; import android.hardware.SensorPrivacyManager; import android.os.Handler; -import android.os.PowerManager; import androidx.annotation.Nullable; import com.android.internal.logging.UiEventLogger; import com.android.keyguard.KeyguardViewController; -import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dagger.ReferenceSystemUIModule; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; -import com.android.systemui.dump.DumpManager; import com.android.systemui.navigationbar.gestural.GestureModule; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.power.EnhancedEstimates; import com.android.systemui.power.dagger.PowerModule; import com.android.systemui.privacy.MediaProjectionPrivacyItemMonitor; import com.android.systemui.privacy.PrivacyItemMonitor; @@ -64,8 +59,7 @@ import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; -import com.android.systemui.statusbar.policy.BatteryController; -import com.android.systemui.statusbar.policy.BatteryControllerImpl; +import com.android.systemui.statusbar.policy.AospPolicyModule; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedControllerImpl; @@ -86,18 +80,21 @@ import dagger.Provides; import dagger.multibindings.IntoSet; /** - * A dagger module for injecting default implementations of components of System UI that may be - * overridden by the System UI implementation. + * A TV specific version of {@link ReferenceSystemUIModule}. + * + * Code here should be specific to the TV variant of SystemUI and will not be included in other + * variants of SystemUI. */ -@Module(includes = { - GestureModule.class, - PowerModule.class, - QSModule.class, - ReferenceScreenshotModule.class, - VolumeModule.class, - }, - subcomponents = { - }) +@Module( + includes = { + AospPolicyModule.class, + GestureModule.class, + PowerModule.class, + QSModule.class, + ReferenceScreenshotModule.class, + VolumeModule.class, + } +) public abstract class TvSystemUIModule { @SysUISingleton @@ -114,21 +111,6 @@ public abstract class TvSystemUIModule { @Provides @SysUISingleton - static BatteryController provideBatteryController(Context context, - EnhancedEstimates enhancedEstimates, PowerManager powerManager, - BroadcastDispatcher broadcastDispatcher, DemoModeController demoModeController, - DumpManager dumpManager, - @Main Handler mainHandler, @Background Handler bgHandler) { - BatteryController bC = new BatteryControllerImpl(context, enhancedEstimates, powerManager, - broadcastDispatcher, demoModeController, - dumpManager, - mainHandler, bgHandler); - bC.init(); - return bC; - } - - @Provides - @SysUISingleton static SensorPrivacyController provideSensorPrivacyController( SensorPrivacyManager sensorPrivacyManager) { SensorPrivacyController spC = new SensorPrivacyControllerImpl(sensorPrivacyManager); @@ -221,9 +203,9 @@ public abstract class TvSystemUIModule { @Provides @SysUISingleton - static TvNotificationHandler provideTvNotificationHandler(Context context, + static TvNotificationHandler provideTvNotificationHandler( NotificationListener notificationListener) { - return new TvNotificationHandler(context, notificationListener); + return new TvNotificationHandler(notificationListener); } /** diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt index fc20ac241e38..6ed3a093c8e3 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt @@ -26,8 +26,6 @@ import android.os.Trace import android.view.Choreographer import android.view.Display import android.view.DisplayInfo -import android.view.IRotationWatcher -import android.view.IWindowManager import android.view.Surface import android.view.SurfaceControl import android.view.SurfaceControlViewHost @@ -40,6 +38,7 @@ import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.LinearLightRevealEffect import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.util.traceSection import com.android.wm.shell.displayareahelper.DisplayAreaHelper import java.util.Optional @@ -58,7 +57,7 @@ constructor( private val displayAreaHelper: Optional<DisplayAreaHelper>, @Main private val executor: Executor, @UiBackground private val backgroundExecutor: Executor, - private val windowManagerInterface: IWindowManager + private val rotationChangeProvider: RotationChangeProvider, ) { private val transitionListener = TransitionListener() @@ -78,7 +77,7 @@ constructor( fun init() { deviceStateManager.registerCallback(executor, FoldListener()) unfoldTransitionProgressProvider.addCallback(transitionListener) - windowManagerInterface.watchRotation(rotationWatcher, context.display.displayId) + rotationChangeProvider.addCallback(rotationWatcher) val containerBuilder = SurfaceControl.Builder(SurfaceSession()) @@ -86,7 +85,9 @@ constructor( .setName("unfold-overlay-container") displayAreaHelper.get().attachToRootDisplayArea( - Display.DEFAULT_DISPLAY, containerBuilder) { builder -> + Display.DEFAULT_DISPLAY, + containerBuilder + ) { builder -> executor.execute { overlayContainer = builder.build() @@ -244,8 +245,8 @@ constructor( } } - private inner class RotationWatcher : IRotationWatcher.Stub() { - override fun onRotationChanged(newRotation: Int) = + private inner class RotationWatcher : RotationChangeProvider.RotationListener { + override fun onRotationChanged(newRotation: Int) { traceSection("UnfoldLightRevealOverlayAnimation#onRotationChanged") { if (currentRotation != newRotation) { currentRotation = newRotation @@ -253,6 +254,7 @@ constructor( root?.relayout(getLayoutParams()) } } + } } private inner class FoldListener : diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt index eea6ac0e72e9..59ad24a3e7bb 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt @@ -17,11 +17,11 @@ package com.android.systemui.unfold import android.content.Context -import android.view.IWindowManager import com.android.systemui.keyguard.LifecycleScreenStatusProvider import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.system.SystemUnfoldSharedModule import com.android.systemui.unfold.updates.FoldStateProvider +import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider @@ -65,11 +65,11 @@ class UnfoldTransitionModule { @Singleton fun provideNaturalRotationProgressProvider( context: Context, - windowManager: IWindowManager, + rotationChangeProvider: RotationChangeProvider, unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider> ): Optional<NaturalRotationUnfoldProgressProvider> = unfoldTransitionProgressProvider.map { provider -> - NaturalRotationUnfoldProgressProvider(context, windowManager, provider) + NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, provider) } @Provides diff --git a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java index 4dc78f9ec8a6..bf706735d531 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java +++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java @@ -21,7 +21,6 @@ import android.app.Notification; import android.app.Notification.Action; import android.app.NotificationManager; import android.app.PendingIntent; -import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -56,11 +55,12 @@ import javax.inject.Inject; /** */ @SysUISingleton -public class StorageNotification extends CoreStartable { +public class StorageNotification implements CoreStartable { private static final String TAG = "StorageNotification"; private static final String ACTION_SNOOZE_VOLUME = "com.android.systemui.action.SNOOZE_VOLUME"; private static final String ACTION_FINISH_WIZARD = "com.android.systemui.action.FINISH_WIZARD"; + private final Context mContext; // TODO: delay some notifications to avoid bumpy fast operations @@ -69,7 +69,7 @@ public class StorageNotification extends CoreStartable { @Inject public StorageNotification(Context context) { - super(context); + mContext = context; } private static class MoveInfo { diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index 5b522dcc4885..0c72b78a3c46 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -20,6 +20,7 @@ import android.app.Activity; import com.android.settingslib.users.EditUserInfoController; import com.android.systemui.user.data.repository.UserRepositoryModule; +import com.android.systemui.user.ui.dialog.UserDialogModule; import dagger.Binds; import dagger.Module; @@ -32,6 +33,7 @@ import dagger.multibindings.IntoMap; */ @Module( includes = { + UserDialogModule.class, UserRepositoryModule.class, } ) diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt index 108ab43977e9..7da2d47c1226 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt @@ -16,426 +16,42 @@ package com.android.systemui.user -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable -import android.graphics.drawable.GradientDrawable -import android.graphics.drawable.LayerDrawable import android.os.Bundle -import android.os.UserManager -import android.provider.Settings -import android.util.Log -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.ImageView -import android.widget.TextView -import android.window.OnBackInvokedCallback -import android.window.OnBackInvokedDispatcher +import android.view.WindowInsets.Type +import android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE import androidx.activity.ComponentActivity -import androidx.constraintlayout.helper.widget.Flow import androidx.lifecycle.ViewModelProvider -import com.android.internal.annotations.VisibleForTesting -import com.android.internal.util.UserIcons -import com.android.settingslib.Utils -import com.android.systemui.Gefingerpoken import com.android.systemui.R -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.plugins.FalsingManager.LOW_PENALTY -import com.android.systemui.settings.UserTracker -import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter -import com.android.systemui.statusbar.policy.UserSwitcherController -import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.ui.binder.UserSwitcherViewBinder import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import dagger.Lazy import javax.inject.Inject -import kotlin.math.ceil - -private const val USER_VIEW = "user_view" /** Support a fullscreen user switcher */ open class UserSwitcherActivity @Inject constructor( - private val userSwitcherController: UserSwitcherController, - private val broadcastDispatcher: BroadcastDispatcher, private val falsingCollector: FalsingCollector, - private val falsingManager: FalsingManager, - private val userManager: UserManager, - private val userTracker: UserTracker, - private val flags: FeatureFlags, private val viewModelFactory: Lazy<UserSwitcherViewModel.Factory>, ) : ComponentActivity() { - private lateinit var parent: UserSwitcherRootView - private lateinit var broadcastReceiver: BroadcastReceiver - private var popupMenu: UserSwitcherPopupMenu? = null - private lateinit var addButton: View - private var addUserRecords = mutableListOf<UserRecord>() - private val onBackCallback = OnBackInvokedCallback { finish() } - private val userSwitchedCallback: UserTracker.Callback = - object : UserTracker.Callback { - override fun onUserChanged(newUser: Int, userContext: Context) { - finish() - } - } - // When the add users options become available, insert another option to manage users - private val manageUserRecord = - UserRecord( - null /* info */, - null /* picture */, - false /* isGuest */, - false /* isCurrent */, - false /* isAddUser */, - false /* isRestricted */, - false /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - - private val adapter: UserAdapter by lazy { UserAdapter() } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - createActivity() - } - - @VisibleForTesting - fun createActivity() { setContentView(R.layout.user_switcher_fullscreen) - window.decorView.systemUiVisibility = - (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) - if (isUsingModernArchitecture()) { - Log.d(TAG, "Using modern architecture.") - val viewModel = - ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java] - UserSwitcherViewBinder.bind( - view = requireViewById(R.id.user_switcher_root), - viewModel = viewModel, - lifecycleOwner = this, - layoutInflater = layoutInflater, - falsingCollector = falsingCollector, - onFinish = this::finish, - ) - return - } else { - Log.d(TAG, "Not using modern architecture.") - } - - parent = requireViewById<UserSwitcherRootView>(R.id.user_switcher_root) - - parent.touchHandler = - object : Gefingerpoken { - override fun onTouchEvent(ev: MotionEvent?): Boolean { - falsingCollector.onTouchEvent(ev) - return false - } - } - - requireViewById<View>(R.id.cancel).apply { setOnClickListener { _ -> finish() } } - - addButton = - requireViewById<View>(R.id.add).apply { setOnClickListener { _ -> showPopupMenu() } } - - onBackInvokedDispatcher.registerOnBackInvokedCallback( - OnBackInvokedDispatcher.PRIORITY_DEFAULT, - onBackCallback + window.decorView.getWindowInsetsController().apply { + setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE) + hide(Type.systemBars()) + } + val viewModel = + ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java] + UserSwitcherViewBinder.bind( + view = requireViewById(R.id.user_switcher_root), + viewModel = viewModel, + lifecycleOwner = this, + layoutInflater = layoutInflater, + falsingCollector = falsingCollector, + onFinish = this::finish, ) - - userSwitcherController.init(parent) - initBroadcastReceiver() - - parent.post { buildUserViews() } - userTracker.addCallback(userSwitchedCallback, mainExecutor) - } - - private fun showPopupMenu() { - val items = mutableListOf<UserRecord>() - addUserRecords.forEach { items.add(it) } - - var popupMenuAdapter = - ItemAdapter( - this, - R.layout.user_switcher_fullscreen_popup_item, - layoutInflater, - { item: UserRecord -> adapter.getName(this@UserSwitcherActivity, item, true) }, - { item: UserRecord -> - adapter.findUserIcon(item, true).mutate().apply { - setTint( - resources.getColor( - R.color.user_switcher_fullscreen_popup_item_tint, - getTheme() - ) - ) - } - } - ) - popupMenuAdapter.addAll(items) - - popupMenu = - UserSwitcherPopupMenu(this).apply { - setAnchorView(addButton) - setAdapter(popupMenuAdapter) - setOnItemClickListener { parent: AdapterView<*>, view: View, pos: Int, id: Long -> - if (falsingManager.isFalseTap(LOW_PENALTY) || !view.isEnabled()) { - return@setOnItemClickListener - } - // -1 for the header - val item = popupMenuAdapter.getItem(pos - 1) - if (item == manageUserRecord) { - val i = Intent().setAction(Settings.ACTION_USER_SETTINGS) - this@UserSwitcherActivity.startActivity(i) - } else { - adapter.onUserListItemClicked(item) - } - - dismiss() - popupMenu = null - - if (!item.isAddUser) { - this@UserSwitcherActivity.finish() - } - } - - show() - } - } - - private fun buildUserViews() { - var count = 0 - var start = 0 - for (i in 0 until parent.getChildCount()) { - if (parent.getChildAt(i).getTag() == USER_VIEW) { - if (count == 0) start = i - count++ - } - } - parent.removeViews(start, count) - addUserRecords.clear() - val flow = requireViewById<Flow>(R.id.flow) - val totalWidth = parent.width - val userViewCount = adapter.getTotalUserViews() - val maxColumns = getMaxColumns(userViewCount) - val horizontalGap = - resources.getDimensionPixelSize(R.dimen.user_switcher_fullscreen_horizontal_gap) - val totalWidthOfHorizontalGap = (maxColumns - 1) * horizontalGap - val maxWidgetDiameter = (totalWidth - totalWidthOfHorizontalGap) / maxColumns - - flow.setMaxElementsWrap(maxColumns) - - for (i in 0 until adapter.getCount()) { - val item = adapter.getItem(i) - if (adapter.doNotRenderUserView(item)) { - addUserRecords.add(item) - } else { - val userView = adapter.getView(i, null, parent) - userView.requireViewById<ImageView>(R.id.user_switcher_icon).apply { - val lp = layoutParams - if (maxWidgetDiameter < lp.width) { - lp.width = maxWidgetDiameter - lp.height = maxWidgetDiameter - layoutParams = lp - } - } - - userView.setId(View.generateViewId()) - parent.addView(userView) - - // Views must have an id and a parent in order for Flow to lay them out - flow.addView(userView) - - userView.setOnClickListener { v -> - if (falsingManager.isFalseTap(LOW_PENALTY) || !v.isEnabled()) { - return@setOnClickListener - } - - if (!item.isCurrent || item.isGuest) { - adapter.onUserListItemClicked(item) - } - } - } - } - - if (!addUserRecords.isEmpty()) { - addUserRecords.add(manageUserRecord) - addButton.visibility = View.VISIBLE - } else { - addButton.visibility = View.GONE - } - } - - override fun onBackPressed() { - if (isUsingModernArchitecture()) { - return super.onBackPressed() - } - - finish() - } - - override fun onDestroy() { - super.onDestroy() - if (isUsingModernArchitecture()) { - return - } - destroyActivity() - } - - @VisibleForTesting - fun destroyActivity() { - onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackCallback) - broadcastDispatcher.unregisterReceiver(broadcastReceiver) - userTracker.removeCallback(userSwitchedCallback) - } - - private fun initBroadcastReceiver() { - broadcastReceiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val action = intent.getAction() - if (Intent.ACTION_SCREEN_OFF.equals(action)) { - finish() - } - } - } - - val filter = IntentFilter() - filter.addAction(Intent.ACTION_SCREEN_OFF) - broadcastDispatcher.registerReceiver(broadcastReceiver, filter) - } - - @VisibleForTesting - fun getMaxColumns(userCount: Int): Int { - return if (userCount < 5) 4 else ceil(userCount / 2.0).toInt() - } - - private fun isUsingModernArchitecture(): Boolean { - return flags.isEnabled(Flags.MODERN_USER_SWITCHER_ACTIVITY) - } - - /** Provides views to populate the option menu. */ - private class ItemAdapter( - val parentContext: Context, - val resource: Int, - val layoutInflater: LayoutInflater, - val textGetter: (UserRecord) -> String, - val iconGetter: (UserRecord) -> Drawable - ) : ArrayAdapter<UserRecord>(parentContext, resource) { - - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val item = getItem(position) - val view = convertView ?: layoutInflater.inflate(resource, parent, false) - - view.requireViewById<ImageView>(R.id.icon).apply { setImageDrawable(iconGetter(item)) } - view.requireViewById<TextView>(R.id.text).apply { setText(textGetter(item)) } - - return view - } - } - - private inner class UserAdapter : BaseUserSwitcherAdapter(userSwitcherController) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val item = getItem(position) - var view = convertView as ViewGroup? - if (view == null) { - view = - layoutInflater.inflate(R.layout.user_switcher_fullscreen_item, parent, false) - as ViewGroup - } - (view.getChildAt(0) as ImageView).apply { setImageDrawable(getDrawable(item)) } - (view.getChildAt(1) as TextView).apply { setText(getName(getContext(), item)) } - - view.setEnabled(item.isSwitchToEnabled) - UserSwitcherController.setSelectableAlpha(view) - view.setTag(USER_VIEW) - return view - } - - override fun getName(context: Context, item: UserRecord, isTablet: Boolean): String { - return if (item == manageUserRecord) { - getString(R.string.manage_users) - } else { - super.getName(context, item, isTablet) - } - } - - fun findUserIcon(item: UserRecord, isTablet: Boolean = false): Drawable { - if (item == manageUserRecord) { - return getDrawable(R.drawable.ic_manage_users) - } - if (item.info == null) { - return getIconDrawable(this@UserSwitcherActivity, item, isTablet) - } - val userIcon = userManager.getUserIcon(item.info.id) - if (userIcon != null) { - return BitmapDrawable(userIcon) - } - return UserIcons.getDefaultUserIcon(resources, item.info.id, false) - } - - fun getTotalUserViews(): Int { - return users.count { item -> !doNotRenderUserView(item) } - } - - fun doNotRenderUserView(item: UserRecord): Boolean { - return item.isAddUser || item.isAddSupervisedUser || item.isGuest && item.info == null - } - - private fun getDrawable(item: UserRecord): Drawable { - var drawable = - if (item.isGuest) { - getDrawable(R.drawable.ic_account_circle) - } else { - findUserIcon(item) - } - drawable.mutate() - - if (!item.isCurrent && !item.isSwitchToEnabled) { - drawable.setTint( - resources.getColor( - R.color.kg_user_switcher_restricted_avatar_icon_color, - getTheme() - ) - ) - } - - val ld = getDrawable(R.drawable.user_switcher_icon_large).mutate() as LayerDrawable - if (item == userSwitcherController.currentUserRecord) { - (ld.findDrawableByLayerId(R.id.ring) as GradientDrawable).apply { - val stroke = - resources.getDimensionPixelSize(R.dimen.user_switcher_icon_selected_width) - val color = - Utils.getColorAttrDefaultColor( - this@UserSwitcherActivity, - com.android.internal.R.attr.colorAccentPrimary - ) - - setStroke(stroke, color) - } - } - - ld.setDrawableByLayerId(R.id.user_avatar, drawable) - return ld - } - - override fun notifyDataSetChanged() { - super.notifyDataSetChanged() - buildUserViews() - } - } - - companion object { - private const val TAG = "UserSwitcherActivity" } } diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt index ee785b62bd50..088cd93bdf7e 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt @@ -36,9 +36,7 @@ class UserSwitcherPopupMenu( private var adapter: ListAdapter? = null init { - setBackgroundDrawable( - res.getDrawable(R.drawable.bouncer_user_switcher_popup_bg, context.getTheme()) - ) + setBackgroundDrawable(null) setModal(false) setOverlapAnchor(true) } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt new file mode 100644 index 000000000000..4fd55c0e21c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.data.model + +/** Encapsulates the state of settings related to user switching. */ +data class UserSwitcherSettingsModel( + val isSimpleUserSwitcher: Boolean = false, + val isAddUsersFromLockscreen: Boolean = false, + val isUserSwitcherEnabled: Boolean = false, +) diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 035638800f9c..b16dc5403a57 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -18,9 +18,13 @@ package com.android.systemui.user.data.repository import android.content.Context +import android.content.pm.UserInfo import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable +import android.os.UserHandle import android.os.UserManager +import android.provider.Settings +import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources import com.android.internal.util.UserIcons import com.android.systemui.R @@ -29,15 +33,36 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Acts as source of truth for user related data. @@ -55,6 +80,18 @@ interface UserRepository { /** List of available user-related actions. */ val actions: Flow<List<UserActionModel>> + /** User switcher related settings. */ + val userSwitcherSettings: Flow<UserSwitcherSettingsModel> + + /** List of all users on the device. */ + val userInfos: Flow<List<UserInfo>> + + /** [UserInfo] of the currently-selected user. */ + val selectedUserInfo: Flow<UserInfo> + + /** User ID of the last non-guest selected user. */ + val lastSelectedNonGuestUserId: Int + /** Whether actions are available even when locked. */ val isActionableWhenLocked: Flow<Boolean> @@ -62,7 +99,23 @@ interface UserRepository { val isGuestUserAutoCreated: Boolean /** Whether the guest user is currently being reset. */ - val isGuestUserResetting: Boolean + var isGuestUserResetting: Boolean + + /** Whether we've scheduled the creation of a guest user. */ + val isGuestUserCreationScheduled: AtomicBoolean + + /** The user of the secondary service. */ + var secondaryUserId: Int + + /** Whether refresh users should be paused. */ + var isRefreshUsersPaused: Boolean + + /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */ + fun refreshUsers() + + fun getSelectedUserInfo(): UserInfo + + fun isSimpleUserSwitcher(): Boolean } @SysUISingleton @@ -71,9 +124,31 @@ class UserRepositoryImpl constructor( @Application private val appContext: Context, private val manager: UserManager, - controller: UserSwitcherController, + private val controller: UserSwitcherController, + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val globalSettings: GlobalSettings, + private val tracker: UserTracker, + private val featureFlags: FeatureFlags, ) : UserRepository { + private val isNewImpl: Boolean + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) + + private val _userSwitcherSettings = MutableStateFlow<UserSwitcherSettingsModel?>(null) + override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = + _userSwitcherSettings.asStateFlow().filterNotNull() + + private val _userInfos = MutableStateFlow<List<UserInfo>?>(null) + override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull() + + private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null) + override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull() + + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM + private set + private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow { fun send() { trySendWithFailureLogging( @@ -99,16 +174,159 @@ constructor( override val actions: Flow<List<UserActionModel>> = userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } } - override val isActionableWhenLocked: Flow<Boolean> = controller.isAddUsersFromLockScreenEnabled + override val isActionableWhenLocked: Flow<Boolean> = + if (isNewImpl) { + emptyFlow() + } else { + controller.isAddUsersFromLockScreenEnabled + } + + override val isGuestUserAutoCreated: Boolean = + if (isNewImpl) { + appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated) + } else { + controller.isGuestUserAutoCreated + } + + private var _isGuestUserResetting: Boolean = false + override var isGuestUserResetting: Boolean = + if (isNewImpl) { + _isGuestUserResetting + } else { + controller.isGuestUserResetting + } + set(value) = + if (isNewImpl) { + _isGuestUserResetting = value + } else { + error("Not supported in the old implementation!") + } + + override val isGuestUserCreationScheduled = AtomicBoolean() + + override var secondaryUserId: Int = UserHandle.USER_NULL - override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated + override var isRefreshUsersPaused: Boolean = false - override val isGuestUserResetting: Boolean = controller.isGuestUserResetting + init { + if (isNewImpl) { + observeSelectedUser() + observeUserSettings() + } + } + + override fun refreshUsers() { + applicationScope.launch { + val result = withContext(backgroundDispatcher) { manager.aliveUsers } + + if (result != null) { + _userInfos.value = + result + // Users should be sorted by ascending creation time. + .sortedBy { it.creationTime } + // The guest user is always last, regardless of creation time. + .sortedBy { it.isGuest } + } + } + } + + override fun getSelectedUserInfo(): UserInfo { + return checkNotNull(_selectedUserInfo.value) + } + + override fun isSimpleUserSwitcher(): Boolean { + return checkNotNull(_userSwitcherSettings.value?.isSimpleUserSwitcher) + } + + private fun observeSelectedUser() { + conflatedCallbackFlow { + fun send() { + trySendWithFailureLogging(tracker.userInfo, TAG) + } + + val callback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + send() + } + } + + tracker.addCallback(callback, mainDispatcher.asExecutor()) + send() + + awaitClose { tracker.removeCallback(callback) } + } + .onEach { + if (!it.isGuest) { + lastSelectedNonGuestUserId = it.id + } + + _selectedUserInfo.value = it + } + .launchIn(applicationScope) + } + + private fun observeUserSettings() { + globalSettings + .observerFlow( + names = + arrayOf( + SETTING_SIMPLE_USER_SWITCHER, + Settings.Global.ADD_USERS_WHEN_LOCKED, + Settings.Global.USER_SWITCHER_ENABLED, + ), + userId = UserHandle.USER_SYSTEM, + ) + .onStart { emit(Unit) } // Forces an initial update. + .map { getSettings() } + .onEach { _userSwitcherSettings.value = it } + .launchIn(applicationScope) + } + + private suspend fun getSettings(): UserSwitcherSettingsModel { + return withContext(backgroundDispatcher) { + val isSimpleUserSwitcher = + globalSettings.getIntForUser( + SETTING_SIMPLE_USER_SWITCHER, + if ( + appContext.resources.getBoolean( + com.android.internal.R.bool.config_expandLockScreenUserSwitcher + ) + ) { + 1 + } else { + 0 + }, + UserHandle.USER_SYSTEM, + ) != 0 + + val isAddUsersFromLockscreen = + globalSettings.getIntForUser( + Settings.Global.ADD_USERS_WHEN_LOCKED, + 0, + UserHandle.USER_SYSTEM, + ) != 0 + + val isUserSwitcherEnabled = + globalSettings.getIntForUser( + Settings.Global.USER_SWITCHER_ENABLED, + 0, + UserHandle.USER_SYSTEM, + ) != 0 + + UserSwitcherSettingsModel( + isSimpleUserSwitcher = isSimpleUserSwitcher, + isAddUsersFromLockscreen = isAddUsersFromLockscreen, + isUserSwitcherEnabled = isUserSwitcherEnabled, + ) + } + } private fun UserRecord.isUser(): Boolean { return when { isAddUser -> false isAddSupervisedUser -> false + isManageUsers -> false isGuest -> info != null else -> true } @@ -125,6 +343,7 @@ constructor( image = getUserImage(this), isSelected = isCurrent, isSelectable = isSwitchToEnabled || isGuest, + isGuest = isGuest, ) } @@ -133,6 +352,7 @@ constructor( isAddUser -> UserActionModel.ADD_USER isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER isGuest -> UserActionModel.ENTER_GUEST_MODE + isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT else -> error("Don't know how to convert to UserActionModel: $this") } } @@ -162,5 +382,6 @@ constructor( companion object { private const val TAG = "UserRepository" + @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher" } } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt index cf6da9a60d78..d4fb5634bd1d 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt @@ -19,6 +19,7 @@ package com.android.systemui.user.data.source import android.content.pm.UserInfo import android.graphics.Bitmap import android.os.UserHandle +import com.android.settingslib.RestrictedLockUtils /** Encapsulates raw data for a user or an option item related to managing users on the device. */ data class UserRecord( @@ -41,6 +42,14 @@ data class UserRecord( @JvmField val isSwitchToEnabled: Boolean = false, /** Whether this record represents an option to add another supervised user to the device. */ @JvmField val isAddSupervisedUser: Boolean = false, + /** + * An enforcing admin, if the user action represented by this record is disabled by the admin. + * If not disabled, this is `null`. + */ + @JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null, + + /** Whether this record is to go to the Settings page to manage users. */ + @JvmField val isManageUsers: Boolean = false ) { /** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */ fun copyWithIsCurrent(isCurrent: Boolean): UserRecord { @@ -59,6 +68,14 @@ data class UserRecord( } } + /** + * Returns `true` if the user action represented by this record has been disabled by an admin; + * `false` otherwise. + */ + fun isDisabledByAdmin(): Boolean { + return enforcedAdmin != null + } + companion object { @JvmStatic fun createForGuest(): UserRecord { diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt new file mode 100644 index 000000000000..07e5cf9d9df2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.annotation.UserIdInt +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.content.pm.UserInfo +import android.os.RemoteException +import android.os.UserHandle +import android.os.UserManager +import android.util.Log +import android.view.WindowManagerGlobal +import android.widget.Toast +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.QSUserSwitcherEvent +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext + +/** Encapsulates business logic to interact with guest user data and systems. */ +@SysUISingleton +class GuestUserInteractor +@Inject +constructor( + @Application private val applicationContext: Context, + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val manager: UserManager, + private val repository: UserRepository, + private val deviceProvisionedController: DeviceProvisionedController, + private val devicePolicyManager: DevicePolicyManager, + private val refreshUsersScheduler: RefreshUsersScheduler, + private val uiEventLogger: UiEventLogger, +) { + /** Whether the device is configured to always have a guest user available. */ + val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated + + /** Whether the guest user is currently being reset. */ + val isGuestUserResetting: Boolean = repository.isGuestUserResetting + + /** Notifies that the device has finished booting. */ + fun onDeviceBootCompleted() { + applicationScope.launch { + if (isDeviceAllowedToAddGuest()) { + guaranteePresent() + return@launch + } + + suspendCancellableCoroutine<Unit> { continuation -> + val callback = + object : DeviceProvisionedController.DeviceProvisionedListener { + override fun onDeviceProvisionedChanged() { + continuation.resumeWith(Result.success(Unit)) + deviceProvisionedController.removeCallback(this) + } + } + + deviceProvisionedController.addCallback(callback) + } + + if (isDeviceAllowedToAddGuest()) { + guaranteePresent() + } + } + } + + /** Creates a guest user and switches to it. */ + fun createAndSwitchTo( + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + selectUser: (userId: Int) -> Unit, + ) { + applicationScope.launch { + val newGuestUserId = create(showDialog, dismissDialog) + if (newGuestUserId != UserHandle.USER_NULL) { + selectUser(newGuestUserId) + } + } + } + + /** Exits the guest user, switching back to the last non-guest user or to the default user. */ + fun exit( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + forceRemoveGuestOnExit: Boolean, + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + switchUser: (userId: Int) -> Unit, + ) { + val currentUserInfo = repository.getSelectedUserInfo() + if (currentUserInfo.id != guestUserId) { + Log.w( + TAG, + "User requesting to start a new session ($guestUserId) is not current user" + + " (${currentUserInfo.id})" + ) + return + } + + if (!currentUserInfo.isGuest) { + Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest") + return + } + + applicationScope.launch { + var newUserId = UserHandle.USER_SYSTEM + if (targetUserId == UserHandle.USER_NULL) { + // When a target user is not specified switch to last non guest user: + val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId + if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) { + val info = + withContext(backgroundDispatcher) { + manager.getUserInfo(lastSelectedNonGuestUserHandle) + } + if (info != null && info.isEnabled && info.supportsSwitchToByUser()) { + newUserId = info.id + } + } + } else { + newUserId = targetUserId + } + + if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE) + remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser) + } else { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH) + switchUser(newUserId) + } + } + } + + /** + * Guarantees that the guest user is present on the device, creating it if needed and if allowed + * to. + */ + suspend fun guaranteePresent() { + if (!isDeviceAllowedToAddGuest()) { + return + } + + val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() } + if (guestUser == null) { + scheduleCreation() + } + } + + /** Removes the guest user from the device. */ + suspend fun remove( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + switchUser: (userId: Int) -> Unit, + ) { + val currentUser: UserInfo = repository.getSelectedUserInfo() + if (currentUser.id != guestUserId) { + Log.w( + TAG, + "User requesting to start a new session ($guestUserId) is not current user" + + " ($currentUser.id)" + ) + return + } + + if (!currentUser.isGuest) { + Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest") + return + } + + val marked = + withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) } + if (!marked) { + Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId") + return + } + + if (targetUserId == UserHandle.USER_NULL) { + // Create a new guest in the foreground, and then immediately switch to it + val newGuestId = create(showDialog, dismissDialog) + if (newGuestId == UserHandle.USER_NULL) { + Log.e(TAG, "Could not create new guest, switching back to system user") + switchUser(UserHandle.USER_SYSTEM) + withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) } + try { + WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null) + } catch (e: RemoteException) { + Log.e( + TAG, + "Couldn't remove guest because ActivityManager or WindowManager is dead" + ) + } + return + } + + switchUser(newGuestId) + + withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) } + } else { + if (repository.isGuestUserAutoCreated) { + repository.isGuestUserResetting = true + } + switchUser(targetUserId) + manager.removeUser(currentUser.id) + } + } + + /** + * Creates the guest user and adds it to the device. + * + * @param showDialog A function to invoke to show a dialog. + * @param dismissDialog A function to invoke to dismiss a dialog. + * @return The user ID of the newly-created guest user. + */ + private suspend fun create( + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + ): Int { + return withContext(mainDispatcher) { + showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + val guestUserId = createInBackground() + dismissDialog() + if (guestUserId != UserHandle.USER_NULL) { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD) + } else { + Toast.makeText( + applicationContext, + com.android.settingslib.R.string.add_guest_failed, + Toast.LENGTH_SHORT, + ) + .show() + } + + guestUserId + } + } + + /** Schedules the creation of the guest user. */ + private suspend fun scheduleCreation() { + if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) { + return + } + + withContext(backgroundDispatcher) { + val newGuestUserId = createInBackground() + repository.isGuestUserCreationScheduled.set(false) + repository.isGuestUserResetting = false + if (newGuestUserId == UserHandle.USER_NULL) { + Log.w(TAG, "Could not create new guest while exiting existing guest") + // Refresh users so that we still display "Guest" if + // config_guestUserAutoCreated=true + refreshUsersScheduler.refreshIfNotPaused() + } + } + } + + /** + * Creates a guest user and return its multi-user user ID. + * + * This method does not check if a guest already exists before it makes a call to [UserManager] + * to create a new one. + * + * @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if + * the guest couldn't be created. + */ + @UserIdInt + private suspend fun createInBackground(): Int { + return withContext(backgroundDispatcher) { + try { + val guestUser = manager.createGuest(applicationContext) + if (guestUser != null) { + guestUser.id + } else { + Log.e( + TAG, + "Couldn't create guest, most likely because there already exists one!" + ) + UserHandle.USER_NULL + } + } catch (e: UserManager.UserOperationException) { + Log.e(TAG, "Couldn't create guest user!", e) + UserHandle.USER_NULL + } + } + } + + private fun isDeviceAllowedToAddGuest(): Boolean { + return deviceProvisionedController.isDeviceProvisioned && + !devicePolicyManager.isDeviceManaged + } + + companion object { + private const val TAG = "GuestUserInteractor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt new file mode 100644 index 000000000000..8f36821a955e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** Encapsulates logic for pausing, unpausing, and scheduling a delayed job. */ +@SysUISingleton +class RefreshUsersScheduler +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + private val repository: UserRepository, +) { + private var scheduledUnpauseJob: Job? = null + private var isPaused = false + + fun pause() { + applicationScope.launch(mainDispatcher) { + isPaused = true + scheduledUnpauseJob?.cancel() + scheduledUnpauseJob = + applicationScope.launch { + delay(PAUSE_REFRESH_USERS_TIMEOUT_MS) + unpauseAndRefresh() + } + } + } + + fun unpauseAndRefresh() { + applicationScope.launch(mainDispatcher) { + isPaused = false + refreshIfNotPaused() + } + } + + fun refreshIfNotPaused() { + applicationScope.launch(mainDispatcher) { + if (isPaused) { + return@launch + } + + repository.refreshUsers() + } + } + + companion object { + private const val PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt new file mode 100644 index 000000000000..dc004f3603a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.os.UserHandle +import android.os.UserManager +import com.android.systemui.user.data.repository.UserRepository + +/** Utilities related to user management actions. */ +object UserActionsUtil { + + /** Returns `true` if it's possible to add a guest user to the device; `false` otherwise. */ + fun canCreateGuest( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + if (!isUserSwitcherEnabled) { + return false + } + + return currentUserCanCreateUsers(manager, repository) || + anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled) + } + + /** Returns `true` if it's possible to add a user to the device; `false` otherwise. */ + fun canCreateUser( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + if (!isUserSwitcherEnabled) { + return false + } + + if ( + !currentUserCanCreateUsers(manager, repository) && + !anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled) + ) { + return false + } + + return manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY) + } + + /** + * Returns `true` if it's possible to add a supervised user to the device; `false` otherwise. + */ + fun canCreateSupervisedUser( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + supervisedUserPackageName: String? + ): Boolean { + if (supervisedUserPackageName.isNullOrEmpty()) { + return false + } + + return canCreateUser( + manager, + repository, + isUserSwitcherEnabled, + isAddUsersFromLockScreenEnabled + ) + } + + fun canManageUsers( + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + return isUserSwitcherEnabled && + (repository.getSelectedUserInfo().isAdmin || isAddUsersFromLockScreenEnabled) + } + + /** + * Returns `true` if the current user is allowed to add users to the device; `false` otherwise. + */ + private fun currentUserCanCreateUsers( + manager: UserManager, + repository: UserRepository, + ): Boolean { + val currentUser = repository.getSelectedUserInfo() + if (!currentUser.isAdmin && currentUser.id != UserHandle.USER_SYSTEM) { + return false + } + + return systemCanCreateUsers(manager) + } + + /** Returns `true` if the system can add users to the device; `false` otherwise. */ + private fun systemCanCreateUsers( + manager: UserManager, + ): Boolean { + return !manager.hasBaseUserRestriction(UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM) + } + + /** Returns `true` if it's allowed to add users to the device at all; `false` otherwise. */ + private fun anyoneCanCreateUsers( + manager: UserManager, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + return systemCanCreateUsers(manager) && isAddUsersFromLockScreenEnabled + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index 3c5b9697c013..dda78aad54c6 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -17,94 +17,747 @@ package com.android.systemui.user.domain.interactor +import android.annotation.SuppressLint +import android.annotation.UserIdInt +import android.app.ActivityManager +import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.content.pm.UserInfo +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.RemoteException +import android.os.UserHandle +import android.os.UserManager import android.provider.Settings +import android.util.Log +import com.android.internal.util.UserIcons +import com.android.systemui.R +import com.android.systemui.SystemUISecondaryUserService +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.kotlin.pairwise +import java.io.PrintWriter import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext /** Encapsulates business logic to interact with user data and systems. */ @SysUISingleton class UserInteractor @Inject constructor( - repository: UserRepository, + @Application private val applicationContext: Context, + private val repository: UserRepository, private val controller: UserSwitcherController, private val activityStarter: ActivityStarter, - keyguardInteractor: KeyguardInteractor, + private val keyguardInteractor: KeyguardInteractor, + private val featureFlags: FeatureFlags, + private val manager: UserManager, + @Application private val applicationScope: CoroutineScope, + telephonyInteractor: TelephonyInteractor, + broadcastDispatcher: BroadcastDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val activityManager: ActivityManager, + private val refreshUsersScheduler: RefreshUsersScheduler, + private val guestUserInteractor: GuestUserInteractor, ) { + /** + * Defines interface for classes that can be notified when the state of users on the device is + * changed. + */ + interface UserCallback { + /** Returns `true` if this callback can be cleaned-up. */ + fun isEvictable(): Boolean = false + + /** Notifies that the state of users on the device has changed. */ + fun onUserStateChanged() + } + + private val isNewImpl: Boolean + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) + + private val supervisedUserPackageName: String? + get() = + applicationContext.getString( + com.android.internal.R.string.config_supervisedUserCreationPackage + ) + + private val callbackMutex = Mutex() + private val callbacks = mutableSetOf<UserCallback>() + /** List of current on-device users to select from. */ - val users: Flow<List<UserModel>> = repository.users + val users: Flow<List<UserModel>> + get() = + if (isNewImpl) { + combine( + repository.userInfos, + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, settings -> + toUserModels( + userInfos = userInfos, + selectedUserId = selectedUserInfo.id, + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) + } + } else { + repository.users + } /** The currently-selected user. */ - val selectedUser: Flow<UserModel> = repository.selectedUser + val selectedUser: Flow<UserModel> + get() = + if (isNewImpl) { + combine( + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { selectedUserInfo, settings -> + val selectedUserId = selectedUserInfo.id + checkNotNull( + toUserModel( + userInfo = selectedUserInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId), + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) + ) + } + } else { + repository.selectedUser + } /** List of user-switcher related actions that are available. */ - val actions: Flow<List<UserActionModel>> = - combine( - repository.isActionableWhenLocked, - keyguardInteractor.isKeyguardShowing, - ) { isActionableWhenLocked, isLocked -> - isActionableWhenLocked || !isLocked - } - .flatMapLatest { isActionable -> - if (isActionable) { - repository.actions.map { actions -> - actions + - if (actions.isNotEmpty()) { - // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because - // that's a user - // switcher specific action that is not known to the our data source - // or other - // features. - listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - } else { - // If no actions, don't add the navigate action. - emptyList() + val actions: Flow<List<UserActionModel>> + get() = + if (isNewImpl) { + combine( + repository.selectedUserInfo, + repository.userInfos, + repository.userSwitcherSettings, + keyguardInteractor.isKeyguardShowing, + ) { _, userInfos, settings, isDeviceLocked -> + buildList { + val hasGuestUser = userInfos.any { it.isGuest } + if ( + !hasGuestUser && + (guestUserInteractor.isGuestUserAutoCreated || + UserActionsUtil.canCreateGuest( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + )) + ) { + add(UserActionModel.ENTER_GUEST_MODE) + } + + if (!isDeviceLocked || settings.isAddUsersFromLockscreen) { + // The device is locked and our setting to allow actions that add users + // from the lock-screen is not enabled. The guest action from above is + // always allowed, even when the device is locked, but the various "add + // user" actions below are not. We can finish building the list here. + + val canCreateUsers = + UserActionsUtil.canCreateUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + + if (canCreateUsers) { + add(UserActionModel.ADD_USER) + } + + if ( + UserActionsUtil.canCreateSupervisedUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + supervisedUserPackageName, + ) + ) { + add(UserActionModel.ADD_SUPERVISED_USER) } + } + + if ( + UserActionsUtil.canManageUsers( + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + ) { + add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + } } - } else { - // If not actionable it means that we're not allowed to show actions when locked - // and we - // are locked. Therefore, we should show no actions. - flowOf(emptyList()) } + } else { + combine( + repository.isActionableWhenLocked, + keyguardInteractor.isKeyguardShowing, + ) { isActionableWhenLocked, isLocked -> + isActionableWhenLocked || !isLocked + } + .flatMapLatest { isActionable -> + if (isActionable) { + repository.actions + } else { + // If not actionable it means that we're not allowed to show actions + // when + // locked and we are locked. Therefore, we should show no actions. + flowOf(emptyList()) + } + } } + val userRecords: StateFlow<ArrayList<UserRecord>> = + if (isNewImpl) { + combine( + repository.userInfos, + repository.selectedUserInfo, + actions, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, actionModels, settings -> + ArrayList( + userInfos.map { + toRecord( + userInfo = it, + selectedUserId = selectedUserInfo.id, + ) + } + + actionModels.map { + toRecord( + action = it, + selectedUserId = selectedUserInfo.id, + isRestricted = + it != UserActionModel.ENTER_GUEST_MODE && + it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT && + !settings.isAddUsersFromLockscreen, + ) + } + ) + } + .onEach { notifyCallbacks() } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = ArrayList(), + ) + } else { + MutableStateFlow(ArrayList()) + } + + val selectedUserRecord: StateFlow<UserRecord?> = + if (isNewImpl) { + repository.selectedUserInfo + .map { selectedUserInfo -> + toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + } else { + MutableStateFlow(null) + } + /** Whether the device is configured to always have a guest user available. */ - val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated + val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated /** Whether the guest user is currently being reset. */ - val isGuestUserResetting: Boolean = repository.isGuestUserResetting + val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting + + private val _dialogShowRequests = MutableStateFlow<ShowDialogRequestModel?>(null) + val dialogShowRequests: Flow<ShowDialogRequestModel?> = _dialogShowRequests.asStateFlow() + + private val _dialogDismissRequests = MutableStateFlow<Unit?>(null) + val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow() + + val isSimpleUserSwitcher: Boolean + get() = + if (isNewImpl) { + repository.isSimpleUserSwitcher() + } else { + error("Not supported in the old implementation!") + } + + init { + if (isNewImpl) { + refreshUsersScheduler.refreshIfNotPaused() + telephonyInteractor.callState + .distinctUntilChanged() + .onEach { refreshUsersScheduler.refreshIfNotPaused() } + .launchIn(applicationScope) + + combine( + broadcastDispatcher.broadcastFlow( + filter = + IntentFilter().apply { + addAction(Intent.ACTION_USER_ADDED) + addAction(Intent.ACTION_USER_REMOVED) + addAction(Intent.ACTION_USER_INFO_CHANGED) + addAction(Intent.ACTION_USER_SWITCHED) + addAction(Intent.ACTION_USER_STOPPED) + addAction(Intent.ACTION_USER_UNLOCKED) + }, + user = UserHandle.SYSTEM, + map = { intent, _ -> intent }, + ), + repository.selectedUserInfo.pairwise(null), + ) { intent, selectedUserChange -> + Pair(intent, selectedUserChange.previousValue) + } + .onEach { (intent, previousSelectedUser) -> + onBroadcastReceived(intent, previousSelectedUser) + } + .launchIn(applicationScope) + } + } + + fun addCallback(callback: UserCallback) { + applicationScope.launch { callbackMutex.withLock { callbacks.add(callback) } } + } + + fun removeCallback(callback: UserCallback) { + applicationScope.launch { callbackMutex.withLock { callbacks.remove(callback) } } + } + + fun refreshUsers() { + refreshUsersScheduler.refreshIfNotPaused() + } + + fun onDialogShown() { + _dialogShowRequests.value = null + } + + fun onDialogDismissed() { + _dialogDismissRequests.value = null + } + + fun dump(pw: PrintWriter) { + pw.println("UserInteractor state:") + pw.println(" lastSelectedNonGuestUserId=${repository.lastSelectedNonGuestUserId}") + + val users = userRecords.value.filter { it.info != null } + pw.println(" userCount=${userRecords.value.count { LegacyUserDataHelper.isUser(it) }}") + for (i in users.indices) { + pw.println(" ${users[i]}") + } + + val actions = userRecords.value.filter { it.info == null } + pw.println(" actionCount=${userRecords.value.count { !LegacyUserDataHelper.isUser(it) }}") + for (i in actions.indices) { + pw.println(" ${actions[i]}") + } + + pw.println("isSimpleUserSwitcher=$isSimpleUserSwitcher") + pw.println("isGuestUserAutoCreated=$isGuestUserAutoCreated") + } + + fun onDeviceBootCompleted() { + guestUserInteractor.onDeviceBootCompleted() + } + + /** Switches to the user or executes the action represented by the given record. */ + fun onRecordSelected( + record: UserRecord, + dialogShower: UserSwitchDialogController.DialogShower? = null, + ) { + if (LegacyUserDataHelper.isUser(record)) { + // It's safe to use checkNotNull around record.info because isUser only returns true + // if record.info is not null. + selectUser(checkNotNull(record.info).id, dialogShower) + } else { + executeAction(LegacyUserDataHelper.toUserActionModel(record), dialogShower) + } + } /** Switches to the user with the given user ID. */ fun selectUser( - userId: Int, + newlySelectedUserId: Int, + dialogShower: UserSwitchDialogController.DialogShower? = null, ) { - controller.onUserSelected(userId, /* dialogShower= */ null) + if (isNewImpl) { + val currentlySelectedUserInfo = repository.getSelectedUserInfo() + if ( + newlySelectedUserId == currentlySelectedUserInfo.id && + currentlySelectedUserInfo.isGuest + ) { + // Here when clicking on the currently-selected guest user to leave guest mode + // and return to the previously-selected non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = repository.lastSelectedNonGuestUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + dialogShower = dialogShower, + ) + ) + return + } + + if (currentlySelectedUserInfo.isGuest) { + // Here when switching from guest to a non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = newlySelectedUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + dialogShower = dialogShower, + ) + ) + return + } + + dialogShower?.dismiss() + + switchUser(newlySelectedUserId) + } else { + controller.onUserSelected(newlySelectedUserId, dialogShower) + } } /** Executes the given action. */ - fun executeAction(action: UserActionModel) { - when (action) { - UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) - UserActionModel.ADD_USER -> controller.showAddUserDialog(null) - UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> - activityStarter.startActivity( - Intent(Settings.ACTION_USER_SETTINGS), - /* dismissShade= */ false, + fun executeAction( + action: UserActionModel, + dialogShower: UserSwitchDialogController.DialogShower? = null, + ) { + if (isNewImpl) { + when (action) { + UserActionModel.ENTER_GUEST_MODE -> + guestUserInteractor.createAndSwitchTo( + this::showDialog, + this::dismissDialog, + ) { userId -> + selectUser(userId, dialogShower) + } + UserActionModel.ADD_USER -> { + val currentUser = repository.getSelectedUserInfo() + showDialog( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = currentUser.userHandle, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, + dialogShower = dialogShower, + ) + ) + } + UserActionModel.ADD_SUPERVISED_USER -> + activityStarter.startActivity( + Intent() + .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) + .setPackage(supervisedUserPackageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + /* dismissShade= */ true, + ) + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ true, + ) + } + } else { + when (action) { + UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) + UserActionModel.ADD_USER -> controller.showAddUserDialog(null) + UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ false, + ) + } + } + } + + fun exitGuestUser( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + forceRemoveGuestOnExit: Boolean, + ) { + guestUserInteractor.exit( + guestUserId = guestUserId, + targetUserId = targetUserId, + forceRemoveGuestOnExit = forceRemoveGuestOnExit, + showDialog = this::showDialog, + dismissDialog = this::dismissDialog, + switchUser = this::switchUser, + ) + } + + fun removeGuestUser( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + ) { + applicationScope.launch { + guestUserInteractor.remove( + guestUserId = guestUserId, + targetUserId = targetUserId, + ::showDialog, + ::dismissDialog, + ::selectUser, + ) + } + } + + private fun showDialog(request: ShowDialogRequestModel) { + _dialogShowRequests.value = request + } + + private fun dismissDialog() { + _dialogDismissRequests.value = Unit + } + + private fun notifyCallbacks() { + applicationScope.launch { + callbackMutex.withLock { + val iterator = callbacks.iterator() + while (iterator.hasNext()) { + val callback = iterator.next() + if (!callback.isEvictable()) { + callback.onUserStateChanged() + } else { + iterator.remove() + } + } + } + } + } + + private suspend fun toRecord( + userInfo: UserInfo, + selectedUserId: Int, + ): UserRecord { + return LegacyUserDataHelper.createRecord( + context = applicationContext, + manager = manager, + userInfo = userInfo, + picture = null, + isCurrent = userInfo.id == selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId), + ) + } + + private suspend fun toRecord( + action: UserActionModel, + selectedUserId: Int, + isRestricted: Boolean, + ): UserRecord { + return LegacyUserDataHelper.createRecord( + context = applicationContext, + selectedUserId = selectedUserId, + actionType = action, + isRestricted = isRestricted, + isSwitchToEnabled = + canSwitchUsers(selectedUserId) && + // If the user is auto-created is must not be currently resetting. + !(isGuestUserAutoCreated && isGuestUserResetting), + ) + } + + private fun switchUser(userId: Int) { + // TODO(b/246631653): track jank and latency like in the old impl. + refreshUsersScheduler.pause() + try { + activityManager.switchUser(userId) + } catch (e: RemoteException) { + Log.e(TAG, "Couldn't switch user.", e) + } + } + + private suspend fun onBroadcastReceived( + intent: Intent, + previousUserInfo: UserInfo?, + ) { + val shouldRefreshAllUsers = + when (intent.action) { + Intent.ACTION_USER_SWITCHED -> { + dismissDialog() + val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) + if (previousUserInfo?.id != selectedUserId) { + notifyCallbacks() + restartSecondaryService(selectedUserId) + } + if (guestUserInteractor.isGuestUserAutoCreated) { + guestUserInteractor.guaranteePresent() + } + true + } + Intent.ACTION_USER_INFO_CHANGED -> true + Intent.ACTION_USER_UNLOCKED -> { + // If we unlocked the system user, we should refresh all users. + intent.getIntExtra( + Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL, + ) == UserHandle.USER_SYSTEM + } + else -> true + } + + if (shouldRefreshAllUsers) { + refreshUsersScheduler.unpauseAndRefresh() + } + } + + private fun restartSecondaryService(@UserIdInt userId: Int) { + val intent = Intent(applicationContext, SystemUISecondaryUserService::class.java) + // Disconnect from the old secondary user's service + val secondaryUserId = repository.secondaryUserId + if (secondaryUserId != UserHandle.USER_NULL) { + applicationContext.stopServiceAsUser( + intent, + UserHandle.of(secondaryUserId), + ) + repository.secondaryUserId = UserHandle.USER_NULL + } + + // Connect to the new secondary user's service (purely to ensure that a persistent + // SystemUI application is created for that user) + if (userId != UserHandle.USER_SYSTEM) { + applicationContext.startServiceAsUser( + intent, + UserHandle.of(userId), + ) + repository.secondaryUserId = userId + } + } + + private suspend fun toUserModels( + userInfos: List<UserInfo>, + selectedUserId: Int, + isUserSwitcherEnabled: Boolean, + ): List<UserModel> { + val canSwitchUsers = canSwitchUsers(selectedUserId) + + return userInfos + // The guest user should go in the last position. + .sortedBy { it.isGuest } + .mapNotNull { userInfo -> + toUserModel( + userInfo = userInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers, + isUserSwitcherEnabled = isUserSwitcherEnabled, + ) + } + } + + private suspend fun toUserModel( + userInfo: UserInfo, + selectedUserId: Int, + canSwitchUsers: Boolean, + isUserSwitcherEnabled: Boolean, + ): UserModel? { + val userId = userInfo.id + val isSelected = userId == selectedUserId + + return when { + // When the user switcher is not enabled in settings, we only show the primary user. + !isUserSwitcherEnabled && !userInfo.isPrimary -> null + + // We avoid showing disabled users. + !userInfo.isEnabled -> null + userInfo.isGuest -> + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = true, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers, + isGuest = true, ) + userInfo.supportsSwitchToByUser() -> + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = false, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers || isSelected, + isGuest = false, + ) + else -> null } } + + private suspend fun canSwitchUsers(selectedUserId: Int): Boolean { + return withContext(backgroundDispatcher) { + manager.getUserSwitchability(UserHandle.of(selectedUserId)) + } == UserManager.SWITCHABILITY_STATUS_OK + } + + @SuppressLint("UseCompatLoadingForDrawables") + private suspend fun getUserImage( + isGuest: Boolean, + userId: Int, + ): Drawable { + if (isGuest) { + return checkNotNull(applicationContext.getDrawable(R.drawable.ic_account_circle)) + } + + // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them. + // TODO(b/246631653): downscale the bitmaps to R.dimen.max_avatar_size if requested. + val userIcon = withContext(backgroundDispatcher) { manager.getUserIcon(userId) } + if (userIcon != null) { + return BitmapDrawable(userIcon) + } + + return UserIcons.getDefaultUserIcon( + applicationContext.resources, + userId, + /* light= */ false + ) + } + + companion object { + private const val TAG = "UserInteractor" + } } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt new file mode 100644 index 000000000000..177356e6b573 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.model + +import android.os.UserHandle +import com.android.systemui.qs.user.UserSwitchDialogController + +/** Encapsulates a request to show a dialog. */ +sealed class ShowDialogRequestModel( + open val dialogShower: UserSwitchDialogController.DialogShower? = null, +) { + data class ShowAddUserDialog( + val userHandle: UserHandle, + val isKeyguardShowing: Boolean, + val showEphemeralMessage: Boolean, + override val dialogShower: UserSwitchDialogController.DialogShower?, + ) : ShowDialogRequestModel(dialogShower) + + data class ShowUserCreationDialog( + val isGuest: Boolean, + ) : ShowDialogRequestModel() + + data class ShowExitGuestDialog( + val guestUserId: Int, + val targetUserId: Int, + val isGuestEphemeral: Boolean, + val isKeyguardShowing: Boolean, + val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit, + override val dialogShower: UserSwitchDialogController.DialogShower?, + ) : ShowDialogRequestModel(dialogShower) +} diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt new file mode 100644 index 000000000000..03a7470a3fe6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.legacyhelper.data + +import android.content.Context +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.os.UserManager +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin +import com.android.settingslib.RestrictedLockUtilsInternal +import com.android.systemui.R +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.shared.model.UserActionModel + +/** + * Defines utility functions for helping with legacy data code for users. + * + * We need these to avoid code duplication between logic inside the UserSwitcherController and in + * modern architecture classes such as repositories, interactors, and view-models. If we ever + * simplify UserSwitcherController (or delete it), the code here could be moved into its call-sites. + */ +object LegacyUserDataHelper { + + @JvmStatic + fun createRecord( + context: Context, + manager: UserManager, + picture: Bitmap?, + userInfo: UserInfo, + isCurrent: Boolean, + canSwitchUsers: Boolean, + ): UserRecord { + val isGuest = userInfo.isGuest + return UserRecord( + info = userInfo, + picture = + getPicture( + manager = manager, + context = context, + userInfo = userInfo, + picture = picture, + ), + isGuest = isGuest, + isCurrent = isCurrent, + isSwitchToEnabled = canSwitchUsers || (isCurrent && !isGuest), + ) + } + + @JvmStatic + fun createRecord( + context: Context, + selectedUserId: Int, + actionType: UserActionModel, + isRestricted: Boolean, + isSwitchToEnabled: Boolean, + ): UserRecord { + return UserRecord( + isGuest = actionType == UserActionModel.ENTER_GUEST_MODE, + isAddUser = actionType == UserActionModel.ADD_USER, + isAddSupervisedUser = actionType == UserActionModel.ADD_SUPERVISED_USER, + isRestricted = isRestricted, + isSwitchToEnabled = isSwitchToEnabled, + enforcedAdmin = + getEnforcedAdmin( + context = context, + selectedUserId = selectedUserId, + ), + isManageUsers = actionType == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + } + + fun toUserActionModel(record: UserRecord): UserActionModel { + check(!isUser(record)) + + return when { + record.isAddUser -> UserActionModel.ADD_USER + record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER + record.isGuest -> UserActionModel.ENTER_GUEST_MODE + record.isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT + else -> error("Not a known action: $record") + } + } + + fun isUser(record: UserRecord): Boolean { + return record.info != null + } + + private fun getEnforcedAdmin( + context: Context, + selectedUserId: Int, + ): EnforcedAdmin? { + val admin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, + UserManager.DISALLOW_ADD_USER, + selectedUserId, + ) + ?: return null + + return if ( + !RestrictedLockUtilsInternal.hasBaseUserRestriction( + context, + UserManager.DISALLOW_ADD_USER, + selectedUserId, + ) + ) { + admin + } else { + null + } + } + + private fun getPicture( + context: Context, + manager: UserManager, + userInfo: UserInfo, + picture: Bitmap?, + ): Bitmap? { + if (userInfo.isGuest) { + return null + } + + if (picture != null) { + return picture + } + + val unscaledOrNull = manager.getUserIcon(userInfo.id) ?: return null + + val avatarSize = context.resources.getDimensionPixelSize(R.dimen.max_avatar_size) + return Bitmap.createScaledBitmap( + unscaledOrNull, + avatarSize, + avatarSize, + /* filter= */ true, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt index 15fdc352d864..e74232df3ac3 100644 --- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt @@ -22,7 +22,6 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import com.android.systemui.R import com.android.systemui.user.data.source.UserRecord -import kotlin.math.ceil /** * Defines utility functions for helping with legacy UI code for users. @@ -33,16 +32,6 @@ import kotlin.math.ceil */ object LegacyUserUiHelper { - /** Returns the maximum number of columns for user items in the user switcher. */ - fun getMaxUserSwitcherItemColumns(userCount: Int): Int { - // TODO(b/243844097): remove this once we remove the old user switcher implementation. - return if (userCount < 5) { - 4 - } else { - ceil(userCount / 2.0).toInt() - } - } - @JvmStatic @DrawableRes fun getUserSwitcherActionIconResourceId( @@ -50,6 +39,7 @@ object LegacyUserUiHelper { isGuest: Boolean, isAddSupervisedUser: Boolean, isTablet: Boolean = false, + isManageUsers: Boolean, ): Int { return if (isAddUser && isTablet) { R.drawable.ic_account_circle_filled @@ -59,6 +49,8 @@ object LegacyUserUiHelper { R.drawable.ic_account_circle } else if (isAddSupervisedUser) { R.drawable.ic_add_supervised_user + } else if (isManageUsers) { + R.drawable.ic_manage_users } else { R.drawable.ic_avatar_user } @@ -85,6 +77,7 @@ object LegacyUserUiHelper { isAddUser = record.isAddUser, isAddSupervisedUser = record.isAddSupervisedUser, isTablet = isTablet, + isManageUsers = record.isManageUsers, ) ) } @@ -114,8 +107,9 @@ object LegacyUserUiHelper { isAddUser: Boolean, isAddSupervisedUser: Boolean, isTablet: Boolean = false, + isManageUsers: Boolean, ): Int { - check(isGuest || isAddUser || isAddSupervisedUser) + check(isGuest || isAddUser || isAddSupervisedUser || isManageUsers) return when { isGuest && isGuestUserAutoCreated && isGuestUserResetting -> @@ -125,6 +119,7 @@ object LegacyUserUiHelper { isGuest -> com.android.internal.R.string.guest_name isAddUser -> com.android.settingslib.R.string.user_add_user isAddSupervisedUser -> R.string.add_user_supervised + isManageUsers -> R.string.manage_users else -> error("This should never happen!") } } diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt index bf7977a600e9..2095683ccb4c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt @@ -32,4 +32,6 @@ data class UserModel( val isSelected: Boolean, /** Whether this use is selectable. A non-selectable user cannot be switched to. */ val isSelectable: Boolean, + /** Whether this model represents the guest user. */ + val isGuest: Boolean, ) diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt index 938417f9dbe3..968af59e6c45 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt @@ -18,12 +18,15 @@ package com.android.systemui.user.ui.binder import android.content.Context +import android.view.Gravity import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.LinearLayout.SHOW_DIVIDER_MIDDLE import android.widget.TextView import androidx.constraintlayout.helper.widget.Flow as FlowWidget import androidx.core.view.isVisible @@ -36,6 +39,7 @@ import com.android.systemui.R import com.android.systemui.classifier.FalsingCollector import com.android.systemui.user.UserSwitcherPopupMenu import com.android.systemui.user.UserSwitcherRootView +import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.ui.viewmodel.UserActionViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import com.android.systemui.util.children @@ -168,15 +172,10 @@ object UserSwitcherViewBinder { onDismissed: () -> Unit, ): UserSwitcherPopupMenu { return UserSwitcherPopupMenu(context).apply { + this.setDropDownGravity(Gravity.END) this.anchorView = anchorView setAdapter(adapter) setOnDismissListener { onDismissed() } - setOnItemClickListener { _, _, position, _ -> - val itemPositionExcludingHeader = position - 1 - adapter.getItem(itemPositionExcludingHeader).onClicked() - dismiss() - } - show() } } @@ -186,38 +185,67 @@ object UserSwitcherViewBinder { private val layoutInflater: LayoutInflater, ) : BaseAdapter() { - private val items = mutableListOf<UserActionViewModel>() + private var sections = listOf<List<UserActionViewModel>>() override fun getCount(): Int { - return items.size + return sections.size } - override fun getItem(position: Int): UserActionViewModel { - return items[position] + override fun getItem(position: Int): List<UserActionViewModel> { + return sections[position] } override fun getItemId(position: Int): Long { - return getItem(position).viewKey + return position.toLong() } override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = - convertView - ?: layoutInflater.inflate( + val section = getItem(position) + val context = parent.context + val sectionView = + convertView as? LinearLayout + ?: LinearLayout(context, null).apply { + this.orientation = LinearLayout.VERTICAL + this.background = + parent.resources.getDrawable( + R.drawable.bouncer_user_switcher_popup_bg, + context.theme + ) + this.showDividers = SHOW_DIVIDER_MIDDLE + this.dividerDrawable = + context.getDrawable( + R.drawable.fullscreen_userswitcher_menu_item_divider + ) + } + sectionView.removeAllViewsInLayout() + + for (viewModel in section) { + val view = + layoutInflater.inflate( R.layout.user_switcher_fullscreen_popup_item, - parent, - false + /* parent= */ null ) - val viewModel = getItem(position) - view.requireViewById<ImageView>(R.id.icon).setImageResource(viewModel.iconResourceId) - view.requireViewById<TextView>(R.id.text).text = - view.resources.getString(viewModel.textResourceId) - return view + view + .requireViewById<ImageView>(R.id.icon) + .setImageResource(viewModel.iconResourceId) + view.requireViewById<TextView>(R.id.text).text = + view.resources.getString(viewModel.textResourceId) + view.setOnClickListener { viewModel.onClicked() } + sectionView.addView(view) + } + return sectionView } fun setItems(items: List<UserActionViewModel>) { - this.items.clear() - this.items.addAll(items) + val primarySection = + items.filter { + it.viewKey != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong() + } + val secondarySection = + items.filter { + it.viewKey == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong() + } + this.sections = listOf(primarySection, secondarySection) notifyDataSetChanged() } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt new file mode 100644 index 000000000000..a9d66de118e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.android.systemui.user.ui.dialog + +import android.app.ActivityManager +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.os.UserHandle +import com.android.settingslib.R +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.user.CreateUserActivity + +/** Dialog for adding a new user to the device. */ +class AddUserDialog( + context: Context, + userHandle: UserHandle, + isKeyguardShowing: Boolean, + showEphemeralMessage: Boolean, + private val falsingManager: FalsingManager, + private val broadcastSender: BroadcastSender, + private val dialogLaunchAnimator: DialogLaunchAnimator +) : SystemUIDialog(context) { + + private val onClickListener = + object : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface, which: Int) { + val penalty = + if (which == BUTTON_NEGATIVE) { + FalsingManager.NO_PENALTY + } else { + FalsingManager.MODERATE_PENALTY + } + if (falsingManager.isFalseTap(penalty)) { + return + } + + if (which == BUTTON_NEUTRAL) { + cancel() + return + } + + dialogLaunchAnimator.dismissStack(this@AddUserDialog) + if (ActivityManager.isUserAMonkey()) { + return + } + + // Use broadcast instead of ShadeController, as this dialog may have started in + // another + // process where normal dagger bindings are not available. + broadcastSender.sendBroadcastAsUser( + Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), + UserHandle.CURRENT + ) + + context.startActivityAsUser( + CreateUserActivity.createIntentForStart(context), + userHandle, + ) + } + } + + init { + setTitle(R.string.user_add_user_title) + val message = + context.getString(R.string.user_add_user_message_short) + + if (showEphemeralMessage) { + context.getString( + com.android.systemui.R.string.user_add_user_message_guest_remove + ) + } else { + "" + } + setMessage(message) + + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + + setButton( + BUTTON_POSITIVE, + context.getString(android.R.string.ok), + onClickListener, + ) + + setWindowOnTop(this, isKeyguardShowing) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt new file mode 100644 index 000000000000..19ad44d8649f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.android.systemui.user.ui.dialog + +import android.annotation.UserIdInt +import android.content.Context +import android.content.DialogInterface +import com.android.settingslib.R +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.phone.SystemUIDialog + +/** Dialog for exiting the guest user. */ +class ExitGuestDialog( + context: Context, + private val guestUserId: Int, + private val isGuestEphemeral: Boolean, + private val targetUserId: Int, + isKeyguardShowing: Boolean, + private val falsingManager: FalsingManager, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val onExitGuestUserListener: OnExitGuestUserListener, +) : SystemUIDialog(context) { + + fun interface OnExitGuestUserListener { + fun onExitGuestUser( + @UserIdInt guestId: Int, + @UserIdInt targetId: Int, + forceRemoveGuest: Boolean, + ) + } + + private val onClickListener = + object : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface, which: Int) { + val penalty = + if (which == BUTTON_NEGATIVE) { + FalsingManager.NO_PENALTY + } else { + FalsingManager.MODERATE_PENALTY + } + if (falsingManager.isFalseTap(penalty)) { + return + } + + if (isGuestEphemeral) { + if (which == BUTTON_POSITIVE) { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Ephemeral guest: exit guest, guest is removed by the system + // on exit, since its marked ephemeral + onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, false) + } else if (which == BUTTON_NEGATIVE) { + // Cancel clicked, do nothing + cancel() + } + } else { + when (which) { + BUTTON_POSITIVE -> { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Non-ephemeral guest: exit guest, guest is not removed by the system + // on exit, since its marked non-ephemeral + onExitGuestUserListener.onExitGuestUser( + guestUserId, + targetUserId, + false + ) + } + BUTTON_NEGATIVE -> { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Non-ephemeral guest: remove guest and then exit + onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, true) + } + BUTTON_NEUTRAL -> { + // Cancel clicked, do nothing + cancel() + } + } + } + } + } + + init { + if (isGuestEphemeral) { + setTitle(context.getString(R.string.guest_exit_dialog_title)) + setMessage(context.getString(R.string.guest_exit_dialog_message)) + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + setButton( + BUTTON_POSITIVE, + context.getString(R.string.guest_exit_dialog_button), + onClickListener, + ) + } else { + setTitle(context.getString(R.string.guest_exit_dialog_title_non_ephemeral)) + setMessage(context.getString(R.string.guest_exit_dialog_message_non_ephemeral)) + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + setButton( + BUTTON_NEGATIVE, + context.getString(R.string.guest_exit_clear_data_button), + onClickListener, + ) + setButton( + BUTTON_POSITIVE, + context.getString(R.string.guest_exit_save_data_button), + onClickListener, + ) + } + setWindowOnTop(this, isKeyguardShowing) + setCanceledOnTouchOutside(false) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt new file mode 100644 index 000000000000..c1d2f4788147 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.ui.dialog + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface UserDialogModule { + + @Binds + @IntoMap + @ClassKey(UserSwitcherDialogCoordinator::class) + fun bindFeature(impl: UserSwitcherDialogCoordinator): CoreStartable +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt new file mode 100644 index 000000000000..e9217209530b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.ui.dialog + +import android.app.Dialog +import android.content.Context +import com.android.internal.jank.InteractionJankMonitor +import com.android.settingslib.users.UserCreatingDialog +import com.android.systemui.CoreStartable +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import dagger.Lazy +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +/** Coordinates dialogs for user switcher logic. */ +@SysUISingleton +class UserSwitcherDialogCoordinator +@Inject +constructor( + @Application private val context: Lazy<Context>, + @Application private val applicationScope: Lazy<CoroutineScope>, + private val falsingManager: Lazy<FalsingManager>, + private val broadcastSender: Lazy<BroadcastSender>, + private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>, + private val interactor: Lazy<UserInteractor>, + private val featureFlags: Lazy<FeatureFlags>, +) : CoreStartable { + + private var currentDialog: Dialog? = null + + override fun start() { + if (featureFlags.get().isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) { + return + } + + startHandlingDialogShowRequests() + startHandlingDialogDismissRequests() + } + + private fun startHandlingDialogShowRequests() { + applicationScope.get().launch { + interactor.get().dialogShowRequests.filterNotNull().collect { request -> + currentDialog?.let { + if (it.isShowing) { + it.cancel() + } + } + + val (dialog, dialogCuj) = + when (request) { + is ShowDialogRequestModel.ShowAddUserDialog -> + Pair( + AddUserDialog( + context = context.get(), + userHandle = request.userHandle, + isKeyguardShowing = request.isKeyguardShowing, + showEphemeralMessage = request.showEphemeralMessage, + falsingManager = falsingManager.get(), + broadcastSender = broadcastSender.get(), + dialogLaunchAnimator = dialogLaunchAnimator.get(), + ), + DialogCuj( + InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, + INTERACTION_JANK_ADD_NEW_USER_TAG, + ), + ) + is ShowDialogRequestModel.ShowUserCreationDialog -> + Pair( + UserCreatingDialog( + context.get(), + request.isGuest, + ), + null, + ) + is ShowDialogRequestModel.ShowExitGuestDialog -> + Pair( + ExitGuestDialog( + context = context.get(), + guestUserId = request.guestUserId, + isGuestEphemeral = request.isGuestEphemeral, + targetUserId = request.targetUserId, + isKeyguardShowing = request.isKeyguardShowing, + falsingManager = falsingManager.get(), + dialogLaunchAnimator = dialogLaunchAnimator.get(), + onExitGuestUserListener = request.onExitGuestUser, + ), + DialogCuj( + InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, + INTERACTION_JANK_EXIT_GUEST_MODE_TAG, + ), + ) + } + currentDialog = dialog + + if (request.dialogShower != null && dialogCuj != null) { + request.dialogShower?.showDialog(dialog, dialogCuj) + } else { + dialog.show() + } + + interactor.get().onDialogShown() + } + } + } + + private fun startHandlingDialogDismissRequests() { + applicationScope.get().launch { + interactor.get().dialogDismissRequests.filterNotNull().collect { + currentDialog?.let { + if (it.isShowing) { + it.cancel() + } + } + + interactor.get().onDialogDismissed() + } + } + } + + companion object { + private const val INTERACTION_JANK_ADD_NEW_USER_TAG = "add_new_user" + private const val INTERACTION_JANK_EXIT_GUEST_MODE_TAG = "exit_guest_mode" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index 398341d256d2..d857e85bac53 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -19,14 +19,17 @@ package com.android.systemui.user.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.android.systemui.R import com.android.systemui.common.ui.drawable.CircularDrawable +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel import javax.inject.Inject +import kotlin.math.ceil import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine @@ -36,19 +39,20 @@ import kotlinx.coroutines.flow.map class UserSwitcherViewModel private constructor( private val userInteractor: UserInteractor, + private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, + private val featureFlags: FeatureFlags, ) : ViewModel() { + private val isNewImpl: Boolean + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) + /** On-device users. */ val users: Flow<List<UserViewModel>> = userInteractor.users.map { models -> models.map { user -> toViewModel(user) } } /** The maximum number of columns that the user selection grid should use. */ - val maximumUserColumns: Flow<Int> = - users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) } - - /** Whether the button to open the user action menu is visible. */ - val isOpenMenuButtonVisible: Flow<Boolean> = userInteractor.actions.map { it.isNotEmpty() } + val maximumUserColumns: Flow<Int> = users.map { getMaxUserSwitcherItemColumns(it.size) } private val _isMenuVisible = MutableStateFlow(false) /** @@ -60,7 +64,11 @@ private constructor( val menu: Flow<List<UserActionViewModel>> = userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } } + /** Whether the button to open the user action menu is visible. */ + val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() } + private val hasCancelButtonBeenClicked = MutableStateFlow(false) + private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false) /** * Whether the observer should finish the experience. Once consumed, [onFinished] must be called @@ -81,6 +89,7 @@ private constructor( */ fun onFinished() { hasCancelButtonBeenClicked.value = false + isFinishRequiredDueToExecutedAction.value = false } /** Notifies that the user has clicked the "open menu" button. */ @@ -98,6 +107,15 @@ private constructor( _isMenuVisible.value = false } + /** Returns the maximum number of columns for user items in the user switcher. */ + private fun getMaxUserSwitcherItemColumns(userCount: Int): Int { + return if (userCount < 5) { + 4 + } else { + ceil(userCount / 2.0).toInt() + } + } + private fun createFinishRequestedFlow(): Flow<Boolean> { var mostRecentSelectedUserId: Int? = null var mostRecentIsInteractive: Boolean? = null @@ -120,8 +138,10 @@ private constructor( }, // When the cancel button is clicked, we should finish. hasCancelButtonBeenClicked, - ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked -> - selectedUserChanged || screenTurnedOff || cancelButtonClicked + // If an executed action told us to finish, we should finish, + isFinishRequiredDueToExecutedAction, + ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish -> + selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish } } @@ -149,28 +169,36 @@ private constructor( return UserActionViewModel( viewKey = model.ordinal.toLong(), iconResourceId = - if (model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) { - R.drawable.ic_manage_users - } else { - LegacyUserUiHelper.getUserSwitcherActionIconResourceId( - isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, - isAddUser = model == UserActionModel.ADD_USER, - isGuest = model == UserActionModel.ENTER_GUEST_MODE, - ) - }, + LegacyUserUiHelper.getUserSwitcherActionIconResourceId( + isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, + isAddUser = model == UserActionModel.ADD_USER, + isGuest = model == UserActionModel.ENTER_GUEST_MODE, + isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + isTablet = true, + ), textResourceId = - if (model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) { - R.string.manage_users - } else { - LegacyUserUiHelper.getUserSwitcherActionTextResourceId( - isGuest = model == UserActionModel.ENTER_GUEST_MODE, - isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, - isGuestUserResetting = userInteractor.isGuestUserResetting, - isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, - isAddUser = model == UserActionModel.ADD_USER, - ) - }, - onClicked = { userInteractor.executeAction(action = model) }, + LegacyUserUiHelper.getUserSwitcherActionTextResourceId( + isGuest = model == UserActionModel.ENTER_GUEST_MODE, + isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated, + isGuestUserResetting = guestUserInteractor.isGuestUserResetting, + isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, + isAddUser = model == UserActionModel.ADD_USER, + isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + isTablet = true, + ), + onClicked = { + userInteractor.executeAction(action = model) + // We don't finish because we want to show a dialog over the full-screen UI and + // that dialog can be dismissed in case the user changes their mind and decides not + // to add a user. + // + // We finish for all other actions because they navigate us away from the + // full-screen experience or are destructive (like changing to the guest user). + val shouldFinish = model != UserActionModel.ADD_USER + if (shouldFinish) { + isFinishRequiredDueToExecutedAction.value = true + } + }, ) } @@ -186,13 +214,17 @@ private constructor( @Inject constructor( private val userInteractor: UserInteractor, + private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, + private val featureFlags: FeatureFlags, ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return UserSwitcherViewModel( userInteractor = userInteractor, + guestUserInteractor = guestUserInteractor, powerInteractor = powerInteractor, + featureFlags = featureFlags, ) as T } diff --git a/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java b/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java index 5f7d74542fff..a925e384d3be 100644 --- a/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java +++ b/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java @@ -67,6 +67,8 @@ public class CarrierConfigTracker private boolean mDefaultCarrierProvisionsWifiMergedNetworks; private boolean mDefaultShowOperatorNameConfigLoaded; private boolean mDefaultShowOperatorNameConfig; + private boolean mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded; + private boolean mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig; @Inject public CarrierConfigTracker( @@ -207,6 +209,22 @@ public class CarrierConfigTracker } /** + * Returns KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN value for + * the default carrier config. + */ + public boolean getAlwaysShowPrimarySignalBarInOpportunisticNetworkDefault() { + if (!mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded) { + mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig = CarrierConfigManager + .getDefaultConfig().getBoolean(CarrierConfigManager + .KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN + ); + mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded = true; + } + + return mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig; + } + + /** * Returns the KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL value for the given subId, or the * default value if no override exists * diff --git a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java index 53da213eb38e..2efeda932ff3 100644 --- a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java +++ b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java @@ -32,7 +32,8 @@ import java.util.Arrays; import javax.inject.Inject; // NOT Singleton. Started per-user. -public class NotificationChannels extends CoreStartable { +/** */ +public class NotificationChannels implements CoreStartable { public static String ALERTS = "ALR"; public static String SCREENSHOTS_HEADSUP = "SCN_HEADSUP"; // Deprecated. Please use or create a more specific channel that users will better understand @@ -45,9 +46,11 @@ public class NotificationChannels extends CoreStartable { public static String INSTANT = "INS"; public static String SETUP = "STP"; + private final Context mContext; + @Inject public NotificationChannels(Context context) { - super(context); + mContext = context; } public static void createAll(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java index 8c736dc9fe70..81ae6e851fb9 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java @@ -25,6 +25,7 @@ import android.os.Process; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.BroadcastRunning; import com.android.systemui.dagger.qualifiers.LongRunning; import com.android.systemui.dagger.qualifiers.Main; @@ -51,6 +52,17 @@ public abstract class SysUIConcurrencyModule { return thread.getLooper(); } + /** BroadcastRunning Looper (for sending and receiving broadcasts) */ + @Provides + @SysUISingleton + @BroadcastRunning + public static Looper provideBroadcastRunningLooper() { + HandlerThread thread = new HandlerThread("BroadcastRunning", + Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + return thread.getLooper(); + } + /** Long running tasks Looper */ @Provides @SysUISingleton @@ -83,7 +95,17 @@ public abstract class SysUIConcurrencyModule { } /** - * Provide a Long running Executor by default. + * Provide a BroadcastRunning Executor (for sending and receiving broadcasts). + */ + @Provides + @SysUISingleton + @BroadcastRunning + public static Executor provideBroadcastRunningExecutor(@BroadcastRunning Looper looper) { + return new ExecutorImpl(looper); + } + + /** + * Provide a Long running Executor. */ @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java index db35437e77b9..2c317dd391c0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java +++ b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java @@ -34,9 +34,26 @@ public abstract class Condition implements CallbackController<Condition.Callback private final String mTag = getClass().getSimpleName(); private final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>(); - private boolean mIsConditionMet = false; + private final boolean mOverriding; + private Boolean mIsConditionMet; private boolean mStarted = false; - private boolean mOverriding = false; + + /** + * By default, conditions have an initial value of false and are not overriding. + */ + public Condition() { + this(false, false); + } + + /** + * Constructor for specifying initial state and overriding condition attribute. + * @param initialConditionMet Initial state of the condition. + * @param overriding Whether this condition overrides others. + */ + protected Condition(Boolean initialConditionMet, boolean overriding) { + mIsConditionMet = initialConditionMet; + mOverriding = overriding; + } /** * Starts monitoring the condition. @@ -49,14 +66,6 @@ public abstract class Condition implements CallbackController<Condition.Callback protected abstract void stop(); /** - * Sets whether this condition's value overrides others in determining the overall state. - */ - public void setOverriding(boolean overriding) { - mOverriding = overriding; - updateCondition(mIsConditionMet); - } - - /** * Returns whether the current condition overrides */ public boolean isOverridingCondition() { @@ -110,13 +119,31 @@ public abstract class Condition implements CallbackController<Condition.Callback * @param isConditionMet True if the condition has been fulfilled. False otherwise. */ protected void updateCondition(boolean isConditionMet) { - if (mIsConditionMet == isConditionMet) { + if (mIsConditionMet != null && mIsConditionMet == isConditionMet) { return; } if (shouldLog()) Log.d(mTag, "updating condition to " + isConditionMet); mIsConditionMet = isConditionMet; + sendUpdate(); + } + /** + * Clears the set condition value. This is purposefully separate from + * {@link #updateCondition(boolean)} to avoid confusion around {@code null} values. + */ + protected void clearCondition() { + if (mIsConditionMet == null) { + return; + } + + if (shouldLog()) Log.d(mTag, "clearing condition"); + + mIsConditionMet = null; + sendUpdate(); + } + + private void sendUpdate() { final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator(); while (iterator.hasNext()) { final Callback cb = iterator.next().get(); @@ -128,14 +155,31 @@ public abstract class Condition implements CallbackController<Condition.Callback } } + /** + * Returns whether the condition is set. This method should be consulted to understand the + * value of {@link #isConditionMet()}. + * @return {@code true} if value is present, {@code false} otherwise. + */ + public boolean isConditionSet() { + return mIsConditionMet != null; + } + + /** + * Returns whether the condition has been met. Note that this method will return {@code false} + * if the condition is not set as well. + */ public boolean isConditionMet() { - return mIsConditionMet; + return Boolean.TRUE.equals(mIsConditionMet); } - private boolean shouldLog() { + protected final boolean shouldLog() { return Log.isLoggable(mTag, Log.DEBUG); } + protected final String getTag() { + return mTag; + } + /** * Callback that receives updates about whether the condition has been fulfilled. */ diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java index dac8a0bff28a..cb430ba454f0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java +++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java @@ -57,12 +57,16 @@ public class Monitor { } public void update() { + // Only consider set conditions. + final Collection<Condition> setConditions = mSubscription.mConditions.stream() + .filter(Condition::isConditionSet).collect(Collectors.toSet()); + // Overriding conditions do not override each other - final Collection<Condition> overridingConditions = mSubscription.mConditions.stream() + final Collection<Condition> overridingConditions = setConditions.stream() .filter(Condition::isOverridingCondition).collect(Collectors.toSet()); final Collection<Condition> targetCollection = overridingConditions.isEmpty() - ? mSubscription.mConditions : overridingConditions; + ? setConditions : overridingConditions; final boolean newAllConditionsMet = targetCollection.isEmpty() ? true : targetCollection .stream() @@ -113,6 +117,7 @@ public class Monitor { final SubscriptionState state = new SubscriptionState(subscription); mExecutor.execute(() -> { + if (shouldLog()) Log.d(mTag, "adding subscription"); mSubscriptions.put(token, state); // Add and associate conditions. @@ -139,7 +144,7 @@ public class Monitor { */ public void removeSubscription(@NotNull Subscription.Token token) { mExecutor.execute(() -> { - if (shouldLog()) Log.d(mTag, "removing callback"); + if (shouldLog()) Log.d(mTag, "removing subscription"); if (!mSubscriptions.containsKey(token)) { Log.e(mTag, "subscription not present:" + token); return; diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt new file mode 100644 index 000000000000..9653985cb6e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.android.systemui.util.kotlin + +import android.view.View +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import java.util.function.Consumer +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect + +/** + * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to + * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners + * during onViewAttached() and removing during onViewRemoved() + */ +@JvmOverloads +fun <T> collectFlow( + view: View, + flow: Flow<T>, + consumer: Consumer<T>, + state: Lifecycle.State = Lifecycle.State.CREATED, +) { + view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java index 619e50b47f13..a0a0372426ec 100644 --- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java @@ -564,12 +564,13 @@ public class GarbageMonitor implements Dumpable { /** */ @SysUISingleton - public static class Service extends CoreStartable implements Dumpable { + public static class Service implements CoreStartable, Dumpable { + private final Context mContext; private final GarbageMonitor mGarbageMonitor; @Inject public Service(Context context, GarbageMonitor garbageMonitor) { - super(context); + mContext = context; mGarbageMonitor = garbageMonitor; } diff --git a/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto new file mode 100644 index 000000000000..b7166d96d401 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package com.android.systemui.util; + +option java_multiple_files = true; + +message ComponentNameProto { + string package_name = 1; + string class_name = 2; +} diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt new file mode 100644 index 000000000000..0b8257da8fb5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.util.settings + +import android.annotation.UserIdInt +import android.database.ContentObserver +import android.os.UserHandle +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Kotlin extension functions for [SettingsProxy]. */ +object SettingsProxyExt { + + /** Returns a flow of [Unit] that is invoked each time that content is updated. */ + fun SettingsProxy.observerFlow( + vararg names: String, + @UserIdInt userId: Int = UserHandle.USER_CURRENT, + ): Flow<Unit> { + return conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + names.forEach { name -> registerContentObserverForUser(name, observer, userId) } + + awaitClose { unregisterContentObserver(observer) } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/view/ViewUtil.kt b/packages/SystemUI/src/com/android/systemui/util/view/ViewUtil.kt index 613a797f020c..6160b00379ef 100644 --- a/packages/SystemUI/src/com/android/systemui/util/view/ViewUtil.kt +++ b/packages/SystemUI/src/com/android/systemui/util/view/ViewUtil.kt @@ -1,5 +1,22 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.android.systemui.util.view +import android.graphics.Rect import android.view.View import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject @@ -23,4 +40,22 @@ class ViewUtil @Inject constructor() { top <= y && y <= top + view.height } + + /** + * Sets [outRect] to be the view's location within its window. + */ + fun setRectToViewWindowLocation(view: View, outRect: Rect) { + val locInWindow = IntArray(2) + view.getLocationInWindow(locInWindow) + + val x = locInWindow[0] + val y = locInWindow[1] + + outRect.set( + x, + y, + x + view.width, + y + view.height, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index bf7c4598b5da..7c022eb41cd5 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -65,6 +65,7 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.qs.tiles.DndTile; @@ -178,7 +179,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa WakefulnessLifecycle wakefulnessLifecycle, CaptioningManager captioningManager, KeyguardManager keyguardManager, - ActivityManager activityManager + ActivityManager activityManager, + DumpManager dumpManager ) { mContext = context.getApplicationContext(); mPackageManager = packageManager; @@ -207,7 +209,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mCaptioningManager = captioningManager; mKeyguardManager = keyguardManager; mActivityManager = activityManager; - + dumpManager.registerDumpable("VolumeDialogControllerImpl", this); boolean accessibilityVolumeStreamActive = accessibilityManager .isAccessibilityVolumeStreamActive(); diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java index 87fb2a692682..0b3521b048c4 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUI.java @@ -31,18 +31,19 @@ import java.io.PrintWriter; import javax.inject.Inject; @SysUISingleton -public class VolumeUI extends CoreStartable { +public class VolumeUI implements CoreStartable { private static final String TAG = "VolumeUI"; private static boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); private final Handler mHandler = new Handler(); private boolean mEnabled; + private final Context mContext; private VolumeDialogComponent mVolumeComponent; @Inject public VolumeUI(Context context, VolumeDialogComponent volumeDialogComponent) { - super(context); + mContext = context; mVolumeComponent = volumeDialogComponent; } @@ -59,8 +60,7 @@ public class VolumeUI extends CoreStartable { } @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); + public void onConfigurationChanged(Configuration newConfig) { if (!mEnabled) return; mVolumeComponent.onConfigurationChanged(newConfig); } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index 0f7e14374e60..42d7d52a71ab 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -16,21 +16,17 @@ package com.android.systemui.wallpapers; -import static android.view.Display.DEFAULT_DISPLAY; - import static com.android.systemui.flags.Flags.USE_CANVAS_RENDERER; import android.app.WallpaperColors; import android.app.WallpaperManager; -import android.content.ComponentCallbacks2; -import android.content.Context; import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.graphics.RectF; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; -import android.os.AsyncTask; import android.os.Handler; import android.os.HandlerThread; import android.os.SystemClock; @@ -40,8 +36,6 @@ import android.util.ArraySet; import android.util.Log; import android.util.MathUtils; import android.util.Size; -import android.view.Display; -import android.view.DisplayInfo; import android.view.Surface; import android.view.SurfaceHolder; import android.view.WindowManager; @@ -49,8 +43,11 @@ import android.view.WindowManager; import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.wallpapers.canvas.ImageCanvasWallpaperRenderer; +import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.wallpapers.canvas.WallpaperColorExtractor; import com.android.systemui.wallpapers.gl.EglHelper; import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer; @@ -59,6 +56,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -78,15 +76,28 @@ public class ImageWallpaper extends WallpaperService { private final ArrayList<RectF> mLocalColorsToAdd = new ArrayList<>(); private final ArraySet<RectF> mColorAreas = new ArraySet<>(); private volatile int mPages = 1; + private boolean mPagesComputed = false; private HandlerThread mWorker; // scaled down version private Bitmap mMiniBitmap; private final FeatureFlags mFeatureFlags; + // used in canvasEngine to load/unload the bitmap and extract the colors + @Background + private final DelayableExecutor mBackgroundExecutor; + private static final int DELAY_UNLOAD_BITMAP = 2000; + + @Main + private final Executor mMainExecutor; + @Inject - public ImageWallpaper(FeatureFlags featureFlags) { + public ImageWallpaper(FeatureFlags featureFlags, + @Background DelayableExecutor backgroundExecutor, + @Main Executor mainExecutor) { super(); mFeatureFlags = featureFlags; + mBackgroundExecutor = backgroundExecutor; + mMainExecutor = mainExecutor; } @Override @@ -339,7 +350,6 @@ public class ImageWallpaper extends WallpaperService { imgArea.left = 0; imgArea.right = 1; } - return imgArea; } @@ -510,69 +520,85 @@ public class ImageWallpaper extends WallpaperService { class CanvasEngine extends WallpaperService.Engine implements DisplayListener { - - // time [ms] before unloading the wallpaper after it is loaded - private static final int DELAY_FORGET_WALLPAPER = 5000; - - private final Runnable mUnloadWallpaperCallback = this::unloadWallpaper; - private WallpaperManager mWallpaperManager; - private ImageCanvasWallpaperRenderer mImageCanvasWallpaperRenderer; + private final WallpaperColorExtractor mWallpaperColorExtractor; + private SurfaceHolder mSurfaceHolder; + @VisibleForTesting + static final int MIN_SURFACE_WIDTH = 128; + @VisibleForTesting + static final int MIN_SURFACE_HEIGHT = 128; private Bitmap mBitmap; + private boolean mWideColorGamut = false; - private Display mDisplay; - private final DisplayInfo mTmpDisplayInfo = new DisplayInfo(); - - private AsyncTask<Void, Void, Bitmap> mLoader; - private boolean mNeedsDrawAfterLoadingWallpaper = false; + /* + * Counter to unload the bitmap as soon as possible. + * Before any bitmap operation, this is incremented. + * After an operation completion, this is decremented (synchronously), + * and if the count is 0, unload the bitmap + */ + private int mBitmapUsages = 0; + private final Object mLock = new Object(); CanvasEngine() { super(); setFixedSizeAllowed(true); setShowForAllUsers(true); - } + mWallpaperColorExtractor = new WallpaperColorExtractor( + mBackgroundExecutor, + new WallpaperColorExtractor.WallpaperColorExtractorCallback() { + @Override + public void onColorsProcessed(List<RectF> regions, + List<WallpaperColors> colors) { + CanvasEngine.this.onColorsProcessed(regions, colors); + } - void trimMemory(int level) { - if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW - && level <= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL - && isBitmapLoaded()) { - if (DEBUG) { - Log.d(TAG, "trimMemory"); - } - unloadWallpaper(); + @Override + public void onMiniBitmapUpdated() { + CanvasEngine.this.onMiniBitmapUpdated(); + } + + @Override + public void onActivated() { + setOffsetNotificationsEnabled(true); + } + + @Override + public void onDeactivated() { + setOffsetNotificationsEnabled(false); + } + }); + + // if the number of pages is already computed, transmit it to the color extractor + if (mPagesComputed) { + mWallpaperColorExtractor.onPageChanged(mPages); } } @Override public void onCreate(SurfaceHolder surfaceHolder) { + Trace.beginSection("ImageWallpaper.CanvasEngine#onCreate"); if (DEBUG) { Log.d(TAG, "onCreate"); } + mWallpaperManager = getDisplayContext().getSystemService(WallpaperManager.class); + mSurfaceHolder = surfaceHolder; + Rect dimensions = mWallpaperManager.peekBitmapDimensions(); + int width = Math.max(MIN_SURFACE_WIDTH, dimensions.width()); + int height = Math.max(MIN_SURFACE_HEIGHT, dimensions.height()); + mSurfaceHolder.setFixedSize(width, height); - mWallpaperManager = getSystemService(WallpaperManager.class); - super.onCreate(surfaceHolder); - - final Context displayContext = getDisplayContext(); - final int displayId = displayContext == null ? DEFAULT_DISPLAY : - displayContext.getDisplayId(); - DisplayManager dm = getSystemService(DisplayManager.class); - if (dm != null) { - mDisplay = dm.getDisplay(displayId); - if (mDisplay == null) { - Log.e(TAG, "Cannot find display! Fallback to default."); - mDisplay = dm.getDisplay(DEFAULT_DISPLAY); - } - } - setOffsetNotificationsEnabled(false); - - mImageCanvasWallpaperRenderer = new ImageCanvasWallpaperRenderer(surfaceHolder); - loadWallpaper(false); + getDisplayContext().getSystemService(DisplayManager.class) + .registerDisplayListener(this, null); + getDisplaySizeAndUpdateColorExtractor(); + Trace.endSection(); } @Override public void onDestroy() { - super.onDestroy(); - unloadWallpaper(); + getDisplayContext().getSystemService(DisplayManager.class) + .unregisterDisplayListener(this); + mWallpaperColorExtractor.cleanUp(); + unloadBitmap(); } @Override @@ -581,31 +607,30 @@ public class ImageWallpaper extends WallpaperService { } @Override + public boolean shouldWaitForEngineShown() { + return true; + } + + @Override public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (DEBUG) { Log.d(TAG, "onSurfaceChanged: width=" + width + ", height=" + height); } - super.onSurfaceChanged(holder, format, width, height); - mImageCanvasWallpaperRenderer.setSurfaceHolder(holder); - drawFrame(false); } @Override public void onSurfaceDestroyed(SurfaceHolder holder) { - super.onSurfaceDestroyed(holder); if (DEBUG) { Log.i(TAG, "onSurfaceDestroyed"); } - mImageCanvasWallpaperRenderer.setSurfaceHolder(null); + mSurfaceHolder = null; } @Override public void onSurfaceCreated(SurfaceHolder holder) { - super.onSurfaceCreated(holder); if (DEBUG) { Log.i(TAG, "onSurfaceCreated"); } - mImageCanvasWallpaperRenderer.setSurfaceHolder(holder); } @Override @@ -613,135 +638,90 @@ public class ImageWallpaper extends WallpaperService { if (DEBUG) { Log.d(TAG, "onSurfaceRedrawNeeded"); } - super.onSurfaceRedrawNeeded(holder); - // At the end of this method we should have drawn into the surface. - // This means that the bitmap should be loaded synchronously if - // it was already unloaded. - if (!isBitmapLoaded()) { - setBitmap(mWallpaperManager.getBitmap(true /* hardware */)); - } - drawFrame(true); + drawFrame(); } - private DisplayInfo getDisplayInfo() { - mDisplay.getDisplayInfo(mTmpDisplayInfo); - return mTmpDisplayInfo; + private void drawFrame() { + mBackgroundExecutor.execute(this::drawFrameSynchronized); } - private void drawFrame(boolean forceRedraw) { - if (!mImageCanvasWallpaperRenderer.isSurfaceHolderLoaded()) { + private void drawFrameSynchronized() { + synchronized (mLock) { + drawFrameInternal(); + } + } + + private void drawFrameInternal() { + if (mSurfaceHolder == null) { Log.e(TAG, "attempt to draw a frame without a valid surface"); return; } + // load the wallpaper if not already done if (!isBitmapLoaded()) { - // ensure that we load the wallpaper. - // if the wallpaper is currently loading, this call will have no effect. - loadWallpaper(true); - return; + loadWallpaperAndDrawFrameInternal(); + } else { + mBitmapUsages++; + + // drawing is done on the main thread + mMainExecutor.execute(() -> { + drawFrameOnCanvas(mBitmap); + reportEngineShown(false); + unloadBitmapIfNotUsed(); + }); } - mImageCanvasWallpaperRenderer.drawFrame(mBitmap, forceRedraw); } - private void setBitmap(Bitmap bitmap) { - if (bitmap == null) { - Log.e(TAG, "Attempt to set a null bitmap"); - } else if (mBitmap == bitmap) { - Log.e(TAG, "The value of bitmap is the same"); - } else if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) { - Log.e(TAG, "Attempt to set an invalid wallpaper of length " - + bitmap.getWidth() + "x" + bitmap.getHeight()); - } else { - if (mBitmap != null) { - mBitmap.recycle(); + @VisibleForTesting + void drawFrameOnCanvas(Bitmap bitmap) { + Trace.beginSection("ImageWallpaper.CanvasEngine#drawFrame"); + Surface surface = mSurfaceHolder.getSurface(); + Canvas canvas = mWideColorGamut + ? surface.lockHardwareWideColorGamutCanvas() + : surface.lockHardwareCanvas(); + if (canvas != null) { + Rect dest = mSurfaceHolder.getSurfaceFrame(); + try { + canvas.drawBitmap(bitmap, null, dest, null); + } finally { + surface.unlockCanvasAndPost(canvas); } - mBitmap = bitmap; } + Trace.endSection(); } - private boolean isBitmapLoaded() { + @VisibleForTesting + boolean isBitmapLoaded() { return mBitmap != null && !mBitmap.isRecycled(); } - /** - * Loads the wallpaper on background thread and schedules updating the surface frame, - * and if {@code needsDraw} is set also draws a frame. - * - * If loading is already in-flight, subsequent loads are ignored (but needDraw is or-ed to - * the active request). - * - */ - private void loadWallpaper(boolean needsDraw) { - mNeedsDrawAfterLoadingWallpaper |= needsDraw; - if (mLoader != null) { - if (DEBUG) { - Log.d(TAG, "Skipping loadWallpaper, already in flight "); + private void unloadBitmapIfNotUsed() { + mBackgroundExecutor.execute(this::unloadBitmapIfNotUsedSynchronized); + } + + private void unloadBitmapIfNotUsedSynchronized() { + synchronized (mLock) { + mBitmapUsages -= 1; + if (mBitmapUsages <= 0) { + mBitmapUsages = 0; + unloadBitmapInternal(); } - return; } - mLoader = new AsyncTask<Void, Void, Bitmap>() { - @Override - protected Bitmap doInBackground(Void... params) { - Throwable exception; - try { - Bitmap wallpaper = mWallpaperManager.getBitmap(true /* hardware */); - if (wallpaper != null - && wallpaper.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) { - throw new RuntimeException("Wallpaper is too large to draw!"); - } - return wallpaper; - } catch (RuntimeException | OutOfMemoryError e) { - exception = e; - } - - if (isCancelled()) { - return null; - } - - // Note that if we do fail at this, and the default wallpaper can't - // be loaded, we will go into a cycle. Don't do a build where the - // default wallpaper can't be loaded. - Log.w(TAG, "Unable to load wallpaper!", exception); - try { - mWallpaperManager.clear(); - } catch (IOException ex) { - // now we're really screwed. - Log.w(TAG, "Unable reset to default wallpaper!", ex); - } - - if (isCancelled()) { - return null; - } - - try { - return mWallpaperManager.getBitmap(true /* hardware */); - } catch (RuntimeException | OutOfMemoryError e) { - Log.w(TAG, "Unable to load default wallpaper!", e); - } - return null; - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - setBitmap(bitmap); - - if (mNeedsDrawAfterLoadingWallpaper) { - drawFrame(true); - } + } - mLoader = null; - mNeedsDrawAfterLoadingWallpaper = false; - scheduleUnloadWallpaper(); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + private void unloadBitmap() { + mBackgroundExecutor.execute(this::unloadBitmapSynchronized); } - private void unloadWallpaper() { - if (mLoader != null) { - mLoader.cancel(false); - mLoader = null; + private void unloadBitmapSynchronized() { + synchronized (mLock) { + mBitmapUsages = 0; + unloadBitmapInternal(); } + } + private void unloadBitmapInternal() { + Trace.beginSection("ImageWallpaper.CanvasEngine#unloadBitmap"); if (mBitmap != null) { mBitmap.recycle(); } @@ -750,12 +730,131 @@ public class ImageWallpaper extends WallpaperService { final Surface surface = getSurfaceHolder().getSurface(); surface.hwuiDestroy(); mWallpaperManager.forgetLoadedWallpaper(); + Trace.endSection(); + } + + private void loadWallpaperAndDrawFrameInternal() { + Trace.beginSection("ImageWallpaper.CanvasEngine#loadWallpaper"); + boolean loadSuccess = false; + Bitmap bitmap; + try { + bitmap = mWallpaperManager.getBitmap(false); + if (bitmap != null + && bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) { + throw new RuntimeException("Wallpaper is too large to draw!"); + } + } catch (RuntimeException | OutOfMemoryError exception) { + + // Note that if we do fail at this, and the default wallpaper can't + // be loaded, we will go into a cycle. Don't do a build where the + // default wallpaper can't be loaded. + Log.w(TAG, "Unable to load wallpaper!", exception); + try { + mWallpaperManager.clear(WallpaperManager.FLAG_SYSTEM); + } catch (IOException ex) { + // now we're really screwed. + Log.w(TAG, "Unable reset to default wallpaper!", ex); + } + + try { + bitmap = mWallpaperManager.getBitmap(false); + } catch (RuntimeException | OutOfMemoryError e) { + Log.w(TAG, "Unable to load default wallpaper!", e); + bitmap = null; + } + } + + if (bitmap == null) { + Log.w(TAG, "Could not load bitmap"); + } else if (bitmap.isRecycled()) { + Log.e(TAG, "Attempt to load a recycled bitmap"); + } else if (mBitmap == bitmap) { + Log.e(TAG, "Loaded a bitmap that was already loaded"); + } else if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) { + Log.e(TAG, "Attempt to load an invalid wallpaper of length " + + bitmap.getWidth() + "x" + bitmap.getHeight()); + } else { + // at this point, loading is done correctly. + loadSuccess = true; + // recycle the previously loaded bitmap + if (mBitmap != null) { + mBitmap.recycle(); + } + mBitmap = bitmap; + mWideColorGamut = mWallpaperManager.wallpaperSupportsWcg( + WallpaperManager.FLAG_SYSTEM); + + // +2 usages for the color extraction and the delayed unload. + mBitmapUsages += 2; + recomputeColorExtractorMiniBitmap(); + drawFrameInternal(); + + /* + * after loading, the bitmap will be unloaded after all these conditions: + * - the frame is redrawn + * - the mini bitmap from color extractor is recomputed + * - the DELAY_UNLOAD_BITMAP has passed + */ + mBackgroundExecutor.executeDelayed( + this::unloadBitmapIfNotUsedSynchronized, DELAY_UNLOAD_BITMAP); + } + // even if the bitmap cannot be loaded, call reportEngineShown + if (!loadSuccess) reportEngineShown(false); + Trace.endSection(); } - private void scheduleUnloadWallpaper() { - Handler handler = getMainThreadHandler(); - handler.removeCallbacks(mUnloadWallpaperCallback); - handler.postDelayed(mUnloadWallpaperCallback, DELAY_FORGET_WALLPAPER); + private void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors) { + try { + notifyLocalColorsChanged(regions, colors); + } catch (RuntimeException e) { + Log.e(TAG, e.getMessage(), e); + } + } + + @VisibleForTesting + void recomputeColorExtractorMiniBitmap() { + mWallpaperColorExtractor.onBitmapChanged(mBitmap); + } + + @VisibleForTesting + void onMiniBitmapUpdated() { + unloadBitmapIfNotUsed(); + } + + @Override + public boolean supportsLocalColorExtraction() { + return true; + } + + @Override + public void addLocalColorsAreas(@NonNull List<RectF> regions) { + // this call will activate the offset notifications + // if no colors were being processed before + mWallpaperColorExtractor.addLocalColorsAreas(regions); + } + + @Override + public void removeLocalColorsAreas(@NonNull List<RectF> regions) { + // this call will deactivate the offset notifications + // if we are no longer processing colors + mWallpaperColorExtractor.removeLocalColorAreas(regions); + } + + @Override + public void onOffsetsChanged(float xOffset, float yOffset, + float xOffsetStep, float yOffsetStep, + int xPixelOffset, int yPixelOffset) { + final int pages; + if (xOffsetStep > 0 && xOffsetStep <= 1) { + pages = Math.round(1 / xOffsetStep) + 1; + } else { + pages = 1; + } + if (pages != mPages || !mPagesComputed) { + mPages = pages; + mPagesComputed = true; + mWallpaperColorExtractor.onPageChanged(mPages); + } } @Override @@ -764,13 +863,46 @@ public class ImageWallpaper extends WallpaperService { } @Override + public void onDisplayRemoved(int displayId) { + + } + + @Override public void onDisplayChanged(int displayId) { + // changes the display in the color extractor + // the new display dimensions will be used in the next color computation + if (displayId == getDisplayContext().getDisplayId()) { + getDisplaySizeAndUpdateColorExtractor(); + } + } + private void getDisplaySizeAndUpdateColorExtractor() { + Rect window = getDisplayContext() + .getSystemService(WindowManager.class) + .getCurrentWindowMetrics() + .getBounds(); + mWallpaperColorExtractor.setDisplayDimensions(window.width(), window.height()); } + @Override - public void onDisplayRemoved(int displayId) { + protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) { + super.dump(prefix, fd, out, args); + out.print(prefix); out.print("Engine="); out.println(this); + out.print(prefix); out.print("valid surface="); + out.println(getSurfaceHolder() != null && getSurfaceHolder().getSurface() != null + ? getSurfaceHolder().getSurface().isValid() + : "null"); + + out.print(prefix); out.print("surface frame="); + out.println(getSurfaceHolder() != null ? getSurfaceHolder().getSurfaceFrame() : "null"); + + out.print(prefix); out.print("bitmap="); + out.println(mBitmap == null ? "null" + : mBitmap.isRecycled() ? "recycled" + : mBitmap.getWidth() + "x" + mBitmap.getHeight()); + mWallpaperColorExtractor.dump(prefix, fd, out, args); } } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java deleted file mode 100644 index fdba16ed2059..000000000000 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRenderer.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.wallpapers.canvas; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.util.Log; -import android.view.SurfaceHolder; - -import com.android.internal.annotations.VisibleForTesting; - -/** - * Helper to draw a wallpaper on a surface. - * It handles the geometry regarding the dimensions of the display and the wallpaper, - * and rescales the surface and the wallpaper accordingly. - */ -public class ImageCanvasWallpaperRenderer { - - private static final String TAG = ImageCanvasWallpaperRenderer.class.getSimpleName(); - private static final boolean DEBUG = false; - - private SurfaceHolder mSurfaceHolder; - //private Bitmap mBitmap = null; - - @VisibleForTesting - static final int MIN_SURFACE_WIDTH = 128; - @VisibleForTesting - static final int MIN_SURFACE_HEIGHT = 128; - - private boolean mSurfaceRedrawNeeded; - - private int mLastSurfaceWidth = -1; - private int mLastSurfaceHeight = -1; - - public ImageCanvasWallpaperRenderer(SurfaceHolder surfaceHolder) { - mSurfaceHolder = surfaceHolder; - } - - /** - * Set the surface holder on which to draw. - * Should be called when the surface holder is created or changed - * @param surfaceHolder the surface on which to draw the wallpaper - */ - public void setSurfaceHolder(SurfaceHolder surfaceHolder) { - mSurfaceHolder = surfaceHolder; - } - - /** - * Check if a surface holder is loaded - * @return true if a valid surfaceHolder has been set. - */ - public boolean isSurfaceHolderLoaded() { - return mSurfaceHolder != null; - } - - /** - * Computes and set the surface dimensions, by using the play and the bitmap dimensions. - * The Bitmap must be loaded before any call to this function - */ - private boolean updateSurfaceSize(Bitmap bitmap) { - int surfaceWidth = Math.max(MIN_SURFACE_WIDTH, bitmap.getWidth()); - int surfaceHeight = Math.max(MIN_SURFACE_HEIGHT, bitmap.getHeight()); - boolean surfaceChanged = - surfaceWidth != mLastSurfaceWidth || surfaceHeight != mLastSurfaceHeight; - if (surfaceChanged) { - /* - Used a fixed size surface, because we are special. We can do - this because we know the current design of window animations doesn't - cause this to break. - */ - mSurfaceHolder.setFixedSize(surfaceWidth, surfaceHeight); - mLastSurfaceWidth = surfaceWidth; - mLastSurfaceHeight = surfaceHeight; - } - return surfaceChanged; - } - - /** - * Draw a the wallpaper on the surface. - * The bitmap and the surface must be loaded before calling - * this function. - * @param forceRedraw redraw the wallpaper even if no changes are detected - */ - public void drawFrame(Bitmap bitmap, boolean forceRedraw) { - - if (bitmap == null || bitmap.isRecycled()) { - Log.e(TAG, "Attempt to draw frame before background is loaded:"); - return; - } - - if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) { - Log.e(TAG, "Attempt to set an invalid wallpaper of length " - + bitmap.getWidth() + "x" + bitmap.getHeight()); - return; - } - - mSurfaceRedrawNeeded |= forceRedraw; - boolean surfaceChanged = updateSurfaceSize(bitmap); - - boolean redrawNeeded = surfaceChanged || mSurfaceRedrawNeeded; - mSurfaceRedrawNeeded = false; - - if (!redrawNeeded) { - if (DEBUG) { - Log.d(TAG, "Suppressed drawFrame since redraw is not needed "); - } - return; - } - - if (DEBUG) { - Log.d(TAG, "Redrawing wallpaper"); - } - drawWallpaperWithCanvas(bitmap); - } - - @VisibleForTesting - void drawWallpaperWithCanvas(Bitmap bitmap) { - Canvas c = mSurfaceHolder.lockHardwareCanvas(); - if (c != null) { - Rect dest = mSurfaceHolder.getSurfaceFrame(); - Log.i(TAG, "Redrawing in rect: " + dest + " with surface size: " - + mLastSurfaceWidth + "x" + mLastSurfaceHeight); - try { - c.drawBitmap(bitmap, null, dest, null); - } finally { - mSurfaceHolder.unlockCanvasAndPost(c); - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java new file mode 100644 index 000000000000..e2e4555bb965 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.systemui.wallpapers.canvas; + +import android.app.WallpaperColors; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Trace; +import android.util.ArraySet; +import android.util.Log; +import android.util.MathUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.util.Assert; +import com.android.systemui.wallpapers.ImageWallpaper; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; + +/** + * This class is used by the {@link ImageWallpaper} to extract colors from areas of a wallpaper. + * It uses a background executor, and uses callbacks to inform that the work is done. + * It uses a downscaled version of the wallpaper to extract the colors. + */ +public class WallpaperColorExtractor { + + private Bitmap mMiniBitmap; + + @VisibleForTesting + static final int SMALL_SIDE = 128; + + private static final String TAG = WallpaperColorExtractor.class.getSimpleName(); + private static final @NonNull RectF LOCAL_COLOR_BOUNDS = + new RectF(0, 0, 1, 1); + + private int mDisplayWidth = -1; + private int mDisplayHeight = -1; + private int mPages = -1; + private int mBitmapWidth = -1; + private int mBitmapHeight = -1; + + private final Object mLock = new Object(); + + private final List<RectF> mPendingRegions = new ArrayList<>(); + private final Set<RectF> mProcessedRegions = new ArraySet<>(); + + @Background + private final Executor mBackgroundExecutor; + + private final WallpaperColorExtractorCallback mWallpaperColorExtractorCallback; + + /** + * Interface to handle the callbacks after the different steps of the color extraction + */ + public interface WallpaperColorExtractorCallback { + /** + * Callback after the colors of new regions have been extracted + * @param regions the list of new regions that have been processed + * @param colors the resulting colors for these regions, in the same order as the regions + */ + void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors); + + /** + * Callback after the mini bitmap is computed, to indicate that the wallpaper bitmap is + * no longer used by the color extractor and can be safely recycled + */ + void onMiniBitmapUpdated(); + + /** + * Callback to inform that the extractor has started processing colors + */ + void onActivated(); + + /** + * Callback to inform that no more colors are being processed + */ + void onDeactivated(); + } + + /** + * Creates a new color extractor. + * @param backgroundExecutor the executor on which the color extraction will be performed + * @param wallpaperColorExtractorCallback an interface to handle the callbacks from + * the color extractor. + */ + public WallpaperColorExtractor(@Background Executor backgroundExecutor, + WallpaperColorExtractorCallback wallpaperColorExtractorCallback) { + mBackgroundExecutor = backgroundExecutor; + mWallpaperColorExtractorCallback = wallpaperColorExtractorCallback; + } + + /** + * Used by the outside to inform that the display size has changed. + * The new display size will be used in the next computations, but the current colors are + * not recomputed. + */ + public void setDisplayDimensions(int displayWidth, int displayHeight) { + mBackgroundExecutor.execute(() -> + setDisplayDimensionsSynchronized(displayWidth, displayHeight)); + } + + private void setDisplayDimensionsSynchronized(int displayWidth, int displayHeight) { + synchronized (mLock) { + if (displayWidth == mDisplayWidth && displayHeight == mDisplayHeight) return; + mDisplayWidth = displayWidth; + mDisplayHeight = displayHeight; + processColorsInternal(); + } + } + + /** + * @return whether color extraction is currently in use + */ + private boolean isActive() { + return mPendingRegions.size() + mProcessedRegions.size() > 0; + } + + /** + * Should be called when the wallpaper is changed. + * This will recompute the mini bitmap + * and restart the extraction of all areas + * @param bitmap the new wallpaper + */ + public void onBitmapChanged(@NonNull Bitmap bitmap) { + mBackgroundExecutor.execute(() -> onBitmapChangedSynchronized(bitmap)); + } + + private void onBitmapChangedSynchronized(@NonNull Bitmap bitmap) { + synchronized (mLock) { + if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { + Log.e(TAG, "Attempt to extract colors from an invalid bitmap"); + return; + } + mBitmapWidth = bitmap.getWidth(); + mBitmapHeight = bitmap.getHeight(); + mMiniBitmap = createMiniBitmap(bitmap); + mWallpaperColorExtractorCallback.onMiniBitmapUpdated(); + recomputeColors(); + } + } + + /** + * Should be called when the number of pages is changed + * This will restart the extraction of all areas + * @param pages the total number of pages of the launcher + */ + public void onPageChanged(int pages) { + mBackgroundExecutor.execute(() -> onPageChangedSynchronized(pages)); + } + + private void onPageChangedSynchronized(int pages) { + synchronized (mLock) { + if (mPages == pages) return; + mPages = pages; + if (mMiniBitmap != null && !mMiniBitmap.isRecycled()) { + recomputeColors(); + } + } + } + + // helper to recompute colors, to be called in synchronized methods + private void recomputeColors() { + mPendingRegions.addAll(mProcessedRegions); + mProcessedRegions.clear(); + processColorsInternal(); + } + + /** + * Add new regions to extract + * This will trigger the color extraction and call the callback only for these new regions + * @param regions The areas of interest in our wallpaper (in screen pixel coordinates) + */ + public void addLocalColorsAreas(@NonNull List<RectF> regions) { + if (regions.size() > 0) { + mBackgroundExecutor.execute(() -> addLocalColorsAreasSynchronized(regions)); + } else { + Log.w(TAG, "Attempt to add colors with an empty list"); + } + } + + private void addLocalColorsAreasSynchronized(@NonNull List<RectF> regions) { + synchronized (mLock) { + boolean wasActive = isActive(); + mPendingRegions.addAll(regions); + if (!wasActive && isActive()) { + mWallpaperColorExtractorCallback.onActivated(); + } + processColorsInternal(); + } + } + + /** + * Remove regions to extract. If a color extraction is ongoing does not stop it. + * But if there are subsequent changes that restart the extraction, the removed regions + * will not be recomputed. + * @param regions The areas of interest in our wallpaper (in screen pixel coordinates) + */ + public void removeLocalColorAreas(@NonNull List<RectF> regions) { + mBackgroundExecutor.execute(() -> removeLocalColorAreasSynchronized(regions)); + } + + private void removeLocalColorAreasSynchronized(@NonNull List<RectF> regions) { + synchronized (mLock) { + boolean wasActive = isActive(); + mPendingRegions.removeAll(regions); + regions.forEach(mProcessedRegions::remove); + if (wasActive && !isActive()) { + mWallpaperColorExtractorCallback.onDeactivated(); + } + } + } + + /** + * Clean up the memory (in particular, the mini bitmap) used by this class. + */ + public void cleanUp() { + mBackgroundExecutor.execute(this::cleanUpSynchronized); + } + + private void cleanUpSynchronized() { + synchronized (mLock) { + if (mMiniBitmap != null) { + mMiniBitmap.recycle(); + mMiniBitmap = null; + } + mProcessedRegions.clear(); + mPendingRegions.clear(); + } + } + + private Bitmap createMiniBitmap(@NonNull Bitmap bitmap) { + Trace.beginSection("WallpaperColorExtractor#createMiniBitmap"); + // if both sides of the image are larger than SMALL_SIDE, downscale the bitmap. + int smallestSide = Math.min(bitmap.getWidth(), bitmap.getHeight()); + float scale = Math.min(1.0f, (float) SMALL_SIDE / smallestSide); + Bitmap result = createMiniBitmap(bitmap, + (int) (scale * bitmap.getWidth()), + (int) (scale * bitmap.getHeight())); + Trace.endSection(); + return result; + } + + @VisibleForTesting + Bitmap createMiniBitmap(@NonNull Bitmap bitmap, int width, int height) { + return Bitmap.createScaledBitmap(bitmap, width, height, false); + } + + private WallpaperColors getLocalWallpaperColors(@NonNull RectF area) { + RectF imageArea = pageToImgRect(area); + if (imageArea == null || !LOCAL_COLOR_BOUNDS.contains(imageArea)) { + return null; + } + Rect subImage = new Rect( + (int) Math.floor(imageArea.left * mMiniBitmap.getWidth()), + (int) Math.floor(imageArea.top * mMiniBitmap.getHeight()), + (int) Math.ceil(imageArea.right * mMiniBitmap.getWidth()), + (int) Math.ceil(imageArea.bottom * mMiniBitmap.getHeight())); + if (subImage.isEmpty()) { + // Do not notify client. treat it as too small to sample + return null; + } + return getLocalWallpaperColors(subImage); + } + + @VisibleForTesting + WallpaperColors getLocalWallpaperColors(@NonNull Rect subImage) { + Assert.isNotMainThread(); + Bitmap colorImg = Bitmap.createBitmap(mMiniBitmap, + subImage.left, subImage.top, subImage.width(), subImage.height()); + return WallpaperColors.fromBitmap(colorImg); + } + + /** + * Transform the logical coordinates into wallpaper coordinates. + * + * Logical coordinates are organised such that the various pages are non-overlapping. So, + * if there are n pages, the first page will have its X coordinate on the range [0-1/n]. + * + * The real pages are overlapping. If the Wallpaper are a width Ww and the screen a width + * Ws, the relative width of a page Wr is Ws/Ww. This does not change if the number of + * pages increase. + * If there are n pages, the page k starts at the offset k * (1 - Wr) / (n - 1), as the + * last page is at position (1-Wr) and the others are regularly spread on the range [0- + * (1-Wr)]. + */ + private RectF pageToImgRect(RectF area) { + // Width of a page for the caller of this API. + float virtualPageWidth = 1f / (float) mPages; + float leftPosOnPage = (area.left % virtualPageWidth) / virtualPageWidth; + float rightPosOnPage = (area.right % virtualPageWidth) / virtualPageWidth; + int currentPage = (int) Math.floor(area.centerX() / virtualPageWidth); + + if (mDisplayWidth <= 0 || mDisplayHeight <= 0) { + Log.e(TAG, "Trying to extract colors with invalid display dimensions"); + return null; + } + + RectF imgArea = new RectF(); + imgArea.bottom = area.bottom; + imgArea.top = area.top; + + float imageScale = Math.min(((float) mBitmapHeight) / mDisplayHeight, 1); + float mappedScreenWidth = mDisplayWidth * imageScale; + float pageWidth = Math.min(1.0f, + mBitmapWidth > 0 ? mappedScreenWidth / (float) mBitmapWidth : 1.f); + float pageOffset = (1 - pageWidth) / (float) (mPages - 1); + + imgArea.left = MathUtils.constrain( + leftPosOnPage * pageWidth + currentPage * pageOffset, 0, 1); + imgArea.right = MathUtils.constrain( + rightPosOnPage * pageWidth + currentPage * pageOffset, 0, 1); + if (imgArea.left > imgArea.right) { + // take full page + imgArea.left = 0; + imgArea.right = 1; + } + return imgArea; + } + + /** + * Extract the colors from the pending regions, + * then notify the callback with the resulting colors for these regions + * This method should only be called synchronously + */ + private void processColorsInternal() { + /* + * if the miniBitmap is not yet loaded, that means the onBitmapChanged has not yet been + * called, and thus the wallpaper is not yet loaded. In that case, exit, the function + * will be called again when the bitmap is loaded and the miniBitmap is computed. + */ + if (mMiniBitmap == null || mMiniBitmap.isRecycled()) return; + + /* + * if the screen size or number of pages is not yet known, exit + * the function will be called again once the screen size and page are known + */ + if (mDisplayWidth < 0 || mDisplayHeight < 0 || mPages < 0) return; + + Trace.beginSection("WallpaperColorExtractor#processColorsInternal"); + List<WallpaperColors> processedColors = new ArrayList<>(); + for (int i = 0; i < mPendingRegions.size(); i++) { + RectF nextArea = mPendingRegions.get(i); + WallpaperColors colors = getLocalWallpaperColors(nextArea); + + mProcessedRegions.add(nextArea); + processedColors.add(colors); + } + List<RectF> processedRegions = new ArrayList<>(mPendingRegions); + mPendingRegions.clear(); + Trace.endSection(); + + mWallpaperColorExtractorCallback.onColorsProcessed(processedRegions, processedColors); + } + + /** + * Called to dump current state. + * @param prefix prefix. + * @param fd fd. + * @param out out. + * @param args args. + */ + public void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) { + out.print(prefix); out.print("display="); out.println(mDisplayWidth + "x" + mDisplayHeight); + out.print(prefix); out.print("mPages="); out.println(mPages); + + out.print(prefix); out.print("bitmap dimensions="); + out.println(mBitmapWidth + "x" + mBitmapHeight); + + out.print(prefix); out.print("bitmap="); + out.println(mMiniBitmap == null ? "null" + : mMiniBitmap.isRecycled() ? "recycled" + : mMiniBitmap.getWidth() + "x" + mMiniBitmap.getHeight()); + + out.print(prefix); out.print("PendingRegions size="); out.print(mPendingRegions.size()); + out.print(prefix); out.print("ProcessedRegions size="); out.print(mProcessedRegions.size()); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 3472cb1c2a7d..309f1681b964 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -22,6 +22,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_B import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; @@ -55,6 +56,8 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tracing.nano.SystemUiTraceProto; +import com.android.wm.shell.desktopmode.DesktopMode; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.floating.FloatingTasks; import com.android.wm.shell.nano.WmShellTraceProto; import com.android.wm.shell.onehanded.OneHanded; @@ -89,8 +92,10 @@ import javax.inject.Inject; * -> WMShell starts and binds SysUI with Shell components via exported Shell interfaces */ @SysUISingleton -public final class WMShell extends CoreStartable - implements CommandQueue.Callbacks, ProtoTraceable<SystemUiTraceProto> { +public final class WMShell implements + CoreStartable, + CommandQueue.Callbacks, + ProtoTraceable<SystemUiTraceProto> { private static final String TAG = WMShell.class.getName(); private static final int INVALID_SYSUI_STATE_MASK = SYSUI_STATE_DIALOG_SHOWING @@ -102,12 +107,14 @@ public final class WMShell extends CoreStartable | SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; + private final Context mContext; // Shell interfaces private final ShellInterface mShell; private final Optional<Pip> mPipOptional; private final Optional<SplitScreen> mSplitScreenOptional; private final Optional<OneHanded> mOneHandedOptional; private final Optional<FloatingTasks> mFloatingTasksOptional; + private final Optional<DesktopMode> mDesktopModeOptional; private final CommandQueue mCommandQueue; private final ConfigurationController mConfigurationController; @@ -163,12 +170,14 @@ public final class WMShell extends CoreStartable private WakefulnessLifecycle.Observer mWakefulnessObserver; @Inject - public WMShell(Context context, + public WMShell( + Context context, ShellInterface shell, Optional<Pip> pipOptional, Optional<SplitScreen> splitScreenOptional, Optional<OneHanded> oneHandedOptional, Optional<FloatingTasks> floatingTasksOptional, + Optional<DesktopMode> desktopMode, CommandQueue commandQueue, ConfigurationController configurationController, KeyguardStateController keyguardStateController, @@ -179,7 +188,7 @@ public final class WMShell extends CoreStartable WakefulnessLifecycle wakefulnessLifecycle, UserTracker userTracker, @Main Executor sysUiMainExecutor) { - super(context); + mContext = context; mShell = shell; mCommandQueue = commandQueue; mConfigurationController = configurationController; @@ -190,6 +199,7 @@ public final class WMShell extends CoreStartable mPipOptional = pipOptional; mSplitScreenOptional = splitScreenOptional; mOneHandedOptional = oneHandedOptional; + mDesktopModeOptional = desktopMode; mWakefulnessLifecycle = wakefulnessLifecycle; mProtoTracer = protoTracer; mUserTracker = userTracker; @@ -215,6 +225,7 @@ public final class WMShell extends CoreStartable mPipOptional.ifPresent(this::initPip); mSplitScreenOptional.ifPresent(this::initSplitScreen); mOneHandedOptional.ifPresent(this::initOneHanded); + mDesktopModeOptional.ifPresent(this::initDesktopMode); } @VisibleForTesting @@ -322,6 +333,16 @@ public final class WMShell extends CoreStartable }); } + void initDesktopMode(DesktopMode desktopMode) { + desktopMode.addListener(new DesktopModeTaskRepository.VisibleTasksListener() { + @Override + public void onVisibilityChanged(boolean hasFreeformTasks) { + mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE, hasFreeformTasks) + .commitUpdate(DEFAULT_DISPLAY); + } + }, mSysUiMainExecutor); + } + @Override public void writeToProto(SystemUiTraceProto proto) { if (proto.wmShell == null) { diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index ba2804572ef5..1b404a82145b 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -88,6 +88,11 @@ android:excludeFromRecents="true" /> + <activity android:name=".settings.brightness.BrightnessDialogTest$TestDialog" + android:exported="false" + android:excludeFromRecents="true" + /> + <activity android:name="com.android.systemui.screenshot.ScrollViewActivity" android:exported="false" /> diff --git a/packages/SystemUI/tests/res/layout/custom_view_dark.xml b/packages/SystemUI/tests/res/layout/custom_view_dark.xml index 9e460a5819a9..112d73d2d7f2 100644 --- a/packages/SystemUI/tests/res/layout/custom_view_dark.xml +++ b/packages/SystemUI/tests/res/layout/custom_view_dark.xml @@ -14,6 +14,7 @@ limitations under the License. --> <ImageView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/custom_view_dark_image" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ff000000" diff --git a/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java index 80385e69cfa4..f5cd0ca7ab3b 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/AdminSecondaryLockScreenControllerTest.java @@ -41,6 +41,7 @@ import android.testing.TestableLooper.RunWithLooper; import android.testing.ViewUtils; import android.view.SurfaceControlViewHost; import android.view.SurfaceView; +import android.view.View; import androidx.test.filters.SmallTest; @@ -84,6 +85,7 @@ public class AdminSecondaryLockScreenControllerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mKeyguardSecurityContainer = spy(new KeyguardSecurityContainer(mContext)); + mKeyguardSecurityContainer.setId(View.generateViewId()); ViewUtils.attachView(mKeyguardSecurityContainer); mTestableLooper = TestableLooper.get(this); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt new file mode 100644 index 000000000000..7b9b39f23c29 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard + +import android.content.Context +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.util.AttributeSet +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class BouncerKeyguardMessageAreaTest : SysuiTestCase() { + class FakeBouncerKeyguardMessageArea(context: Context, attrs: AttributeSet?) : + BouncerKeyguardMessageArea(context, attrs) { + override val SHOW_DURATION_MILLIS = 0L + override val HIDE_DURATION_MILLIS = 0L + } + lateinit var underTest: BouncerKeyguardMessageArea + + @Before + fun setup() { + underTest = FakeBouncerKeyguardMessageArea(context, null) + } + + @Test + fun testSetSameMessage() { + val underTestSpy = spy(underTest) + underTestSpy.setMessage("abc") + underTestSpy.setMessage("abc") + verify(underTestSpy, times(1)).text = "abc" + } + + @Test + fun testSetDifferentMessage() { + underTest.setMessage("abc") + underTest.setMessage("def") + assertThat(underTest.text).isEqualTo("def") + } + + @Test + fun testSetNullMessage() { + underTest.setMessage(null) + assertThat(underTest.text).isEqualTo("") + } + + @Test + fun testSetNullClearsPreviousMessage() { + underTest.setMessage("something not null") + assertThat(underTest.text).isEqualTo("something not null") + + underTest.setMessage(null) + assertThat(underTest.text).isEqualTo("") + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java index c2c7dde562a2..ecf7e0d46373 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java @@ -29,7 +29,6 @@ import static junit.framework.TestCase.assertFalse; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -171,7 +170,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { reset(mCarrierTextCallback); List<SubscriptionInfo> list = new ArrayList<>(); list.add(TEST_SUBSCRIPTION); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn(list); + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); when(mKeyguardUpdateMonitor.getSimState(0)).thenReturn(TelephonyManager.SIM_STATE_READY); mKeyguardUpdateMonitor.mServiceStates = new HashMap<>(); @@ -191,7 +190,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { reset(mCarrierTextCallback); List<SubscriptionInfo> list = new ArrayList<>(); list.add(TEST_SUBSCRIPTION); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn(list); + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); when(mKeyguardUpdateMonitor.getSimState(0)).thenReturn(TelephonyManager.SIM_STATE_READY); when(mKeyguardUpdateMonitor.getSimState(1)).thenReturn( TelephonyManager.SIM_STATE_CARD_IO_ERROR); @@ -224,7 +223,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { @Test public void testWrongSlots() { reset(mCarrierTextCallback); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn( + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn( new ArrayList<>()); when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn( TelephonyManager.SIM_STATE_CARD_IO_ERROR); @@ -238,7 +237,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { @Test public void testMoreSlotsThanSubs() { reset(mCarrierTextCallback); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn( + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn( new ArrayList<>()); // STOPSHIP(b/130246708) This line makes sure that SubscriptionManager provides the @@ -289,7 +288,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { list.add(TEST_SUBSCRIPTION); when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn( TelephonyManager.SIM_STATE_READY); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn(list); + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); mKeyguardUpdateMonitor.mServiceStates = new HashMap<>(); @@ -314,7 +313,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { list.add(TEST_SUBSCRIPTION_ROAMING); when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn( TelephonyManager.SIM_STATE_READY); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn(list); + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); mKeyguardUpdateMonitor.mServiceStates = new HashMap<>(); @@ -339,7 +338,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { list.add(TEST_SUBSCRIPTION_NULL); when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn( TelephonyManager.SIM_STATE_READY); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn(list); + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); mKeyguardUpdateMonitor.mServiceStates = new HashMap<>(); @@ -364,7 +363,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { list.add(TEST_SUBSCRIPTION_NULL); when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn( TelephonyManager.SIM_STATE_READY); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn(list); + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); mockWifi(); mKeyguardUpdateMonitor.mServiceStates = new HashMap<>(); @@ -396,7 +395,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { @Test public void testCreateInfo_noSubscriptions() { reset(mCarrierTextCallback); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn( + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn( new ArrayList<>()); ArgumentCaptor<CarrierTextManager.CarrierTextCallbackInfo> captor = @@ -421,7 +420,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { list.add(TEST_SUBSCRIPTION); when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn( TelephonyManager.SIM_STATE_READY); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn(list); + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); mKeyguardUpdateMonitor.mServiceStates = new HashMap<>(); @@ -446,7 +445,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { when(mKeyguardUpdateMonitor.getSimState(anyInt())) .thenReturn(TelephonyManager.SIM_STATE_READY) .thenReturn(TelephonyManager.SIM_STATE_NOT_READY); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn(list); + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); mKeyguardUpdateMonitor.mServiceStates = new HashMap<>(); @@ -471,7 +470,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { when(mKeyguardUpdateMonitor.getSimState(anyInt())) .thenReturn(TelephonyManager.SIM_STATE_NOT_READY) .thenReturn(TelephonyManager.SIM_STATE_READY); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn(list); + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); mKeyguardUpdateMonitor.mServiceStates = new HashMap<>(); @@ -498,7 +497,7 @@ public class CarrierTextManagerTest extends SysuiTestCase { .thenReturn(TelephonyManager.SIM_STATE_READY) .thenReturn(TelephonyManager.SIM_STATE_NOT_READY) .thenReturn(TelephonyManager.SIM_STATE_READY); - when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo(anyBoolean())).thenReturn(list); + when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list); mKeyguardUpdateMonitor.mServiceStates = new HashMap<>(); ArgumentCaptor<CarrierTextManager.CarrierTextCallbackInfo> captor = diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 25e7dbb5f26d..1c3656d71d82 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -17,15 +17,22 @@ package com.android.keyguard import android.content.BroadcastReceiver import android.testing.AndroidTestingRunner +import android.view.View import android.widget.TextView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.FeatureFlags -import com.android.systemui.plugins.Clock +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.plugins.ClockAnimations +import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockEvents -import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.plugins.ClockFaceController +import com.android.systemui.plugins.ClockFaceEvents +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.mockito.any @@ -35,11 +42,15 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import java.util.TimeZone import java.util.concurrent.Executor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock @@ -54,29 +65,43 @@ import org.mockito.junit.MockitoJUnit class ClockEventControllerTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() - @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var keyguardInteractor: KeyguardInteractor @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var batteryController: BatteryController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var animations: ClockAnimations @Mock private lateinit var events: ClockEvents - @Mock private lateinit var clock: Clock + @Mock private lateinit var clock: ClockController @Mock private lateinit var mainExecutor: Executor @Mock private lateinit var bgExecutor: Executor @Mock private lateinit var featureFlags: FeatureFlags - - private lateinit var clockEventController: ClockEventController + @Mock private lateinit var smallClockController: ClockFaceController + @Mock private lateinit var largeClockController: ClockFaceController + @Mock private lateinit var smallClockEvents: ClockFaceEvents + @Mock private lateinit var largeClockEvents: ClockFaceEvents + @Mock private lateinit var parentView: View + @Mock private lateinit var transitionRepository: KeyguardTransitionRepository + private lateinit var repository: FakeKeyguardRepository + @Mock private lateinit var logBuffer: LogBuffer + private lateinit var underTest: ClockEventController @Before fun setUp() { - whenever(clock.smallClock).thenReturn(TextView(context)) - whenever(clock.largeClock).thenReturn(TextView(context)) + whenever(clock.smallClock).thenReturn(smallClockController) + whenever(clock.largeClock).thenReturn(largeClockController) + whenever(smallClockController.view).thenReturn(TextView(context)) + whenever(largeClockController.view).thenReturn(TextView(context)) + whenever(smallClockController.events).thenReturn(smallClockEvents) + whenever(largeClockController.events).thenReturn(largeClockEvents) whenever(clock.events).thenReturn(events) whenever(clock.animations).thenReturn(animations) - clockEventController = ClockEventController( - statusBarStateController, + repository = FakeKeyguardRepository() + + underTest = ClockEventController( + KeyguardInteractor(repository = repository), + KeyguardTransitionInteractor(repository = transitionRepository), broadcastDispatcher, batteryController, keyguardUpdateMonitor, @@ -85,45 +110,47 @@ class ClockEventControllerTest : SysuiTestCase() { context, mainExecutor, bgExecutor, + logBuffer, featureFlags ) + underTest.clock = clock + + runBlocking(IMMEDIATE) { + underTest.registerListeners(parentView) + + repository.setDozing(true) + repository.setDozeAmount(1f) + } } @Test fun clockSet_validateInitialization() { - clockEventController.clock = clock - verify(clock).initialize(any(), anyFloat(), anyFloat()) } @Test fun clockUnset_validateState() { - clockEventController.clock = clock - clockEventController.clock = null + underTest.clock = null - assertEquals(clockEventController.clock, null) + assertEquals(underTest.clock, null) } @Test - fun themeChanged_verifyClockPaletteUpdated() { - clockEventController.clock = clock - verify(events).onColorPaletteChanged(any(), any(), any()) - - clockEventController.registerListeners() + fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) { + verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) + verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() verify(configurationController).addCallback(capture(captor)) captor.value.onThemeChanged() - verify(events, times(2)).onColorPaletteChanged(any(), any(), any()) + verify(events).onColorPaletteChanged(any()) } @Test - fun fontChanged_verifyFontSizeUpdated() { - clockEventController.clock = clock - verify(events).onColorPaletteChanged(any(), any(), any()) - - clockEventController.registerListeners() + fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) { + verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) + verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() verify(configurationController).addCallback(capture(captor)) @@ -133,10 +160,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() @@ -148,26 +172,21 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - - val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() - verify(batteryController).addCallback(capture(batteryCaptor)) - val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() - verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) - keyguardCaptor.value.onKeyguardVisibilityChanged(true) - batteryCaptor.value.onBatteryLevelChanged(10, false, true) - batteryCaptor.value.onBatteryLevelChanged(10, false, true) - - verify(animations, times(1)).charge() - } + fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() = + runBlocking(IMMEDIATE) { + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(true) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + batteryCaptor.value.onBatteryLevelChanged(10, false, true) + + verify(animations, times(1)).charge() + } @Test - fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) { val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() verify(batteryController).addCallback(capture(batteryCaptor)) val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() @@ -179,25 +198,20 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() { - clockEventController.clock = clock - clockEventController.registerListeners() - - val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() - verify(batteryController).addCallback(capture(batteryCaptor)) - val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() - verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) - keyguardCaptor.value.onKeyguardVisibilityChanged(true) - batteryCaptor.value.onBatteryLevelChanged(10, false, false) - - verify(animations, never()).charge() - } + fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() = + runBlocking(IMMEDIATE) { + val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(batteryCaptor)) + val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor)) + keyguardCaptor.value.onKeyguardVisibilityChanged(true) + batteryCaptor.value.onBatteryLevelChanged(10, false, false) + + verify(animations, never()).charge() + } @Test - fun localeCallback_verifyClockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun localeCallback_verifyClockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<BroadcastReceiver>() verify(broadcastDispatcher).registerReceiver( capture(captor), any(), eq(null), eq(null), anyInt(), eq(null) @@ -208,10 +222,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_visibilityChanged_clockDozeCalled() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) @@ -223,10 +234,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_timeFormat_clockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeFormatChanged("12h") @@ -235,11 +243,8 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_timezoneChanged_clockNotified() { + fun keyguardCallback_timezoneChanged_clockNotified() = runBlocking(IMMEDIATE) { val mockTimeZone = mock<TimeZone>() - clockEventController.clock = clock - clockEventController.registerListeners() - val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onTimeZoneChanged(mockTimeZone) @@ -248,10 +253,7 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_userSwitched_clockNotified() { - clockEventController.clock = clock - clockEventController.registerListeners() - + fun keyguardCallback_userSwitched_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) captor.value.onUserSwitchComplete(10) @@ -260,25 +262,27 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_verifyKeyguardChanged() { - clockEventController.clock = clock - clockEventController.registerListeners() + fun keyguardCallback_verifyKeyguardChanged() = runBlocking(IMMEDIATE) { + val job = underTest.listenForDozeAmount(this) + repository.setDozeAmount(0.4f) - val captor = argumentCaptor<StatusBarStateController.StateListener>() - verify(statusBarStateController).addCallback(capture(captor)) - captor.value.onDozeAmountChanged(0.4f, 0.6f) + yield() verify(animations).doze(0.4f) + + job.cancel() } @Test - fun unregisterListeners_validate() { - clockEventController.clock = clock - clockEventController.unregisterListeners() + fun unregisterListeners_validate() = runBlocking(IMMEDIATE) { + underTest.unregisterListeners() verify(broadcastDispatcher).unregisterReceiver(any()) verify(configurationController).removeCallback(any()) verify(batteryController).removeCallback(any()) verify(keyguardUpdateMonitor).removeCallback(any()) - verify(statusBarStateController).removeCallback(any()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt index aa671d1e3790..91b544b8265c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt @@ -17,7 +17,6 @@ package com.android.keyguard import android.hardware.biometrics.BiometricSourceType -import org.mockito.Mockito.verify import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId @@ -30,9 +29,10 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.Captor import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -63,7 +63,6 @@ class KeyguardBiometricLockoutLoggerTest : SysuiTestCase() { whenever(keyguardUpdateMonitor.strongAuthTracker).thenReturn(strongAuthTracker) whenever(sessionTracker.getSessionId(anyInt())).thenReturn(sessionId) keyguardBiometricLockoutLogger = KeyguardBiometricLockoutLogger( - mContext, uiEventLogger, keyguardUpdateMonitor, sessionTracker) @@ -195,4 +194,4 @@ class KeyguardBiometricLockoutLoggerTest : SysuiTestCase() { verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallbackCaptor.capture()) updateMonitorCallback = updateMonitorCallbackCaptor.value } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 635ee9ea1a2f..61c7bb500e6a 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.when; import android.content.res.Resources; import android.database.ContentObserver; +import android.graphics.Rect; import android.net.Uri; import android.os.UserHandle; import android.provider.Settings; @@ -43,9 +44,9 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.plugins.Clock; +import com.android.systemui.plugins.ClockAnimations; +import com.android.systemui.plugins.ClockController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.AnimatableClockView; import com.android.systemui.shared.clocks.ClockRegistry; @@ -87,7 +88,7 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { @Mock KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; @Mock - private Clock mClock; + private ClockController mClock; @Mock DumpManager mDumpManager; @Mock @@ -103,8 +104,6 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private FrameLayout mLargeClockFrame; @Mock private SecureSettings mSecureSettings; - @Mock - private FeatureFlags mFeatureFlags; private final View mFakeSmartspaceView = new View(mContext); @@ -141,8 +140,7 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { mSecureSettings, mExecutor, mDumpManager, - mClockEventController, - mFeatureFlags + mClockEventController ); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); @@ -262,9 +260,22 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { verify(mView).switchToClock(KeyguardClockSwitch.SMALL, /* animate */ true); } + @Test + public void testGetClockAnimationsForwardsToClock() { + ClockController mockClockController = mock(ClockController.class); + ClockAnimations mockClockAnimations = mock(ClockAnimations.class); + when(mClockEventController.getClock()).thenReturn(mockClockController); + when(mockClockController.getAnimations()).thenReturn(mockClockAnimations); + + Rect r1 = new Rect(1, 2, 3, 4); + Rect r2 = new Rect(5, 6, 7, 8); + mController.getClockAnimations().onPositionUpdated(r1, r2, 0.2f); + verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.2f); + } + private void verifyAttachment(VerificationMode times) { verify(mClockRegistry, times).registerClockChangeListener( any(ClockRegistry.ClockChangeListener.class)); - verify(mClockEventController, times).registerListeners(); + verify(mClockEventController, times).registerListeners(mView); } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java index a0295d09826f..254f9531ef83 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java @@ -41,7 +41,8 @@ import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.Clock; +import com.android.systemui.plugins.ClockController; +import com.android.systemui.plugins.ClockFaceController; import com.android.systemui.statusbar.StatusBarState; import org.junit.Before; @@ -61,7 +62,13 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { ViewGroup mMockKeyguardSliceView; @Mock - Clock mClock; + ClockController mClock; + + @Mock + ClockFaceController mSmallClock; + + @Mock + ClockFaceController mLargeClock; private FrameLayout mSmallClockFrame; private FrameLayout mLargeClockFrame; @@ -75,8 +82,11 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { when(mMockKeyguardSliceView.findViewById(R.id.keyguard_status_area)) .thenReturn(mMockKeyguardSliceView); - when(mClock.getSmallClock()).thenReturn(new TextView(getContext())); - when(mClock.getLargeClock()).thenReturn(new TextView(getContext())); + when(mClock.getSmallClock()).thenReturn(mSmallClock); + when(mClock.getLargeClock()).thenReturn(mLargeClock); + + when(mSmallClock.getView()).thenReturn(new TextView(getContext())); + when(mLargeClock.getView()).thenReturn(new TextView(getContext())); LayoutInflater layoutInflater = LayoutInflater.from(getContext()); layoutInflater.setPrivateFactory(new LayoutInflater.Factory2() { @@ -124,41 +134,49 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { public void onPluginConnected_showClock() { mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD); - assertEquals(mClock.getSmallClock().getParent(), mSmallClockFrame); - assertEquals(mClock.getLargeClock().getParent(), mLargeClockFrame); + assertEquals(mClock.getSmallClock().getView().getParent(), mSmallClockFrame); + assertEquals(mClock.getLargeClock().getView().getParent(), mLargeClockFrame); } @Test public void onPluginConnected_showSecondPluginClock() { // GIVEN a plugin has already connected - Clock otherClock = mock(Clock.class); - when(otherClock.getSmallClock()).thenReturn(new TextView(getContext())); - when(otherClock.getLargeClock()).thenReturn(new TextView(getContext())); + ClockController otherClock = mock(ClockController.class); + ClockFaceController smallClock = mock(ClockFaceController.class); + ClockFaceController largeClock = mock(ClockFaceController.class); + when(otherClock.getSmallClock()).thenReturn(smallClock); + when(otherClock.getLargeClock()).thenReturn(largeClock); + when(smallClock.getView()).thenReturn(new TextView(getContext())); + when(largeClock.getView()).thenReturn(new TextView(getContext())); mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD); mKeyguardClockSwitch.setClock(otherClock, StatusBarState.KEYGUARD); // THEN only the view from the second plugin should be a child of KeyguardClockSwitch. - assertThat(otherClock.getSmallClock().getParent()).isEqualTo(mSmallClockFrame); - assertThat(otherClock.getLargeClock().getParent()).isEqualTo(mLargeClockFrame); - assertThat(mClock.getSmallClock().getParent()).isNull(); - assertThat(mClock.getLargeClock().getParent()).isNull(); + assertThat(otherClock.getSmallClock().getView().getParent()).isEqualTo(mSmallClockFrame); + assertThat(otherClock.getLargeClock().getView().getParent()).isEqualTo(mLargeClockFrame); + assertThat(mClock.getSmallClock().getView().getParent()).isNull(); + assertThat(mClock.getLargeClock().getView().getParent()).isNull(); } @Test public void onPluginDisconnected_secondOfTwoDisconnected() { // GIVEN two plugins are connected - Clock otherClock = mock(Clock.class); - when(otherClock.getSmallClock()).thenReturn(new TextView(getContext())); - when(otherClock.getLargeClock()).thenReturn(new TextView(getContext())); + ClockController otherClock = mock(ClockController.class); + ClockFaceController smallClock = mock(ClockFaceController.class); + ClockFaceController largeClock = mock(ClockFaceController.class); + when(otherClock.getSmallClock()).thenReturn(smallClock); + when(otherClock.getLargeClock()).thenReturn(largeClock); + when(smallClock.getView()).thenReturn(new TextView(getContext())); + when(largeClock.getView()).thenReturn(new TextView(getContext())); mKeyguardClockSwitch.setClock(otherClock, StatusBarState.KEYGUARD); mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD); // WHEN the second plugin is disconnected mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD); // THEN nothing should be shown - assertThat(otherClock.getSmallClock().getParent()).isNull(); - assertThat(otherClock.getLargeClock().getParent()).isNull(); - assertThat(mClock.getSmallClock().getParent()).isNull(); - assertThat(mClock.getLargeClock().getParent()).isNull(); + assertThat(otherClock.getSmallClock().getView().getParent()).isNull(); + assertThat(otherClock.getLargeClock().getView().getParent()).isNull(); + assertThat(mClock.getSmallClock().getView().getParent()).isNull(); + assertThat(mClock.getLargeClock().getView().getParent()).isNull(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java index 69524e5a4537..5d2b0ca4e7ea 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java @@ -17,13 +17,11 @@ package com.android.keyguard; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; -import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -92,19 +90,4 @@ public class KeyguardMessageAreaControllerTest extends SysuiTestCase { mMessageAreaController.setIsVisible(true); verify(mKeyguardMessageArea).setIsVisible(true); } - - @Test - public void testSetMessageIfEmpty_empty() { - mMessageAreaController.setMessage(""); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); - verify(mKeyguardMessageArea).setMessage(R.string.keyguard_enter_your_pin); - } - - @Test - public void testSetMessageIfEmpty_notEmpty() { - mMessageAreaController.setMessage("abc"); - mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin); - verify(mKeyguardMessageArea, never()).setMessage(getContext() - .getResources().getText(R.string.keyguard_enter_your_pin)); - } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt index b89dbd98968a..b369098cafc0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt @@ -114,9 +114,8 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() { } @Test - fun onResume_testSetInitialText() { - keyguardPasswordViewController.onResume(KeyguardSecurityView.SCREEN_ON) - verify(mKeyguardMessageAreaController) - .setMessageIfEmpty(R.string.keyguard_enter_your_password) + fun startAppearAnimation() { + keyguardPasswordViewController.startAppearAnimation() + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password) } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index 3262a77b7711..9eff70487c74 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt @@ -100,16 +100,16 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { } @Test - fun onPause_clearsTextField() { + fun onPause_resetsText() { mKeyguardPatternViewController.init() mKeyguardPatternViewController.onPause() - verify(mKeyguardMessageAreaController).setMessage("") + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) } + @Test - fun onResume_setInitialText() { - mKeyguardPatternViewController.onResume(KeyguardSecurityView.SCREEN_ON) - verify(mKeyguardMessageAreaController) - .setMessageIfEmpty(R.string.keyguard_enter_your_pattern) + fun startAppearAnimation() { + mKeyguardPatternViewController.startAppearAnimation() + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index 97d556b04aa4..ce1101f389c0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -113,11 +113,4 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON); verify(mPasswordEntry).requestFocus(); } - - @Test - public void onResume_setInitialText() { - mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON); - verify(mKeyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin); - } } - diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 9e5bfe53ea05..d9efdeaea04c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -98,6 +98,6 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Test fun startAppearAnimation() { pinViewController.startAppearAnimation() - verify(keyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin) + verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin) } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index c6ebaa8bb46c..b885d546c517 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -146,6 +146,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback; + @Captor + private ArgumentCaptor<KeyguardSecurityContainer.SwipeListener> mSwipeListenerArgumentCaptor; private Configuration mConfiguration; @@ -221,15 +223,17 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { public void onResourcesUpdate_callsThroughOnRotationChange() { // Rotation is the same, shouldn't cause an update mKeyguardSecurityContainerController.updateResources(); - verify(mView, never()).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager, - mUserSwitcherController); + verify(mView, never()).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), + eq(mUserSwitcherController), + any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class)); // Update rotation. Should trigger update mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE; mKeyguardSecurityContainerController.updateResources(); - verify(mView).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager, - mUserSwitcherController); + verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), + eq(mUserSwitcherController), + any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class)); } private void touchDown() { @@ -263,8 +267,9 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { .thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController); mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern); - verify(mView).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager, - mUserSwitcherController); + verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), + eq(mUserSwitcherController), + any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class)); } @Test @@ -275,8 +280,9 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { .thenReturn((KeyguardInputViewController) mKeyguardPasswordViewController); mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern); - verify(mView).initMode(MODE_ONE_HANDED, mGlobalSettings, mFalsingManager, - mUserSwitcherController); + verify(mView).initMode(eq(MODE_ONE_HANDED), eq(mGlobalSettings), eq(mFalsingManager), + eq(mUserSwitcherController), + any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class)); } @Test @@ -285,8 +291,26 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { setupGetSecurityView(); mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password); - verify(mView).initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager, - mUserSwitcherController); + verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), + eq(mUserSwitcherController), + any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class)); + } + + @Test + public void addUserSwitcherCallback() { + ArgumentCaptor<KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback> + captor = ArgumentCaptor.forClass( + KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class); + + setupGetSecurityView(); + + mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password); + verify(mView).initMode(anyInt(), any(GlobalSettings.class), any(FalsingManager.class), + any(UserSwitcherController.class), + captor.capture()); + captor.getValue().showUnlockToContinueMessage(); + verify(mKeyguardPasswordViewControllerMock).showMessage( + getContext().getString(R.string.keyguard_unlock_to_continue), null); } @Test @@ -453,6 +477,64 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { verify(mKeyguardUpdateMonitor, never()).getUserHasTrust(anyInt()); } + @Test + public void onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(false); + setupGetSecurityView(); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardUpdateMonitor).requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); + } + + @Test + public void onSwipeUp_whenFaceDetectionIsRunning_doesNotInitiateFaceAuth() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(true); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardUpdateMonitor, never()) + .requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); + } + + @Test + public void onSwipeUp_whenFaceDetectionIsTriggered_hidesBouncerMessage() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(true); + setupGetSecurityView(); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardPasswordViewControllerMock).showMessage(null, null); + } + + @Test + public void onSwipeUp_whenFaceDetectionIsNotTriggered_retainsBouncerMessage() { + KeyguardSecurityContainer.SwipeListener registeredSwipeListener = + getRegisteredSwipeListener(); + when(mKeyguardUpdateMonitor.requestFaceAuth(true, + FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(false); + setupGetSecurityView(); + + registeredSwipeListener.onSwipeUp(); + + verify(mKeyguardPasswordViewControllerMock, never()).showMessage(null, null); + } + + private KeyguardSecurityContainer.SwipeListener getRegisteredSwipeListener() { + mKeyguardSecurityContainerController.onViewAttached(); + verify(mView).setSwipeListener(mSwipeListenerArgumentCaptor.capture()); + return mSwipeListenerArgumentCaptor.getValue(); + } + private void setupConditionsToEnableSideFpsHint() { attachView(); setSideFpsHintEnabledFromResources(true); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 43f6f1aac097..82d3ca785161 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -21,10 +21,14 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE; import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT; import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT; -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.systemBars; +import static androidx.constraintlayout.widget.ConstraintSet.CHAIN_SPREAD; +import static androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT; +import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; +import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT; + import static com.android.keyguard.KeyguardSecurityContainer.MODE_DEFAULT; import static com.android.keyguard.KeyguardSecurityContainer.MODE_ONE_HANDED; @@ -32,9 +36,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -43,19 +44,17 @@ import android.content.res.Configuration; import android.graphics.Insets; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; -import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; -import android.widget.FrameLayout; +import androidx.constraintlayout.widget.ConstraintSet; import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.user.data.source.UserRecord; import com.android.systemui.util.settings.GlobalSettings; @@ -64,8 +63,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -78,14 +75,11 @@ import java.util.ArrayList; public class KeyguardSecurityContainerTest extends SysuiTestCase { private static final int VIEW_WIDTH = 1600; - - private int mScreenWidth; - private int mFakeMeasureSpec; + private static final int VIEW_HEIGHT = 900; @Rule public MockitoRule mRule = MockitoJUnit.rule(); - @Mock private KeyguardSecurityViewFlipper mSecurityViewFlipper; @Mock private GlobalSettings mGlobalSettings; @@ -93,66 +87,39 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { private FalsingManager mFalsingManager; @Mock private UserSwitcherController mUserSwitcherController; - @Mock - private KeyguardStateController mKeyguardStateController; - @Captor - private ArgumentCaptor<FrameLayout.LayoutParams> mLayoutCaptor; private KeyguardSecurityContainer mKeyguardSecurityContainer; - private FrameLayout.LayoutParams mSecurityViewFlipperLayoutParams; @Before public void setup() { // Needed here, otherwise when mKeyguardSecurityContainer is created below, it'll cache // the real references (rather than the TestableResources that this call creates). mContext.ensureTestableResources(); - mSecurityViewFlipperLayoutParams = new FrameLayout.LayoutParams( - MATCH_PARENT, MATCH_PARENT); - when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); + mSecurityViewFlipper = new KeyguardSecurityViewFlipper(getContext()); + mSecurityViewFlipper.setId(View.generateViewId()); mKeyguardSecurityContainer = new KeyguardSecurityContainer(getContext()); + mKeyguardSecurityContainer.setRight(VIEW_WIDTH); + mKeyguardSecurityContainer.setLeft(0); + mKeyguardSecurityContainer.setTop(0); + mKeyguardSecurityContainer.setBottom(VIEW_HEIGHT); + mKeyguardSecurityContainer.setId(View.generateViewId()); mKeyguardSecurityContainer.mSecurityViewFlipper = mSecurityViewFlipper; mKeyguardSecurityContainer.addView(mSecurityViewFlipper, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); when(mUserSwitcherController.getCurrentUserName()).thenReturn("Test User"); when(mUserSwitcherController.isKeyguardShowing()).thenReturn(true); - - mScreenWidth = getUiDevice().getDisplayWidth(); - mFakeMeasureSpec = View - .MeasureSpec.makeMeasureSpec(mScreenWidth, View.MeasureSpec.EXACTLY); - } - - @Test - public void onMeasure_usesHalfWidthWithOneHandedModeEnabled() { - mKeyguardSecurityContainer.initMode(MODE_ONE_HANDED, mGlobalSettings, mFalsingManager, - mUserSwitcherController); - - int halfWidthMeasureSpec = - View.MeasureSpec.makeMeasureSpec(mScreenWidth / 2, View.MeasureSpec.EXACTLY); - mKeyguardSecurityContainer.onMeasure(mFakeMeasureSpec, mFakeMeasureSpec); - - verify(mSecurityViewFlipper).measure(halfWidthMeasureSpec, mFakeMeasureSpec); } - - @Test - public void onMeasure_usesFullWidthWithOneHandedModeDisabled() { - mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager, - mUserSwitcherController); - - mKeyguardSecurityContainer.measure(mFakeMeasureSpec, mFakeMeasureSpec); - verify(mSecurityViewFlipper).measure(mFakeMeasureSpec, mFakeMeasureSpec); - } - @Test - public void onMeasure_respectsViewInsets() { + public void testOnApplyWindowInsets() { int paddingBottom = getContext().getResources() .getDimensionPixelSize(R.dimen.keyguard_security_view_bottom_margin); int imeInsetAmount = paddingBottom + 1; int systemBarInsetAmount = 0; mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager, - mUserSwitcherController); + mUserSwitcherController, () -> {}); Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount); Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount); @@ -162,24 +129,19 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { .setInsetsIgnoringVisibility(systemBars(), systemBarInset) .build(); - // It's reduced by the max of the systembar and IME, so just subtract IME inset. - int expectedHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec( - mScreenWidth - imeInsetAmount, View.MeasureSpec.EXACTLY); - mKeyguardSecurityContainer.onApplyWindowInsets(insets); - mKeyguardSecurityContainer.measure(mFakeMeasureSpec, mFakeMeasureSpec); - verify(mSecurityViewFlipper).measure(mFakeMeasureSpec, expectedHeightMeasureSpec); + assertThat(mKeyguardSecurityContainer.getPaddingBottom()).isEqualTo(imeInsetAmount); } @Test - public void onMeasure_respectsViewInsets_largerSystembar() { + public void testOnApplyWindowInsets_largerSystembar() { int imeInsetAmount = 0; int paddingBottom = getContext().getResources() .getDimensionPixelSize(R.dimen.keyguard_security_view_bottom_margin); int systemBarInsetAmount = paddingBottom + 1; mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager, - mUserSwitcherController); + mUserSwitcherController, () -> {}); Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount); Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount); @@ -189,25 +151,23 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { .setInsetsIgnoringVisibility(systemBars(), systemBarInset) .build(); - int expectedHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec( - mScreenWidth - systemBarInsetAmount, View.MeasureSpec.EXACTLY); - mKeyguardSecurityContainer.onApplyWindowInsets(insets); - mKeyguardSecurityContainer.measure(mFakeMeasureSpec, mFakeMeasureSpec); - verify(mSecurityViewFlipper).measure(mFakeMeasureSpec, expectedHeightMeasureSpec); + assertThat(mKeyguardSecurityContainer.getPaddingBottom()).isEqualTo(systemBarInsetAmount); } - private void setupForUpdateKeyguardPosition(boolean oneHandedMode) { - int mode = oneHandedMode ? MODE_ONE_HANDED : MODE_DEFAULT; - mKeyguardSecurityContainer.initMode(mode, mGlobalSettings, mFalsingManager, - mUserSwitcherController); - - mKeyguardSecurityContainer.measure(mFakeMeasureSpec, mFakeMeasureSpec); - mKeyguardSecurityContainer.layout(0, 0, mScreenWidth, mScreenWidth); - - // Clear any interactions with the mock so we know the interactions definitely come from the - // below testing. - reset(mSecurityViewFlipper); + @Test + public void testDefaultViewMode() { + mKeyguardSecurityContainer.initMode(MODE_ONE_HANDED, mGlobalSettings, mFalsingManager, + mUserSwitcherController, () -> { + }); + mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager, + mUserSwitcherController, () -> {}); + ConstraintSet.Constraint viewFlipperConstraint = + getViewConstraint(mSecurityViewFlipper.getId()); + assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.startToStart).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.endToEnd).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.bottomToBottom).isEqualTo(PARENT_ID); } @Test @@ -217,13 +177,22 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { mKeyguardSecurityContainer.getWidth() - 1f); verify(mGlobalSettings).putInt(ONE_HANDED_KEYGUARD_SIDE, ONE_HANDED_KEYGUARD_SIDE_RIGHT); - assertSecurityTranslationX( - mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth()); + ConstraintSet.Constraint viewFlipperConstraint = + getViewConstraint(mSecurityViewFlipper.getId()); + assertThat(viewFlipperConstraint.layout.widthPercent).isEqualTo(0.5f); + assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.rightToRight).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.leftToLeft).isEqualTo(-1); mKeyguardSecurityContainer.updatePositionByTouchX(1f); verify(mGlobalSettings).putInt(ONE_HANDED_KEYGUARD_SIDE, ONE_HANDED_KEYGUARD_SIDE_LEFT); - verify(mSecurityViewFlipper).setTranslationX(0.0f); + viewFlipperConstraint = + getViewConstraint(mSecurityViewFlipper.getId()); + assertThat(viewFlipperConstraint.layout.widthPercent).isEqualTo(0.5f); + assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.leftToLeft).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.rightToRight).isEqualTo(-1); } @Test @@ -232,10 +201,16 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { mKeyguardSecurityContainer.updatePositionByTouchX( mKeyguardSecurityContainer.getWidth() - 1f); - verify(mSecurityViewFlipper, never()).setTranslationX(anyInt()); + ConstraintSet.Constraint viewFlipperConstraint = + getViewConstraint(mSecurityViewFlipper.getId()); + assertThat(viewFlipperConstraint.layout.rightToRight).isEqualTo(-1); + assertThat(viewFlipperConstraint.layout.leftToLeft).isEqualTo(-1); mKeyguardSecurityContainer.updatePositionByTouchX(1f); - verify(mSecurityViewFlipper, never()).setTranslationX(anyInt()); + viewFlipperConstraint = + getViewConstraint(mSecurityViewFlipper.getId()); + assertThat(viewFlipperConstraint.layout.rightToRight).isEqualTo(-1); + assertThat(viewFlipperConstraint.layout.leftToLeft).isEqualTo(-1); } @Test @@ -249,17 +224,31 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { setupUserSwitcher(); mKeyguardSecurityContainer.onConfigurationChanged(landscapeConfig); - // THEN views are oriented side by side - assertSecurityGravity(Gravity.LEFT | Gravity.BOTTOM); - assertUserSwitcherGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); - assertSecurityTranslationX( - mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth()); - assertUserSwitcherTranslationX(0f); - + ConstraintSet.Constraint viewFlipperConstraint = + getViewConstraint(mSecurityViewFlipper.getId()); + ConstraintSet.Constraint userSwitcherConstraint = + getViewConstraint(R.id.keyguard_bouncer_user_switcher); + assertThat(viewFlipperConstraint.layout.rightToRight).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.leftToRight).isEqualTo( + R.id.keyguard_bouncer_user_switcher); + assertThat(userSwitcherConstraint.layout.leftToLeft).isEqualTo(PARENT_ID); + assertThat(userSwitcherConstraint.layout.rightToLeft).isEqualTo( + mSecurityViewFlipper.getId()); + assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.bottomToBottom).isEqualTo(PARENT_ID); + assertThat(userSwitcherConstraint.layout.topToTop).isEqualTo(PARENT_ID); + assertThat(userSwitcherConstraint.layout.bottomToBottom).isEqualTo(PARENT_ID); + assertThat(userSwitcherConstraint.layout.bottomMargin).isEqualTo( + getContext().getResources().getDimensionPixelSize( + R.dimen.bouncer_user_switcher_y_trans)); + assertThat(viewFlipperConstraint.layout.horizontalChainStyle).isEqualTo(CHAIN_SPREAD); + assertThat(userSwitcherConstraint.layout.horizontalChainStyle).isEqualTo(CHAIN_SPREAD); + assertThat(viewFlipperConstraint.layout.mHeight).isEqualTo(MATCH_CONSTRAINT); + assertThat(userSwitcherConstraint.layout.mHeight).isEqualTo(MATCH_CONSTRAINT); } @Test - public void testUserSwitcherModeViewGravityPortrait() { + public void testUserSwitcherModeViewPositionPortrait() { // GIVEN one user has been setup and in landscape when(mUserSwitcherController.getUsers()).thenReturn(buildUserRecords(1)); Configuration portraitConfig = configuration(ORIENTATION_PORTRAIT); @@ -267,15 +256,28 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { // WHEN UserSwitcherViewMode is initialized and config has changed setupUserSwitcher(); - reset(mSecurityViewFlipper); - when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); mKeyguardSecurityContainer.onConfigurationChanged(portraitConfig); - // THEN views are both centered horizontally - assertSecurityGravity(Gravity.CENTER_HORIZONTAL); - assertUserSwitcherGravity(Gravity.CENTER_HORIZONTAL); - assertSecurityTranslationX(0); - assertUserSwitcherTranslationX(0); + ConstraintSet.Constraint viewFlipperConstraint = + getViewConstraint(mSecurityViewFlipper.getId()); + ConstraintSet.Constraint userSwitcherConstraint = + getViewConstraint(R.id.keyguard_bouncer_user_switcher); + + assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.bottomToBottom).isEqualTo(PARENT_ID); + assertThat(userSwitcherConstraint.layout.topToTop).isEqualTo(PARENT_ID); + assertThat(userSwitcherConstraint.layout.topMargin).isEqualTo( + getContext().getResources().getDimensionPixelSize( + R.dimen.bouncer_user_switcher_y_trans)); + assertThat(viewFlipperConstraint.layout.leftToLeft).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.rightToRight).isEqualTo(PARENT_ID); + assertThat(userSwitcherConstraint.layout.leftToLeft).isEqualTo(PARENT_ID); + assertThat(userSwitcherConstraint.layout.rightToRight).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.verticalChainStyle).isEqualTo(CHAIN_SPREAD); + assertThat(userSwitcherConstraint.layout.verticalChainStyle).isEqualTo(CHAIN_SPREAD); + assertThat(viewFlipperConstraint.layout.mHeight).isEqualTo(MATCH_CONSTRAINT); + assertThat(userSwitcherConstraint.layout.mHeight).isEqualTo(WRAP_CONTENT); + assertThat(userSwitcherConstraint.layout.mWidth).isEqualTo(WRAP_CONTENT); } @Test @@ -337,9 +339,9 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_LEFT); mKeyguardSecurityContainer.onConfigurationChanged(new Configuration()); - assertSecurityTranslationX(0); - assertUserSwitcherTranslationX( - mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth()); + ConstraintSet.Constraint viewFlipperConstraint = getViewConstraint( + mSecurityViewFlipper.getId()); + assertThat(viewFlipperConstraint.layout.leftToLeft).isEqualTo(PARENT_ID); } private Configuration configuration(@Configuration.Orientation int orientation) { @@ -348,28 +350,6 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { return config; } - private void assertSecurityTranslationX(float translation) { - verify(mSecurityViewFlipper).setTranslationX(translation); - } - - private void assertUserSwitcherTranslationX(float translation) { - ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById( - R.id.keyguard_bouncer_user_switcher); - assertThat(userSwitcher.getTranslationX()).isEqualTo(translation); - } - - private void assertUserSwitcherGravity(@Gravity.GravityFlags int gravity) { - ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById( - R.id.keyguard_bouncer_user_switcher); - assertThat(((FrameLayout.LayoutParams) userSwitcher.getLayoutParams()).gravity) - .isEqualTo(gravity); - } - - private void assertSecurityGravity(@Gravity.GravityFlags int gravity) { - verify(mSecurityViewFlipper, atLeastOnce()).setLayoutParams(mLayoutCaptor.capture()); - assertThat(mLayoutCaptor.getValue().gravity).isEqualTo(gravity); - } - private void setViewWidth(int width) { mKeyguardSecurityContainer.setRight(width); mKeyguardSecurityContainer.setLeft(0); @@ -398,10 +378,7 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { private void setupUserSwitcher() { when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_RIGHT); mKeyguardSecurityContainer.initMode(KeyguardSecurityContainer.MODE_USER_SWITCHER, - mGlobalSettings, mFalsingManager, mUserSwitcherController); - // reset mSecurityViewFlipper so setup doesn't influence test verifications - reset(mSecurityViewFlipper); - when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); + mGlobalSettings, mFalsingManager, mUserSwitcherController, () -> {}); } private ArrayList<UserRecord> buildUserRecords(int count) { @@ -411,8 +388,22 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { 0 /* flags */); users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */, false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */)); + false /* isAddSupervisedUser */, null /* enforcedAdmin */, + false /* isManageUsers */)); } return users; } + + private void setupForUpdateKeyguardPosition(boolean oneHandedMode) { + int mode = oneHandedMode ? MODE_ONE_HANDED : MODE_DEFAULT; + mKeyguardSecurityContainer.initMode(mode, mGlobalSettings, mFalsingManager, + mUserSwitcherController, () -> {}); + } + + /** Get the ConstraintLayout constraint of the view. */ + private ConstraintSet.Constraint getViewConstraint(int viewId) { + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.clone(mKeyguardSecurityContainer); + return constraintSet.getConstraint(viewId); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java index 4dcaa7cf8c09..c94c97c9b638 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java @@ -16,12 +16,16 @@ package com.android.keyguard; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.graphics.Rect; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -108,4 +112,16 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase { configurationListenerArgumentCaptor.getValue().onLocaleListChanged(); verify(mKeyguardClockSwitchController).onLocaleListChanged(); } + + @Test + public void getClockAnimations_forwardsToClockSwitch() { + ClockAnimations mockClockAnimations = mock(ClockAnimations.class); + when(mKeyguardClockSwitchController.getClockAnimations()).thenReturn(mockClockAnimations); + + Rect r1 = new Rect(1, 2, 3, 4); + Rect r2 = new Rect(5, 6, 7, 8); + mController.getClockAnimations().onPositionUpdated(r1, r2, 0.3f); + + verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.3f); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 9c64c1b06a17..ebfb4d4d1778 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -25,6 +25,7 @@ import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT; +import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED; import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT; import static com.google.common.truth.Truth.assertThat; @@ -59,6 +60,7 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; +import android.hardware.SensorPrivacyManager; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricSourceType; @@ -79,13 +81,13 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.service.dreams.IDreamManager; import android.telephony.ServiceState; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; -import android.testing.TestableContext; import android.testing.TestableLooper; import com.android.dx.mockito.inline.extended.ExtendedMockito; @@ -170,6 +172,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private DevicePolicyManager mDevicePolicyManager; @Mock + private IDreamManager mDreamManager; + @Mock private KeyguardBypassController mKeyguardBypassController; @Mock private SubscriptionManager mSubscriptionManager; @@ -178,6 +182,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private TelephonyManager mTelephonyManager; @Mock + private SensorPrivacyManager mSensorPrivacyManager; + @Mock private StatusBarStateController mStatusBarStateController; @Mock private AuthController mAuthController; @@ -219,7 +225,6 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { private TestableLooper mTestableLooper; private Handler mHandler; private TestableKeyguardUpdateMonitor mKeyguardUpdateMonitor; - private TestableContext mSpiedContext; private MockitoSession mMockitoSession; private StatusBarStateController.StateListener mStatusBarStateListener; private IBiometricEnabledOnKeyguardCallback mBiometricEnabledOnKeyguardCallback; @@ -228,9 +233,6 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Before public void setup() throws RemoteException { MockitoAnnotations.initMocks(this); - mSpiedContext = spy(mContext); - when(mPackageManager.hasSystemFeature(anyString())).thenReturn(true); - when(mSpiedContext.getPackageManager()).thenReturn(mPackageManager); when(mActivityService.getCurrentUser()).thenReturn(mCurrentUserInfo); when(mActivityService.getCurrentUserId()).thenReturn(mCurrentUserId); when(mFaceManager.isHardwareDetected()).thenReturn(true); @@ -279,14 +281,6 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .thenReturn(new ServiceState()); when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings); when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false); - mSpiedContext.addMockSystemService(TrustManager.class, mTrustManager); - mSpiedContext.addMockSystemService(FingerprintManager.class, mFingerprintManager); - mSpiedContext.addMockSystemService(BiometricManager.class, mBiometricManager); - mSpiedContext.addMockSystemService(FaceManager.class, mFaceManager); - mSpiedContext.addMockSystemService(UserManager.class, mUserManager); - mSpiedContext.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager); - mSpiedContext.addMockSystemService(SubscriptionManager.class, mSubscriptionManager); - mSpiedContext.addMockSystemService(TelephonyManager.class, mTelephonyManager); mMockitoSession = ExtendedMockito.mockitoSession() .spyStatic(SubscriptionManager.class) @@ -301,7 +295,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mTestableLooper = TestableLooper.get(this); allowTestableLooperAsMainThread(); - mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext); + mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); verify(mBiometricManager) .registerEnabledOnKeyguardCallback(mBiometricEnabledCallbackArgCaptor.capture()); @@ -356,7 +350,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mTelephonyManager.getSimState(anyInt())).thenReturn(state); when(mSubscriptionManager.getSubscriptionIds(anyInt())).thenReturn(new int[]{subId}); - KeyguardUpdateMonitor testKUM = new TestableKeyguardUpdateMonitor(mSpiedContext); + KeyguardUpdateMonitor testKUM = new TestableKeyguardUpdateMonitor(mContext); mTestableLooper.processAllMessages(); @@ -614,7 +608,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { public void testTriesToAuthenticate_whenKeyguard() { mKeyguardUpdateMonitor.dispatchStartedWakingUp(); mTestableLooper.processAllMessages(); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); } @@ -624,7 +618,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.dispatchStartedWakingUp(); mTestableLooper.processAllMessages(); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); } @@ -637,7 +631,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.dispatchStartedWakingUp(); mTestableLooper.processAllMessages(); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); } @@ -654,6 +648,36 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT); } + @Test + public void requestFaceAuth_whenFaceAuthWasStarted_returnsTrue() throws RemoteException { + // This satisfies all the preconditions to run face auth. + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + bouncerFullyVisibleAndNotGoingToSleep(); + mTestableLooper.processAllMessages(); + + boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true, + NOTIFICATION_PANEL_CLICKED); + + assertThat(didFaceAuthRun).isTrue(); + } + + @Test + public void requestFaceAuth_whenFaceAuthWasNotStarted_returnsFalse() throws RemoteException { + // This ensures face auth won't run. + biometricsDisabledForCurrentUser(); + mTestableLooper.processAllMessages(); + + boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true, + NOTIFICATION_PANEL_CLICKED); + + assertThat(didFaceAuthRun).isFalse(); + } + private void testStrongAuthExceptOnBouncer(int strongAuth) { when(mKeyguardBypassController.canBypass()).thenReturn(true); mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController); @@ -661,7 +685,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.dispatchStartedWakingUp(); mTestableLooper.processAllMessages(); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); // Stop scanning when bouncer becomes visible @@ -675,7 +699,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testTriesToAuthenticate_whenAssistant() { - mKeyguardUpdateMonitor.setKeyguardOccluded(true); + mKeyguardUpdateMonitor.setKeyguardShowing(true, true); mKeyguardUpdateMonitor.setAssistantVisible(true); verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); @@ -690,7 +714,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */, KeyguardUpdateMonitor.getCurrentUser(), 0 /* flags */, new ArrayList<>()); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); } @@ -700,7 +724,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mTestableLooper.processAllMessages(); mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */, KeyguardUpdateMonitor.getCurrentUser(), 0 /* flags */, new ArrayList<>()); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); } @@ -712,7 +736,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn( KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); } @@ -724,7 +748,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn( KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); } @@ -745,7 +769,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { public void testFaceAndFingerprintLockout_onlyFace() { mKeyguardUpdateMonitor.dispatchStartedWakingUp(); mTestableLooper.processAllMessages(); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); faceAuthLockedOut(); @@ -756,7 +780,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { public void testFaceAndFingerprintLockout_onlyFingerprint() { mKeyguardUpdateMonitor.dispatchStartedWakingUp(); mTestableLooper.processAllMessages(); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback .onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT_PERMANENT, ""); @@ -768,7 +792,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { public void testFaceAndFingerprintLockout() { mKeyguardUpdateMonitor.dispatchStartedWakingUp(); mTestableLooper.processAllMessages(); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); faceAuthLockedOut(); mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback @@ -867,7 +891,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.dispatchStartedWakingUp(); mTestableLooper.processAllMessages(); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); verify(mFingerprintManager).authenticate(any(), any(), any(), any(), anyInt(), anyInt(), @@ -912,7 +936,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mTestableLooper.processAllMessages(); List<SubscriptionInfo> listToVerify = mKeyguardUpdateMonitor - .getFilteredSubscriptionInfo(false); + .getFilteredSubscriptionInfo(); assertThat(listToVerify.size()).isEqualTo(1); assertThat(listToVerify.get(0)).isEqualTo(TEST_SUBSCRIPTION_2); } @@ -1040,8 +1064,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { public void testOccludingAppFingerprintListeningState() { // GIVEN keyguard isn't visible (app occluding) mKeyguardUpdateMonitor.dispatchStartedWakingUp(); - mKeyguardUpdateMonitor.setKeyguardOccluded(true); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(false); + mKeyguardUpdateMonitor.setKeyguardShowing(true, true); when(mStrongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true); // THEN we shouldn't listen for fingerprints @@ -1056,8 +1079,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { public void testOccludingAppRequestsFingerprint() { // GIVEN keyguard isn't visible (app occluding) mKeyguardUpdateMonitor.dispatchStartedWakingUp(); - mKeyguardUpdateMonitor.setKeyguardOccluded(true); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(false); + mKeyguardUpdateMonitor.setKeyguardShowing(true, true); // WHEN an occluding app requests fp mKeyguardUpdateMonitor.requestFingerprintAuthOnOccludingApp(true); @@ -1149,7 +1171,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { setKeyguardBouncerVisibility(false /* isVisible */); mKeyguardUpdateMonitor.dispatchStartedWakingUp(); when(mKeyguardBypassController.canBypass()).thenReturn(true); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); // WHEN status bar state reports a change to the keyguard that would normally indicate to // start running face auth @@ -1160,8 +1182,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // listening state to update assertThat(mKeyguardUpdateMonitor.isFaceDetectionRunning()).isEqualTo(false); - // WHEN biometric listening state is updated - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + // WHEN biometric listening state is updated when showing state changes from false => true + mKeyguardUpdateMonitor.setKeyguardShowing(false, false); + mKeyguardUpdateMonitor.setKeyguardShowing(true, false); // THEN face unlock is running assertThat(mKeyguardUpdateMonitor.isFaceDetectionRunning()).isEqualTo(true); @@ -1202,9 +1225,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testShouldListenForFace_whenFaceManagerNotAvailable_returnsFalse() { cleanupKeyguardUpdateMonitor(); - mSpiedContext.addMockSystemService(FaceManager.class, null); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(false); - mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext); + mFaceManager = null; + + mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse(); } @@ -1258,7 +1281,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // This disables face auth when(mUserManager.isPrimaryUser()).thenReturn(false); mKeyguardUpdateMonitor = - new TestableKeyguardUpdateMonitor(mSpiedContext); + new TestableKeyguardUpdateMonitor(mContext); // Face auth should run when the following is true. keyguardNotGoingAway(); @@ -1482,6 +1505,27 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void testShouldListenForFace_udfpsBouncerIsShowingButDeviceGoingToSleep_returnsFalse() + throws RemoteException { + // Preconditions for face auth to run + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + deviceNotGoingToSleep(); + mKeyguardUpdateMonitor.setUdfpsBouncerShowing(true); + mTestableLooper.processAllMessages(); + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + + deviceGoingToSleep(); + mTestableLooper.processAllMessages(); + + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse(); + } + + @Test public void testShouldListenForFace_whenFaceIsLockedOut_returnsFalse() throws RemoteException { // Preconditions for face auth to run @@ -1506,7 +1550,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { public void testFingerprintCanAuth_whenCancellationNotReceivedAndAuthFailed() { mKeyguardUpdateMonitor.dispatchStartedWakingUp(); mTestableLooper.processAllMessages(); - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + keyguardIsVisible(); verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); verify(mFingerprintManager).authenticate(any(), any(), any(), any(), anyInt(), anyInt(), @@ -1515,7 +1559,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.onFaceAuthenticated(0, false); // Make sure keyguard is going away after face auth attempt, and that it calls // updateBiometricStateListeningState. - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(false); + mKeyguardUpdateMonitor.setKeyguardShowing(false, false); mTestableLooper.processAllMessages(); verify(mHandler).postDelayed(mKeyguardUpdateMonitor.mFpCancelNotReceived, @@ -1527,15 +1571,16 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { verify(mHandler, times(1)).removeCallbacks(mKeyguardUpdateMonitor.mFpCancelNotReceived); mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */); mTestableLooper.processAllMessages(); - assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(anyBoolean())).isEqualTo(true); + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isEqualTo(true); + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isEqualTo(true); } @Test public void testFingerAcquired_wakesUpPowerManager() { cleanupKeyguardUpdateMonitor(); - mSpiedContext.getOrCreateTestableResources().addOverride( + mContext.getOrCreateTestableResources().addOverride( com.android.internal.R.bool.kg_wake_on_acquire_start, true); - mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext); + mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); fingerprintAcquireStart(); verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString()); @@ -1544,9 +1589,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testFingerAcquired_doesNotWakeUpPowerManager() { cleanupKeyguardUpdateMonitor(); - mSpiedContext.getOrCreateTestableResources().addOverride( + mContext.getOrCreateTestableResources().addOverride( com.android.internal.R.bool.kg_wake_on_acquire_start, false); - mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext); + mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); fingerprintAcquireStart(); verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); @@ -1574,7 +1619,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } private void keyguardIsVisible() { - mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(true); + mKeyguardUpdateMonitor.setKeyguardShowing(true, false); } private void triggerAuthInterrupt() { @@ -1667,6 +1712,10 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.dispatchFinishedGoingToSleep(/* value doesn't matter */1); } + private void deviceGoingToSleep() { + mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(/* value doesn't matter */1); + } + private void deviceIsInteractive() { mKeyguardUpdateMonitor.dispatchStartedWakingUp(); } @@ -1716,7 +1765,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mAuthController, mTelephonyListenerManager, mInteractionJankMonitor, mLatencyTracker, mActiveUnlockConfig, mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker, - mPowerManager); + mPowerManager, mTrustManager, mSubscriptionManager, mUserManager, + mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager, + mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager); setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java new file mode 100644 index 000000000000..ae8f419d4e64 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.AnimatedStateListDrawable; +import android.util.Pair; +import android.view.View; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.AuthRippleController; +import com.android.systemui.doze.util.BurnInHelperKt; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.After; +import org.junit.Before; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +public class LockIconViewControllerBaseTest extends SysuiTestCase { + protected static final String UNLOCKED_LABEL = "unlocked"; + protected static final int PADDING = 10; + + protected MockitoSession mStaticMockSession; + + protected @Mock LockIconView mLockIconView; + protected @Mock AnimatedStateListDrawable mIconDrawable; + protected @Mock Context mContext; + protected @Mock Resources mResources; + protected @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager; + protected @Mock StatusBarStateController mStatusBarStateController; + protected @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor; + protected @Mock KeyguardViewController mKeyguardViewController; + protected @Mock KeyguardStateController mKeyguardStateController; + protected @Mock FalsingManager mFalsingManager; + protected @Mock AuthController mAuthController; + protected @Mock DumpManager mDumpManager; + protected @Mock AccessibilityManager mAccessibilityManager; + protected @Mock ConfigurationController mConfigurationController; + protected @Mock VibratorHelper mVibrator; + protected @Mock AuthRippleController mAuthRippleController; + protected @Mock FeatureFlags mFeatureFlags; + protected @Mock KeyguardTransitionRepository mTransitionRepository; + protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock()); + + protected LockIconViewController mUnderTest; + + // Capture listeners so that they can be used to send events + @Captor protected ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor = + ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); + + @Captor protected ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor = + ArgumentCaptor.forClass(KeyguardStateController.Callback.class); + protected KeyguardStateController.Callback mKeyguardStateCallback; + + @Captor protected ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor = + ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); + protected StatusBarStateController.StateListener mStatusBarStateListener; + + @Captor protected ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor; + protected AuthController.Callback mAuthControllerCallback; + + @Captor protected ArgumentCaptor<KeyguardUpdateMonitorCallback> + mKeyguardUpdateMonitorCallbackCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + protected KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; + + @Captor protected ArgumentCaptor<Point> mPointCaptor; + + @Before + public void setUp() throws Exception { + mStaticMockSession = mockitoSession() + .mockStatic(BurnInHelperKt.class) + .strictness(Strictness.LENIENT) + .startMocking(); + MockitoAnnotations.initMocks(this); + + setupLockIconViewMocks(); + when(mContext.getResources()).thenReturn(mResources); + when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager); + Rect windowBounds = new Rect(0, 0, 800, 1200); + when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds); + when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL); + when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable); + when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING); + when(mAuthController.getScaleFactor()).thenReturn(1f); + + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); + when(mStatusBarStateController.isDozing()).thenReturn(false); + when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); + + mUnderTest = new LockIconViewController( + mLockIconView, + mStatusBarStateController, + mKeyguardUpdateMonitor, + mKeyguardViewController, + mKeyguardStateController, + mFalsingManager, + mAuthController, + mDumpManager, + mAccessibilityManager, + mConfigurationController, + mDelayableExecutor, + mVibrator, + mAuthRippleController, + mResources, + new KeyguardTransitionInteractor(mTransitionRepository), + new KeyguardInteractor(new FakeKeyguardRepository()), + mFeatureFlags + ); + } + + @After + public void tearDown() { + mStaticMockSession.finishMocking(); + } + + protected Pair<Float, Point> setupUdfps() { + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true); + final Point udfpsLocation = new Point(50, 75); + final float radius = 33f; + when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation); + when(mAuthController.getUdfpsRadius()).thenReturn(radius); + + return new Pair(radius, udfpsLocation); + } + + protected void setupShowLockIcon() { + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); + when(mStatusBarStateController.isDozing()).thenReturn(false); + when(mStatusBarStateController.getDozeAmount()).thenReturn(0f); + when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); + when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false); + } + + protected void captureAuthControllerCallback() { + verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture()); + mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue(); + } + + protected void captureKeyguardStateCallback() { + verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture()); + mKeyguardStateCallback = mKeyguardStateCaptor.getValue(); + } + + protected void captureStatusBarStateListener() { + verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture()); + mStatusBarStateListener = mStatusBarStateCaptor.getValue(); + } + + protected void captureKeyguardUpdateMonitorCallback() { + verify(mKeyguardUpdateMonitor).registerCallback( + mKeyguardUpdateMonitorCallbackCaptor.capture()); + mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue(); + } + + protected void setupLockIconViewMocks() { + when(mLockIconView.getResources()).thenReturn(mResources); + when(mLockIconView.getContext()).thenReturn(mContext); + } + + protected void resetLockIconView() { + reset(mLockIconView); + setupLockIconViewMocks(); + } + + protected void init(boolean useMigrationFlag) { + when(mFeatureFlags.isEnabled(DOZING_MIGRATION_1)).thenReturn(useMigrationFlag); + mUnderTest.init(); + + verify(mLockIconView, atLeast(1)).addOnAttachStateChangeListener(mAttachCaptor.capture()); + mAttachCaptor.getValue().onViewAttachedToWindow(mLockIconView); + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java new file mode 100644 index 000000000000..da40595a4f12 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard; + +import static com.android.keyguard.LockIconView.ICON_LOCK; +import static com.android.keyguard.LockIconView.ICON_UNLOCK; + +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.Point; +import android.hardware.biometrics.BiometricSourceType; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Pair; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.doze.util.BurnInHelperKt; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class LockIconViewControllerTest extends LockIconViewControllerBaseTest { + + @Test + public void testUpdateFingerprintLocationOnInit() { + // GIVEN fp sensor location is available pre-attached + Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location + + // WHEN lock icon view controller is initialized and attached + init(/* useMigrationFlag= */false); + + // THEN lock icon view location is updated to the udfps location with UDFPS radius + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testUpdatePaddingBasedOnResolutionScale() { + // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5 + Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location + when(mAuthController.getScaleFactor()).thenReturn(5f); + + // WHEN lock icon view controller is initialized and attached + init(/* useMigrationFlag= */false); + + // THEN lock icon view location is updated with the scaled radius + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING * 5)); + } + + @Test + public void testUpdateLockIconLocationOnAuthenticatorsRegistered() { + // GIVEN fp sensor location is not available pre-init + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); + init(/* useMigrationFlag= */false); + resetLockIconView(); // reset any method call counts for when we verify method calls later + + // GIVEN fp sensor location is available post-attached + captureAuthControllerCallback(); + Pair<Float, Point> udfps = setupUdfps(); + + // WHEN all authenticators are registered + mAuthControllerCallback.onAllAuthenticatorsRegistered(); + mDelayableExecutor.runAllReady(); + + // THEN lock icon view location is updated with the same coordinates as auth controller vals + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testUpdateLockIconLocationOnUdfpsLocationChanged() { + // GIVEN fp sensor location is not available pre-init + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); + init(/* useMigrationFlag= */false); + resetLockIconView(); // reset any method call counts for when we verify method calls later + + // GIVEN fp sensor location is available post-attached + captureAuthControllerCallback(); + Pair<Float, Point> udfps = setupUdfps(); + + // WHEN udfps location changes + mAuthControllerCallback.onUdfpsLocationChanged(); + mDelayableExecutor.runAllReady(); + + // THEN lock icon view location is updated with the same coordinates as auth controller vals + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() { + // GIVEN Udpfs sensor location is available + setupUdfps(); + + // WHEN the view is attached + init(/* useMigrationFlag= */false); + + // THEN the lock icon view background should be enabled + verify(mLockIconView).setUseBackground(true); + } + + @Test + public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() { + // GIVEN Udfps sensor location is not supported + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + + // WHEN the view is attached + init(/* useMigrationFlag= */false); + + // THEN the lock icon view background should be disabled + verify(mLockIconView).setUseBackground(false); + } + + @Test + public void testUnlockIconShows_biometricUnlockedTrue() { + // GIVEN UDFPS sensor location is available + setupUdfps(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureKeyguardUpdateMonitorCallback(); + + // GIVEN user has unlocked with a biometric auth (ie: face auth) + when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true); + reset(mLockIconView); + + // WHEN face auth's biometric running state changes + mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false, + BiometricSourceType.FACE); + + // THEN the unlock icon is shown + verify(mLockIconView).setContentDescription(UNLOCKED_LABEL); + } + + @Test + public void testLockIconStartState() { + // GIVEN lock icon state + setupShowLockIcon(); + + // WHEN lock icon controller is initialized + init(/* useMigrationFlag= */false); + + // THEN the lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, false); + } + + @Test + public void testLockIcon_updateToUnlock() { + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureKeyguardStateCallback(); + reset(mLockIconView); + + // WHEN the unlocked state changes to canDismissLockScreen=true + when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); + mKeyguardStateCallback.onUnlockedChanged(); + + // THEN the unlock should show + verify(mLockIconView).updateIcon(ICON_UNLOCK, false); + } + + @Test + public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() { + // GIVEN udfps not enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false); + + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN the dozing state changes + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + + // THEN the icon is cleared + verify(mLockIconView).clearIcon(); + } + + @Test + public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() { + // GIVEN udfps enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); + + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN the dozing state changes + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + + // THEN the AOD lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, true); + } + + @Test + public void testBurnInOffsetsUpdated_onDozeAmountChanged() { + // GIVEN udfps enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); + + // GIVEN burn-in offset = 5 + int burnInOffset = 5; + when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset); + + // GIVEN starting state for the lock icon (keyguard) + setupShowLockIcon(); + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN dozing updates + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + mStatusBarStateListener.onDozeAmountChanged(1f, 1f); + + // THEN the view's translation is updated to use the AoD burn-in offsets + verify(mLockIconView).setTranslationY(burnInOffset); + verify(mLockIconView).setTranslationX(burnInOffset); + reset(mLockIconView); + + // WHEN the device is no longer dozing + mStatusBarStateListener.onDozingChanged(false /* isDozing */); + mStatusBarStateListener.onDozeAmountChanged(0f, 0f); + + // THEN the view is updated to NO translation (no burn-in offsets anymore) + verify(mLockIconView).setTranslationY(0); + verify(mLockIconView).setTranslationX(0); + + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt new file mode 100644 index 000000000000..d2c54b4cc0e7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.keyguard.LockIconView.ICON_LOCK +import com.android.systemui.doze.util.getBurnInOffset +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class LockIconViewControllerWithCoroutinesTest : LockIconViewControllerBaseTest() { + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() = + runBlocking(IMMEDIATE) { + // GIVEN udfps not enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false) + + // GIVEN starting state for the lock icon + setupShowLockIcon() + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN the dozing state changes + mUnderTest.mIsDozingCallback.accept(true) + + // THEN the icon is cleared + verify(mLockIconView).clearIcon() + } + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testLockIcon_updateToAodLock_whenUdfpsEnrolled() = + runBlocking(IMMEDIATE) { + // GIVEN udfps enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true) + + // GIVEN starting state for the lock icon + setupShowLockIcon() + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN the dozing state changes + mUnderTest.mIsDozingCallback.accept(true) + + // THEN the AOD lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, true) + } + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testBurnInOffsetsUpdated_onDozeAmountChanged() = + runBlocking(IMMEDIATE) { + // GIVEN udfps enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true) + + // GIVEN burn-in offset = 5 + val burnInOffset = 5 + whenever(getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset) + + // GIVEN starting state for the lock icon (keyguard) + setupShowLockIcon() + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN dozing updates + mUnderTest.mIsDozingCallback.accept(true) + mUnderTest.mDozeTransitionCallback.accept(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) + + // THEN the view's translation is updated to use the AoD burn-in offsets + verify(mLockIconView).setTranslationY(burnInOffset.toFloat()) + verify(mLockIconView).setTranslationX(burnInOffset.toFloat()) + reset(mLockIconView) + + // WHEN the device is no longer dozing + mUnderTest.mIsDozingCallback.accept(false) + mUnderTest.mDozeTransitionCallback.accept(TransitionStep(AOD, LOCKSCREEN, 0f, FINISHED)) + + // THEN the view is updated to NO translation (no burn-in offsets anymore) + verify(mLockIconView).setTranslationY(0f) + verify(mLockIconView).setTranslationX(0f) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index df10dfe9f160..181839ab512f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -36,6 +36,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -231,7 +232,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Override - protected void onConfigurationChanged(Configuration newConfig) { + public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mExecutor.runAllReady(); } @@ -255,6 +256,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { }); mScreenDecorations.mDisplayInfo = mDisplayInfo; doReturn(1f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio(); + doNothing().when(mScreenDecorations).updateOverlayProviderViews(any()); reset(mTunerService); try { @@ -1005,18 +1007,13 @@ public class ScreenDecorationsTest extends SysuiTestCase { assertEquals(new Size(3, 3), resDelegate.getTopRoundedSize()); assertEquals(new Size(4, 4), resDelegate.getBottomRoundedSize()); - setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, - getTestsDrawable(com.android.systemui.tests.R.drawable.rounded4px) - /* roundedTopDrawable */, - getTestsDrawable(com.android.systemui.tests.R.drawable.rounded5px) - /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* privacyDot */, false /* faceScanning*/); + doReturn(2f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio(); mDisplayInfo.rotation = Surface.ROTATION_270; mScreenDecorations.onConfigurationChanged(null); - assertEquals(new Size(4, 4), resDelegate.getTopRoundedSize()); - assertEquals(new Size(5, 5), resDelegate.getBottomRoundedSize()); + assertEquals(new Size(6, 6), resDelegate.getTopRoundedSize()); + assertEquals(new Size(8, 8), resDelegate.getBottomRoundedSize()); } @Test @@ -1293,51 +1290,6 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Test - public void testOnDisplayChanged_hwcLayer() { - setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, - null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */); - final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport(); - decorationSupport.format = PixelFormat.R_8; - doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport(); - - // top cutout - mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP)); - - mScreenDecorations.start(); - - final ScreenDecorHwcLayer hwcLayer = mScreenDecorations.mScreenDecorHwcLayer; - spyOn(hwcLayer); - doReturn(mDisplay).when(hwcLayer).getDisplay(); - - mScreenDecorations.mDisplayListener.onDisplayChanged(1); - - verify(hwcLayer, times(1)).onDisplayChanged(any()); - } - - @Test - public void testOnDisplayChanged_nonHwcLayer() { - setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, - null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */); - - // top cutout - mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP)); - - mScreenDecorations.start(); - - final ScreenDecorations.DisplayCutoutView cutoutView = (ScreenDecorations.DisplayCutoutView) - mScreenDecorations.getOverlayView(R.id.display_cutout); - assertNotNull(cutoutView); - spyOn(cutoutView); - doReturn(mDisplay).when(cutoutView).getDisplay(); - - mScreenDecorations.mDisplayListener.onDisplayChanged(1); - - verify(cutoutView, times(1)).onDisplayChanged(any()); - } - - @Test public void testHasSameProvidersWithNullOverlays() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt index 8fc048920ea7..6ab54a374d30 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt @@ -718,7 +718,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { } @Test - fun animatesViewRemovalFromStartToEnd() { + fun animatesViewRemovalFromStartToEnd_viewHasSiblings() { setUpRootWithChildren() val child = rootView.getChildAt(0) @@ -742,6 +742,35 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { } @Test + fun animatesViewRemovalFromStartToEnd_viewHasNoSiblings() { + rootView = LinearLayout(mContext) + (rootView as LinearLayout).orientation = LinearLayout.HORIZONTAL + (rootView as LinearLayout).weightSum = 1f + + val onlyChild = View(mContext) + rootView.addView(onlyChild) + forceLayout() + + val success = ViewHierarchyAnimator.animateRemoval( + onlyChild, + destination = ViewHierarchyAnimator.Hotspot.LEFT, + interpolator = Interpolators.LINEAR + ) + + assertTrue(success) + assertNotNull(onlyChild.getTag(R.id.tag_animator)) + checkBounds(onlyChild, l = 0, t = 0, r = 200, b = 100) + advanceAnimation(onlyChild, 0.5f) + checkBounds(onlyChild, l = 0, t = 0, r = 100, b = 100) + advanceAnimation(onlyChild, 1.0f) + checkBounds(onlyChild, l = 0, t = 0, r = 0, b = 100) + endAnimation(rootView) + endAnimation(onlyChild) + assertEquals(0, rootView.childCount) + assertFalse(onlyChild in rootView.children) + } + + @Test fun animatesViewRemovalRespectingDestination() { // CENTER setUpRootWithChildren() @@ -906,6 +935,251 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { checkBounds(remainingChild, l = 0, t = 0, r = 100, b = 100) } + /* ******** start of animatesViewRemoval_includeMarginsTrue tests ******** */ + @Test + fun animatesViewRemoval_includeMarginsTrue_center() { + setUpRootWithChildren(includeMarginsOnFirstChild = true) + val removedChild = rootView.getChildAt(0) + val originalLeft = removedChild.left + val originalTop = removedChild.top + val originalRight = removedChild.right + val originalBottom = removedChild.bottom + + val success = ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.CENTER, + includeMargins = true, + ) + forceLayout() + + assertTrue(success) + assertNotNull(removedChild.getTag(R.id.tag_animator)) + advanceAnimation(removedChild, 1.0f) + val expectedX = ((originalLeft - M_LEFT) + (originalRight + M_RIGHT)) / 2 + val expectedY = ((originalTop - M_TOP) + (originalBottom + M_BOTTOM)) / 2 + + checkBounds( + removedChild, + l = expectedX, + t = expectedY, + r = expectedX, + b = expectedY + ) + } + + @Test + fun animatesViewRemoval_includeMarginsTrue_left() { + setUpRootWithChildren(includeMarginsOnFirstChild = true) + val removedChild = rootView.getChildAt(0) + val originalLeft = removedChild.left + val originalTop = removedChild.top + val originalBottom = removedChild.bottom + + val success = ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.LEFT, + includeMargins = true, + ) + forceLayout() + + assertTrue(success) + assertNotNull(removedChild.getTag(R.id.tag_animator)) + advanceAnimation(removedChild, 1.0f) + checkBounds( + removedChild, + l = originalLeft - M_LEFT, + t = originalTop, + r = originalLeft - M_LEFT, + b = originalBottom + ) + } + + @Test + fun animatesViewRemoval_includeMarginsTrue_topLeft() { + setUpRootWithChildren(includeMarginsOnFirstChild = true) + val removedChild = rootView.getChildAt(0) + val originalLeft = removedChild.left + val originalTop = removedChild.top + + val success = ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT, + includeMargins = true, + ) + forceLayout() + + assertTrue(success) + assertNotNull(removedChild.getTag(R.id.tag_animator)) + advanceAnimation(removedChild, 1.0f) + checkBounds( + removedChild, + l = originalLeft - M_LEFT, + t = originalTop - M_TOP, + r = originalLeft - M_LEFT, + b = originalTop - M_TOP + ) + } + + @Test + fun animatesViewRemoval_includeMarginsTrue_top() { + setUpRootWithChildren(includeMarginsOnFirstChild = true) + val removedChild = rootView.getChildAt(0) + val originalLeft = removedChild.left + val originalTop = removedChild.top + val originalRight = removedChild.right + + val success = ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.TOP, + includeMargins = true, + ) + forceLayout() + + assertTrue(success) + assertNotNull(removedChild.getTag(R.id.tag_animator)) + advanceAnimation(removedChild, 1.0f) + checkBounds( + removedChild, + l = originalLeft, + t = originalTop - M_TOP, + r = originalRight, + b = originalTop - M_TOP + ) + } + + @Test + fun animatesViewRemoval_includeMarginsTrue_topRight() { + setUpRootWithChildren(includeMarginsOnFirstChild = true) + val removedChild = rootView.getChildAt(0) + val originalTop = removedChild.top + val originalRight = removedChild.right + + val success = ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT, + includeMargins = true, + ) + forceLayout() + + assertTrue(success) + assertNotNull(removedChild.getTag(R.id.tag_animator)) + advanceAnimation(removedChild, 1.0f) + checkBounds( + removedChild, + l = originalRight + M_RIGHT, + t = originalTop - M_TOP, + r = originalRight + M_RIGHT, + b = originalTop - M_TOP + ) + } + + @Test + fun animatesViewRemoval_includeMarginsTrue_right() { + setUpRootWithChildren(includeMarginsOnFirstChild = true) + val removedChild = rootView.getChildAt(0) + val originalTop = removedChild.top + val originalRight = removedChild.right + val originalBottom = removedChild.bottom + + val success = ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.RIGHT, + includeMargins = true, + ) + forceLayout() + + assertTrue(success) + assertNotNull(removedChild.getTag(R.id.tag_animator)) + advanceAnimation(removedChild, 1.0f) + checkBounds( + removedChild, + l = originalRight + M_RIGHT, + t = originalTop, + r = originalRight + M_RIGHT, + b = originalBottom + ) + } + + @Test + fun animatesViewRemoval_includeMarginsTrue_bottomRight() { + setUpRootWithChildren(includeMarginsOnFirstChild = true) + val removedChild = rootView.getChildAt(0) + val originalRight = removedChild.right + val originalBottom = removedChild.bottom + + val success = ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT, + includeMargins = true, + ) + forceLayout() + + assertTrue(success) + assertNotNull(removedChild.getTag(R.id.tag_animator)) + advanceAnimation(removedChild, 1.0f) + checkBounds( + removedChild, + l = originalRight + M_RIGHT, + t = originalBottom + M_BOTTOM, + r = originalRight + M_RIGHT, + b = originalBottom + M_BOTTOM + ) + } + + @Test + fun animatesViewRemoval_includeMarginsTrue_bottom() { + setUpRootWithChildren(includeMarginsOnFirstChild = true) + val removedChild = rootView.getChildAt(0) + val originalLeft = removedChild.left + val originalRight = removedChild.right + val originalBottom = removedChild.bottom + + val success = ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.BOTTOM, + includeMargins = true, + ) + forceLayout() + + assertTrue(success) + assertNotNull(removedChild.getTag(R.id.tag_animator)) + advanceAnimation(removedChild, 1.0f) + checkBounds( + removedChild, + l = originalLeft, + t = originalBottom + M_BOTTOM, + r = originalRight, + b = originalBottom + M_BOTTOM + ) + } + + @Test + fun animatesViewRemoval_includeMarginsTrue_bottomLeft() { + setUpRootWithChildren(includeMarginsOnFirstChild = true) + val removedChild = rootView.getChildAt(0) + val originalLeft = removedChild.left + val originalBottom = removedChild.bottom + + val success = ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT, + includeMargins = true, + ) + forceLayout() + + assertTrue(success) + assertNotNull(removedChild.getTag(R.id.tag_animator)) + advanceAnimation(removedChild, 1.0f) + checkBounds( + removedChild, + l = originalLeft - M_LEFT, + t = originalBottom + M_BOTTOM, + r = originalLeft - M_LEFT, + b = originalBottom + M_BOTTOM + ) + } + /* ******** end of animatesViewRemoval_includeMarginsTrue tests ******** */ + @Test fun animatesChildrenDuringViewRemoval() { setUpRootWithChildren() @@ -964,6 +1238,60 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { } @Test + fun animateRemoval_runnableRunsWhenAnimationEnds() { + var runnableRun = false + val onAnimationEndRunnable = { runnableRun = true } + + setUpRootWithChildren() + forceLayout() + val removedView = rootView.getChildAt(0) + + ViewHierarchyAnimator.animateRemoval( + removedView, + onAnimationEnd = onAnimationEndRunnable + ) + endAnimation(removedView) + + assertEquals(true, runnableRun) + } + + @Test + fun animateRemoval_runnableDoesNotRunWhenAnimationCancelled() { + var runnableRun = false + val onAnimationEndRunnable = { runnableRun = true } + + setUpRootWithChildren() + forceLayout() + val removedView = rootView.getChildAt(0) + + ViewHierarchyAnimator.animateRemoval( + removedView, + onAnimationEnd = onAnimationEndRunnable + ) + cancelAnimation(removedView) + + assertEquals(false, runnableRun) + } + + @Test + fun animationRemoval_runnableDoesNotRunWhenOnlyPartwayThroughAnimation() { + var runnableRun = false + val onAnimationEndRunnable = { runnableRun = true } + + setUpRootWithChildren() + forceLayout() + val removedView = rootView.getChildAt(0) + + ViewHierarchyAnimator.animateRemoval( + removedView, + onAnimationEnd = onAnimationEndRunnable + ) + advanceAnimation(removedView, 0.5f) + + assertEquals(false, runnableRun) + } + + @Test fun cleansUpListenersCorrectly() { val firstChild = View(mContext) firstChild.layoutParams = LinearLayout.LayoutParams(50 /* width */, 100 /* height */) @@ -1132,7 +1460,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { checkBounds(rootView, l = 10, t = 10, r = 50, b = 50) } - private fun setUpRootWithChildren() { + private fun setUpRootWithChildren(includeMarginsOnFirstChild: Boolean = false) { rootView = LinearLayout(mContext) (rootView as LinearLayout).orientation = LinearLayout.HORIZONTAL (rootView as LinearLayout).weightSum = 1f @@ -1146,13 +1474,26 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val secondChild = View(mContext) rootView.addView(secondChild) - val childParams = LinearLayout.LayoutParams( + val firstChildParams = LinearLayout.LayoutParams( 0 /* width */, LinearLayout.LayoutParams.MATCH_PARENT ) - childParams.weight = 0.5f - firstChild.layoutParams = childParams - secondChild.layoutParams = childParams + firstChildParams.weight = 0.5f + if (includeMarginsOnFirstChild) { + firstChildParams.leftMargin = M_LEFT + firstChildParams.topMargin = M_TOP + firstChildParams.rightMargin = M_RIGHT + firstChildParams.bottomMargin = M_BOTTOM + } + firstChild.layoutParams = firstChildParams + + val secondChildParams = LinearLayout.LayoutParams( + 0 /* width */, + LinearLayout.LayoutParams.MATCH_PARENT + ) + secondChildParams.weight = 0.5f + secondChild.layoutParams = secondChildParams + firstGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */) (firstGrandChild.layoutParams as RelativeLayout.LayoutParams) .addRule(RelativeLayout.ALIGN_PARENT_START) @@ -1232,3 +1573,9 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { } } } + +// Margin values. +private const val M_LEFT = 14 +private const val M_TOP = 16 +private const val M_RIGHT = 18 +private const val M_BOTTOM = 20 diff --git a/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java new file mode 100644 index 000000000000..dd9683f83c37 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.assist.ui; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class DisplayUtilsTest extends SysuiTestCase { + + @Mock + Resources mResources; + @Mock + Context mMockContext; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testGetCornerRadii_noOverlay() { + assertEquals(0, DisplayUtils.getCornerRadiusBottom(mContext)); + assertEquals(0, DisplayUtils.getCornerRadiusTop(mContext)); + } + + @Test + public void testGetCornerRadii_onlyDefaultOverridden() { + when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size)).thenReturn(100); + when(mMockContext.getResources()).thenReturn(mResources); + + assertEquals(100, DisplayUtils.getCornerRadiusBottom(mMockContext)); + assertEquals(100, DisplayUtils.getCornerRadiusTop(mMockContext)); + } + + @Test + public void testGetCornerRadii_allOverridden() { + when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size)).thenReturn(100); + when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size_top)).thenReturn( + 150); + when(mResources.getDimensionPixelSize(R.dimen.config_rounded_mask_size_bottom)).thenReturn( + 200); + when(mMockContext.getResources()).thenReturn(mResources); + + assertEquals(200, DisplayUtils.getCornerRadiusBottom(mMockContext)); + assertEquals(150, DisplayUtils.getCornerRadiusTop(mMockContext)); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 4a5b23c02e40..e8c760c3e140 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -52,6 +52,7 @@ import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyLong import org.mockito.Mockito.eq import org.mockito.Mockito.never +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit @@ -123,6 +124,21 @@ class AuthContainerViewTest : SysuiTestCase() { } @Test + fun testDismissBeforeIntroEnd() { + val container = initializeFingerprintContainer() + waitForIdleSync() + + // STATE_ANIMATING_IN = 1 + container?.mContainerState = 1 + + container.dismissWithoutCallback(false) + + // the first time is triggered by initializeFingerprintContainer() + // the second time was triggered by dismissWithoutCallback() + verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L) + } + + @Test fun testDismissesOnFocusLoss() { val container = initializeFingerprintContainer() waitForIdleSync() @@ -322,6 +338,13 @@ class AuthContainerViewTest : SysuiTestCase() { } @Test + fun testLayoutParams_hasShowWhenLockedFlag() { + val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") + assertThat((layoutParams.flags and WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) != 0) + .isTrue() + } + + @Test fun testLayoutParams_hasDimbehindWindowFlag() { val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") val lpFlags = layoutParams.flags diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index 37bb0c296735..0b528a5c8d1e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -117,13 +117,12 @@ class AuthRippleControllerTest : SysuiTestCase() { } @Test - fun testFingerprintTrigger_KeyguardVisible_Ripple() { - // GIVEN fp exists, keyguard is visible, user doesn't need strong auth + fun testFingerprintTrigger_KeyguardShowing_Ripple() { + // GIVEN fp exists, keyguard is showing, user doesn't need strong auth val fpsLocation = Point(5, 5) `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation) controller.onViewAttached() - `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(true) - `when`(keyguardUpdateMonitor.isDreaming).thenReturn(false) + `when`(keyguardStateController.isShowing).thenReturn(true) `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(false) // WHEN fingerprint authenticated @@ -140,39 +139,15 @@ class AuthRippleControllerTest : SysuiTestCase() { } @Test - fun testFingerprintTrigger_Dreaming_Ripple() { - // GIVEN fp exists, keyguard is visible, user doesn't need strong auth - val fpsLocation = Point(5, 5) - `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation) - controller.onViewAttached() - `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(false) - `when`(keyguardUpdateMonitor.isDreaming).thenReturn(true) - `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(false) - - // WHEN fingerprint authenticated - val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) - verify(keyguardUpdateMonitor).registerCallback(captor.capture()) - captor.value.onBiometricAuthenticated( - 0 /* userId */, - BiometricSourceType.FINGERPRINT /* type */, - false /* isStrongBiometric */) - - // THEN update sensor location and show ripple - verify(rippleView).setFingerprintSensorLocation(fpsLocation, 0f) - verify(rippleView).startUnlockedRipple(any()) - } - - @Test - fun testFingerprintTrigger_KeyguardNotVisible_NotDreaming_NoRipple() { + fun testFingerprintTrigger_KeyguardNotShowing_NoRipple() { // GIVEN fp exists & user doesn't need strong auth val fpsLocation = Point(5, 5) `when`(authController.udfpsLocation).thenReturn(fpsLocation) controller.onViewAttached() `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(false) - // WHEN keyguard is NOT visible & fingerprint authenticated - `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(false) - `when`(keyguardUpdateMonitor.isDreaming).thenReturn(false) + // WHEN keyguard is NOT showing & fingerprint authenticated + `when`(keyguardStateController.isShowing).thenReturn(false) val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) verify(keyguardUpdateMonitor).registerCallback(captor.capture()) captor.value.onBiometricAuthenticated( @@ -186,11 +161,11 @@ class AuthRippleControllerTest : SysuiTestCase() { @Test fun testFingerprintTrigger_StrongAuthRequired_NoRipple() { - // GIVEN fp exists & keyguard is visible + // GIVEN fp exists & keyguard is showing val fpsLocation = Point(5, 5) `when`(authController.udfpsLocation).thenReturn(fpsLocation) controller.onViewAttached() - `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(true) + `when`(keyguardStateController.isShowing).thenReturn(true) // WHEN user needs strong auth & fingerprint authenticated `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(true) @@ -207,12 +182,12 @@ class AuthRippleControllerTest : SysuiTestCase() { @Test fun testFaceTriggerBypassEnabled_Ripple() { - // GIVEN face auth sensor exists, keyguard is visible & strong auth isn't required + // GIVEN face auth sensor exists, keyguard is showing & strong auth isn't required val faceLocation = Point(5, 5) `when`(authController.faceSensorLocation).thenReturn(faceLocation) controller.onViewAttached() - `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(true) + `when`(keyguardStateController.isShowing).thenReturn(true) `when`(keyguardUpdateMonitor.userNeedsStrongAuth()).thenReturn(false) // WHEN bypass is enabled & face authenticated @@ -299,7 +274,7 @@ class AuthRippleControllerTest : SysuiTestCase() { val fpsLocation = Point(5, 5) `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation) controller.onViewAttached() - `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(true) + `when`(keyguardStateController.isShowing).thenReturn(true) `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true) controller.showUnlockRipple(BiometricSourceType.FINGERPRINT) @@ -317,7 +292,7 @@ class AuthRippleControllerTest : SysuiTestCase() { val faceLocation = Point(5, 5) `when`(authController.faceSensorLocation).thenReturn(faceLocation) controller.onViewAttached() - `when`(keyguardUpdateMonitor.isKeyguardVisible).thenReturn(true) + `when`(keyguardStateController.isShowing).thenReturn(true) `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true) `when`(authController.isUdfpsFingerDown).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index 5c564e65ea86..c85334db9499 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -17,13 +17,24 @@ package com.android.systemui.biometrics import android.graphics.Rect -import android.hardware.biometrics.BiometricOverlayConstants.* +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS +import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING +import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR +import android.hardware.biometrics.BiometricOverlayConstants.ShowReason import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.IUdfpsOverlayControllerCallback +import android.provider.Settings import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper -import android.view.* +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.Surface import android.view.Surface.Rotation +import android.view.View +import android.view.WindowManager import android.view.accessibility.AccessibilityManager import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor @@ -32,11 +43,11 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.time.SystemClock @@ -52,8 +63,8 @@ import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit private const val REQUEST_ID = 2L @@ -75,7 +86,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var accessibilityManager: AccessibilityManager @Mock private lateinit var statusBarStateController: StatusBarStateController - @Mock private lateinit var panelExpansionStateManager: PanelExpansionStateManager + @Mock private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var dialogManager: SystemUIDialogManager @@ -114,14 +125,18 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { whenever(udfpsEnrollView.context).thenReturn(context) } - private fun withReason(@ShowReason reason: Int, block: () -> Unit) { + private fun withReason( + @ShowReason reason: Int, + isDebuggable: Boolean = false, + block: () -> Unit + ) { controllerOverlay = UdfpsControllerOverlay( context, fingerprintManager, inflater, windowManager, accessibilityManager, - statusBarStateController, panelExpansionStateManager, statusBarKeyguardViewManager, + statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager, keyguardUpdateMonitor, dialogManager, dumpManager, transitionController, configurationController, systemClock, keyguardStateController, unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason, - controllerCallback, onTouch, activityLaunchAnimator + controllerCallback, onTouch, activityLaunchAnimator, isDebuggable ) block() } @@ -141,11 +156,29 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { } @Test + fun showUdfpsOverlay_locate_withEnrollmentUiRemoved() { + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1) + withReason(REASON_ENROLL_FIND_SENSOR, isDebuggable = true) { + showUdfpsOverlay(isEnrollUseCase = false) + } + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0) + } + + @Test fun showUdfpsOverlay_enroll() = withReason(REASON_ENROLL_ENROLLING) { showUdfpsOverlay(isEnrollUseCase = true) } @Test + fun showUdfpsOverlay_enroll_withEnrollmentUiRemoved() { + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1) + withReason(REASON_ENROLL_ENROLLING, isDebuggable = true) { + showUdfpsOverlay(isEnrollUseCase = false) + } + Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0) + } + + @Test fun showUdfpsOverlay_other() = withReason(REASON_AUTH_OTHER) { showUdfpsOverlay() } private fun withRotation(@Rotation rotation: Int, block: () -> Unit) { @@ -362,21 +395,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_0 // touch at 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) // touch at 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) // touch at 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) } fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) { @@ -384,21 +429,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_90 // touch at 0 degrees -> 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 90 degrees -> 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) // touch at 180 degrees -> 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) // touch at 270 degrees -> 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) } fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) { @@ -406,21 +463,33 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context.resources.getStringArray(R.array.udfps_accessibility_touch_hints) val rotation = Surface.ROTATION_270 // touch at 0 degrees -> 270 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[3]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[3]) // touch at 90 degrees -> 0 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[0]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, -1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[0]) // touch at 180 degrees -> 90 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[1]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + -1.0f /* x */, 0.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[1]) // touch at 270 degrees -> 180 degrees - assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */, - 0.0f /* sensorX */, 0.0f /* sensorY */, rotation)) - .isEqualTo(touchHints[2]) + assertThat( + controllerOverlay.onTouchOutsideOfSensorAreaImpl( + 0.0f /* x */, 1.0f /* y */, + 0.0f /* sensorX */, 0.0f /* sensorY */, rotation + ) + ).isEqualTo(touchHints[2]) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 53e30fdb0a4a..1dd053912ae4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -38,6 +38,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Rect; +import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricOverlayConstants; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.SensorProperties; @@ -71,12 +72,12 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.SystemUIDialogManager; import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.Execution; @@ -168,6 +169,8 @@ public class UdfpsControllerTest extends SysuiTestCase { @Mock private LatencyTracker mLatencyTracker; private FakeExecutor mFgExecutor; + @Mock + private UdfpsDisplayMode mUdfpsDisplayMode; // Stuff for configuring mocks @Mock @@ -245,7 +248,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mWindowManager, mStatusBarStateController, mFgExecutor, - new PanelExpansionStateManager(), + new ShadeExpansionStateManager(), mStatusBarKeyguardViewManager, mDumpManager, mKeyguardUpdateMonitor, @@ -257,7 +260,6 @@ public class UdfpsControllerTest extends SysuiTestCase { mVibrator, mUdfpsHapticsSimulator, mUdfpsShell, - Optional.of(mDisplayModeProvider), mKeyguardStateController, mDisplayManager, mHandler, @@ -274,6 +276,7 @@ public class UdfpsControllerTest extends SysuiTestCase { verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); mScreenObserver = mScreenObserverCaptor.getValue(); mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, new UdfpsOverlayParams()); + mUdfpsController.setUdfpsDisplayMode(mUdfpsDisplayMode); } @Test @@ -664,7 +667,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); when(mUdfpsView.isDisplayConfigured()).thenReturn(true); // WHEN it is cancelled - mUdfpsController.onCancelUdfps(); + mUdfpsController.cancelAodInterrupt(); // THEN the display is unconfigured verify(mUdfpsView).unconfigureDisplay(); } @@ -688,7 +691,7 @@ public class UdfpsControllerTest extends SysuiTestCase { } @Test - public void aodInterruptCancelTimeoutActionWhenFingerUp() throws RemoteException { + public void aodInterruptCancelTimeoutActionOnFingerUp() throws RemoteException { when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true); @@ -740,6 +743,56 @@ public class UdfpsControllerTest extends SysuiTestCase { } @Test + public void aodInterruptCancelTimeoutActionOnAcquired() throws RemoteException { + when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); + when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true); + + // GIVEN AOD interrupt + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID, + BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); + mScreenObserver.onScreenTurnedOn(); + mFgExecutor.runAllReady(); + mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); + mFgExecutor.runAllReady(); + + // Configure UdfpsView to accept the acquired event + when(mUdfpsView.isDisplayConfigured()).thenReturn(true); + + // WHEN acquired is received + mOverlayController.onAcquired(TEST_UDFPS_SENSOR_ID, + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD); + + // Configure UdfpsView to accept the ACTION_DOWN event + when(mUdfpsView.isDisplayConfigured()).thenReturn(false); + + // WHEN ACTION_DOWN is received + verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); + MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); + mBiometricsExecutor.runAllReady(); + downEvent.recycle(); + + // WHEN ACTION_MOVE is received + MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0); + mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent); + mBiometricsExecutor.runAllReady(); + moveEvent.recycle(); + mFgExecutor.runAllReady(); + + // Configure UdfpsView to accept the finger up event + when(mUdfpsView.isDisplayConfigured()).thenReturn(true); + + // WHEN it times out + mFgExecutor.advanceClockToNext(); + mFgExecutor.runAllReady(); + + // THEN the display should be unconfigured once. If the timeout action is not + // cancelled, the display would be unconfigured twice which would cause two + // FP attempts. + verify(mUdfpsView, times(1)).unconfigureDisplay(); + } + + @Test public void aodInterruptScreenOff() throws RemoteException { // GIVEN screen off mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java new file mode 100644 index 000000000000..7e35b261b352 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.hardware.fingerprint.IUdfpsHbmListener; +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper.RunWithLooper; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.util.concurrency.FakeExecution; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@RunWithLooper(setAsMainLooper = true) +public class UdfpsDisplayModeTest extends SysuiTestCase { + private static final int DISPLAY_ID = 0; + + @Mock + private AuthController mAuthController; + @Mock + private IUdfpsHbmListener mDisplayCallback; + @Mock + private Runnable mOnEnabled; + @Mock + private Runnable mOnDisabled; + + private final FakeExecution mExecution = new FakeExecution(); + private UdfpsDisplayMode mHbmController; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + // Force mContext to always return DISPLAY_ID + Context contextSpy = spy(mContext); + when(contextSpy.getDisplayId()).thenReturn(DISPLAY_ID); + + // Set up mocks. + when(mAuthController.getUdfpsHbmListener()).thenReturn(mDisplayCallback); + + // Create a real controller with mock dependencies. + mHbmController = new UdfpsDisplayMode(contextSpy, mExecution, mAuthController); + } + + @Test + public void roundTrip() throws RemoteException { + // Enable the UDFPS mode. + mHbmController.enable(mOnEnabled); + + // Should set the appropriate refresh rate for UDFPS and notify the caller. + verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID)); + verify(mOnEnabled).run(); + + // Disable the UDFPS mode. + mHbmController.disable(mOnDisabled); + + // Should unset the refresh rate and notify the caller. + verify(mOnDisabled).run(); + verify(mDisplayCallback).onHbmDisabled(eq(DISPLAY_ID)); + } + + @Test + public void mustNotEnableMoreThanOnce() throws RemoteException { + // First request to enable the UDFPS mode. + mHbmController.enable(mOnEnabled); + + // Should set the appropriate refresh rate for UDFPS and notify the caller. + verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID)); + verify(mOnEnabled).run(); + + // Second request to enable the UDFPS mode, while it's still enabled. + mHbmController.enable(mOnEnabled); + + // Should ignore the second request. + verifyNoMoreInteractions(mDisplayCallback); + verifyNoMoreInteractions(mOnEnabled); + } + + @Test + public void mustNotDisableMoreThanOnce() throws RemoteException { + // Disable the UDFPS mode. + mHbmController.enable(mOnEnabled); + + // Should set the appropriate refresh rate for UDFPS and notify the caller. + verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID)); + verify(mOnEnabled).run(); + + // First request to disable the UDFPS mode. + mHbmController.disable(mOnDisabled); + + // Should unset the refresh rate and notify the caller. + verify(mOnDisabled).run(); + verify(mDisplayCallback).onHbmDisabled(eq(DISPLAY_ID)); + + // Second request to disable the UDFPS mode, when it's already disabled. + mHbmController.disable(mOnDisabled); + + // Should ignore the second request. + verifyNoMoreInteractions(mOnDisabled); + verifyNoMoreInteractions(mDisplayCallback); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java index b61bda8edd10..c0f9c82fb131 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java @@ -41,14 +41,14 @@ import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionChangeEvent; +import com.android.systemui.shade.ShadeExpansionListener; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.SystemUIDialogManager; import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -76,7 +76,7 @@ public class UdfpsKeyguardViewControllerTest extends SysuiTestCase { @Mock private StatusBarStateController mStatusBarStateController; @Mock - private PanelExpansionStateManager mPanelExpansionStateManager; + private ShadeExpansionStateManager mShadeExpansionStateManager; @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @Mock @@ -109,8 +109,8 @@ public class UdfpsKeyguardViewControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerCaptor; private StatusBarStateController.StateListener mStatusBarStateListener; - @Captor private ArgumentCaptor<PanelExpansionListener> mExpansionListenerCaptor; - private List<PanelExpansionListener> mExpansionListeners; + @Captor private ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor; + private List<ShadeExpansionListener> mExpansionListeners; @Captor private ArgumentCaptor<StatusBarKeyguardViewManager.AlternateAuthInterceptor> mAltAuthInterceptorCaptor; @@ -130,7 +130,7 @@ public class UdfpsKeyguardViewControllerTest extends SysuiTestCase { mController = new UdfpsKeyguardViewController( mView, mStatusBarStateController, - mPanelExpansionStateManager, + mShadeExpansionStateManager, mStatusBarKeyguardViewManager, mKeyguardUpdateMonitor, mDumpManager, @@ -182,8 +182,8 @@ public class UdfpsKeyguardViewControllerTest extends SysuiTestCase { mController.onViewDetached(); verify(mStatusBarStateController).removeCallback(mStatusBarStateListener); - for (PanelExpansionListener listener : mExpansionListeners) { - verify(mPanelExpansionStateManager).removeExpansionListener(listener); + for (ShadeExpansionListener listener : mExpansionListeners) { + verify(mShadeExpansionStateManager).removeExpansionListener(listener); } verify(mKeyguardStateController).removeCallback(mKeyguardStateControllerCallback); } @@ -513,7 +513,7 @@ public class UdfpsKeyguardViewControllerTest extends SysuiTestCase { } private void captureStatusBarExpansionListeners() { - verify(mPanelExpansionStateManager, times(2)) + verify(mShadeExpansionStateManager, times(2)) .addExpansionListener(mExpansionListenerCaptor.capture()); // first (index=0) is from super class, UdfpsAnimationViewController. // second (index=1) is from UdfpsKeyguardViewController @@ -521,10 +521,10 @@ public class UdfpsKeyguardViewControllerTest extends SysuiTestCase { } private void updateStatusBarExpansion(float fraction, boolean expanded) { - PanelExpansionChangeEvent event = - new PanelExpansionChangeEvent( + ShadeExpansionChangeEvent event = + new ShadeExpansionChangeEvent( fraction, expanded, /* tracking= */ false, /* dragDownPxAmount= */ 0f); - for (PanelExpansionListener listener : mExpansionListeners) { + for (ShadeExpansionListener listener : mExpansionListeners) { listener.onPanelExpansionChanged(event); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt index 434cb48bc422..25bc91f413de 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt @@ -96,7 +96,7 @@ class BroadcastDispatcherTest : SysuiTestCase() { @Mock private lateinit var removalPendingStore: PendingRemovalStore - private lateinit var executor: Executor + private lateinit var mainExecutor: Executor @Captor private lateinit var argumentCaptor: ArgumentCaptor<ReceiverData> @@ -108,11 +108,12 @@ class BroadcastDispatcherTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) - executor = FakeExecutor(FakeSystemClock()) - `when`(mockContext.mainExecutor).thenReturn(executor) + mainExecutor = FakeExecutor(FakeSystemClock()) + `when`(mockContext.mainExecutor).thenReturn(mainExecutor) broadcastDispatcher = TestBroadcastDispatcher( mockContext, + mainExecutor, testableLooper.looper, mock(Executor::class.java), mock(DumpManager::class.java), @@ -148,9 +149,9 @@ class BroadcastDispatcherTest : SysuiTestCase() { @Test fun testAddingReceiverToCorrectUBR_executor() { - broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, executor, user0) + broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, mainExecutor, user0) broadcastDispatcher.registerReceiver( - broadcastReceiverOther, intentFilterOther, executor, user1) + broadcastReceiverOther, intentFilterOther, mainExecutor, user1) testableLooper.processAllMessages() @@ -427,8 +428,9 @@ class BroadcastDispatcherTest : SysuiTestCase() { private class TestBroadcastDispatcher( context: Context, - bgLooper: Looper, - executor: Executor, + mainExecutor: Executor, + backgroundRunningLooper: Looper, + backgroundRunningExecutor: Executor, dumpManager: DumpManager, logger: BroadcastDispatcherLogger, userTracker: UserTracker, @@ -436,8 +438,9 @@ class BroadcastDispatcherTest : SysuiTestCase() { var mockUBRMap: Map<Int, UserBroadcastDispatcher> ) : BroadcastDispatcher( context, - bgLooper, - executor, + mainExecutor, + backgroundRunningLooper, + backgroundRunningExecutor, dumpManager, logger, userTracker, diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java index b2a9e8209495..6bc7308a6a40 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java @@ -145,6 +145,35 @@ public class BrightLineClassifierTest extends SysuiTestCase { } @Test + public void testIsFalseTouch_SeekBar_FalseTouch() { + when(mClassifierA.classifyGesture(anyInt(), anyDouble(), anyDouble())) + .thenReturn(mFalsedResult); + when(mSingleTapClassfier.isTap(any(List.class), anyDouble())).thenReturn(mFalsedResult); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).isTrue(); + } + + @Test + public void testIsFalseTouch_SeekBar_RealTouch() { + when(mSingleTapClassfier.isTap(any(List.class), anyDouble())).thenReturn(mFalsedResult); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).isFalse(); + } + + @Test + public void testIsFalseTouch_SeekBar_FalseTap() { + when(mClassifierA.classifyGesture(anyInt(), anyDouble(), anyDouble())) + .thenReturn(mFalsedResult); + when(mSingleTapClassfier.isTap(any(List.class), anyDouble())).thenReturn(mFalsedResult); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).isTrue(); + } + + @Test + public void testIsFalseTouch_SeekBar_RealTap() { + when(mClassifierA.classifyGesture(anyInt(), anyDouble(), anyDouble())) + .thenReturn(mFalsedResult); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).isFalse(); + } + + @Test public void testIsFalseTouch_ClassifierBRejects() { when(mClassifierB.classifyGesture(anyInt(), anyDouble(), anyDouble())) .thenReturn(mFalsedResult); diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java index 3e9cf1e51b63..fa9c41a3cbb6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java @@ -35,6 +35,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerFake; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.policy.BatteryController; @@ -71,6 +72,8 @@ public class FalsingCollectorImplTest extends SysuiTestCase { @Mock private KeyguardStateController mKeyguardStateController; @Mock + private ShadeExpansionStateManager mShadeExpansionStateManager; + @Mock private BatteryController mBatteryController; private final DockManagerFake mDockManager = new DockManagerFake(); private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); @@ -85,7 +88,8 @@ public class FalsingCollectorImplTest extends SysuiTestCase { mFalsingCollector = new FalsingCollectorImpl(mFalsingDataProvider, mFalsingManager, mKeyguardUpdateMonitor, mHistoryTracker, mProximitySensor, - mStatusBarStateController, mKeyguardStateController, mBatteryController, + mStatusBarStateController, mKeyguardStateController, mShadeExpansionStateManager, + mBatteryController, mDockManager, mFakeExecutor, mFakeSystemClock); } @@ -137,9 +141,9 @@ public class FalsingCollectorImplTest extends SysuiTestCase { public void testUnregisterSensor_QS() { mFalsingCollector.onScreenTurningOn(); reset(mProximitySensor); - mFalsingCollector.setQsExpanded(true); + mFalsingCollector.onQsExpansionChanged(true); verify(mProximitySensor).unregister(any(ThresholdSensor.Listener.class)); - mFalsingCollector.setQsExpanded(false); + mFalsingCollector.onQsExpansionChanged(false); verify(mProximitySensor).register(any(ThresholdSensor.Listener.class)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java index d70d6fc6baa6..588edb770047 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java @@ -19,6 +19,7 @@ package com.android.systemui.classifier; import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER; import static com.android.systemui.classifier.Classifier.LEFT_AFFORDANCE; +import static com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR; import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS; import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN; import static com.android.systemui.classifier.Classifier.PULSE_EXPAND; @@ -406,4 +407,46 @@ public class TypeClassifierTest extends ClassifierTest { when(mDataProvider.isRight()).thenReturn(true); assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isTrue(); } + + @Test + public void testPass_MediaSeekbar() { + when(mDataProvider.isVertical()).thenReturn(false); + + when(mDataProvider.isUp()).thenReturn(false); // up and right should cause no effect. + when(mDataProvider.isRight()).thenReturn(false); + assertThat(mClassifier.classifyGesture(MEDIA_SEEKBAR, 0.5, 0).isFalse()).isFalse(); + + when(mDataProvider.isUp()).thenReturn(true); + when(mDataProvider.isRight()).thenReturn(false); + assertThat(mClassifier.classifyGesture(MEDIA_SEEKBAR, 0.5, 0).isFalse()).isFalse(); + + when(mDataProvider.isUp()).thenReturn(false); + when(mDataProvider.isRight()).thenReturn(true); + assertThat(mClassifier.classifyGesture(MEDIA_SEEKBAR, 0.5, 0).isFalse()).isFalse(); + + when(mDataProvider.isUp()).thenReturn(true); + when(mDataProvider.isRight()).thenReturn(true); + assertThat(mClassifier.classifyGesture(MEDIA_SEEKBAR, 0.5, 0).isFalse()).isFalse(); + } + + @Test + public void testFalse_MediaSeekbar() { + when(mDataProvider.isVertical()).thenReturn(true); + + when(mDataProvider.isUp()).thenReturn(false); // up and right should cause no effect. + when(mDataProvider.isRight()).thenReturn(false); + assertThat(mClassifier.classifyGesture(MEDIA_SEEKBAR, 0.5, 0).isFalse()).isTrue(); + + when(mDataProvider.isUp()).thenReturn(true); + when(mDataProvider.isRight()).thenReturn(false); + assertThat(mClassifier.classifyGesture(MEDIA_SEEKBAR, 0.5, 0).isFalse()).isTrue(); + + when(mDataProvider.isUp()).thenReturn(false); + when(mDataProvider.isRight()).thenReturn(true); + assertThat(mClassifier.classifyGesture(MEDIA_SEEKBAR, 0.5, 0).isFalse()).isTrue(); + + when(mDataProvider.isUp()).thenReturn(true); + when(mDataProvider.isRight()).thenReturn(true); + assertThat(mClassifier.classifyGesture(MEDIA_SEEKBAR, 0.5, 0).isFalse()).isTrue(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java index 91214a85ddd5..e7e6918325a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java @@ -38,6 +38,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.util.DeviceConfigProxyFake; import org.junit.Before; @@ -47,6 +49,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import javax.inject.Provider; @SmallTest @RunWith(AndroidJUnit4.class) @@ -55,11 +60,15 @@ public class ClipboardListenerTest extends SysuiTestCase { @Mock private ClipboardManager mClipboardManager; @Mock - private ClipboardOverlayControllerFactory mClipboardOverlayControllerFactory; + private ClipboardOverlayControllerLegacyFactory mClipboardOverlayControllerLegacyFactory; + @Mock + private ClipboardOverlayControllerLegacy mOverlayControllerLegacy; @Mock private ClipboardOverlayController mOverlayController; @Mock private UiEventLogger mUiEventLogger; + @Mock + private FeatureFlags mFeatureFlags; private DeviceConfigProxyFake mDeviceConfigProxy; private ClipData mSampleClipData; @@ -72,12 +81,17 @@ public class ClipboardListenerTest extends SysuiTestCase { @Captor private ArgumentCaptor<String> mStringCaptor; + @Spy + private Provider<ClipboardOverlayController> mOverlayControllerProvider; + @Before public void setup() { + mOverlayControllerProvider = () -> mOverlayController; + MockitoAnnotations.initMocks(this); - when(mClipboardOverlayControllerFactory.create(any())).thenReturn( - mOverlayController); + when(mClipboardOverlayControllerLegacyFactory.create(any())) + .thenReturn(mOverlayControllerLegacy); when(mClipboardManager.hasPrimaryClip()).thenReturn(true); @@ -94,7 +108,8 @@ public class ClipboardListenerTest extends SysuiTestCase { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "false", false); ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); listener.start(); verifyZeroInteractions(mClipboardManager); verifyZeroInteractions(mUiEventLogger); @@ -105,7 +120,8 @@ public class ClipboardListenerTest extends SysuiTestCase { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); listener.start(); verify(mClipboardManager).addPrimaryClipChangedListener(any()); verifyZeroInteractions(mUiEventLogger); @@ -113,16 +129,58 @@ public class ClipboardListenerTest extends SysuiTestCase { @Test public void test_consecutiveCopies() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false); + mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); listener.start(); listener.onPrimaryClipChanged(); - verify(mClipboardOverlayControllerFactory).create(any()); + verify(mClipboardOverlayControllerLegacyFactory).create(any()); - verify(mOverlayController).setClipData(mClipDataCaptor.capture(), mStringCaptor.capture()); + verify(mOverlayControllerLegacy).setClipData( + mClipDataCaptor.capture(), mStringCaptor.capture()); + + assertEquals(mSampleClipData, mClipDataCaptor.getValue()); + assertEquals(mSampleSource, mStringCaptor.getValue()); + + verify(mOverlayControllerLegacy).setOnSessionCompleteListener(mRunnableCaptor.capture()); + + // Should clear the overlay controller + mRunnableCaptor.getValue().run(); + + listener.onPrimaryClipChanged(); + + verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any()); + + // Not calling the runnable here, just change the clip again and verify that the overlay is + // NOT recreated. + + listener.onPrimaryClipChanged(); + + verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any()); + verifyZeroInteractions(mOverlayControllerProvider); + } + + @Test + public void test_consecutiveCopies_new() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true); + + mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, + "true", false); + ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); + listener.start(); + listener.onPrimaryClipChanged(); + + verify(mOverlayControllerProvider).get(); + + verify(mOverlayController).setClipData( + mClipDataCaptor.capture(), mStringCaptor.capture()); assertEquals(mSampleClipData, mClipDataCaptor.getValue()); assertEquals(mSampleSource, mStringCaptor.getValue()); @@ -134,14 +192,15 @@ public class ClipboardListenerTest extends SysuiTestCase { listener.onPrimaryClipChanged(); - verify(mClipboardOverlayControllerFactory, times(2)).create(any()); + verify(mOverlayControllerProvider, times(2)).get(); // Not calling the runnable here, just change the clip again and verify that the overlay is // NOT recreated. listener.onPrimaryClipChanged(); - verify(mClipboardOverlayControllerFactory, times(2)).create(any()); + verify(mOverlayControllerProvider, times(2)).get(); + verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory); } @Test @@ -169,4 +228,40 @@ public class ClipboardListenerTest extends SysuiTestCase { assertTrue(ClipboardListener.shouldSuppressOverlay(suppressableClipData, ClipboardListener.SHELL_PACKAGE, false)); } + + @Test + public void test_logging_enterAndReenter() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false); + + ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); + listener.start(); + + listener.onPrimaryClipChanged(); + listener.onPrimaryClipChanged(); + + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); + } + + @Test + public void test_logging_enterAndReenter_new() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true); + + ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); + listener.start(); + + listener.onPrimaryClipChanged(); + listener.onPrimaryClipChanged(); + + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java new file mode 100644 index 000000000000..b7f1c1a9f001 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.clipboardoverlay; + +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.animation.Animator; +import android.content.ClipData; +import android.content.ClipDescription; +import android.net.Uri; +import android.os.PersistableBundle; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.screenshot.TimeoutHandler; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ClipboardOverlayControllerTest extends SysuiTestCase { + + private ClipboardOverlayController mOverlayController; + @Mock + private ClipboardOverlayView mClipboardOverlayView; + @Mock + private ClipboardOverlayWindow mClipboardOverlayWindow; + @Mock + private BroadcastSender mBroadcastSender; + @Mock + private TimeoutHandler mTimeoutHandler; + @Mock + private UiEventLogger mUiEventLogger; + + @Mock + private Animator mAnimator; + + private ClipData mSampleClipData; + + @Captor + private ArgumentCaptor<ClipboardOverlayView.ClipboardOverlayCallbacks> mOverlayCallbacksCaptor; + private ClipboardOverlayView.ClipboardOverlayCallbacks mCallbacks; + + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator); + when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator); + + mSampleClipData = new ClipData("Test", new String[]{"text/plain"}, + new ClipData.Item("Test Item")); + + mOverlayController = new ClipboardOverlayController( + mContext, + mClipboardOverlayView, + mClipboardOverlayWindow, + getFakeBroadcastDispatcher(), + mBroadcastSender, + mTimeoutHandler, + mUiEventLogger); + verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture()); + mCallbacks = mOverlayCallbacksCaptor.getValue(); + } + + @After + public void tearDown() { + mOverlayController.hideImmediate(); + } + + @Test + public void test_setClipData_nullData() { + ClipData clipData = null; + mOverlayController.setClipData(clipData, ""); + + verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); + verify(mClipboardOverlayView, times(0)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_invalidImageData() { + ClipData clipData = new ClipData("", new String[]{"image/png"}, + new ClipData.Item(Uri.parse(""))); + + mOverlayController.setClipData(clipData, ""); + + verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); + verify(mClipboardOverlayView, times(0)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_textData() { + mOverlayController.setClipData(mSampleClipData, ""); + + verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false); + verify(mClipboardOverlayView, times(1)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_sensitiveTextData() { + ClipDescription description = mSampleClipData.getDescription(); + PersistableBundle b = new PersistableBundle(); + b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true); + description.setExtras(b); + ClipData data = new ClipData(description, mSampleClipData.getItemAt(0)); + mOverlayController.setClipData(data, ""); + + verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true); + verify(mClipboardOverlayView, times(1)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_repeatedCalls() { + when(mAnimator.isRunning()).thenReturn(true); + + mOverlayController.setClipData(mSampleClipData, ""); + mOverlayController.setClipData(mSampleClipData, ""); + + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_viewCallbacks_onShareTapped() { + mOverlayController.setClipData(mSampleClipData, ""); + + mCallbacks.onShareButtonTapped(); + + verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED); + verify(mClipboardOverlayView, times(1)).getExitAnimation(); + } + + @Test + public void test_viewCallbacks_onDismissTapped() { + mOverlayController.setClipData(mSampleClipData, ""); + + mCallbacks.onDismissButtonTapped(); + + verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + verify(mClipboardOverlayView, times(1)).getExitAnimation(); + } + + @Test + public void test_multipleDismissals_dismissesOnce() { + mCallbacks.onSwipeDismissInitiated(mAnimator); + mCallbacks.onDismissButtonTapped(); + mCallbacks.onSwipeDismissInitiated(mAnimator); + mCallbacks.onDismissButtonTapped(); + + verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); + verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java deleted file mode 100644 index c7c2cd8d7b4b..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.clipboardoverlay; - -import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.provider.DeviceConfig; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.internal.logging.UiEventLogger; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.util.DeviceConfigProxyFake; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class ClipboardOverlayEventTest extends SysuiTestCase { - - @Mock - private ClipboardManager mClipboardManager; - @Mock - private ClipboardOverlayControllerFactory mClipboardOverlayControllerFactory; - @Mock - private ClipboardOverlayController mOverlayController; - @Mock - private UiEventLogger mUiEventLogger; - - private final String mSampleSource = "Example source"; - - private ClipboardListener mClipboardListener; - - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - when(mClipboardOverlayControllerFactory.create(any())).thenReturn( - mOverlayController); - when(mClipboardManager.hasPrimaryClip()).thenReturn(true); - - ClipData sampleClipData = new ClipData("Test", new String[]{"text/plain"}, - new ClipData.Item("Test Item")); - when(mClipboardManager.getPrimaryClip()).thenReturn(sampleClipData); - when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource); - - DeviceConfigProxyFake deviceConfigProxy = new DeviceConfigProxyFake(); - deviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, - "true", false); - - mClipboardListener = new ClipboardListener(getContext(), deviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); - } - - @Test - public void test_enterAndReenter() { - mClipboardListener.start(); - - mClipboardListener.onPrimaryClipChanged(); - mClipboardListener.onPrimaryClipChanged(); - - verify(mUiEventLogger, times(1)).log( - ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); - verify(mUiEventLogger, times(1)).log( - ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt index f93336134900..93a1868b72f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt @@ -24,12 +24,11 @@ import androidx.annotation.DrawableRes import androidx.test.filters.SmallTest import com.android.internal.R as InternalR import com.android.systemui.R as SystemUIR -import com.android.systemui.tests.R import com.android.systemui.SysuiTestCase +import com.android.systemui.tests.R import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test - import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations @@ -102,14 +101,11 @@ class RoundedCornerResDelegateTest : SysuiTestCase() { assertEquals(Size(3, 3), roundedCornerResDelegate.topRoundedSize) assertEquals(Size(4, 4), roundedCornerResDelegate.bottomRoundedSize) - setupResources(radius = 100, - roundedTopDrawable = getTestsDrawable(R.drawable.rounded4px), - roundedBottomDrawable = getTestsDrawable(R.drawable.rounded5px)) - + roundedCornerResDelegate.physicalPixelDisplaySizeRatio = 2f roundedCornerResDelegate.updateDisplayUniqueId(null, 1) - assertEquals(Size(4, 4), roundedCornerResDelegate.topRoundedSize) - assertEquals(Size(5, 5), roundedCornerResDelegate.bottomRoundedSize) + assertEquals(Size(6, 6), roundedCornerResDelegate.topRoundedSize) + assertEquals(Size(8, 8), roundedCornerResDelegate.bottomRoundedSize) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java index 6a55a60c2fda..5bbd8109d8f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java @@ -16,6 +16,9 @@ package com.android.systemui.doze; +import static android.content.res.Configuration.UI_MODE_NIGHT_YES; +import static android.content.res.Configuration.UI_MODE_TYPE_CAR; + import static com.android.systemui.doze.DozeMachine.State.DOZE; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_DOCKED; @@ -38,16 +41,17 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.UiModeManager; import android.content.res.Configuration; import android.hardware.display.AmbientDisplayConfiguration; import android.testing.AndroidTestingRunner; import android.testing.UiThreadTest; import android.view.Display; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; @@ -78,25 +82,30 @@ public class DozeMachineTest extends SysuiTestCase { @Mock private DozeHost mHost; @Mock - private UiModeManager mUiModeManager; + private DozeMachine.Part mPartMock; + @Mock + private DozeMachine.Part mAnotherPartMock; private DozeServiceFake mServiceFake; private WakeLockFake mWakeLockFake; - private AmbientDisplayConfiguration mConfigMock; - private DozeMachine.Part mPartMock; + private AmbientDisplayConfiguration mAmbientDisplayConfigMock; @Before public void setUp() { MockitoAnnotations.initMocks(this); mServiceFake = new DozeServiceFake(); mWakeLockFake = new WakeLockFake(); - mConfigMock = mock(AmbientDisplayConfiguration.class); - mPartMock = mock(DozeMachine.Part.class); + mAmbientDisplayConfigMock = mock(AmbientDisplayConfiguration.class); when(mDockManager.isDocked()).thenReturn(false); when(mDockManager.isHidden()).thenReturn(false); - mMachine = new DozeMachine(mServiceFake, mConfigMock, mWakeLockFake, - mWakefulnessLifecycle, mUiModeManager, mDozeLog, mDockManager, - mHost, new DozeMachine.Part[]{mPartMock}); + mMachine = new DozeMachine(mServiceFake, + mAmbientDisplayConfigMock, + mWakeLockFake, + mWakefulnessLifecycle, + mDozeLog, + mDockManager, + mHost, + new DozeMachine.Part[]{mPartMock, mAnotherPartMock}); } @Test @@ -108,7 +117,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); mMachine.requestState(INITIALIZED); @@ -118,7 +127,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_goesToAod() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -138,7 +147,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_afterDockPaused_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); @@ -151,7 +160,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); mMachine.requestState(INITIALIZED); @@ -162,7 +171,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -184,7 +193,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_afterDockPaused_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); @@ -197,7 +206,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_afterDockPaused_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); @@ -209,7 +218,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false); mMachine.requestState(INITIALIZED); mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION); mMachine.requestState(DOZE_PULSING); @@ -222,7 +231,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_goesToAoD() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION); mMachine.requestState(DOZE_PULSING); @@ -236,7 +245,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_alwaysOnSuppressed_goesToSuppressed() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); mMachine.requestState(INITIALIZED); mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION); mMachine.requestState(DOZE_PULSING); @@ -287,7 +296,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_afterDockPaused_goesToDoze() { - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -303,7 +312,7 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testPulseDone_alwaysOnSuppressed_afterDockPaused_goesToDoze() { when(mHost.isAlwaysOnSuppressed()).thenReturn(true); - when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true); when(mDockManager.isDocked()).thenReturn(true); when(mDockManager.isHidden()).thenReturn(true); mMachine.requestState(INITIALIZED); @@ -471,7 +480,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testTransitionToInitialized_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); verify(mPartMock).transitionTo(UNINITIALIZED, INITIALIZED); @@ -481,7 +492,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testTransitionToFinish_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(FINISH); @@ -490,7 +503,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozeToDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE); @@ -499,7 +514,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozeAoDToDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE_AOD); @@ -508,7 +525,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozePulsingBrightDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE_PULSING_BRIGHT); @@ -517,7 +536,9 @@ public class DozeMachineTest extends SysuiTestCase { @Test public void testDozeAodDockedDozeSuspendTriggers_carModeIsEnabled() { - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + Configuration configuration = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(configuration); mMachine.requestState(INITIALIZED); mMachine.requestState(DOZE_AOD_DOCKED); @@ -525,7 +546,35 @@ public class DozeMachineTest extends SysuiTestCase { } @Test + public void testOnConfigurationChanged_propagatesUiModeTypeToParts() { + Configuration newConfig = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(newConfig); + + verify(mPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + verify(mAnotherPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + } + + @Test + public void testOnConfigurationChanged_propagatesOnlyUiModeChangesToParts() { + Configuration newConfig = configWithCarNightUiMode(); + + mMachine.onConfigurationChanged(newConfig); + mMachine.onConfigurationChanged(newConfig); + + verify(mPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + verify(mAnotherPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR); + } + + @Test public void testDozeSuppressTriggers_screenState() { assertEquals(Display.STATE_OFF, DOZE_SUSPEND_TRIGGERS.screenState(null)); } + + @NonNull + private Configuration configWithCarNightUiMode() { + Configuration configuration = Configuration.EMPTY; + configuration.uiMode = UI_MODE_TYPE_CAR | UI_MODE_NIGHT_YES; + return configuration; + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java index 9ffc5a57cef6..c40c187d572a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java @@ -423,7 +423,7 @@ public class DozeSensorsTest extends SysuiTestCase { @Test public void testGesturesAllInitiallyRespectSettings() { - DozeSensors dozeSensors = new DozeSensors(getContext(), mSensorManager, mDozeParameters, + DozeSensors dozeSensors = new DozeSensors(mSensorManager, mDozeParameters, mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog, mProximitySensor, mFakeSettings, mAuthController, mDevicePostureController); @@ -435,7 +435,7 @@ public class DozeSensorsTest extends SysuiTestCase { private class TestableDozeSensors extends DozeSensors { TestableDozeSensors() { - super(getContext(), mSensorManager, mDozeParameters, + super(mSensorManager, mDozeParameters, mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog, mProximitySensor, mFakeSettings, mAuthController, mDevicePostureController); diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java index 0f29dcd5a939..32b994538e12 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java @@ -10,14 +10,14 @@ * 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 andatest + * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.doze; -import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE; -import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE; +import static android.content.res.Configuration.UI_MODE_TYPE_CAR; +import static android.content.res.Configuration.UI_MODE_TYPE_NORMAL; import static com.android.systemui.doze.DozeMachine.State.DOZE; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD; @@ -26,17 +26,16 @@ import static com.android.systemui.doze.DozeMachine.State.FINISH; import static com.android.systemui.doze.DozeMachine.State.INITIALIZED; import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.UiModeManager; -import android.content.BroadcastReceiver; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; import android.hardware.display.AmbientDisplayConfiguration; import android.testing.AndroidTestingRunner; import android.testing.UiThreadTest; @@ -44,13 +43,13 @@ import android.testing.UiThreadTest; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.statusbar.phone.BiometricUnlockController; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.AdditionalMatchers; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -71,10 +70,6 @@ public class DozeSuppressorTest extends SysuiTestCase { @Mock private AmbientDisplayConfiguration mConfig; @Mock - private BroadcastDispatcher mBroadcastDispatcher; - @Mock - private UiModeManager mUiModeManager; - @Mock private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy; @Mock private BiometricUnlockController mBiometricUnlockController; @@ -83,13 +78,6 @@ public class DozeSuppressorTest extends SysuiTestCase { private DozeMachine mDozeMachine; @Captor - private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor; - @Captor - private ArgumentCaptor<IntentFilter> mIntentFilterCaptor; - private BroadcastReceiver mBroadcastReceiver; - private IntentFilter mIntentFilter; - - @Captor private ArgumentCaptor<DozeHost.Callback> mDozeHostCaptor; private DozeHost.Callback mDozeHostCallback; @@ -106,8 +94,6 @@ public class DozeSuppressorTest extends SysuiTestCase { mDozeHost, mConfig, mDozeLog, - mBroadcastDispatcher, - mUiModeManager, mBiometricUnlockControllerLazy); mDozeSuppressor.setDozeMachine(mDozeMachine); @@ -122,36 +108,35 @@ public class DozeSuppressorTest extends SysuiTestCase { public void testRegistersListenersOnInitialized_unregisteredOnFinish() { // check that receivers and callbacks registered mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); captureDozeHostCallback(); // check that receivers and callbacks are unregistered mDozeSuppressor.transitionTo(INITIALIZED, FINISH); - verify(mBroadcastDispatcher).unregisterReceiver(mBroadcastReceiver); verify(mDozeHost).removeCallback(mDozeHostCallback); } @Test public void testSuspendTriggersDoze_carMode() { // GIVEN car mode - when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); // WHEN dozing begins mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); // THEN doze continues with all doze triggers disabled. - verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS); + verify(mDozeMachine, atLeastOnce()).requestState(DOZE_SUSPEND_TRIGGERS); + verify(mDozeMachine, never()) + .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS))); } @Test public void testSuspendTriggersDoze_enterCarMode() { // GIVEN currently dozing mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); mDozeSuppressor.transitionTo(INITIALIZED, DOZE); // WHEN car mode entered - mBroadcastReceiver.onReceive(null, new Intent(ACTION_ENTER_CAR_MODE)); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); // THEN doze continues with all doze triggers disabled. verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS); @@ -160,13 +145,13 @@ public class DozeSuppressorTest extends SysuiTestCase { @Test public void testDozeResume_exitCarMode() { // GIVEN currently suspended, with AOD not enabled + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(false); mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS); // WHEN exiting car mode - mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE)); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL); // THEN doze is resumed verify(mDozeMachine).requestState(DOZE); @@ -175,19 +160,53 @@ public class DozeSuppressorTest extends SysuiTestCase { @Test public void testDozeAoDResume_exitCarMode() { // GIVEN currently suspended, with AOD not enabled + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(true); mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); - captureBroadcastReceiver(); mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS); // WHEN exiting car mode - mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE)); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL); // THEN doze AOD is resumed verify(mDozeMachine).requestState(DOZE_AOD); } @Test + public void testUiModeDoesNotChange_noStateTransition() { + mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); + clearInvocations(mDozeMachine); + + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + + verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS); + verify(mDozeMachine, never()) + .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS))); + } + + @Test + public void testUiModeTypeChange_whenDozeMachineIsNotReady_doesNotDoAnything() { + when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true); + + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + + verify(mDozeMachine, never()).requestState(any()); + } + + @Test + public void testUiModeTypeChange_CarModeEnabledAndDozeMachineNotReady_suspendsTriggersAfter() { + when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true); + mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR); + verify(mDozeMachine, never()).requestState(any()); + + mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED); + + verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS); + } + + @Test public void testEndDoze_unprovisioned() { // GIVEN device unprovisioned when(mDozeHost.isProvisioned()).thenReturn(false); @@ -276,14 +295,4 @@ public class DozeSuppressorTest extends SysuiTestCase { verify(mDozeHost).addCallback(mDozeHostCaptor.capture()); mDozeHostCallback = mDozeHostCaptor.getValue(); } - - private void captureBroadcastReceiver() { - verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiverCaptor.capture(), - mIntentFilterCaptor.capture()); - mBroadcastReceiver = mBroadcastReceiverCaptor.getValue(); - mIntentFilter = mIntentFilterCaptor.getValue(); - assertEquals(2, mIntentFilter.countActions()); - org.hamcrest.MatcherAssert.assertThat(() -> mIntentFilter.actionsIterator(), - containsInAnyOrder(ACTION_ENTER_CAR_MODE, ACTION_EXIT_CAR_MODE)); - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java index 6436981aee06..6091d3a93f14 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java @@ -23,10 +23,10 @@ import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -88,6 +88,8 @@ public class DozeTriggersTest extends SysuiTestCase { @Mock private ProximityCheck mProximityCheck; @Mock + private DozeLog mDozeLog; + @Mock private AuthController mAuthController; @Mock private UiEventLogger mUiEventLogger; @@ -127,7 +129,7 @@ public class DozeTriggersTest extends SysuiTestCase { mTriggers = new DozeTriggers(mContext, mHost, config, dozeParameters, asyncSensorManager, wakeLock, mDockManager, mProximitySensor, - mProximityCheck, mock(DozeLog.class), mBroadcastDispatcher, new FakeSettings(), + mProximityCheck, mDozeLog, mBroadcastDispatcher, new FakeSettings(), mAuthController, mUiEventLogger, mSessionTracker, mKeyguardStateController, mDevicePostureController); mTriggers.setDozeMachine(mMachine); @@ -144,6 +146,12 @@ public class DozeTriggersTest extends SysuiTestCase { mTriggers.transitionTo(DozeMachine.State.INITIALIZED, DozeMachine.State.DOZE); clearInvocations(mMachine); + ArgumentCaptor<Boolean> boolCaptor = ArgumentCaptor.forClass(Boolean.class); + doAnswer(invocation -> + when(mHost.isPulsePending()).thenReturn(boolCaptor.getValue()) + ).when(mHost).setPulsePending(boolCaptor.capture()); + + when(mHost.isPulsingBlocked()).thenReturn(false); mProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 1)); captor.getValue().onNotificationAlerted(null /* pulseSuppressedListener */); mProximitySensor.alertListeners(); @@ -160,6 +168,29 @@ public class DozeTriggersTest extends SysuiTestCase { } @Test + public void testOnNotification_noPulseIfPulseIsNotPendingAnymore() { + when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE); + ArgumentCaptor<DozeHost.Callback> captor = ArgumentCaptor.forClass(DozeHost.Callback.class); + doAnswer(invocation -> null).when(mHost).addCallback(captor.capture()); + + mTriggers.transitionTo(UNINITIALIZED, DozeMachine.State.INITIALIZED); + mTriggers.transitionTo(DozeMachine.State.INITIALIZED, DozeMachine.State.DOZE); + clearInvocations(mMachine); + when(mHost.isPulsingBlocked()).thenReturn(false); + + // GIVEN pulsePending = false + when(mHost.isPulsePending()).thenReturn(false); + + // WHEN prox check returns FAR + mProximitySensor.setLastEvent(new ThresholdSensorEvent(false, 2)); + captor.getValue().onNotificationAlerted(null /* pulseSuppressedListener */); + mProximitySensor.alertListeners(); + + // THEN don't request pulse because the pending pulse was abandoned early + verify(mMachine, never()).requestPulse(anyInt()); + } + + @Test public void testTransitionTo_disablesAndEnablesTouchSensors() { when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE); @@ -237,6 +268,11 @@ public class DozeTriggersTest extends SysuiTestCase { when(mSessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD)) .thenReturn(keyguardSessionId); + ArgumentCaptor<Boolean> boolCaptor = ArgumentCaptor.forClass(Boolean.class); + doAnswer(invocation -> + when(mHost.isPulsePending()).thenReturn(boolCaptor.getValue()) + ).when(mHost).setPulsePending(boolCaptor.capture()); + // WHEN quick pick up is triggered mTriggers.onSensor(DozeLog.REASON_SENSOR_QUICK_PICKUP, 100, 100, null); @@ -308,6 +344,16 @@ public class DozeTriggersTest extends SysuiTestCase { verify(mProximityCheck).destroy(); } + @Test + public void testIsExecutingTransition_dropPulse() { + when(mHost.isPulsePending()).thenReturn(false); + when(mMachine.isExecutingTransition()).thenReturn(true); + + mTriggers.onSensor(DozeLog.PULSE_REASON_SENSOR_LONG_PRESS, 100, 100, null); + + verify(mDozeLog).tracePulseDropped(anyString(), eq(null)); + } + private void waitForSensorManager() { mExecutor.runAllReady(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index eec33ca2ff78..f370be190761 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -19,6 +19,7 @@ package com.android.systemui.dreams; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -27,10 +28,10 @@ import android.content.ComponentName; import android.content.Intent; import android.os.IBinder; import android.os.RemoteException; -import android.service.dreams.DreamService; import android.service.dreams.IDreamOverlay; import android.service.dreams.IDreamOverlayCallback; import android.testing.AndroidTestingRunner; +import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.WindowManagerImpl; @@ -53,6 +54,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -61,6 +64,7 @@ import org.mockito.MockitoAnnotations; public class DreamOverlayServiceTest extends SysuiTestCase { private static final ComponentName LOW_LIGHT_COMPONENT = new ComponentName("package", "lowlight"); + private static final String DREAM_COMPONENT = "package/dream"; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); @@ -108,12 +112,14 @@ public class DreamOverlayServiceTest extends SysuiTestCase { @Mock UiEventLogger mUiEventLogger; + @Captor + ArgumentCaptor<View> mViewCaptor; + DreamOverlayService mService; @Before public void setup() { MockitoAnnotations.initMocks(this); - mContext.addMockSystemService(WindowManager.class, mWindowManager); when(mDreamOverlayComponent.getDreamOverlayContainerViewController()) .thenReturn(mDreamOverlayContainerViewController); @@ -129,7 +135,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { when(mDreamOverlayContainerViewController.getContainerView()) .thenReturn(mDreamOverlayContainerView); - mService = new DreamOverlayService(mContext, mMainExecutor, + mService = new DreamOverlayService(mContext, mMainExecutor, mWindowManager, mDreamOverlayComponentFactory, mStateController, mKeyguardUpdateMonitor, @@ -143,7 +149,8 @@ public class DreamOverlayServiceTest extends SysuiTestCase { final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); // Inform the overlay service of dream starting. - overlay.startDream(mWindowParams, mDreamOverlayCallback); + overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*shouldShowComplication*/); mMainExecutor.runAllReady(); verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START); @@ -157,7 +164,8 @@ public class DreamOverlayServiceTest extends SysuiTestCase { final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); // Inform the overlay service of dream starting. - overlay.startDream(mWindowParams, mDreamOverlayCallback); + overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*shouldShowComplication*/); mMainExecutor.runAllReady(); verify(mWindowManager).addView(any(), any()); @@ -169,7 +177,8 @@ public class DreamOverlayServiceTest extends SysuiTestCase { final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); // Inform the overlay service of dream starting. - overlay.startDream(mWindowParams, mDreamOverlayCallback); + overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*shouldShowComplication*/); mMainExecutor.runAllReady(); verify(mDreamOverlayContainerViewController).init(); @@ -186,49 +195,76 @@ public class DreamOverlayServiceTest extends SysuiTestCase { final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); // Inform the overlay service of dream starting. - overlay.startDream(mWindowParams, mDreamOverlayCallback); + overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*shouldShowComplication*/); mMainExecutor.runAllReady(); verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView); } @Test - public void testShouldShowComplicationsFalseByDefault() { - mService.onBind(new Intent()); + public void testShouldShowComplicationsSetByStartDream() throws RemoteException { + final IBinder proxy = mService.onBind(new Intent()); + final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); - assertThat(mService.shouldShowComplications()).isFalse(); + // Inform the overlay service of dream starting. + overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + true /*shouldShowComplication*/); + + assertThat(mService.shouldShowComplications()).isTrue(); } @Test - public void testShouldShowComplicationsSetByIntentExtra() { - final Intent intent = new Intent(); - intent.putExtra(DreamService.EXTRA_SHOW_COMPLICATIONS, true); - mService.onBind(intent); + public void testLowLightSetByStartDream() throws RemoteException { + final IBinder proxy = mService.onBind(new Intent()); + final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); - assertThat(mService.shouldShowComplications()).isTrue(); + // Inform the overlay service of dream starting. + overlay.startDream(mWindowParams, mDreamOverlayCallback, + LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/); + mMainExecutor.runAllReady(); + + assertThat(mService.getDreamComponent()).isEqualTo(LOW_LIGHT_COMPONENT); + verify(mStateController).setLowLightActive(true); } @Test - public void testLowLightSetByIntentExtra() throws RemoteException { - final Intent intent = new Intent(); - intent.putExtra(DreamService.EXTRA_DREAM_COMPONENT, LOW_LIGHT_COMPONENT); - - final IBinder proxy = mService.onBind(intent); + public void testDestroy() throws RemoteException { + final IBinder proxy = mService.onBind(new Intent()); final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); - assertThat(mService.getDreamComponent()).isEqualTo(LOW_LIGHT_COMPONENT); // Inform the overlay service of dream starting. - overlay.startDream(mWindowParams, mDreamOverlayCallback); + overlay.startDream(mWindowParams, mDreamOverlayCallback, + LOW_LIGHT_COMPONENT.flattenToString(), false /*shouldShowComplication*/); mMainExecutor.runAllReady(); - verify(mStateController).setLowLightActive(true); + // Verify view added. + verify(mWindowManager).addView(mViewCaptor.capture(), any()); + + // Service destroyed. + mService.onDestroy(); + mMainExecutor.runAllReady(); + + // Verify view removed. + verify(mWindowManager).removeView(mViewCaptor.getValue()); + + // Verify state correctly set. + verify(mKeyguardUpdateMonitor).removeCallback(any()); + verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED); + verify(mStateController).setOverlayActive(false); + verify(mStateController).setLowLightActive(false); } @Test - public void testDestroy() { + public void testDoNotRemoveViewOnDestroyIfOverlayNotStarted() { + // Service destroyed without ever starting dream. mService.onDestroy(); mMainExecutor.runAllReady(); + // Verify no view is removed. + verify(mWindowManager, never()).removeView(any()); + + // Verify state still correctly set. verify(mKeyguardUpdateMonitor).removeCallback(any()); verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED); verify(mStateController).setOverlayActive(false); @@ -245,7 +281,8 @@ public class DreamOverlayServiceTest extends SysuiTestCase { final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); // Inform the overlay service of dream starting. - overlay.startDream(mWindowParams, mDreamOverlayCallback); + overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*shouldShowComplication*/); // Destroy the service. mService.onDestroy(); @@ -255,4 +292,44 @@ public class DreamOverlayServiceTest extends SysuiTestCase { verify(mWindowManager, never()).addView(any(), any()); } + + @Test + public void testResetCurrentOverlayWhenConnectedToNewDream() throws RemoteException { + final IBinder proxy = mService.onBind(new Intent()); + final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); + + // Inform the overlay service of dream starting. Do not show dream complications. + overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*shouldShowComplication*/); + mMainExecutor.runAllReady(); + + // Verify that a new window is added. + verify(mWindowManager).addView(mViewCaptor.capture(), any()); + final View windowDecorView = mViewCaptor.getValue(); + + // Assert that the overlay is not showing complications. + assertThat(mService.shouldShowComplications()).isFalse(); + + clearInvocations(mDreamOverlayComponent); + clearInvocations(mWindowManager); + + // New dream starting with dream complications showing. Note that when a new dream is + // binding to the dream overlay service, it receives the same instance of IBinder as the + // first one. + overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + true /*shouldShowComplication*/); + mMainExecutor.runAllReady(); + + // Assert that the overlay is showing complications. + assertThat(mService.shouldShowComplications()).isTrue(); + + // Verify that the old overlay window has been removed, and a new one created. + verify(mWindowManager).removeView(windowDecorView); + verify(mWindowManager).addView(any(), any()); + + // Verify that new instances of overlay container view controller and overlay touch monitor + // are created. + verify(mDreamOverlayComponent).getDreamOverlayContainerViewController(); + verify(mDreamOverlayComponent).getDreamOverlayTouchMonitor(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java index 2448f1a7633d..849ac5ef90d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java @@ -297,10 +297,10 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase { } /** - * Ensures margin is applied + * Ensures default margin is applied */ @Test - public void testMargin() { + public void testDefaultMargin() { final int margin = 5; final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout, margin, mTouchSession, 0, 0); @@ -373,6 +373,74 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase { } /** + * Ensures complication margin is applied + */ + @Test + public void testComplicationMargin() { + final int defaultMargin = 5; + final int complicationMargin = 10; + final ComplicationLayoutEngine engine = + new ComplicationLayoutEngine(mLayout, defaultMargin, mTouchSession, 0, 0); + + final ViewInfo firstViewInfo = new ViewInfo( + new ComplicationLayoutParams( + 100, + 100, + ComplicationLayoutParams.POSITION_TOP + | ComplicationLayoutParams.POSITION_END, + ComplicationLayoutParams.DIRECTION_DOWN, + 0, + complicationMargin), + Complication.CATEGORY_STANDARD, + mLayout); + + addComplication(engine, firstViewInfo); + + final ViewInfo secondViewInfo = new ViewInfo( + new ComplicationLayoutParams( + 100, + 100, + ComplicationLayoutParams.POSITION_TOP + | ComplicationLayoutParams.POSITION_END, + ComplicationLayoutParams.DIRECTION_START, + 0), + Complication.CATEGORY_SYSTEM, + mLayout); + + addComplication(engine, secondViewInfo); + + firstViewInfo.clearInvocations(); + secondViewInfo.clearInvocations(); + + final ViewInfo thirdViewInfo = new ViewInfo( + new ComplicationLayoutParams( + 100, + 100, + ComplicationLayoutParams.POSITION_TOP + | ComplicationLayoutParams.POSITION_END, + ComplicationLayoutParams.DIRECTION_START, + 1), + Complication.CATEGORY_SYSTEM, + mLayout); + + addComplication(engine, thirdViewInfo); + + // The first added view should now be underneath the second view. + verifyChange(firstViewInfo, false, lp -> { + assertThat(lp.topToBottom == thirdViewInfo.view.getId()).isTrue(); + assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue(); + assertThat(lp.topMargin).isEqualTo(complicationMargin); + }); + + // The second view should be in underneath the third view. + verifyChange(secondViewInfo, false, lp -> { + assertThat(lp.endToStart == thirdViewInfo.view.getId()).isTrue(); + assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue(); + assertThat(lp.getMarginEnd()).isEqualTo(defaultMargin); + }); + } + + /** * Ensures layout in a particular position updates. */ @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java index 967b30d07e63..cb7e47b28bcd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java @@ -97,6 +97,35 @@ public class ComplicationLayoutParamsTest extends SysuiTestCase { } /** + * Ensures unspecified margin uses default. + */ + @Test + public void testUnspecifiedMarginUsesDefault() { + final ComplicationLayoutParams params = new ComplicationLayoutParams( + 100, + 100, + ComplicationLayoutParams.POSITION_TOP, + ComplicationLayoutParams.DIRECTION_DOWN, + 3); + assertThat(params.getMargin(10) == 10).isTrue(); + } + + /** + * Ensures specified margin is used instead of default. + */ + @Test + public void testSpecifiedMargin() { + final ComplicationLayoutParams params = new ComplicationLayoutParams( + 100, + 100, + ComplicationLayoutParams.POSITION_TOP, + ComplicationLayoutParams.DIRECTION_DOWN, + 3, + 10); + assertThat(params.getMargin(5) == 10).isTrue(); + } + + /** * Ensures ComplicationLayoutParams is properly duplicated on copy construction. */ @Test @@ -106,12 +135,36 @@ public class ComplicationLayoutParamsTest extends SysuiTestCase { 100, ComplicationLayoutParams.POSITION_TOP, ComplicationLayoutParams.DIRECTION_DOWN, + 3, + 10); + final ComplicationLayoutParams copy = new ComplicationLayoutParams(params); + + assertThat(copy.getDirection() == params.getDirection()).isTrue(); + assertThat(copy.getPosition() == params.getPosition()).isTrue(); + assertThat(copy.getWeight() == params.getWeight()).isTrue(); + assertThat(copy.getMargin(0) == params.getMargin(1)).isTrue(); + assertThat(copy.height == params.height).isTrue(); + assertThat(copy.width == params.width).isTrue(); + } + + /** + * Ensures ComplicationLayoutParams is properly duplicated on copy construction with unspecified + * margin. + */ + @Test + public void testCopyConstructionWithUnspecifiedMargin() { + final ComplicationLayoutParams params = new ComplicationLayoutParams( + 100, + 100, + ComplicationLayoutParams.POSITION_TOP, + ComplicationLayoutParams.DIRECTION_DOWN, 3); final ComplicationLayoutParams copy = new ComplicationLayoutParams(params); assertThat(copy.getDirection() == params.getDirection()).isTrue(); assertThat(copy.getPosition() == params.getPosition()).isTrue(); assertThat(copy.getWeight() == params.getWeight()).isTrue(); + assertThat(copy.getMargin(1) == params.getMargin(1)).isTrue(); assertThat(copy.height == params.height).isTrue(); assertThat(copy.width == params.width).isTrue(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java index 571dd3d1faf3..9f4a7c820efc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationTypesUpdaterTest.java @@ -71,7 +71,7 @@ public class ComplicationTypesUpdaterTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); when(mDreamBackend.getEnabledComplications()).thenReturn(new HashSet<>()); - mController = new ComplicationTypesUpdater(mContext, mDreamBackend, mExecutor, + mController = new ComplicationTypesUpdater(mDreamBackend, mExecutor, mSecureSettings, mDreamOverlayStateController); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java index 314a30b2d14a..ec448f94ba83 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockTimeComplicationTest.java @@ -82,7 +82,6 @@ public class DreamClockTimeComplicationTest extends SysuiTestCase { public void testComplicationAdded() { final DreamClockTimeComplication.Registrant registrant = new DreamClockTimeComplication.Registrant( - mContext, mDreamOverlayStateController, mComplication); registrant.start(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java index 04ff7aed480d..aa8c93edce68 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java @@ -29,9 +29,12 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.testing.AndroidTestingRunner; +import android.view.View; +import android.widget.ImageView; import androidx.test.filters.SmallTest; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.controls.ControlsServiceInfo; import com.android.systemui.controls.controller.ControlsController; @@ -40,6 +43,7 @@ import com.android.systemui.controls.dagger.ControlsComponent; import com.android.systemui.controls.management.ControlsListingController; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dreams.complication.dagger.DreamHomeControlsComplicationComponent; +import com.android.systemui.plugins.ActivityStarter; import org.junit.Before; import org.junit.Test; @@ -79,6 +83,15 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { @Captor private ArgumentCaptor<ControlsListingController.ControlsListingCallback> mCallbackCaptor; + @Mock + private ImageView mView; + + @Mock + private ActivityStarter mActivityStarter; + + @Mock + UiEventLogger mUiEventLogger; + @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -102,7 +115,7 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { @Test public void complicationAvailability_serviceNotAvailable_noFavorites_doNotAddComplication() { final DreamHomeControlsComplication.Registrant registrant = - new DreamHomeControlsComplication.Registrant(mContext, mComplication, + new DreamHomeControlsComplication.Registrant(mComplication, mDreamOverlayStateController, mControlsComponent); registrant.start(); @@ -115,7 +128,7 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { @Test public void complicationAvailability_serviceAvailable_noFavorites_doNotAddComplication() { final DreamHomeControlsComplication.Registrant registrant = - new DreamHomeControlsComplication.Registrant(mContext, mComplication, + new DreamHomeControlsComplication.Registrant(mComplication, mDreamOverlayStateController, mControlsComponent); registrant.start(); @@ -128,7 +141,7 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { @Test public void complicationAvailability_serviceNotAvailable_haveFavorites_doNotAddComplication() { final DreamHomeControlsComplication.Registrant registrant = - new DreamHomeControlsComplication.Registrant(mContext, mComplication, + new DreamHomeControlsComplication.Registrant(mComplication, mDreamOverlayStateController, mControlsComponent); registrant.start(); @@ -141,7 +154,7 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { @Test public void complicationAvailability_serviceAvailable_haveFavorites_addComplication() { final DreamHomeControlsComplication.Registrant registrant = - new DreamHomeControlsComplication.Registrant(mContext, mComplication, + new DreamHomeControlsComplication.Registrant(mComplication, mDreamOverlayStateController, mControlsComponent); registrant.start(); @@ -151,6 +164,30 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { verify(mDreamOverlayStateController).addComplication(mComplication); } + /** + * Ensures clicking home controls chip logs UiEvent. + */ + @Test + public void testClick_logsUiEvent() { + final DreamHomeControlsComplication.DreamHomeControlsChipViewController viewController = + new DreamHomeControlsComplication.DreamHomeControlsChipViewController( + mView, + mActivityStarter, + mContext, + mControlsComponent, + mUiEventLogger); + viewController.onViewAttached(); + + final ArgumentCaptor<View.OnClickListener> clickListenerCaptor = + ArgumentCaptor.forClass(View.OnClickListener.class); + verify(mView).setOnClickListener(clickListenerCaptor.capture()); + + clickListenerCaptor.getValue().onClick(mView); + verify(mUiEventLogger).log( + DreamHomeControlsComplication.DreamHomeControlsChipViewController + .DreamOverlayEvent.DREAM_HOME_CONTROLS_TAPPED); + } + private void setHaveFavorites(boolean value) { final List<StructureInfo> favorites = mock(List.class); when(favorites.isEmpty()).thenReturn(!value); diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java index 522b5b5a8530..50f27ea27ae9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java @@ -33,7 +33,7 @@ import com.android.systemui.ActivityIntentHelper; import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.media.MediaCarouselController; +import com.android.systemui.media.controls.ui.MediaCarouselController; import com.android.systemui.media.dream.MediaDreamComplication; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.NotificationLockscreenUserManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java index fa8f88a08368..c8b2b2556828 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java @@ -24,7 +24,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.smartspace.SmartspaceTarget; -import android.content.Context; import android.testing.AndroidTestingRunner; import android.view.View; @@ -48,8 +47,6 @@ import java.util.Collections; @SmallTest @RunWith(AndroidTestingRunner.class) public class SmartSpaceComplicationTest extends SysuiTestCase { - @Mock - private Context mContext; @Mock private DreamSmartspaceController mSmartspaceController; @@ -80,7 +77,6 @@ public class SmartSpaceComplicationTest extends SysuiTestCase { private SmartSpaceComplication.Registrant getRegistrant() { return new SmartSpaceComplication.Registrant( - mContext, mDreamOverlayStateController, mComplication, mSmartspaceController); diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java index c3fca29a9883..4bd53c00327f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java @@ -41,12 +41,12 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; +import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.KeyguardBouncer; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent; import com.android.wm.shell.animation.FlingAnimationUtils; import org.junit.Before; @@ -285,8 +285,8 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float dragDownAmount = event2.getY() - event1.getY(); // Ensure correct expansion passed in. - PanelExpansionChangeEvent event = - new PanelExpansionChangeEvent( + ShadeExpansionChangeEvent event = + new ShadeExpansionChangeEvent( expansion, /* expanded= */ false, /* tracking= */ true, dragDownAmount); verify(mStatusBarKeyguardViewManager).onPanelExpansionChanged(event); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt index fc672016a886..65ae90b8f7e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt @@ -17,11 +17,19 @@ package com.android.systemui.dump import androidx.test.filters.SmallTest +import com.android.systemui.CoreStartable import com.android.systemui.Dumpable +import com.android.systemui.ProtoDumpable import com.android.systemui.SysuiTestCase -import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.google.common.truth.Truth.assertThat +import java.io.FileDescriptor +import java.io.PrintWriter +import java.io.StringWriter +import javax.inject.Provider import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -29,7 +37,6 @@ import org.mockito.Mockito.anyInt import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import java.io.PrintWriter @SmallTest class DumpHandlerTest : SysuiTestCase() { @@ -43,6 +50,8 @@ class DumpHandlerTest : SysuiTestCase() { @Mock private lateinit var pw: PrintWriter + @Mock + private lateinit var fd: FileDescriptor @Mock private lateinit var dumpable1: Dumpable @@ -52,6 +61,11 @@ class DumpHandlerTest : SysuiTestCase() { private lateinit var dumpable3: Dumpable @Mock + private lateinit var protoDumpable1: ProtoDumpable + @Mock + private lateinit var protoDumpable2: ProtoDumpable + + @Mock private lateinit var buffer1: LogBuffer @Mock private lateinit var buffer2: LogBuffer @@ -66,7 +80,9 @@ class DumpHandlerTest : SysuiTestCase() { mContext, dumpManager, logBufferEulogizer, - mutableMapOf(), + mutableMapOf( + EmptyCoreStartable::class.java to Provider { EmptyCoreStartable() } + ), exceptionHandlerManager ) } @@ -82,7 +98,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN some of them are dumped explicitly val args = arrayOf("dumpable1", "dumpable3", "buffer2") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN only the requested ones have their dump() method called verify(dumpable1).dump(pw, args) @@ -101,7 +117,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN that module is dumped val args = arrayOf("dumpable1") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN its dump() method is called verify(dumpable1).dump(pw, args) @@ -118,7 +134,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN a critical dump is requested val args = arrayOf("--dump-priority", "CRITICAL") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN all modules are dumped (but no buffers) verify(dumpable1).dump(pw, args) @@ -139,7 +155,7 @@ class DumpHandlerTest : SysuiTestCase() { // WHEN a normal dump is requested val args = arrayOf("--dump-priority", "NORMAL") - dumpHandler.dump(pw, args) + dumpHandler.dump(fd, pw, args) // THEN all buffers are dumped (but no modules) verify(dumpable1, never()).dump( @@ -154,4 +170,44 @@ class DumpHandlerTest : SysuiTestCase() { verify(buffer1).dump(pw, 0) verify(buffer2).dump(pw, 0) } -}
\ No newline at end of file + + @Test + fun testConfigDump() { + // GIVEN a StringPrintWriter + val stringWriter = StringWriter() + val spw = PrintWriter(stringWriter) + + // When a config dump is requested + dumpHandler.dump(fd, spw, arrayOf("config")) + + assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName) + } + + @Test + fun testDumpAllProtoDumpables() { + dumpManager.registerDumpable("protoDumpable1", protoDumpable1) + dumpManager.registerDumpable("protoDumpable2", protoDumpable2) + + val args = arrayOf(DumpHandler.PROTO) + dumpHandler.dump(fd, pw, args) + + verify(protoDumpable1).dumpProto(any(), eq(args)) + verify(protoDumpable2).dumpProto(any(), eq(args)) + } + + @Test + fun testDumpSingleProtoDumpable() { + dumpManager.registerDumpable("protoDumpable1", protoDumpable1) + dumpManager.registerDumpable("protoDumpable2", protoDumpable2) + + val args = arrayOf(DumpHandler.PROTO, "protoDumpable1") + dumpHandler.dump(fd, pw, args) + + verify(protoDumpable1).dumpProto(any(), eq(args)) + verify(protoDumpable2, never()).dumpProto(any(), any()) + } + + private class EmptyCoreStartable : CoreStartable { + override fun start() {} + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt index bd029a727ee3..64547f4463d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt @@ -16,9 +16,9 @@ package com.android.systemui.dump -import com.android.systemui.log.LogBuffer -import com.android.systemui.log.LogLevel -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.plugins.log.LogcatEchoTracker /** * Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests. diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt index 4511193d41d3..20a82c63cfdd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt @@ -21,15 +21,10 @@ import android.content.Intent import android.content.pm.PackageManager.NameNotFoundException import android.content.res.Resources import android.test.suitebuilder.annotation.SmallTest -import com.android.internal.statusbar.IStatusBarService import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager -import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.util.DeviceConfigProxyFake import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.argumentCaptor -import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.withArgCaptor @@ -46,18 +41,16 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.anyString -import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.inOrder import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions -import org.mockito.Mockito.verifyZeroInteractions import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations /** - * NOTE: This test is for the version of FeatureFlagManager in src-release, which should not allow - * overriding, and should never return any value other than the one provided as the default. + * NOTE: This test is for the version of FeatureFlagManager in src-debug, which allows overriding + * the default. */ @SmallTest class FeatureFlagsDebugTest : SysuiTestCase() { @@ -68,10 +61,8 @@ class FeatureFlagsDebugTest : SysuiTestCase() { @Mock private lateinit var secureSettings: SecureSettings @Mock private lateinit var systemProperties: SystemPropertiesHelper @Mock private lateinit var resources: Resources - @Mock private lateinit var dumpManager: DumpManager @Mock private lateinit var commandRegistry: CommandRegistry - @Mock private lateinit var barService: IStatusBarService - @Mock private lateinit var pw: PrintWriter + @Mock private lateinit var restarter: Restarter private val flagMap = mutableMapOf<Int, Flag<*>>() private lateinit var broadcastReceiver: BroadcastReceiver private lateinit var clearCacheAction: Consumer<Int> @@ -92,12 +83,10 @@ class FeatureFlagsDebugTest : SysuiTestCase() { secureSettings, systemProperties, resources, - dumpManager, deviceConfig, serverFlagReader, flagMap, - commandRegistry, - barService + restarter ) verify(flagManager).onSettingsChangedAction = any() broadcastReceiver = withArgCaptor { @@ -366,53 +355,6 @@ class FeatureFlagsDebugTest : SysuiTestCase() { } @Test - fun statusBarCommand_IsRegistered() { - verify(commandRegistry).registerCommand(anyString(), any()) - } - - @Test - fun noOpCommand() { - val cmd = captureCommand() - - cmd.execute(pw, ArrayList()) - verify(pw, atLeastOnce()).println() - verify(flagManager).readFlagValue<Boolean>(eq(1), any()) - verifyZeroInteractions(secureSettings) - } - - @Test - fun readFlagCommand() { - addFlag(UnreleasedFlag(1)) - val cmd = captureCommand() - cmd.execute(pw, listOf("1")) - verify(flagManager).readFlagValue<Boolean>(eq(1), any()) - } - - @Test - fun setFlagCommand() { - addFlag(UnreleasedFlag(1)) - val cmd = captureCommand() - cmd.execute(pw, listOf("1", "on")) - verifyPutData(1, "{\"type\":\"boolean\",\"value\":true}") - } - - @Test - fun toggleFlagCommand() { - addFlag(ReleasedFlag(1)) - val cmd = captureCommand() - cmd.execute(pw, listOf("1", "toggle")) - verifyPutData(1, "{\"type\":\"boolean\",\"value\":false}", 2) - } - - @Test - fun eraseFlagCommand() { - addFlag(ReleasedFlag(1)) - val cmd = captureCommand() - cmd.execute(pw, listOf("1", "erase")) - verify(secureSettings).putStringForUser(eq("key-1"), eq(""), anyInt()) - } - - @Test fun dumpFormat() { val flag1 = ReleasedFlag(1) val flag2 = ResourceBooleanFlag(2, 1002) @@ -471,13 +413,6 @@ class FeatureFlagsDebugTest : SysuiTestCase() { return flag } - private fun captureCommand(): Command { - val captor = argumentCaptor<Function0<Command>>() - verify(commandRegistry).registerCommand(anyString(), capture(captor)) - - return captor.value.invoke() - } - private fun dumpToString(): String { val sw = StringWriter() val pw = PrintWriter(sw) diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt index e94b5202956d..575c14262b74 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt @@ -19,17 +19,12 @@ import android.content.pm.PackageManager.NameNotFoundException import android.content.res.Resources import android.test.suitebuilder.annotation.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager import com.android.systemui.util.DeviceConfigProxyFake -import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat -import org.junit.After import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -43,7 +38,6 @@ class FeatureFlagsReleaseTest : SysuiTestCase() { @Mock private lateinit var mResources: Resources @Mock private lateinit var mSystemProperties: SystemPropertiesHelper - @Mock private lateinit var mDumpManager: DumpManager private val serverFlagReader = ServerFlagReaderFake() private val deviceConfig = DeviceConfigProxyFake() @@ -55,15 +49,7 @@ class FeatureFlagsReleaseTest : SysuiTestCase() { mResources, mSystemProperties, deviceConfig, - serverFlagReader, - mDumpManager) - } - - @After - fun onFinished() { - // The dump manager should be registered with even for the release version, but that's it. - verify(mDumpManager).registerDumpable(any(), any()) - verifyNoMoreInteractions(mDumpManager) + serverFlagReader) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt new file mode 100644 index 000000000000..9628ee93ceff --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import android.test.suitebuilder.annotation.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any +import java.io.PrintWriter +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +class FlagCommandTest : SysuiTestCase() { + + @Mock private lateinit var featureFlags: FeatureFlagsDebug + @Mock private lateinit var pw: PrintWriter + private val flagMap = mutableMapOf<Int, Flag<*>>() + private val flagA = UnreleasedFlag(500) + private val flagB = ReleasedFlag(501) + + private lateinit var cmd: FlagCommand + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + whenever(featureFlags.isEnabled(any(UnreleasedFlag::class.java))).thenReturn(false) + whenever(featureFlags.isEnabled(any(ReleasedFlag::class.java))).thenReturn(true) + flagMap.put(flagA.id, flagA) + flagMap.put(flagB.id, flagB) + + cmd = FlagCommand(featureFlags, flagMap) + } + + @Test + fun readFlagCommand() { + cmd.execute(pw, listOf(flagA.id.toString())) + Mockito.verify(featureFlags).isEnabled(flagA) + } + + @Test + fun setFlagCommand() { + cmd.execute(pw, listOf(flagB.id.toString(), "on")) + Mockito.verify(featureFlags).setBooleanFlagInternal(flagB, true) + } + + @Test + fun toggleFlagCommand() { + cmd.execute(pw, listOf(flagB.id.toString(), "toggle")) + Mockito.verify(featureFlags).setBooleanFlagInternal(flagB, false) + } + + @Test + fun eraseFlagCommand() { + cmd.execute(pw, listOf(flagA.id.toString(), "erase")) + Mockito.verify(featureFlags).eraseFlag(flagA) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java index d418836b5753..7f55d388c34f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsImeTest.java @@ -49,6 +49,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import java.io.IOException; import java.util.concurrent.TimeUnit; import java.util.function.BooleanSupplier; @@ -148,8 +149,12 @@ public class GlobalActionsImeTest extends SysuiTestCase { return false; } - private static void executeShellCommand(String cmd) { - InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(cmd); + private void executeShellCommand(String cmd) { + try { + runShellCommand(cmd); + } catch (IOException e) { + throw new RuntimeException(e); + } } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java deleted file mode 100644 index b5e9e8decb4c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; - -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyObject; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.res.Resources; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.view.View; - -import androidx.test.filters.SmallTest; - -import com.android.keyguard.AnimatableClockController; -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.settingslib.Utils; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shared.clocks.AnimatableClockView; -import com.android.systemui.statusbar.policy.BatteryController; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.MockitoSession; -import org.mockito.quality.Strictness; - -import java.util.concurrent.Executor; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper -public class AnimatableClockControllerTest extends SysuiTestCase { - @Mock - private AnimatableClockView mClockView; - @Mock - private StatusBarStateController mStatusBarStateController; - @Mock - private BroadcastDispatcher mBroadcastDispatcher; - @Mock - private BatteryController mBatteryController; - @Mock - private KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @Mock - private Resources mResources; - @Mock - private Executor mMainExecutor; - @Mock - private Executor mBgExecutor; - @Mock - private FeatureFlags mFeatureFlags; - - private MockitoSession mStaticMockSession; - private AnimatableClockController mAnimatableClockController; - - // Capture listeners so that they can be used to send events - @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor = - ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); - private View.OnAttachStateChangeListener mAttachListener; - - @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor; - private StatusBarStateController.StateListener mStatusBarStateCallback; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - mStaticMockSession = mockitoSession() - .mockStatic(Utils.class) - .strictness(Strictness.LENIENT) // it's ok if mocked classes aren't used - .startMocking(); - when(Utils.getColorAttrDefaultColor(anyObject(), anyInt())).thenReturn(0); - - mAnimatableClockController = new AnimatableClockController( - mClockView, - mStatusBarStateController, - mBroadcastDispatcher, - mBatteryController, - mKeyguardUpdateMonitor, - mResources, - mMainExecutor, - mBgExecutor, - mFeatureFlags - ); - mAnimatableClockController.init(); - captureAttachListener(); - } - - @After - public void tearDown() { - mStaticMockSession.finishMocking(); - } - - @Test - public void testOnAttachedUpdatesDozeStateToTrue() { - // GIVEN dozing - when(mStatusBarStateController.isDozing()).thenReturn(true); - when(mStatusBarStateController.getDozeAmount()).thenReturn(1f); - - // WHEN the clock view gets attached - mAttachListener.onViewAttachedToWindow(mClockView); - - // THEN the clock controller updated its dozing state to true - assertTrue(mAnimatableClockController.isDozing()); - } - - @Test - public void testOnAttachedUpdatesDozeStateToFalse() { - // GIVEN not dozing - when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarStateController.getDozeAmount()).thenReturn(0f); - - // WHEN the clock view gets attached - mAttachListener.onViewAttachedToWindow(mClockView); - - // THEN the clock controller updated its dozing state to false - assertFalse(mAnimatableClockController.isDozing()); - } - - private void captureAttachListener() { - verify(mClockView).addOnAttachStateChangeListener(mAttachCaptor.capture()); - mAttachListener = mAttachCaptor.getValue(); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 21c018a0419d..2c3ddd574b0f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -19,6 +19,7 @@ package com.android.systemui.keyguard; import static android.view.WindowManagerPolicyConstants.OFF_BECAUSE_OF_USER; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -59,6 +60,7 @@ import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; @@ -111,6 +113,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private FalsingCollectorFake mFalsingCollector; + private @Mock CentralSurfaces mCentralSurfaces; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -176,7 +180,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { // and the keyguard goes away mViewMediator.setShowingLocked(false); - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false); + when(mKeyguardStateController.isShowing()).thenReturn(false); mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false); TestableLooper.get(this).processAllMessages(); @@ -201,7 +205,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { // and the keyguard goes away mViewMediator.setShowingLocked(false); - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false); + when(mKeyguardStateController.isShowing()).thenReturn(false); mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false); TestableLooper.get(this).processAllMessages(); @@ -229,12 +233,54 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { } @Test + public void testBouncerPrompt_nonStrongIdleTimeout() { + // GIVEN trust agents enabled and biometrics are enrolled + when(mUpdateMonitor.isTrustUsuallyManaged(anyInt())).thenReturn(true); + when(mUpdateMonitor.isUnlockingWithBiometricsPossible(anyInt())).thenReturn(true); + + // WHEN the strong auth reason is STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT + KeyguardUpdateMonitor.StrongAuthTracker strongAuthTracker = + mock(KeyguardUpdateMonitor.StrongAuthTracker.class); + when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(strongAuthTracker); + when(strongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true); + when(strongAuthTracker.isNonStrongBiometricAllowedAfterIdleTimeout( + anyInt())).thenReturn(false); + when(strongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn( + STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT); + + // THEN the bouncer prompt reason should return + // STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT + assertEquals(KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT, + mViewMediator.mViewMediatorCallback.getBouncerPromptReason()); + } + + @Test public void testHideSurfaceBehindKeyguardMarksKeyguardNotGoingAway() { mViewMediator.hideSurfaceBehindKeyguard(); verify(mKeyguardStateController).notifyKeyguardGoingAway(false); } + @Test + public void testUpdateIsKeyguardAfterOccludeAnimationEnds() { + mViewMediator.mOccludeAnimationController.onLaunchAnimationEnd( + false /* isExpandingFullyAbove */); + + // Since the updateIsKeyguard call is delayed during the animation, ensure it's called once + // it ends. + verify(mCentralSurfaces).updateIsKeyguard(); + } + + @Test + public void testUpdateIsKeyguardAfterOccludeAnimationIsCancelled() { + mViewMediator.mOccludeAnimationController.onLaunchAnimationCancelled( + null /* newKeyguardOccludedState */); + + // Since the updateIsKeyguard call is delayed during the animation, ensure it's called if + // it's cancelled. + verify(mCentralSurfaces).updateIsKeyguard(); + } + private void createAndStartViewMediator() { mViewMediator = new KeyguardViewMediator( mContext, @@ -264,5 +310,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mNotificationShadeWindowControllerLazy, () -> mActivityLaunchAnimator); mViewMediator.start(); + + mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java deleted file mode 100644 index cefd68dde0a0..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java +++ /dev/null @@ -1,476 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; -import static com.android.keyguard.LockIconView.ICON_LOCK; -import static com.android.keyguard.LockIconView.ICON_UNLOCK; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.anyInt; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.AnimatedStateListDrawable; -import android.hardware.biometrics.BiometricSourceType; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.util.Pair; -import android.view.View; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityManager; - -import androidx.test.filters.SmallTest; - -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.keyguard.KeyguardUpdateMonitorCallback; -import com.android.keyguard.KeyguardViewController; -import com.android.keyguard.LockIconView; -import com.android.keyguard.LockIconViewController; -import com.android.systemui.R; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.biometrics.AuthController; -import com.android.systemui.biometrics.AuthRippleController; -import com.android.systemui.doze.util.BurnInHelperKt; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.VibratorHelper; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.MockitoSession; -import org.mockito.quality.Strictness; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper -public class LockIconViewControllerTest extends SysuiTestCase { - private static final String UNLOCKED_LABEL = "unlocked"; - private static final int PADDING = 10; - - private MockitoSession mStaticMockSession; - - private @Mock LockIconView mLockIconView; - private @Mock AnimatedStateListDrawable mIconDrawable; - private @Mock Context mContext; - private @Mock Resources mResources; - private @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager; - private @Mock StatusBarStateController mStatusBarStateController; - private @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private @Mock KeyguardViewController mKeyguardViewController; - private @Mock KeyguardStateController mKeyguardStateController; - private @Mock FalsingManager mFalsingManager; - private @Mock AuthController mAuthController; - private @Mock DumpManager mDumpManager; - private @Mock AccessibilityManager mAccessibilityManager; - private @Mock ConfigurationController mConfigurationController; - private @Mock VibratorHelper mVibrator; - private @Mock AuthRippleController mAuthRippleController; - private FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock()); - - private LockIconViewController mLockIconViewController; - - // Capture listeners so that they can be used to send events - @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor = - ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); - private View.OnAttachStateChangeListener mAttachListener; - - @Captor private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor = - ArgumentCaptor.forClass(KeyguardStateController.Callback.class); - private KeyguardStateController.Callback mKeyguardStateCallback; - - @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor = - ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); - private StatusBarStateController.StateListener mStatusBarStateListener; - - @Captor private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor; - private AuthController.Callback mAuthControllerCallback; - - @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> - mKeyguardUpdateMonitorCallbackCaptor = - ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); - private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; - - @Captor private ArgumentCaptor<Point> mPointCaptor; - - @Before - public void setUp() throws Exception { - mStaticMockSession = mockitoSession() - .mockStatic(BurnInHelperKt.class) - .strictness(Strictness.LENIENT) - .startMocking(); - MockitoAnnotations.initMocks(this); - - setupLockIconViewMocks(); - when(mContext.getResources()).thenReturn(mResources); - when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager); - Rect windowBounds = new Rect(0, 0, 800, 1200); - when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds); - when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL); - when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable); - when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING); - when(mAuthController.getScaleFactor()).thenReturn(1f); - - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); - when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); - - mLockIconViewController = new LockIconViewController( - mLockIconView, - mStatusBarStateController, - mKeyguardUpdateMonitor, - mKeyguardViewController, - mKeyguardStateController, - mFalsingManager, - mAuthController, - mDumpManager, - mAccessibilityManager, - mConfigurationController, - mDelayableExecutor, - mVibrator, - mAuthRippleController, - mResources - ); - } - - @After - public void tearDown() { - mStaticMockSession.finishMocking(); - } - - @Test - public void testUpdateFingerprintLocationOnInit() { - // GIVEN fp sensor location is available pre-attached - Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location - - // WHEN lock icon view controller is initialized and attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN lock icon view location is updated to the udfps location with UDFPS radius - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testUpdatePaddingBasedOnResolutionScale() { - // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5 - Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location - when(mAuthController.getScaleFactor()).thenReturn(5f); - - // WHEN lock icon view controller is initialized and attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN lock icon view location is updated with the scaled radius - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING * 5)); - } - - @Test - public void testUpdateLockIconLocationOnAuthenticatorsRegistered() { - // GIVEN fp sensor location is not available pre-init - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - resetLockIconView(); // reset any method call counts for when we verify method calls later - - // GIVEN fp sensor location is available post-attached - captureAuthControllerCallback(); - Pair<Float, Point> udfps = setupUdfps(); - - // WHEN all authenticators are registered - mAuthControllerCallback.onAllAuthenticatorsRegistered(); - mDelayableExecutor.runAllReady(); - - // THEN lock icon view location is updated with the same coordinates as auth controller vals - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testUpdateLockIconLocationOnUdfpsLocationChanged() { - // GIVEN fp sensor location is not available pre-init - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - resetLockIconView(); // reset any method call counts for when we verify method calls later - - // GIVEN fp sensor location is available post-attached - captureAuthControllerCallback(); - Pair<Float, Point> udfps = setupUdfps(); - - // WHEN udfps location changes - mAuthControllerCallback.onUdfpsLocationChanged(); - mDelayableExecutor.runAllReady(); - - // THEN lock icon view location is updated with the same coordinates as auth controller vals - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() { - // GIVEN Udpfs sensor location is available - setupUdfps(); - - mLockIconViewController.init(); - captureAttachListener(); - - // WHEN the view is attached - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon view background should be enabled - verify(mLockIconView).setUseBackground(true); - } - - @Test - public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() { - // GIVEN Udfps sensor location is not supported - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - - mLockIconViewController.init(); - captureAttachListener(); - - // WHEN the view is attached - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon view background should be disabled - verify(mLockIconView).setUseBackground(false); - } - - @Test - public void testUnlockIconShows_biometricUnlockedTrue() { - // GIVEN UDFPS sensor location is available - setupUdfps(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureKeyguardUpdateMonitorCallback(); - - // GIVEN user has unlocked with a biometric auth (ie: face auth) - when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true); - reset(mLockIconView); - - // WHEN face auth's biometric running state changes - mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false, - BiometricSourceType.FACE); - - // THEN the unlock icon is shown - verify(mLockIconView).setContentDescription(UNLOCKED_LABEL); - } - - @Test - public void testLockIconStartState() { - // GIVEN lock icon state - setupShowLockIcon(); - - // WHEN lock icon controller is initialized - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon should show - verify(mLockIconView).updateIcon(ICON_LOCK, false); - } - - @Test - public void testLockIcon_updateToUnlock() { - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureKeyguardStateCallback(); - reset(mLockIconView); - - // WHEN the unlocked state changes to canDismissLockScreen=true - when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); - mKeyguardStateCallback.onUnlockedChanged(); - - // THEN the unlock should show - verify(mLockIconView).updateIcon(ICON_UNLOCK, false); - } - - @Test - public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() { - // GIVEN udfps not enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false); - - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN the dozing state changes - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - - // THEN the icon is cleared - verify(mLockIconView).clearIcon(); - } - - @Test - public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() { - // GIVEN udfps enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); - - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN the dozing state changes - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - - // THEN the AOD lock icon should show - verify(mLockIconView).updateIcon(ICON_LOCK, true); - } - - @Test - public void testBurnInOffsetsUpdated_onDozeAmountChanged() { - // GIVEN udfps enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); - - // GIVEN burn-in offset = 5 - int burnInOffset = 5; - when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset); - - // GIVEN starting state for the lock icon (keyguard) - setupShowLockIcon(); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN dozing updates - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - mStatusBarStateListener.onDozeAmountChanged(1f, 1f); - - // THEN the view's translation is updated to use the AoD burn-in offsets - verify(mLockIconView).setTranslationY(burnInOffset); - verify(mLockIconView).setTranslationX(burnInOffset); - reset(mLockIconView); - - // WHEN the device is no longer dozing - mStatusBarStateListener.onDozingChanged(false /* isDozing */); - mStatusBarStateListener.onDozeAmountChanged(0f, 0f); - - // THEN the view is updated to NO translation (no burn-in offsets anymore) - verify(mLockIconView).setTranslationY(0); - verify(mLockIconView).setTranslationX(0); - - } - private Pair<Float, Point> setupUdfps() { - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true); - final Point udfpsLocation = new Point(50, 75); - final float radius = 33f; - when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation); - when(mAuthController.getUdfpsRadius()).thenReturn(radius); - - return new Pair(radius, udfpsLocation); - } - - private void setupShowLockIcon() { - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); - when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarStateController.getDozeAmount()).thenReturn(0f); - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); - when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false); - } - - private void captureAuthControllerCallback() { - verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture()); - mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue(); - } - - private void captureAttachListener() { - verify(mLockIconView).addOnAttachStateChangeListener(mAttachCaptor.capture()); - mAttachListener = mAttachCaptor.getValue(); - } - - private void captureKeyguardStateCallback() { - verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture()); - mKeyguardStateCallback = mKeyguardStateCaptor.getValue(); - } - - private void captureStatusBarStateListener() { - verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture()); - mStatusBarStateListener = mStatusBarStateCaptor.getValue(); - } - - private void captureKeyguardUpdateMonitorCallback() { - verify(mKeyguardUpdateMonitor).registerCallback( - mKeyguardUpdateMonitorCallbackCaptor.capture()); - mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue(); - } - - private void setupLockIconViewMocks() { - when(mLockIconView.getResources()).thenReturn(mResources); - when(mLockIconView.getContext()).thenReturn(mContext); - } - - private void resetLockIconView() { - reset(mLockIconView); - setupLockIconViewMocks(); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index ba1e168bc316..7a1568098e4a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.doze.DozeHost import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -33,7 +34,6 @@ import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @SmallTest @@ -116,6 +116,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this) assertThat(latest).isFalse() + assertThat(underTest.isKeyguardShowing()).isFalse() val captor = argumentCaptor<KeyguardStateController.Callback>() verify(keyguardStateController).addCallback(captor.capture()) @@ -123,10 +124,12 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { whenever(keyguardStateController.isShowing).thenReturn(true) captor.value.onKeyguardShowingChanged() assertThat(latest).isTrue() + assertThat(underTest.isKeyguardShowing()).isTrue() whenever(keyguardStateController.isShowing).thenReturn(false) captor.value.onKeyguardShowingChanged() assertThat(latest).isFalse() + assertThat(underTest.isKeyguardShowing()).isFalse() job.cancel() } @@ -150,6 +153,21 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun `isDozing - starts with correct initial value for isDozing`() = runBlockingTest { + var latest: Boolean? = null + + whenever(statusBarStateController.isDozing).thenReturn(true) + var job = underTest.isDozing.onEach { latest = it }.launchIn(this) + assertThat(latest).isTrue() + job.cancel() + + whenever(statusBarStateController.isDozing).thenReturn(false) + job = underTest.isDozing.onEach { latest = it }.launchIn(this) + assertThat(latest).isFalse() + job.cancel() + } + + @Test fun dozeAmount() = runBlockingTest { val values = mutableListOf<Float>() val job = underTest.dozeAmount.onEach(values::add).launchIn(this) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt new file mode 100644 index 000000000000..1b34100b1cef --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import android.animation.AnimationHandler.AnimationFrameCallbackProvider +import android.animation.ValueAnimator +import android.util.Log +import android.util.Log.TerribleFailure +import android.util.Log.TerribleFailureHandler +import android.view.Choreographer.FrameCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.google.common.truth.Truth.assertThat +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardTransitionRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardTransitionRepository + private lateinit var oldWtfHandler: TerribleFailureHandler + private lateinit var wtfHandler: WtfHandler + + @Before + fun setUp() { + underTest = KeyguardTransitionRepository() + wtfHandler = WtfHandler() + oldWtfHandler = Log.setWtfHandler(wtfHandler) + } + + @After + fun tearDown() { + oldWtfHandler?.let { Log.setWtfHandler(it) } + } + + @Test + fun `startTransition runs animator to completion`() = + runBlocking(IMMEDIATE) { + val (animator, provider) = setupAnimator(this) + + val steps = mutableListOf<TransitionStep>() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator)) + + val startTime = System.currentTimeMillis() + while (animator.isRunning()) { + yield() + if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) { + fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION") + } + } + + assertSteps(steps, listWithStep(BigDecimal(.1))) + + job.cancel() + provider.stop() + } + + @Test + fun `startTransition called during another transition fails`() { + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, null)) + underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, BOUNCER, null)) + + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Null animator enables manual control with updateTransition`() = + runBlocking(IMMEDIATE) { + val steps = mutableListOf<TransitionStep>() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + } + + assertThat(steps.size).isEqualTo(3) + assertThat(steps[0]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + assertThat(steps[1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING)) + assertThat(steps[2]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + job.cancel() + } + + @Test + fun `Attempt to manually update transition with invalid UUID throws exception`() { + underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING) + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Attempt to manually update transition after FINISHED state throws exception`() { + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + } + assertThat(wtfHandler.failed).isTrue() + } + + private fun listWithStep(step: BigDecimal): List<BigDecimal> { + val steps = mutableListOf<BigDecimal>() + + var i = BigDecimal.ZERO + while (i.compareTo(BigDecimal.ONE) <= 0) { + steps.add(i) + i = (i + step).setScale(2, RoundingMode.HALF_UP) + } + + return steps + } + + private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) { + // + 2 accounts for start and finish of automated transition + assertThat(steps.size).isEqualTo(fractions.size + 2) + + assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + fractions.forEachIndexed { index, fraction -> + assertThat(steps[index + 1]) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, fraction.toFloat(), TransitionState.RUNNING) + ) + } + assertThat(steps[steps.size - 1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + + assertThat(wtfHandler.failed).isFalse() + } + + private fun setupAnimator( + scope: CoroutineScope + ): Pair<ValueAnimator, TestFrameCallbackProvider> { + val animator = + ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(ANIMATION_DURATION) + } + + val provider = TestFrameCallbackProvider(animator, scope) + provider.start() + + return Pair(animator, provider) + } + + /** Gives direct control over ValueAnimator. See [AnimationHandler] */ + private class TestFrameCallbackProvider( + private val animator: ValueAnimator, + private val scope: CoroutineScope, + ) : AnimationFrameCallbackProvider { + + private var frameCount = 1L + private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null)) + private var job: Job? = null + + fun start() { + animator.getAnimationHandler().setProvider(this) + + job = + scope.launch { + frames.collect { + // Delay is required for AnimationHandler to properly register a callback + delay(1) + val (frameNumber, callback) = it + callback?.doFrame(frameNumber) + } + } + } + + fun stop() { + job?.cancel() + animator.getAnimationHandler().setProvider(null) + } + + override fun postFrameCallback(cb: FrameCallback) { + frames.value = Pair(++frameCount, cb) + } + override fun postCommitCallback(runnable: Runnable) {} + override fun getFrameTime() = frameCount + override fun getFrameDelay() = 1L + override fun setFrameDelay(delay: Long) {} + } + + private class WtfHandler : TerribleFailureHandler { + var failed = false + override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) { + failed = true + } + } + + companion object { + private const val MAX_TEST_DURATION = 100L + private const val ANIMATION_DURATION = 10L + private const val OWNER_NAME = "Test" + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index c5e828eadf9b..b4d5464d1177 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -12,19 +12,20 @@ * 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.keyguard.domain.usecase +package com.android.systemui.keyguard.domain.interactor import android.content.Intent import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator -import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry @@ -34,6 +35,7 @@ import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Test @@ -46,7 +48,6 @@ import org.mockito.ArgumentMatchers.same import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @SmallTest @@ -55,7 +56,15 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { companion object { private val INTENT = Intent("some.intent.action") - private val DRAWABLE = mock<ContainedDrawable>() + private val DRAWABLE = + mock<Icon> { + whenever(this.contentDescription) + .thenReturn( + ContentDescription.Resource( + res = CONTENT_DESCRIPTION_RESOURCE_ID, + ) + ) + } private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 @Parameters( @@ -186,6 +195,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller + @Mock private lateinit var expandable: Expandable private lateinit var underTest: KeyguardQuickAffordanceInteractor @@ -199,6 +209,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + whenever(expandable.activityLaunchController()).thenReturn(animationController) homeControls = object : FakeKeyguardQuickAffordanceConfig() {} underTest = @@ -236,7 +247,6 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { state = KeyguardQuickAffordanceConfig.State.Visible( icon = DRAWABLE, - contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID, ) ) homeControls.onClickedResult = @@ -251,7 +261,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { underTest.onQuickAffordanceClicked( configKey = homeControls::class, - animationController = animationController, + expandable = expandable, ) if (startActivity) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 19d841222a01..65fd6e576650 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -12,26 +12,28 @@ * 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.keyguard.domain.usecase +package com.android.systemui.keyguard.domain.interactor import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase -import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -101,7 +103,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { homeControls.setState( KeyguardQuickAffordanceConfig.State.Visible( icon = ICON, - contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID, + toggle = KeyguardQuickAffordanceToggleState.On, ) ) @@ -120,8 +122,9 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { val visibleModel = latest as KeyguardQuickAffordanceModel.Visible assertThat(visibleModel.configKey).isEqualTo(configKey) assertThat(visibleModel.icon).isEqualTo(ICON) - assertThat(visibleModel.contentDescriptionResourceId) - .isEqualTo(CONTENT_DESCRIPTION_RESOURCE_ID) + assertThat(visibleModel.icon.contentDescription) + .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) + assertThat(visibleModel.toggle).isEqualTo(KeyguardQuickAffordanceToggleState.On) job.cancel() } @@ -131,7 +134,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { quickAccessWallet.setState( KeyguardQuickAffordanceConfig.State.Visible( icon = ICON, - contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID, ) ) @@ -150,8 +152,9 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { val visibleModel = latest as KeyguardQuickAffordanceModel.Visible assertThat(visibleModel.configKey).isEqualTo(configKey) assertThat(visibleModel.icon).isEqualTo(ICON) - assertThat(visibleModel.contentDescriptionResourceId) - .isEqualTo(CONTENT_DESCRIPTION_RESOURCE_ID) + assertThat(visibleModel.icon.contentDescription) + .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) + assertThat(visibleModel.toggle).isEqualTo(KeyguardQuickAffordanceToggleState.NotSupported) job.cancel() } @@ -161,7 +164,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { homeControls.setState( KeyguardQuickAffordanceConfig.State.Visible( icon = ICON, - contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID, ) ) @@ -182,7 +184,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { homeControls.setState( KeyguardQuickAffordanceConfig.State.Visible( icon = ICON, - contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID, ) ) @@ -197,7 +198,14 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { } companion object { - private val ICON: ContainedDrawable = mock() + private val ICON: Icon = mock { + whenever(this.contentDescription) + .thenReturn( + ContentDescription.Resource( + res = CONTENT_DESCRIPTION_RESOURCE_ID, + ) + ) + } private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt index 6ea1daa7704f..e99c139e9e7e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt @@ -17,7 +17,7 @@ package com.android.systemui.keyguard.domain.quickaffordance -import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -40,7 +40,7 @@ abstract class FakeKeyguardQuickAffordanceConfig : KeyguardQuickAffordanceConfig override val state: Flow<KeyguardQuickAffordanceConfig.State> = _state override fun onQuickAffordanceClicked( - animationController: ActivityLaunchAnimator.Controller?, + expandable: Expandable?, ): OnClickedResult { return onClickedResult } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt index dede4ec0210c..a809f0547ee6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt @@ -20,7 +20,7 @@ package com.android.systemui.keyguard.domain.quickaffordance import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult @@ -44,7 +44,7 @@ import org.mockito.MockitoAnnotations class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Mock private lateinit var component: ControlsComponent - @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller + @Mock private lateinit var expandable: Expandable private lateinit var underTest: HomeControlsKeyguardQuickAffordanceConfig @@ -103,7 +103,7 @@ class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() { fun `onQuickAffordanceClicked - canShowWhileLockedSetting is true`() = runBlockingTest { whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true)) - val onClickedResult = underTest.onQuickAffordanceClicked(animationController) + val onClickedResult = underTest.onQuickAffordanceClicked(expandable) assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java) assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isTrue() @@ -113,7 +113,7 @@ class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() { fun `onQuickAffordanceClicked - canShowWhileLockedSetting is false`() = runBlockingTest { whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false)) - val onClickedResult = underTest.onQuickAffordanceClicked(animationController) + val onClickedResult = underTest.onQuickAffordanceClicked(expandable) assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java) assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isFalse() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt index d4fba4126127..329c4db0a75c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt @@ -138,7 +138,7 @@ class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() { assertThat(latest).isInstanceOf(KeyguardQuickAffordanceConfig.State.Visible::class.java) val visibleState = latest as KeyguardQuickAffordanceConfig.State.Visible assertThat(visibleState.icon).isNotNull() - assertThat(visibleState.contentDescriptionResourceId).isNotNull() + assertThat(visibleState.icon.contentDescription).isNotNull() } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt index 5a3a78e9cb04..98dc4c4f6f76 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt @@ -21,12 +21,16 @@ import android.graphics.drawable.Drawable import android.service.quickaccesswallet.GetWalletCardsResponse import android.service.quickaccesswallet.QuickAccessWalletClient import androidx.test.filters.SmallTest +import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator -import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.plugins.ActivityStarter import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.android.systemui.wallet.controller.QuickAccessWalletController import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn @@ -38,7 +42,6 @@ import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @SmallTest @@ -69,8 +72,16 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { val job = underTest.state.onEach { latest = it }.launchIn(this) val visibleModel = latest as KeyguardQuickAffordanceConfig.State.Visible - assertThat(visibleModel.icon).isEqualTo(ContainedDrawable.WithDrawable(ICON)) - assertThat(visibleModel.contentDescriptionResourceId).isNotNull() + assertThat(visibleModel.icon) + .isEqualTo( + Icon.Loaded( + drawable = ICON, + contentDescription = + ContentDescription.Resource( + res = R.string.accessibility_wallet_button, + ), + ) + ) job.cancel() } @@ -125,8 +136,11 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun onQuickAffordanceClicked() { val animationController: ActivityLaunchAnimator.Controller = mock() + val expandable: Expandable = mock { + whenever(this.activityLaunchController()).thenReturn(animationController) + } - assertThat(underTest.onQuickAffordanceClicked(animationController)) + assertThat(underTest.onQuickAffordanceClicked(expandable)) .isEqualTo(KeyguardQuickAffordanceConfig.OnClickedResult.Handled) verify(walletController) .startQuickAccessUiIntent( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index c612091382db..d674c89c0e14 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -20,8 +20,8 @@ import android.content.Intent import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.ActivityLaunchAnimator -import com.android.systemui.containeddrawable.ContainedDrawable +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.Icon import com.android.systemui.doze.util.BurnInHelperWrapper import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor @@ -31,6 +31,7 @@ import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePositio import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController @@ -59,7 +60,7 @@ import org.mockito.MockitoAnnotations @RunWith(JUnit4::class) class KeyguardBottomAreaViewModelTest : SysuiTestCase() { - @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller + @Mock private lateinit var expandable: Expandable @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var keyguardStateController: KeyguardStateController @@ -130,6 +131,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { TestConfig( isVisible = true, isClickable = true, + isActivated = true, icon = mock(), canShowWhileLocked = false, intent = Intent("action"), @@ -505,7 +507,12 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { } KeyguardQuickAffordanceConfig.State.Visible( icon = testConfig.icon ?: error("Icon is unexpectedly null!"), - contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID, + toggle = + when (testConfig.isActivated) { + true -> KeyguardQuickAffordanceToggleState.On + false -> KeyguardQuickAffordanceToggleState.Off + null -> KeyguardQuickAffordanceToggleState.NotSupported + } ) } else { KeyguardQuickAffordanceConfig.State.Hidden @@ -522,12 +529,13 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { checkNotNull(viewModel) assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible) assertThat(viewModel.isClickable).isEqualTo(testConfig.isClickable) + assertThat(viewModel.isActivated).isEqualTo(testConfig.isActivated) if (testConfig.isVisible) { assertThat(viewModel.icon).isEqualTo(testConfig.icon) viewModel.onClicked.invoke( KeyguardQuickAffordanceViewModel.OnClickedParameters( configKey = configKey, - animationController = animationController, + expandable = expandable, ) ) if (testConfig.intent != null) { @@ -543,7 +551,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { private data class TestConfig( val isVisible: Boolean, val isClickable: Boolean = false, - val icon: ContainedDrawable? = null, + val isActivated: Boolean = false, + val icon: Icon? = null, val canShowWhileLocked: Boolean = false, val intent: Intent? = null, ) { @@ -555,6 +564,5 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { companion object { private const val DEFAULT_BURN_IN_OFFSET = 5 private const val RETURNED_BURN_IN_OFFSET = 3 - private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java index b8e9cf48f3e2..dc5522efe406 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java @@ -82,7 +82,6 @@ public class SessionTrackerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mSessionTracker = new SessionTracker( - mContext, mStatusBarService, mAuthController, mKeyguardUpdateMonitor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt deleted file mode 100644 index 5ad354247a04..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt +++ /dev/null @@ -1,401 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media - -import android.app.PendingIntent -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import androidx.test.filters.SmallTest -import com.android.internal.logging.InstanceId -import com.android.systemui.SysuiTestCase -import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener -import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider -import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.util.mockito.capture -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.time.FakeSystemClock -import javax.inject.Provider -import junit.framework.Assert.assertEquals -import junit.framework.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever -import org.mockito.MockitoAnnotations - -private val DATA = MediaTestUtils.emptyMediaData - -private val SMARTSPACE_KEY = "smartspace" - -@SmallTest -@TestableLooper.RunWithLooper(setAsMainLooper = true) -@RunWith(AndroidTestingRunner::class) -class MediaCarouselControllerTest : SysuiTestCase() { - - @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel> - @Mock lateinit var panel: MediaControlPanel - @Mock lateinit var visualStabilityProvider: VisualStabilityProvider - @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager - @Mock lateinit var mediaHostState: MediaHostState - @Mock lateinit var activityStarter: ActivityStarter - @Mock @Main private lateinit var executor: DelayableExecutor - @Mock lateinit var mediaDataManager: MediaDataManager - @Mock lateinit var configurationController: ConfigurationController - @Mock lateinit var falsingCollector: FalsingCollector - @Mock lateinit var falsingManager: FalsingManager - @Mock lateinit var dumpManager: DumpManager - @Mock lateinit var logger: MediaUiEventLogger - @Mock lateinit var debugLogger: MediaCarouselControllerLogger - @Mock lateinit var mediaPlayer: MediaControlPanel - @Mock lateinit var mediaViewController: MediaViewController - @Mock lateinit var smartspaceMediaData: SmartspaceMediaData - @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> - @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener> - - private val clock = FakeSystemClock() - private lateinit var mediaCarouselController: MediaCarouselController - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - mediaCarouselController = MediaCarouselController( - context, - mediaControlPanelFactory, - visualStabilityProvider, - mediaHostStatesManager, - activityStarter, - clock, - executor, - mediaDataManager, - configurationController, - falsingCollector, - falsingManager, - dumpManager, - logger, - debugLogger - ) - verify(mediaDataManager).addListener(capture(listener)) - verify(visualStabilityProvider) - .addPersistentReorderingAllowedListener(capture(visualStabilityCallback)) - whenever(mediaControlPanelFactory.get()).thenReturn(mediaPlayer) - whenever(mediaPlayer.mediaViewController).thenReturn(mediaViewController) - whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData) - MediaPlayerData.clear() - } - - @Test - fun testPlayerOrdering() { - // Test values: key, data, last active time - val playingLocal = Triple("playing local", - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false), - 4500L) - - val playingCast = Triple("playing cast", - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false), - 5000L) - - val pausedLocal = Triple("paused local", - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false), - 1000L) - - val pausedCast = Triple("paused cast", - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false), - 2000L) - - val playingRcn = Triple("playing RCN", - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false), - 5000L) - - val pausedRcn = Triple("paused RCN", - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false), - 5000L) - - val active = Triple("active", - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true), - 250L) - - val resume1 = Triple("resume 1", - DATA.copy(active = false, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true), - 500L) - - val resume2 = Triple("resume 2", - DATA.copy(active = false, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true), - 1000L) - - val activeMoreRecent = Triple("active more recent", - DATA.copy(active = false, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 2L), - 1000L) - - val activeLessRecent = Triple("active less recent", - DATA.copy(active = false, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 1L), - 1000L) - // Expected ordering for media players: - // Actively playing local sessions - // Actively playing cast sessions - // Paused local and cast sessions, by last active - // RCNs - // Resume controls, by last active - - val expected = listOf(playingLocal, playingCast, pausedCast, pausedLocal, playingRcn, - pausedRcn, active, resume2, resume1) - - expected.forEach { - clock.setCurrentTimeMillis(it.third) - MediaPlayerData.addMediaPlayer(it.first, it.second.copy(notificationKey = it.first), - panel, clock, isSsReactivated = false) - } - - for ((index, key) in MediaPlayerData.playerKeys().withIndex()) { - assertEquals(expected.get(index).first, key.data.notificationKey) - } - } - - @Test - fun testOrderWithSmartspace_prioritized() { - testPlayerOrdering() - - // If smartspace is prioritized - MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel, - true, clock) - - // Then it should be shown immediately after any actively playing controls - assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) - } - - @Test - fun testOrderWithSmartspace_notPrioritized() { - testPlayerOrdering() - - // If smartspace is not prioritized - MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel, - false, clock) - - // Then it should be shown at the end of the carousel's active entries - val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1 - assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec) - } - - @Test - fun testSwipeDismiss_logged() { - mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke() - - verify(logger).logSwipeDismiss() - } - - @Test - fun testSettingsButton_logged() { - mediaCarouselController.settingsButton.callOnClick() - - verify(logger).logCarouselSettings() - } - - @Test - fun testLocationChangeQs_logged() { - mediaCarouselController.onDesiredLocationChanged( - MediaHierarchyManager.LOCATION_QS, - mediaHostState, - animate = false) - verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS) - } - - @Test - fun testLocationChangeQqs_logged() { - mediaCarouselController.onDesiredLocationChanged( - MediaHierarchyManager.LOCATION_QQS, - mediaHostState, - animate = false) - verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS) - } - - @Test - fun testLocationChangeLockscreen_logged() { - mediaCarouselController.onDesiredLocationChanged( - MediaHierarchyManager.LOCATION_LOCKSCREEN, - mediaHostState, - animate = false) - verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN) - } - - @Test - fun testLocationChangeDream_logged() { - mediaCarouselController.onDesiredLocationChanged( - MediaHierarchyManager.LOCATION_DREAM_OVERLAY, - mediaHostState, - animate = false) - verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY) - } - - @Test - fun testRecommendationRemoved_logged() { - val packageName = "smartspace package" - val instanceId = InstanceId.fakeInstanceId(123) - - val smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - packageName = packageName, - instanceId = instanceId - ) - MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock) - mediaCarouselController.removePlayer(SMARTSPACE_KEY) - - verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!)) - } - - fun testMediaLoaded_ScrollToActivePlayer() { - listener.value.onMediaDataLoaded("playing local", - null, - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false) - ) - listener.value.onMediaDataLoaded("paused local", - null, - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)) - // adding a media recommendation card. - MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel, - false, clock) - mediaCarouselController.shouldScrollToActivePlayer = true - // switching between media players. - listener.value.onMediaDataLoaded("playing local", - "playing local", - DATA.copy(active = true, isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true) - ) - listener.value.onMediaDataLoaded("paused local", - "paused local", - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)) - - assertEquals( - MediaPlayerData.getMediaPlayerIndex("paused local"), - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex - ) - } - - @Test - fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() { - MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel, - false, clock) - listener.value.onMediaDataLoaded("playing local", - null, - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false) - ) - - var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local") - assertEquals( - playerIndex, - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex - ) - assertEquals(playerIndex, 0) - - // Replaying the same media player one more time. - // And check that the card stays in its position. - listener.value.onMediaDataLoaded("playing local", - null, - DATA.copy(active = true, isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false) - ) - playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local") - assertEquals(playerIndex, 0) - } - - @Test - fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() { - var result = false - mediaCarouselController.updateHostVisibility = { result = true } - - whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true) - listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) - - assertEquals(true, result) - } - - @Test - fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() { - var result = false - mediaCarouselController.updateHostVisibility = { result = true } - - whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false) - listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) - assertEquals(false, result) - - visualStabilityCallback.value.onReorderingAllowed() - assertEquals(true, result) - } - - @Test - fun testGetCurrentVisibleMediaContentIntent() { - val clickIntent1 = mock(PendingIntent::class.java) - val player1 = Triple("player1", - DATA.copy(clickIntent = clickIntent1), - 1000L) - clock.setCurrentTimeMillis(player1.third) - MediaPlayerData.addMediaPlayer(player1.first, - player1.second.copy(notificationKey = player1.first), - panel, clock, isSsReactivated = false) - - assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1) - - val clickIntent2 = mock(PendingIntent::class.java) - val player2 = Triple("player2", - DATA.copy(clickIntent = clickIntent2), - 2000L) - clock.setCurrentTimeMillis(player2.third) - MediaPlayerData.addMediaPlayer(player2.first, - player2.second.copy(notificationKey = player2.first), - panel, clock, isSsReactivated = false) - - // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is - // added to the front because it was active more recently. - assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) - - val clickIntent3 = mock(PendingIntent::class.java) - val player3 = Triple("player3", - DATA.copy(clickIntent = clickIntent3), - 500L) - clock.setCurrentTimeMillis(player3.third) - MediaPlayerData.addMediaPlayer(player3.first, - player3.second.copy(notificationKey = player3.first), - panel, clock, isSsReactivated = false) - - // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is - // added to the end because it was active less recently. - assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt deleted file mode 100644 index 3d9ed5fe7f7b..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.android.systemui.media - -import com.android.internal.logging.InstanceId - -class MediaTestUtils { - companion object { - val emptyMediaData = MediaData( - userId = 0, - initialized = true, - app = null, - appIcon = null, - artist = null, - song = null, - artwork = null, - actions = emptyList(), - actionsToShowInCompact = emptyList(), - packageName = "", - token = null, - clickIntent = null, - device = null, - active = true, - resumeAction = null, - isPlaying = false, - instanceId = InstanceId.fakeInstanceId(-1), - appUid = -1) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt deleted file mode 100644 index ee327937e091..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.android.systemui.media - -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.view.LayoutInflater -import android.widget.FrameLayout -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper -class MediaViewHolderTest : SysuiTestCase() { - - @Test - fun create_succeeds() { - val inflater = LayoutInflater.from(context) - val parent = FrameLayout(context) - - MediaViewHolder.create(inflater, parent) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.kt new file mode 100644 index 000000000000..3437365d9902 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls + +import com.android.internal.logging.InstanceId +import com.android.systemui.media.controls.models.player.MediaData + +class MediaTestUtils { + companion object { + val emptyMediaData = + MediaData( + userId = 0, + initialized = true, + app = null, + appIcon = null, + artist = null, + song = null, + artwork = null, + actions = emptyList(), + actionsToShowInCompact = emptyList(), + packageName = "", + token = null, + clickIntent = null, + device = null, + active = true, + resumeAction = null, + isPlaying = false, + instanceId = InstanceId.fakeInstanceId(-1), + appUid = -1 + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.kt new file mode 100644 index 000000000000..c829d4cbfb71 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.models.player + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.LayoutInflater +import android.widget.FrameLayout +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class MediaViewHolderTest : SysuiTestCase() { + + @Test + fun create_succeeds() { + val inflater = LayoutInflater.from(context) + val parent = FrameLayout(context) + + MediaViewHolder.create(inflater, parent) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt index 9e9cda843c8f..97b18e214550 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.animation.Animator import android.animation.ObjectAnimator @@ -26,6 +26,7 @@ import android.widget.TextView import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.ui.SquigglyProgress import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule @@ -33,8 +34,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit @SmallTest @RunWith(AndroidTestingRunner::class) @@ -56,10 +57,14 @@ class SeekBarObserverTest : SysuiTestCase() { @Before fun setUp() { - context.orCreateTestableResources - .addOverride(R.dimen.qs_media_enabled_seekbar_height, enabledHeight) - context.orCreateTestableResources - .addOverride(R.dimen.qs_media_disabled_seekbar_height, disabledHeight) + context.orCreateTestableResources.addOverride( + R.dimen.qs_media_enabled_seekbar_height, + enabledHeight + ) + context.orCreateTestableResources.addOverride( + R.dimen.qs_media_disabled_seekbar_height, + disabledHeight + ) seekBarView = SeekBar(context) seekBarView.progressDrawable = mockSquigglyProgress @@ -69,11 +74,12 @@ class SeekBarObserverTest : SysuiTestCase() { whenever(mockHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView) whenever(mockHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView) - observer = object : SeekBarObserver(mockHolder) { - override fun buildResetAnimator(targetTime: Int): Animator { - return mockSeekbarAnimator + observer = + object : SeekBarObserver(mockHolder) { + override fun buildResetAnimator(targetTime: Int): Animator { + return mockSeekbarAnimator + } } - } } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt index 82aa6123917e..7cd8e749a6e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.models.player import android.media.MediaMetadata import android.media.session.MediaController @@ -26,13 +26,13 @@ import android.widget.SeekBar import androidx.arch.core.executor.ArchTaskExecutor import androidx.arch.core.executor.TaskExecutor import androidx.test.filters.SmallTest - import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.Classifier +import com.android.systemui.plugins.FalsingManager import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeRepeatableExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat - import org.junit.After import org.junit.Before import org.junit.Ignore @@ -47,8 +47,8 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit @SmallTest @RunWith(AndroidTestingRunner::class) @@ -57,19 +57,22 @@ public class SeekBarViewModelTest : SysuiTestCase() { private lateinit var viewModel: SeekBarViewModel private lateinit var fakeExecutor: FakeExecutor - private val taskExecutor: TaskExecutor = object : TaskExecutor() { - override fun executeOnDiskIO(runnable: Runnable) { - runnable.run() - } - override fun postToMainThread(runnable: Runnable) { - runnable.run() - } - override fun isMainThread(): Boolean { - return true + private val taskExecutor: TaskExecutor = + object : TaskExecutor() { + override fun executeOnDiskIO(runnable: Runnable) { + runnable.run() + } + override fun postToMainThread(runnable: Runnable) { + runnable.run() + } + override fun isMainThread(): Boolean { + return true + } } - } @Mock private lateinit var mockController: MediaController @Mock private lateinit var mockTransport: MediaController.TransportControls + @Mock private lateinit var falsingManager: FalsingManager + @Mock private lateinit var mockBar: SeekBar private val token1 = MediaSession.Token(1, null) private val token2 = MediaSession.Token(2, null) @@ -78,9 +81,10 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Before fun setUp() { fakeExecutor = FakeExecutor(FakeSystemClock()) - viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor)) - viewModel.logSeek = { } + viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor), falsingManager) + viewModel.logSeek = {} whenever(mockController.sessionToken).thenReturn(token1) + whenever(mockBar.context).thenReturn(context) // LiveData to run synchronously ArchTaskExecutor.getInstance().setDelegate(taskExecutor) @@ -132,16 +136,18 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun updateDurationWithPlayback() { // GIVEN that the duration is contained within the metadata val duration = 12000L - val metadata = MediaMetadata.Builder().run { - putLong(MediaMetadata.METADATA_KEY_DURATION, duration) - build() - } + val metadata = + MediaMetadata.Builder().run { + putLong(MediaMetadata.METADATA_KEY_DURATION, duration) + build() + } whenever(mockController.getMetadata()).thenReturn(metadata) // AND a valid playback state (ie. media session is not destroyed) - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -155,10 +161,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun updateDurationWithoutPlayback() { // GIVEN that the duration is contained within the metadata val duration = 12000L - val metadata = MediaMetadata.Builder().run { - putLong(MediaMetadata.METADATA_KEY_DURATION, duration) - build() - } + val metadata = + MediaMetadata.Builder().run { + putLong(MediaMetadata.METADATA_KEY_DURATION, duration) + build() + } whenever(mockController.getMetadata()).thenReturn(metadata) // WHEN the controller is updated viewModel.updateController(mockController) @@ -171,16 +178,18 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun updateDurationNegative() { // GIVEN that the duration is negative val duration = -1L - val metadata = MediaMetadata.Builder().run { - putLong(MediaMetadata.METADATA_KEY_DURATION, duration) - build() - } + val metadata = + MediaMetadata.Builder().run { + putLong(MediaMetadata.METADATA_KEY_DURATION, duration) + build() + } whenever(mockController.getMetadata()).thenReturn(metadata) // AND a valid playback state (ie. media session is not destroyed) - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -192,16 +201,18 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun updateDurationZero() { // GIVEN that the duration is zero val duration = 0L - val metadata = MediaMetadata.Builder().run { - putLong(MediaMetadata.METADATA_KEY_DURATION, duration) - build() - } + val metadata = + MediaMetadata.Builder().run { + putLong(MediaMetadata.METADATA_KEY_DURATION, duration) + build() + } whenever(mockController.getMetadata()).thenReturn(metadata) // AND a valid playback state (ie. media session is not destroyed) - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -215,10 +226,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { // GIVEN that the metadata is null whenever(mockController.getMetadata()).thenReturn(null) // AND a valid playback state (ie. media session is not destroyed) - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -230,10 +242,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun updateElapsedTime() { // GIVEN that the PlaybackState contains the current position val position = 200L - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, position, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, position, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -245,10 +258,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Ignore fun updateSeekAvailable() { // GIVEN that seek is included in actions - val state = PlaybackState.Builder().run { - setActions(PlaybackState.ACTION_SEEK_TO) - build() - } + val state = + PlaybackState.Builder().run { + setActions(PlaybackState.ACTION_SEEK_TO) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -260,10 +274,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Ignore fun updateSeekNotAvailable() { // GIVEN that seek is not included in actions - val state = PlaybackState.Builder().run { - setActions(PlaybackState.ACTION_PLAY) - build() - } + val state = + PlaybackState.Builder().run { + setActions(PlaybackState.ACTION_PLAY) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -315,9 +330,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Ignore fun onSeekProgressWithSeekStarting() { val pos = 42L - with(viewModel) { - onSeekProgress(pos) - } + with(viewModel) { onSeekProgress(pos) } fakeExecutor.runAllReady() // THEN then elapsed time should not be updated assertThat(viewModel.progress.value!!.elapsedTime).isNull() @@ -326,11 +339,12 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Test fun seekStarted_listenerNotified() { var isScrubbing: Boolean? = null - val listener = object : SeekBarViewModel.ScrubbingChangeListener { - override fun onScrubbingChanged(scrubbing: Boolean) { - isScrubbing = scrubbing + val listener = + object : SeekBarViewModel.ScrubbingChangeListener { + override fun onScrubbingChanged(scrubbing: Boolean) { + isScrubbing = scrubbing + } } - } viewModel.setScrubbingChangeListener(listener) viewModel.onSeekStarting() @@ -342,11 +356,12 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Test fun seekEnded_listenerNotified() { var isScrubbing: Boolean? = null - val listener = object : SeekBarViewModel.ScrubbingChangeListener { - override fun onScrubbingChanged(scrubbing: Boolean) { - isScrubbing = scrubbing + val listener = + object : SeekBarViewModel.ScrubbingChangeListener { + override fun onScrubbingChanged(scrubbing: Boolean) { + isScrubbing = scrubbing + } } - } viewModel.setScrubbingChangeListener(listener) // Start seeking @@ -382,9 +397,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { val bar = SeekBar(context) // WHEN we get an onProgressChanged event without an onStartTrackingTouch event - with(viewModel.seekBarListener) { - onProgressChanged(bar, pos, true) - } + with(viewModel.seekBarListener) { onProgressChanged(bar, pos, true) } fakeExecutor.runAllReady() // THEN we immediately update the transport @@ -409,9 +422,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { viewModel.updateController(mockController) // WHEN user starts dragging the seek bar val pos = 42 - val bar = SeekBar(context).apply { - progress = pos - } + val bar = SeekBar(context).apply { progress = pos } viewModel.seekBarListener.onStartTrackingTouch(bar) fakeExecutor.runAllReady() // THEN transport controls should be used @@ -424,9 +435,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { viewModel.updateController(mockController) // WHEN user ends drag val pos = 42 - val bar = SeekBar(context).apply { - progress = pos - } + val bar = SeekBar(context).apply { progress = pos } viewModel.seekBarListener.onStopTrackingTouch(bar) fakeExecutor.runAllReady() // THEN transport controls should be used @@ -440,9 +449,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { // WHEN user starts dragging the seek bar val pos = 42 val progPos = 84 - val bar = SeekBar(context).apply { - progress = pos - } + val bar = SeekBar(context).apply { progress = pos } with(viewModel.seekBarListener) { onStartTrackingTouch(bar) onProgressChanged(bar, progPos, true) @@ -454,12 +461,32 @@ public class SeekBarViewModelTest : SysuiTestCase() { } @Test + fun onFalseTapOrTouch() { + whenever(mockController.getTransportControls()).thenReturn(mockTransport) + whenever(falsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).thenReturn(true) + whenever(falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)).thenReturn(true) + viewModel.updateController(mockController) + val pos = 169 + + viewModel.attachTouchHandlers(mockBar) + with(viewModel.seekBarListener) { + onStartTrackingTouch(mockBar) + onProgressChanged(mockBar, pos, true) + onStopTrackingTouch(mockBar) + } + + // THEN transport controls should not be used + verify(mockTransport, never()).seekTo(pos.toLong()) + } + + @Test fun queuePollTaskWhenPlaying() { // GIVEN that the track is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 100L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 100L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN the controller is updated viewModel.updateController(mockController) @@ -470,10 +497,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Test fun noQueuePollTaskWhenStopped() { // GIVEN that the playback state is stopped - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_STOPPED, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_STOPPED, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN updated viewModel.updateController(mockController) @@ -490,10 +518,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { runAllReady() } // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN updated viewModel.updateController(mockController) @@ -510,10 +539,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { runAllReady() } // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // WHEN updated viewModel.updateController(mockController) @@ -524,10 +554,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Test fun pollTaskQueuesAnotherPollTaskWhenPlaying() { // GIVEN that the track is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 100L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 100L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) viewModel.updateController(mockController) // WHEN the next task runs @@ -544,10 +575,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { // GIVEN listening viewModel.listening = true // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) viewModel.updateController(mockController) with(fakeExecutor) { @@ -570,10 +602,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { // GIVEN listening viewModel.listening = true // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) viewModel.updateController(mockController) with(fakeExecutor) { @@ -599,10 +632,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { // GIVEN listening viewModel.listening = true // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) viewModel.updateController(mockController) with(fakeExecutor) { @@ -632,10 +666,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { runAllReady() } // AND the playback state is playing - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_STOPPED, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_STOPPED, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) viewModel.updateController(mockController) // WHEN start listening @@ -651,10 +686,11 @@ public class SeekBarViewModelTest : SysuiTestCase() { verify(mockController).registerCallback(captor.capture()) val callback = captor.value // WHEN the callback receives an new state - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 100L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 100L, 1f) + build() + } callback.onPlaybackStateChanged(state) with(fakeExecutor) { advanceClockToNext() @@ -668,16 +704,18 @@ public class SeekBarViewModelTest : SysuiTestCase() { @Ignore fun clearSeekBar() { // GIVEN that the duration is contained within the metadata - val metadata = MediaMetadata.Builder().run { - putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L) - build() - } + val metadata = + MediaMetadata.Builder().run { + putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L) + build() + } whenever(mockController.getMetadata()).thenReturn(metadata) // AND a valid playback state (ie. media session is not destroyed) - val state = PlaybackState.Builder().run { - setState(PlaybackState.STATE_PLAYING, 200L, 1f) - build() - } + val state = + PlaybackState.Builder().run { + setState(PlaybackState.STATE_PLAYING, 200L, 1f) + build() + } whenever(mockController.getPlaybackState()).thenReturn(state) // AND the controller has been updated viewModel.updateController(mockController) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt index b5078bc37b84..1d6e980bdb86 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.models.recommendation import android.app.smartspace.SmartspaceAction import android.graphics.drawable.Icon @@ -36,11 +52,11 @@ class SmartspaceMediaDataTest : SysuiTestCase() { @Test fun isValid_tooFewRecs_returnsFalse() { - val data = DEFAULT_DATA.copy( - recommendations = listOf( - SmartspaceAction.Builder("id", "title").setIcon(icon).build() + val data = + DEFAULT_DATA.copy( + recommendations = + listOf(SmartspaceAction.Builder("id", "title").setIcon(icon).build()) ) - ) assertThat(data.isValid()).isFalse() } @@ -50,14 +66,10 @@ class SmartspaceMediaDataTest : SysuiTestCase() { val recommendations = mutableListOf<SmartspaceAction>() // Add one fewer recommendation w/ icon than the number required for (i in 1 until NUM_REQUIRED_RECOMMENDATIONS) { - recommendations.add( - SmartspaceAction.Builder("id", "title").setIcon(icon).build() - ) + recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build()) } for (i in 1 until 3) { - recommendations.add( - SmartspaceAction.Builder("id", "title").setIcon(null).build() - ) + recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(null).build()) } val data = DEFAULT_DATA.copy(recommendations = recommendations) @@ -70,9 +82,7 @@ class SmartspaceMediaDataTest : SysuiTestCase() { val recommendations = mutableListOf<SmartspaceAction>() // Add the number of required recommendations for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS) { - recommendations.add( - SmartspaceAction.Builder("id", "title").setIcon(icon).build() - ) + recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build()) } val data = DEFAULT_DATA.copy(recommendations = recommendations) @@ -85,9 +95,7 @@ class SmartspaceMediaDataTest : SysuiTestCase() { val recommendations = mutableListOf<SmartspaceAction>() // Add more than enough recommendations for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS + 3) { - recommendations.add( - SmartspaceAction.Builder("id", "title").setIcon(icon).build() - ) + recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build()) } val data = DEFAULT_DATA.copy(recommendations = recommendations) @@ -96,13 +104,14 @@ class SmartspaceMediaDataTest : SysuiTestCase() { } } -private val DEFAULT_DATA = SmartspaceMediaData( - targetId = "INVALID", - isActive = false, - packageName = "INVALID", - cardAction = null, - recommendations = emptyList(), - dismissIntent = null, - headphoneConnectionTimeMillis = 0, - instanceId = InstanceId.fakeInstanceId(-1) -) +private val DEFAULT_DATA = + SmartspaceMediaData( + targetId = "INVALID", + isActive = false, + packageName = "INVALID", + cardAction = null, + recommendations = emptyList(), + dismissIntent = null, + headphoneConnectionTimeMillis = 0, + instanceId = InstanceId.fakeInstanceId(-1) + ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java index 04b93d79f83b..4d2d0f05b76a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media; +package com.android.systemui.media.controls.pipeline; import static com.google.common.truth.Truth.assertThat; @@ -26,7 +26,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; -import android.graphics.Color; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -34,6 +33,8 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.InstanceId; import com.android.systemui.SysuiTestCase; +import com.android.systemui.media.controls.models.player.MediaData; +import com.android.systemui.media.controls.models.player.MediaDeviceData; import org.junit.Before; import org.junit.Rule; diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt index 6468fe1a81d7..575b1c6b126e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.app.smartspace.SmartspaceAction import android.testing.AndroidTestingRunner @@ -24,6 +24,11 @@ import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.ui.MediaPlayerData +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -58,24 +63,15 @@ private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!! @TestableLooper.RunWithLooper class MediaDataFilterTest : SysuiTestCase() { - @Mock - private lateinit var listener: MediaDataManager.Listener - @Mock - private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock - private lateinit var broadcastSender: BroadcastSender - @Mock - private lateinit var mediaDataManager: MediaDataManager - @Mock - private lateinit var lockscreenUserManager: NotificationLockscreenUserManager - @Mock - private lateinit var executor: Executor - @Mock - private lateinit var smartspaceData: SmartspaceMediaData - @Mock - private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction - @Mock - private lateinit var logger: MediaUiEventLogger + @Mock private lateinit var listener: MediaDataManager.Listener + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock private lateinit var broadcastSender: BroadcastSender + @Mock private lateinit var mediaDataManager: MediaDataManager + @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager + @Mock private lateinit var executor: Executor + @Mock private lateinit var smartspaceData: SmartspaceMediaData + @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction + @Mock private lateinit var logger: MediaUiEventLogger private lateinit var mediaDataFilter: MediaDataFilter private lateinit var dataMain: MediaData @@ -86,14 +82,16 @@ class MediaDataFilterTest : SysuiTestCase() { fun setup() { MockitoAnnotations.initMocks(this) MediaPlayerData.clear() - mediaDataFilter = MediaDataFilter( - context, - broadcastDispatcher, - broadcastSender, - lockscreenUserManager, - executor, - clock, - logger) + mediaDataFilter = + MediaDataFilter( + context, + broadcastDispatcher, + broadcastSender, + lockscreenUserManager, + executor, + clock, + logger + ) mediaDataFilter.mediaDataManager = mediaDataManager mediaDataFilter.addListener(listener) @@ -101,11 +99,13 @@ class MediaDataFilterTest : SysuiTestCase() { setUser(USER_MAIN) // Set up test media data - dataMain = MediaTestUtils.emptyMediaData.copy( + dataMain = + MediaTestUtils.emptyMediaData.copy( userId = USER_MAIN, packageName = PACKAGE, instanceId = INSTANCE_ID, - appUid = APP_UID) + appUid = APP_UID + ) dataGuest = dataMain.copy(userId = USER_GUEST) `when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY) @@ -113,8 +113,8 @@ class MediaDataFilterTest : SysuiTestCase() { `when`(smartspaceData.isValid()).thenReturn(true) `when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE) `when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem)) - `when`(smartspaceData.headphoneConnectionTimeMillis).thenReturn( - clock.currentTimeMillis() - 100) + `when`(smartspaceData.headphoneConnectionTimeMillis) + .thenReturn(clock.currentTimeMillis() - 100) `when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID) } @@ -130,8 +130,8 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) // THEN we should tell the listener - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false)) } @Test @@ -140,8 +140,8 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) // THEN we should NOT tell the listener - verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean(), - anyInt(), anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } @Test @@ -187,12 +187,12 @@ class MediaDataFilterTest : SysuiTestCase() { setUser(USER_GUEST) // THEN we should add back the guest user media - verify(listener).onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false)) // but not the main user's - verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), - anyInt(), anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), anyInt(), anyBoolean()) } @Test @@ -340,7 +340,7 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) verify(listener) - .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() assertThat(mediaDataFilter.hasActiveMedia()).isFalse() verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) @@ -353,8 +353,8 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean(), - anyInt(), anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse() @@ -370,7 +370,7 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) verify(listener) - .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() assertThat(mediaDataFilter.hasActiveMedia()).isFalse() verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) @@ -400,15 +400,15 @@ class MediaDataFilterTest : SysuiTestCase() { // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should tell listeners to treat the media as not active instead - verify(listener, never()).onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), - anyInt(), anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean()) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse() @@ -423,16 +423,23 @@ class MediaDataFilterTest : SysuiTestCase() { // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should tell listeners to treat the media as active instead val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true), - eq(100), eq(true)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() // Smartspace update shouldn't be propagated for the empty rec list. verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) @@ -445,20 +452,27 @@ class MediaDataFilterTest : SysuiTestCase() { // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should tell listeners to treat the media as active instead val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true), - eq(100), eq(true)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() // Smartspace update should also be propagated but not prioritized. verify(listener) - .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) } @@ -477,14 +491,21 @@ class MediaDataFilterTest : SysuiTestCase() { fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() { val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true), - eq(100), eq(true)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index f9c7d2d5cb41..11eb26b1da02 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.pipeline import android.app.Notification import android.app.Notification.MediaStyle @@ -26,6 +42,13 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.resume.MediaResumeListener +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.SbnBuilder import com.android.systemui.tuner.TunerService @@ -111,58 +134,68 @@ class MediaDataManagerTest : SysuiTestCase() { private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20) - private val originalSmartspaceSetting = Settings.Secure.getInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1) + private val originalSmartspaceSetting = + Settings.Secure.getInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 + ) @Before fun setup() { foregroundExecutor = FakeExecutor(clock) backgroundExecutor = FakeExecutor(clock) smartspaceMediaDataProvider = SmartspaceMediaDataProvider() - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1) - mediaDataManager = MediaDataManager( - context = context, - backgroundExecutor = backgroundExecutor, - foregroundExecutor = foregroundExecutor, - mediaControllerFactory = mediaControllerFactory, - broadcastDispatcher = broadcastDispatcher, - dumpManager = dumpManager, - mediaTimeoutListener = mediaTimeoutListener, - mediaResumeListener = mediaResumeListener, - mediaSessionBasedFilter = mediaSessionBasedFilter, - mediaDeviceManager = mediaDeviceManager, - mediaDataCombineLatest = mediaDataCombineLatest, - mediaDataFilter = mediaDataFilter, - activityStarter = activityStarter, - smartspaceMediaDataProvider = smartspaceMediaDataProvider, - useMediaResumption = true, - useQsMediaPlayer = true, - systemClock = clock, - tunerService = tunerService, - mediaFlags = mediaFlags, - logger = logger + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 ) - verify(tunerService).addTunable(capture(tunableCaptor), - eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)) + mediaDataManager = + MediaDataManager( + context = context, + backgroundExecutor = backgroundExecutor, + foregroundExecutor = foregroundExecutor, + mediaControllerFactory = mediaControllerFactory, + broadcastDispatcher = broadcastDispatcher, + dumpManager = dumpManager, + mediaTimeoutListener = mediaTimeoutListener, + mediaResumeListener = mediaResumeListener, + mediaSessionBasedFilter = mediaSessionBasedFilter, + mediaDeviceManager = mediaDeviceManager, + mediaDataCombineLatest = mediaDataCombineLatest, + mediaDataFilter = mediaDataFilter, + activityStarter = activityStarter, + smartspaceMediaDataProvider = smartspaceMediaDataProvider, + useMediaResumption = true, + useQsMediaPlayer = true, + systemClock = clock, + tunerService = tunerService, + mediaFlags = mediaFlags, + logger = logger + ) + verify(tunerService) + .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)) session = MediaSession(context, "MediaDataManagerTestSession") - mediaNotification = SbnBuilder().run { - setPkg(PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + } + build() + } + metadataBuilder = + MediaMetadata.Builder().apply { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) } - build() - } - metadataBuilder = MediaMetadata.Builder().apply { - putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) - putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) - } whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller) whenever(controller.transportControls).thenReturn(transportControls) whenever(controller.playbackInfo).thenReturn(playbackInfo) - whenever(playbackInfo.playbackType).thenReturn( - MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal // listeners in the internal processing pipeline. It receives events, but ince it is a @@ -170,18 +203,18 @@ class MediaDataManagerTest : SysuiTestCase() { // treat mediaSessionBasedFilter as a listener for testing. listener = mediaSessionBasedFilter - val recommendationExtras = Bundle().apply { - putString("package_name", PACKAGE_NAME) - putParcelable("dismiss_intent", DISMISS_INTENT) - } + val recommendationExtras = + Bundle().apply { + putString("package_name", PACKAGE_NAME) + putParcelable("dismiss_intent", DISMISS_INTENT) + } val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play) whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras) whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction) whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras) whenever(mediaRecommendationItem.icon).thenReturn(icon) - validRecommendationList = listOf( - mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem - ) + validRecommendationList = + listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem) whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE) whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA) whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) @@ -194,8 +227,11 @@ class MediaDataManagerTest : SysuiTestCase() { fun tearDown() { session.release() mediaDataManager.destroy() - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, originalSmartspaceSetting) + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + originalSmartspaceSetting + ) } @Test @@ -212,21 +248,36 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testSetTimedOut_resume_dismissesMedia() { // WHEN resume controls are present, and time out - val desc = MediaDescription.Builder().run { - setTitle(SESSION_TITLE) - build() - } - mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken, - APP_NAME, pendingIntent, PACKAGE_NAME) + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) backgroundExecutor.runAllReady() foregroundExecutor.runAllReady() - verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor), - eq(true), eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true) - verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), - eq(mediaDataCaptor.value.instanceId)) + verify(logger) + .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId)) // THEN it is removed and listeners are informed foregroundExecutor.advanceClockToLast() @@ -243,8 +294,13 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testOnMetaDataLoaded_callsListener() { addNotificationAndLoad() - verify(logger).logActiveMediaAdded(anyInt(), eq(PACKAGE_NAME), - eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_LOCAL)) + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_LOCAL) + ) } @Test @@ -255,56 +311,85 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value!!.active).isTrue() } @Test fun testOnNotificationAdded_isRcn_markedRemote() { - val rcn = SbnBuilder().run { - setPkg(SYSTEM_PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - setRemotePlaybackInfo("Remote device", 0, null) - }) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setRemotePlaybackInfo("Remote device", 0, null) + } + ) + } + build() } - build() - } mediaDataManager.onNotificationAdded(KEY, rcn) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) - assertThat(mediaDataCaptor.value!!.playbackLocation).isEqualTo( - MediaData.PLAYBACK_CAST_REMOTE) - verify(logger).logActiveMediaAdded(anyInt(), eq(SYSTEM_PACKAGE_NAME), - eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.playbackLocation) + .isEqualTo(MediaData.PLAYBACK_CAST_REMOTE) + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(SYSTEM_PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_CAST_REMOTE) + ) } @Test fun testOnNotificationAdded_hasSubstituteName_isUsed() { val subName = "Substitute Name" - val notif = SbnBuilder().run { - modifyNotification(context).also { - it.extras = Bundle().apply { - putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName) + val notif = + SbnBuilder().run { + modifyNotification(context).also { + it.extras = + Bundle().apply { + putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName) + } + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) } - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - }) + build() } - build() - } mediaDataManager.onNotificationAdded(KEY, notif) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value!!.app).isEqualTo(subName) } @@ -314,17 +399,18 @@ class MediaDataManagerTest : SysuiTestCase() { val bundle = Bundle() // wrong data type bundle.putParcelable(Notification.EXTRA_MEDIA_SESSION, Bundle()) - val rcn = SbnBuilder().run { - setPkg(SYSTEM_PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.addExtras(bundle) - it.setStyle(MediaStyle().apply { - setRemotePlaybackInfo("Remote device", 0, null) - }) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.addExtras(bundle) + it.setStyle( + MediaStyle().apply { setRemotePlaybackInfo("Remote device", 0, null) } + ) + } + build() } - build() - } mediaDataManager.loadMediaDataInBg(KEY, rcn, null) // no crash even though the data structure is incorrect @@ -335,18 +421,21 @@ class MediaDataManagerTest : SysuiTestCase() { val bundle = Bundle() // wrong data type bundle.putParcelable(Notification.EXTRA_MEDIA_REMOTE_INTENT, Bundle()) - val rcn = SbnBuilder().run { - setPkg(SYSTEM_PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.addExtras(bundle) - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - setRemotePlaybackInfo("Remote device", 0, null) - }) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.addExtras(bundle) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setRemotePlaybackInfo("Remote device", 0, null) + } + ) + } + build() } - build() - } mediaDataManager.loadMediaDataInBg(KEY, rcn, null) // no crash even though the data structure is incorrect @@ -373,8 +462,14 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationRemoved(KEY) // THEN the media data indicates that it is for resumption verify(listener) - .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.resumption).isTrue() assertThat(mediaDataCaptor.value.isPlaying).isFalse() verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) @@ -389,8 +484,14 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(backgroundExecutor.runAllReady()).isEqualTo(2) assertThat(foregroundExecutor.runAllReady()).isEqualTo(2) verify(listener) - .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) val data = mediaDataCaptor.value assertThat(data.resumption).isFalse() val resumableData = data.copy(resumeAction = Runnable {}) @@ -401,8 +502,14 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationRemoved(KEY) // THEN the data is for resumption and the key is migrated to the package name verify(listener) - .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.resumption).isTrue() verify(listener, never()).onMediaDataRemoved(eq(KEY)) // WHEN the second is removed @@ -410,8 +517,13 @@ class MediaDataManagerTest : SysuiTestCase() { // THEN the data is for resumption and the second key is removed verify(listener) .onMediaDataLoaded( - eq(PACKAGE_NAME), eq(PACKAGE_NAME), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + eq(PACKAGE_NAME), + eq(PACKAGE_NAME), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.resumption).isTrue() verify(listener).onMediaDataRemoved(eq(KEY_2)) } @@ -420,15 +532,20 @@ class MediaDataManagerTest : SysuiTestCase() { fun testOnNotificationRemoved_withResumption_butNotLocal() { // GIVEN that the manager has a notification with a resume action, but is not local whenever(controller.metadata).thenReturn(metadataBuilder.build()) - whenever(playbackInfo.playbackType).thenReturn( - MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) addNotificationAndLoad() val data = mediaDataCaptor.value - val dataRemoteWithResume = data.copy(resumeAction = Runnable {}, - playbackLocation = MediaData.PLAYBACK_CAST_LOCAL) + val dataRemoteWithResume = + data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL) mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume) - verify(logger).logActiveMediaAdded(anyInt(), eq(PACKAGE_NAME), - eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_CAST_LOCAL)) + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_CAST_LOCAL) + ) // WHEN the notification is removed mediaDataManager.onNotificationRemoved(KEY) @@ -440,19 +557,33 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testAddResumptionControls() { // WHEN resumption controls are added - val desc = MediaDescription.Builder().run { - setTitle(SESSION_TITLE) - build() - } + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } val currentTime = clock.elapsedRealtime() - mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken, - APP_NAME, pendingIntent, PACKAGE_NAME) + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) // THEN the media data indicates that it is for resumption verify(listener) - .onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) val data = mediaDataCaptor.value assertThat(data.resumption).isTrue() assertThat(data.song).isEqualTo(SESSION_TITLE) @@ -466,16 +597,31 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testResumptionDisabled_dismissesResumeControls() { // WHEN there are resume controls and resumption is switched off - val desc = MediaDescription.Builder().run { - setTitle(SESSION_TITLE) - build() - } - mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken, - APP_NAME, pendingIntent, PACKAGE_NAME) + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor), - eq(true), eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) val data = mediaDataCaptor.value mediaDataManager.setMediaResumptionEnabled(false) @@ -508,23 +654,30 @@ class MediaDataManagerTest : SysuiTestCase() { fun testBadArtwork_doesNotUse() { // WHEN notification has a too-small artwork val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) - val notif = SbnBuilder().run { - setPkg(PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) - it.setLargeIcon(artwork) + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setLargeIcon(artwork) + } + build() } - build() - } mediaDataManager.onNotificationAdded(KEY, notif) // THEN it still loads assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) verify(listener) - .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) } @Test @@ -533,18 +686,23 @@ class MediaDataManagerTest : SysuiTestCase() { verify(logger).getNewInstanceId() val instanceId = instanceIdSequence.lastInstanceId - verify(listener).onSmartspaceMediaDataLoaded( - eq(KEY_MEDIA_SMARTSPACE), - eq(SmartspaceMediaData( - targetId = KEY_MEDIA_SMARTSPACE, - isActive = true, - packageName = PACKAGE_NAME, - cardAction = mediaSmartspaceBaseAction, - recommendations = validRecommendationList, - dismissIntent = DISMISS_INTENT, - headphoneConnectionTimeMillis = 1234L, - instanceId = InstanceId.fakeInstanceId(instanceId))), - eq(false)) + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = 1234L, + instanceId = InstanceId.fakeInstanceId(instanceId) + ) + ), + eq(false) + ) } @Test @@ -554,23 +712,29 @@ class MediaDataManagerTest : SysuiTestCase() { verify(logger).getNewInstanceId() val instanceId = instanceIdSequence.lastInstanceId - verify(listener).onSmartspaceMediaDataLoaded( - eq(KEY_MEDIA_SMARTSPACE), - eq(EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = KEY_MEDIA_SMARTSPACE, - isActive = true, - dismissIntent = DISMISS_INTENT, - headphoneConnectionTimeMillis = 1234L, - instanceId = InstanceId.fakeInstanceId(instanceId))), - eq(false)) + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = 1234L, + instanceId = InstanceId.fakeInstanceId(instanceId) + ) + ), + eq(false) + ) } @Test fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() { - val recommendationExtras = Bundle().apply { - putString("package_name", PACKAGE_NAME) - putParcelable("dismiss_intent", null) - } + val recommendationExtras = + Bundle().apply { + putString("package_name", PACKAGE_NAME) + putParcelable("dismiss_intent", null) + } whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras) whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction) whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf()) @@ -579,15 +743,20 @@ class MediaDataManagerTest : SysuiTestCase() { verify(logger).getNewInstanceId() val instanceId = instanceIdSequence.lastInstanceId - verify(listener).onSmartspaceMediaDataLoaded( - eq(KEY_MEDIA_SMARTSPACE), - eq(EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = KEY_MEDIA_SMARTSPACE, - isActive = true, - dismissIntent = null, - headphoneConnectionTimeMillis = 1234L, - instanceId = InstanceId.fakeInstanceId(instanceId))), - eq(false)) + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + dismissIntent = null, + headphoneConnectionTimeMillis = 1234L, + instanceId = InstanceId.fakeInstanceId(instanceId) + ) + ), + eq(false) + ) } @Test @@ -595,7 +764,7 @@ class MediaDataManagerTest : SysuiTestCase() { smartspaceMediaDataProvider.onTargetsAvailable(listOf()) verify(logger, never()).getNewInstanceId() verify(listener, never()) - .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) + .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) } @Ignore("b/233283726") @@ -615,15 +784,18 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() { // WHEN media recommendation setting is off - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0) + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 0 + ) tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0") smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) // THEN smartspace signal is ignored verify(listener, never()) - .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) + .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) } @Ignore("b/229838140") @@ -631,12 +803,15 @@ class MediaDataManagerTest : SysuiTestCase() { fun testMediaRecommendationDisabled_removesSmartspaceData() { // GIVEN a media recommendation card is present smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) - verify(listener).onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), - anyBoolean()) + verify(listener) + .onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), anyBoolean()) // WHEN the media recommendation setting is turned off - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0) + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 0 + ) tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0") // THEN listeners are notified @@ -665,8 +840,15 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.setTimedOut(KEY, true, true) // THEN the last active time is not changed - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime) } @@ -687,8 +869,14 @@ class MediaDataManagerTest : SysuiTestCase() { // THEN the last active time is not changed verify(listener) - .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.resumption).isTrue() assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime) @@ -700,17 +888,20 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testTooManyCompactActions_isTruncated() { // GIVEN a notification where too many compact actions were specified - val notif = SbnBuilder().run { - setPkg(PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - setShowActionsInCompactView(0, 1, 2, 3, 4) - }) + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setShowActionsInCompactView(0, 1, 2, 3, 4) + } + ) + } + build() } - build() - } // WHEN the notification is loaded mediaDataManager.onNotificationAdded(KEY, notif) @@ -718,29 +909,35 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) // THEN only the first MAX_COMPACT_ACTIONS are actually set - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) - assertThat(mediaDataCaptor.value.actionsToShowInCompact.size).isEqualTo( - MediaDataManager.MAX_COMPACT_ACTIONS) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.actionsToShowInCompact.size) + .isEqualTo(MediaDataManager.MAX_COMPACT_ACTIONS) } @Test fun testTooManyNotificationActions_isTruncated() { // GIVEN a notification where too many notification actions are added val action = Notification.Action(R.drawable.ic_android, "action", null) - val notif = SbnBuilder().run { - setPkg(PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - }) - for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) { - it.addAction(action) + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) { + it.addAction(action) + } } + build() } - build() - } // WHEN the notification is loaded mediaDataManager.onNotificationAdded(KEY, notif) @@ -748,10 +945,17 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) // THEN only the first MAX_NOTIFICATION_ACTIONS are actually included - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) - assertThat(mediaDataCaptor.value.actions.size).isEqualTo( - MediaDataManager.MAX_NOTIFICATION_ACTIONS) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.actions.size) + .isEqualTo(MediaDataManager.MAX_NOTIFICATION_ACTIONS) } @Test @@ -760,21 +964,29 @@ class MediaDataManagerTest : SysuiTestCase() { whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) whenever(controller.playbackState).thenReturn(null) - val notifWithAction = SbnBuilder().run { - setPkg(PACKAGE_NAME) - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) - it.addAction(android.R.drawable.ic_media_play, desc, null) + val notifWithAction = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.addAction(android.R.drawable.ic_media_play, desc, null) + } + build() } - build() - } mediaDataManager.onNotificationAdded(KEY, notifWithAction) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value!!.semanticActions).isNull() assertThat(mediaDataCaptor.value!!.actions).hasSize(1) @@ -785,11 +997,11 @@ class MediaDataManagerTest : SysuiTestCase() { fun testPlaybackActions_hasPrevNext() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) - val stateActions = PlaybackState.ACTION_PLAY or + val stateActions = + PlaybackState.ACTION_PLAY or PlaybackState.ACTION_SKIP_TO_PREVIOUS or PlaybackState.ACTION_SKIP_TO_NEXT - val stateBuilder = PlaybackState.Builder() - .setActions(stateActions) + val stateBuilder = PlaybackState.Builder().setActions(stateActions) customDesc.forEach { stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) } @@ -801,20 +1013,20 @@ class MediaDataManagerTest : SysuiTestCase() { val actions = mediaDataCaptor.value!!.semanticActions!! assertThat(actions.playOrPause).isNotNull() - assertThat(actions.playOrPause!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_play)) + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) actions.playOrPause!!.action!!.run() verify(transportControls).play() assertThat(actions.prevOrCustom).isNotNull() - assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_prev)) + assertThat(actions.prevOrCustom!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_prev)) actions.prevOrCustom!!.action!!.run() verify(transportControls).skipToPrevious() assertThat(actions.nextOrCustom).isNotNull() - assertThat(actions.nextOrCustom!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_next)) + assertThat(actions.nextOrCustom!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_next)) actions.nextOrCustom!!.action!!.run() verify(transportControls).skipToNext() @@ -830,8 +1042,7 @@ class MediaDataManagerTest : SysuiTestCase() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5") whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) val stateActions = PlaybackState.ACTION_PLAY - val stateBuilder = PlaybackState.Builder() - .setActions(stateActions) + val stateBuilder = PlaybackState.Builder().setActions(stateActions) customDesc.forEach { stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) } @@ -843,8 +1054,8 @@ class MediaDataManagerTest : SysuiTestCase() { val actions = mediaDataCaptor.value!!.semanticActions!! assertThat(actions.playOrPause).isNotNull() - assertThat(actions.playOrPause!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_play)) + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) assertThat(actions.prevOrCustom).isNotNull() assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(customDesc[0]) @@ -863,7 +1074,8 @@ class MediaDataManagerTest : SysuiTestCase() { fun testPlaybackActions_connecting() { whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) val stateActions = PlaybackState.ACTION_PLAY - val stateBuilder = PlaybackState.Builder() + val stateBuilder = + PlaybackState.Builder() .setState(PlaybackState.STATE_BUFFERING, 0, 10f) .setActions(stateActions) whenever(controller.playbackState).thenReturn(stateBuilder.build()) @@ -874,8 +1086,8 @@ class MediaDataManagerTest : SysuiTestCase() { val actions = mediaDataCaptor.value!!.semanticActions!! assertThat(actions.playOrPause).isNotNull() - assertThat(actions.playOrPause!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_connecting)) + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_connecting)) } @Test @@ -883,15 +1095,15 @@ class MediaDataManagerTest : SysuiTestCase() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) val stateActions = PlaybackState.ACTION_PLAY - val stateBuilder = PlaybackState.Builder() - .setActions(stateActions) + val stateBuilder = PlaybackState.Builder().setActions(stateActions) customDesc.forEach { stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) } - val extras = Bundle().apply { - putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) - putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) - } + val extras = + Bundle().apply { + putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) + putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) + } whenever(controller.playbackState).thenReturn(stateBuilder.build()) whenever(controller.extras).thenReturn(extras) @@ -901,8 +1113,8 @@ class MediaDataManagerTest : SysuiTestCase() { val actions = mediaDataCaptor.value!!.semanticActions!! assertThat(actions.playOrPause).isNotNull() - assertThat(actions.playOrPause!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_play)) + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) assertThat(actions.prevOrCustom).isNull() assertThat(actions.nextOrCustom).isNull() @@ -930,8 +1142,8 @@ class MediaDataManagerTest : SysuiTestCase() { val actions = mediaDataCaptor.value!!.semanticActions!! assertThat(actions.playOrPause).isNotNull() - assertThat(actions.playOrPause!!.contentDescription).isEqualTo( - context.getString(R.string.controls_media_button_play)) + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) actions.playOrPause!!.action!!.run() verify(transportControls).play() } @@ -944,30 +1156,43 @@ class MediaDataManagerTest : SysuiTestCase() { // Location is updated to local cast whenever(controller.metadata).thenReturn(metadataBuilder.build()) - whenever(playbackInfo.playbackType).thenReturn( - MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) addNotificationAndLoad() - verify(logger).logPlaybackLocationChange(anyInt(), eq(PACKAGE_NAME), - eq(instanceId), eq(MediaData.PLAYBACK_CAST_LOCAL)) + verify(logger) + .logPlaybackLocationChange( + anyInt(), + eq(PACKAGE_NAME), + eq(instanceId), + eq(MediaData.PLAYBACK_CAST_LOCAL) + ) // update to remote cast - val rcn = SbnBuilder().run { - setPkg(SYSTEM_PACKAGE_NAME) // System package - modifyNotification(context).also { - it.setSmallIcon(android.R.drawable.ic_media_pause) - it.setStyle(MediaStyle().apply { - setMediaSession(session.sessionToken) - setRemotePlaybackInfo("Remote device", 0, null) - }) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) // System package + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setRemotePlaybackInfo("Remote device", 0, null) + } + ) + } + build() } - build() - } mediaDataManager.onNotificationAdded(KEY, rcn) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(logger).logPlaybackLocationChange(anyInt(), eq(SYSTEM_PACKAGE_NAME), - eq(instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE)) + verify(logger) + .logPlaybackLocationChange( + anyInt(), + eq(SYSTEM_PACKAGE_NAME), + eq(instanceId), + eq(MediaData.PLAYBACK_CAST_REMOTE) + ) } @Test @@ -977,14 +1202,19 @@ class MediaDataManagerTest : SysuiTestCase() { verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) // Callback gets an updated state - val state = PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, 0L, 1f) - .build() + val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() callbackCaptor.value.invoke(KEY, state) // Listener is notified of updated state - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), - capture(mediaDataCaptor), eq(true), eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.isPlaying).isTrue() } @@ -996,8 +1226,8 @@ class MediaDataManagerTest : SysuiTestCase() { // No media added with this key callbackCaptor.value.invoke(KEY, state) - verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), - anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } @Test @@ -1015,35 +1245,42 @@ class MediaDataManagerTest : SysuiTestCase() { // Then no changes are made callbackCaptor.value.invoke(KEY, state) - verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), - anyBoolean()) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } @Test fun testPlaybackState_PauseWhenFlagTrue_keyExists_callsListener() { whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) - val state = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 1f) - .build() + val state = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 1f).build() whenever(controller.playbackState).thenReturn(state) addNotificationAndLoad() verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) callbackCaptor.value.invoke(KEY, state) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), - capture(mediaDataCaptor), eq(true), eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.isPlaying).isFalse() assertThat(mediaDataCaptor.value.semanticActions).isNotNull() } @Test fun testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListener() { - val desc = MediaDescription.Builder().run { - setTitle(SESSION_TITLE) - build() - } - val state = PlaybackState.Builder() + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + val state = + PlaybackState.Builder() .setState(PlaybackState.STATE_PAUSED, 0L, 1f) .setActions(PlaybackState.ACTION_PLAY_PAUSE) .build() @@ -1051,13 +1288,13 @@ class MediaDataManagerTest : SysuiTestCase() { // Add resumption controls in order to have semantic actions. // To make sure that they are not null after changing state. mediaDataManager.addResumptionControls( - USER_ID, - desc, - Runnable {}, - session.sessionToken, - APP_NAME, - pendingIntent, - PACKAGE_NAME + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME ) backgroundExecutor.runAllReady() foregroundExecutor.runAllReady() @@ -1066,14 +1303,14 @@ class MediaDataManagerTest : SysuiTestCase() { callbackCaptor.value.invoke(PACKAGE_NAME, state) verify(listener) - .onMediaDataLoaded( - eq(PACKAGE_NAME), - eq(PACKAGE_NAME), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false) - ) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(PACKAGE_NAME), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.isPlaying).isFalse() assertThat(mediaDataCaptor.value.semanticActions).isNotNull() } @@ -1081,7 +1318,8 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testPlaybackStateNull_Pause_keyExists_callsListener() { whenever(controller.playbackState).thenReturn(null) - val state = PlaybackState.Builder() + val state = + PlaybackState.Builder() .setState(PlaybackState.STATE_PAUSED, 0L, 1f) .setActions(PlaybackState.ACTION_PLAY_PAUSE) .build() @@ -1090,20 +1328,32 @@ class MediaDataManagerTest : SysuiTestCase() { verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) callbackCaptor.value.invoke(KEY, state) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), - capture(mediaDataCaptor), eq(true), eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) assertThat(mediaDataCaptor.value.isPlaying).isFalse() assertThat(mediaDataCaptor.value.semanticActions).isNull() } - /** - * Helper function to add a media notification and capture the resulting MediaData - */ + /** Helper function to add a media notification and capture the resulting MediaData */ private fun addNotificationAndLoad() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true), - eq(0), eq(false)) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt index 121c8946d164..a45e9d9fcacf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.bluetooth.BluetoothLeBroadcast import android.bluetooth.BluetoothLeBroadcastMetadata @@ -37,6 +37,10 @@ import com.android.settingslib.media.MediaDevice import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory import com.android.systemui.statusbar.policy.ConfigurationController @@ -109,7 +113,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fakeFgExecutor = FakeExecutor(FakeSystemClock()) fakeBgExecutor = FakeExecutor(FakeSystemClock()) localBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager::class.java) - manager = MediaDeviceManager( + manager = + MediaDeviceManager( context, controllerFactory, lmmFactory, @@ -120,7 +125,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fakeFgExecutor, fakeBgExecutor, dumpster - ) + ) manager.addListener(listener) // Configure mocks. @@ -134,11 +139,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() { // Create a media sesssion and notification for testing. session = MediaSession(context, SESSION_KEY) - mediaData = MediaTestUtils.emptyMediaData.copy( - packageName = PACKAGE, - token = session.sessionToken) - whenever(controllerFactory.create(session.sessionToken)) - .thenReturn(controller) + mediaData = + MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, token = session.sessionToken) + whenever(controllerFactory.create(session.sessionToken)).thenReturn(controller) setupLeAudioConfiguration(false) } @@ -354,7 +357,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() { val deviceCallback = captureCallback() // First set a non-null about-to-connect device deviceCallback.onAboutToConnectDeviceAdded( - "fakeAddress", "AboutToConnectDeviceName", mock(Drawable::class.java) + "fakeAddress", + "AboutToConnectDeviceName", + mock(Drawable::class.java) ) // Run and reset the executors and listeners so we only focus on new events. fakeBgExecutor.runAllReady() @@ -583,8 +588,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun testRemotePlaybackDeviceOverride() { whenever(route.name).thenReturn(DEVICE_NAME) - val deviceData = MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null, - showBroadcastButton = false) + val deviceData = + MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null, showBroadcastButton = false) val mediaDataWithDevice = mediaData.copy(device = deviceData) // GIVEN media data that already has a device set @@ -613,8 +618,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { val data = captureDeviceData(KEY) assertThat(data.showBroadcastButton).isTrue() assertThat(data.enabled).isTrue() - assertThat(data.name).isEqualTo(context.getString( - R.string.broadcasting_description_is_broadcasting)) + assertThat(data.name) + .isEqualTo(context.getString(R.string.broadcasting_description_is_broadcasting)) } @Test @@ -655,20 +660,21 @@ public class MediaDeviceManagerTest : SysuiTestCase() { } fun setupBroadcastCallback(): BluetoothLeBroadcast.Callback { - val callback: BluetoothLeBroadcast.Callback = object : BluetoothLeBroadcast.Callback { - override fun onBroadcastStarted(reason: Int, broadcastId: Int) {} - override fun onBroadcastStartFailed(reason: Int) {} - override fun onBroadcastStopped(reason: Int, broadcastId: Int) {} - override fun onBroadcastStopFailed(reason: Int) {} - override fun onPlaybackStarted(reason: Int, broadcastId: Int) {} - override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} - override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {} - override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {} - override fun onBroadcastMetadataChanged( - broadcastId: Int, - metadata: BluetoothLeBroadcastMetadata - ) {} - } + val callback: BluetoothLeBroadcast.Callback = + object : BluetoothLeBroadcast.Callback { + override fun onBroadcastStarted(reason: Int, broadcastId: Int) {} + override fun onBroadcastStartFailed(reason: Int) {} + override fun onBroadcastStopped(reason: Int, broadcastId: Int) {} + override fun onBroadcastStopFailed(reason: Int) {} + override fun onPlaybackStarted(reason: Int, broadcastId: Int) {} + override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} + override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {} + override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {} + override fun onBroadcastMetadataChanged( + broadcastId: Int, + metadata: BluetoothLeBroadcastMetadata + ) {} + } bluetoothLeBroadcast.registerCallback(fakeFgExecutor, callback) return callback @@ -677,7 +683,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fun setupLeAudioConfiguration(isLeAudio: Boolean) { whenever(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager) whenever(localBluetoothProfileManager.leAudioBroadcastProfile) - .thenReturn(localBluetoothLeBroadcast) + .thenReturn(localBluetoothLeBroadcast) whenever(localBluetoothLeBroadcast.isEnabled(any())).thenReturn(isLeAudio) whenever(localBluetoothLeBroadcast.appSourceName).thenReturn(BROADCAST_APP_NAME) } @@ -685,7 +691,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fun setupBroadcastPackage(currentName: String) { whenever(lmm.packageName).thenReturn(PACKAGE) whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt())) - .thenReturn(applicationInfo) + .thenReturn(applicationInfo) whenever(packageManager.getApplicationLabel(applicationInfo)).thenReturn(currentName) context.setMockPackageManager(packageManager) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilterTest.kt index 558645377936..3099609d42f0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilterTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.media.session.MediaController import android.media.session.MediaController.PlaybackInfo @@ -23,12 +23,12 @@ import android.media.session.MediaSessionManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest - import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock - import org.junit.After import org.junit.Before import org.junit.Rule @@ -42,17 +42,15 @@ import org.mockito.Mockito.any import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit private const val PACKAGE = "PKG" private const val KEY = "TEST_KEY" private const val NOTIF_KEY = "TEST_KEY" -private val info = MediaTestUtils.emptyMediaData.copy( - packageName = PACKAGE, - notificationKey = NOTIF_KEY -) +private val info = + MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, notificationKey = NOTIF_KEY) @SmallTest @RunWith(AndroidTestingRunner::class) @@ -139,10 +137,10 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { // Capture listener bgExecutor.runAllReady() - val listenerCaptor = ArgumentCaptor.forClass( - MediaSessionManager.OnActiveSessionsChangedListener::class.java) - verify(mediaSessionManager).addOnActiveSessionsChangedListener( - listenerCaptor.capture(), any()) + val listenerCaptor = + ArgumentCaptor.forClass(MediaSessionManager.OnActiveSessionsChangedListener::class.java) + verify(mediaSessionManager) + .addOnActiveSessionsChangedListener(listenerCaptor.capture(), any()) sessionListener = listenerCaptor.value filter.addListener(mediaListener) @@ -161,8 +159,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { filter.onMediaDataLoaded(KEY, null, mediaData1) bgExecutor.runAllReady() fgExecutor.runAllReady() - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } @Test @@ -184,8 +182,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } @Test @@ -214,8 +212,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } @Test @@ -230,15 +228,22 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) // WHEN a loaded event is received that matches the local session filter.onMediaDataLoaded(KEY, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is filtered - verify(mediaListener, never()).onMediaDataLoaded( - eq(KEY), eq(null), eq(mediaData2), anyBoolean(), anyInt(), anyBoolean()) + verify(mediaListener, never()) + .onMediaDataLoaded( + eq(KEY), + eq(null), + eq(mediaData2), + anyBoolean(), + anyInt(), + anyBoolean() + ) } @Test @@ -254,8 +259,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { fgExecutor.runAllReady() // THEN the event is not filtered because there isn't a notification for the remote // session. - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } @Test @@ -272,16 +277,22 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) // WHEN a loaded event is received that matches the local session filter.onMediaDataLoaded(key2, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is filtered verify(mediaListener, never()) - .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(), - anyInt(), anyBoolean()) + .onMediaDataLoaded( + eq(key2), + eq(null), + eq(mediaData2), + anyBoolean(), + anyInt(), + anyBoolean() + ) // AND there should be a removed event for key2 verify(mediaListener).onMediaDataRemoved(eq(key2)) } @@ -300,15 +311,15 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) // WHEN a loaded event is received that matches the remote session filter.onMediaDataLoaded(key2, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true), eq(0), eq(false)) } @Test @@ -324,15 +335,15 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) // WHEN a loaded event is received that matches the local session filter.onMediaDataLoaded(KEY, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true), eq(0), eq(false)) } @Test @@ -350,8 +361,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } @Test @@ -373,8 +384,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the key migration event is fired - verify(mediaListener).onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true), eq(0), eq(false)) } @Test @@ -404,14 +415,20 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { fgExecutor.runAllReady() // THEN the key migration event is filtered verify(mediaListener, never()) - .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(), - anyInt(), anyBoolean()) + .onMediaDataLoaded( + eq(key2), + eq(null), + eq(mediaData2), + anyBoolean(), + anyInt(), + anyBoolean() + ) // WHEN a loaded event is received that matches the remote session filter.onMediaDataLoaded(key2, null, mediaData1) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the key migration event is fired - verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true), - eq(0), eq(false)) + verify(mediaListener) + .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true), eq(0), eq(false)) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt index 823d4ae8c447..344dffafb448 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.pipeline import android.media.MediaMetadata import android.media.session.MediaController @@ -23,6 +23,9 @@ import android.media.session.PlaybackState import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.FakeExecutor @@ -41,11 +44,11 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.`when` import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit private const val KEY = "KEY" @@ -70,7 +73,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback> - @Captor private lateinit var dozingCallbackCaptor: + @Captor + private lateinit var dozingCallbackCaptor: ArgumentCaptor<StatusBarStateController.StateListener> @JvmField @Rule val mockito = MockitoJUnit.rule() private lateinit var metadataBuilder: MediaMetadata.Builder @@ -85,36 +89,41 @@ class MediaTimeoutListenerTest : SysuiTestCase() { fun setup() { `when`(mediaControllerFactory.create(any())).thenReturn(mediaController) executor = FakeExecutor(clock) - mediaTimeoutListener = MediaTimeoutListener( - mediaControllerFactory, - executor, - logger, - statusBarStateController, - clock - ) + mediaTimeoutListener = + MediaTimeoutListener( + mediaControllerFactory, + executor, + logger, + statusBarStateController, + clock + ) mediaTimeoutListener.timeoutCallback = timeoutCallback mediaTimeoutListener.stateCallback = stateCallback // Create a media session and notification for testing. - metadataBuilder = MediaMetadata.Builder().apply { - putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) - putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) - } - playbackBuilder = PlaybackState.Builder().apply { - setState(PlaybackState.STATE_PAUSED, 6000L, 1f) - setActions(PlaybackState.ACTION_PLAY) - } - session = MediaSession(context, SESSION_KEY).apply { - setMetadata(metadataBuilder.build()) - setPlaybackState(playbackBuilder.build()) - } + metadataBuilder = + MediaMetadata.Builder().apply { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + } + playbackBuilder = + PlaybackState.Builder().apply { + setState(PlaybackState.STATE_PAUSED, 6000L, 1f) + setActions(PlaybackState.ACTION_PLAY) + } + session = + MediaSession(context, SESSION_KEY).apply { + setMetadata(metadataBuilder.build()) + setPlaybackState(playbackBuilder.build()) + } session.setActive(true) - mediaData = MediaTestUtils.emptyMediaData.copy( - app = PACKAGE, - packageName = PACKAGE, - token = session.sessionToken - ) + mediaData = + MediaTestUtils.emptyMediaData.copy( + app = PACKAGE, + packageName = PACKAGE, + token = session.sessionToken + ) resumeData = mediaData.copy(token = null, active = false, resumption = true) } @@ -212,8 +221,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we're registered testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) + mediaCallbackCaptor.value.onPlaybackStateChanged( + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() + ) assertThat(executor.numPending()).isEqualTo(1) assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) } @@ -223,8 +233,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we have a pending timeout testOnPlaybackStateChanged_schedulesTimeout_whenPaused() - mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()) + mediaCallbackCaptor.value.onPlaybackStateChanged( + PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build() + ) assertThat(executor.numPending()).isEqualTo(0) verify(logger).logTimeoutCancelled(eq(KEY), any()) } @@ -234,8 +245,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we have a pending timeout testOnPlaybackStateChanged_schedulesTimeout_whenPaused() - mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() - .setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()) + mediaCallbackCaptor.value.onPlaybackStateChanged( + PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build() + ) assertThat(executor.numPending()).isEqualTo(1) } @@ -329,9 +341,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnMediaDataLoaded_pausedToResume_updatesTimeout() { // WHEN regular media is paused - val pausedState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f) - .build() + val pausedState = + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() `when`(mediaController.playbackState).thenReturn(pausedState) mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) assertThat(executor.numPending()).isEqualTo(1) @@ -362,9 +373,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() { mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData) // AND that media is resumed - val playingState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f) - .build() + val playingState = + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() `when`(mediaController.playbackState).thenReturn(playingState) mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData) @@ -386,15 +396,11 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnMediaDataLoaded_playbackActionsChanged_noCallback() { // Load media data once - val pausedState = PlaybackState.Builder() - .setActions(PlaybackState.ACTION_PAUSE) - .build() + val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build() loadMediaDataWithPlaybackState(pausedState) // When media data is loaded again, with different actions - val playingState = PlaybackState.Builder() - .setActions(PlaybackState.ACTION_PLAY) - .build() + val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build() loadMediaDataWithPlaybackState(playingState) // Then the callback is not invoked @@ -404,15 +410,11 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnPlaybackStateChanged_playbackActionsChanged_sendsCallback() { // Load media data once - val pausedState = PlaybackState.Builder() - .setActions(PlaybackState.ACTION_PAUSE) - .build() + val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build() loadMediaDataWithPlaybackState(pausedState) // When the playback state changes, and has different actions - val playingState = PlaybackState.Builder() - .setActions(PlaybackState.ACTION_PLAY) - .build() + val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build() mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) // Then the callback is invoked @@ -421,24 +423,30 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnPlaybackStateChanged_differentCustomActions_sendsCallback() { - val customOne = PlaybackState.CustomAction.Builder( + val customOne = + PlaybackState.CustomAction.Builder( "ACTION_1", "custom action 1", - android.R.drawable.ic_media_ff) + android.R.drawable.ic_media_ff + ) .build() - val pausedState = PlaybackState.Builder() + val pausedState = + PlaybackState.Builder() .setActions(PlaybackState.ACTION_PAUSE) .addCustomAction(customOne) .build() loadMediaDataWithPlaybackState(pausedState) // When the playback state actions change - val customTwo = PlaybackState.CustomAction.Builder( - "ACTION_2", - "custom action 2", - android.R.drawable.ic_media_rew) + val customTwo = + PlaybackState.CustomAction.Builder( + "ACTION_2", + "custom action 2", + android.R.drawable.ic_media_rew + ) .build() - val pausedStateTwoActions = PlaybackState.Builder() + val pausedStateTwoActions = + PlaybackState.Builder() .setActions(PlaybackState.ACTION_PAUSE) .addCustomAction(customOne) .addCustomAction(customTwo) @@ -451,9 +459,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnPlaybackStateChanged_sameActions_noCallback() { - val stateWithActions = PlaybackState.Builder() - .setActions(PlaybackState.ACTION_PLAY) - .build() + val stateWithActions = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build() loadMediaDataWithPlaybackState(stateWithActions) // When the playback state updates with the same actions @@ -467,18 +473,20 @@ class MediaTimeoutListenerTest : SysuiTestCase() { fun testOnPlaybackStateChanged_sameCustomActions_noCallback() { val actionName = "custom action" val actionIcon = android.R.drawable.ic_media_ff - val customOne = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon) - .build() - val stateOne = PlaybackState.Builder() + val customOne = + PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build() + val stateOne = + PlaybackState.Builder() .setActions(PlaybackState.ACTION_PAUSE) .addCustomAction(customOne) .build() loadMediaDataWithPlaybackState(stateOne) // When the playback state is updated, but has the same actions - val customTwo = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon) - .build() - val stateTwo = PlaybackState.Builder() + val customTwo = + PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build() + val stateTwo = + PlaybackState.Builder() .setActions(PlaybackState.ACTION_PAUSE) .addCustomAction(customTwo) .build() @@ -491,15 +499,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnMediaDataLoaded_isPlayingChanged_noCallback() { // Load media data in paused state - val pausedState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f) - .build() + val pausedState = + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() loadMediaDataWithPlaybackState(pausedState) // When media data is loaded again but playing - val playingState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, 0L, 1f) - .build() + val playingState = + PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() loadMediaDataWithPlaybackState(playingState) // Then the callback is not invoked @@ -509,15 +515,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnPlaybackStateChanged_isPlayingChanged_sendsCallback() { // Load media data in paused state - val pausedState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f) - .build() + val pausedState = + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() loadMediaDataWithPlaybackState(pausedState) // When the playback state changes to playing - val playingState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, 0L, 1f) - .build() + val playingState = + PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) // Then the callback is invoked @@ -527,15 +531,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnPlaybackStateChanged_isPlayingSame_noCallback() { // Load media data in paused state - val pausedState = PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f) - .build() + val pausedState = + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() loadMediaDataWithPlaybackState(pausedState) // When the playback state is updated, but still not playing - val playingState = PlaybackState.Builder() - .setState(PlaybackState.STATE_STOPPED, 0L, 0f) - .build() + val playingState = + PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build() mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) // Then the callback is not invoked @@ -546,8 +548,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() { // When paused media is loaded testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) + mediaCallbackCaptor.value.onPlaybackStateChanged( + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() + ) verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) // And we doze past the scheduled timeout @@ -571,8 +574,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { val time = clock.currentTimeMillis() clock.setElapsedRealtime(time) testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) + mediaCallbackCaptor.value.onPlaybackStateChanged( + PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() + ) verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) // And we doze, but not past the scheduled timeout diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt index 83168cb87dfe..84fdfd78e9fc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.resume import android.app.PendingIntent import android.content.ComponentName @@ -33,11 +33,16 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT import com.android.systemui.tuner.TunerService import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock -import org.junit.After import com.google.common.truth.Truth.assertThat +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -63,7 +68,9 @@ private const val MEDIA_PREFERENCES = "media_control_prefs" private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3" private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() + private fun <T> eq(value: T): T = Mockito.eq(value) ?: value + private fun <T> any(): T = Mockito.any<T>() @SmallTest @@ -93,26 +100,32 @@ class MediaResumeListenerTest : SysuiTestCase() { private lateinit var resumeListener: MediaResumeListener private val clock = FakeSystemClock() - private var originalQsSetting = Settings.Global.getInt(context.contentResolver, - Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1) - private var originalResumeSetting = Settings.Secure.getInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RESUME, 0) + private var originalQsSetting = + Settings.Global.getInt( + context.contentResolver, + Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, + 1 + ) + private var originalResumeSetting = + Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0) @Before fun setup() { MockitoAnnotations.initMocks(this) - Settings.Global.putInt(context.contentResolver, - Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1) - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RESUME, 1) + Settings.Global.putInt( + context.contentResolver, + Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, + 1 + ) + Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1) whenever(resumeBrowserFactory.create(capture(callbackCaptor), any())) - .thenReturn(resumeBrowser) + .thenReturn(resumeBrowser) // resume components are stored in sharedpreferences whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt())) - .thenReturn(sharedPrefs) + .thenReturn(sharedPrefs) whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS) whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor) whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor) @@ -120,36 +133,59 @@ class MediaResumeListenerTest : SysuiTestCase() { whenever(mockContext.contentResolver).thenReturn(context.contentResolver) executor = FakeExecutor(clock) - resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, - tunerService, resumeBrowserFactory, dumpManager, clock) + resumeListener = + MediaResumeListener( + mockContext, + broadcastDispatcher, + executor, + tunerService, + resumeBrowserFactory, + dumpManager, + clock + ) resumeListener.setManager(mediaDataManager) mediaDataManager.addListener(resumeListener) - data = MediaTestUtils.emptyMediaData.copy( + data = + MediaTestUtils.emptyMediaData.copy( song = TITLE, packageName = PACKAGE_NAME, - token = token) + token = token + ) } @After fun tearDown() { - Settings.Global.putInt(context.contentResolver, - Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, originalQsSetting) - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RESUME, originalResumeSetting) + Settings.Global.putInt( + context.contentResolver, + Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, + originalQsSetting + ) + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RESUME, + originalResumeSetting + ) } @Test fun testWhenNoResumption_doesNothing() { - Settings.Secure.putInt(context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RESUME, 0) + Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0) // When listener is created, we do NOT register a user change listener - val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService, - resumeBrowserFactory, dumpManager, clock) + val listener = + MediaResumeListener( + context, + broadcastDispatcher, + executor, + tunerService, + resumeBrowserFactory, + dumpManager, + clock + ) listener.setManager(mediaDataManager) - verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver), - any(), any(), any(), anyInt(), any()) + verify(broadcastDispatcher, never()) + .registerReceiver(eq(listener.userChangeReceiver), any(), any(), any(), anyInt(), any()) // When data is loaded, we do NOT execute or update anything listener.onMediaDataLoaded(KEY, OLD_KEY, data) @@ -170,9 +206,7 @@ class MediaResumeListenerTest : SysuiTestCase() { fun testOnLoad_checksForResume_badService() { setUpMbsWithValidResolveInfo() - whenever(resumeBrowser.testConnection()).thenAnswer { - callbackCaptor.value.onError() - } + whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() } // When media data is loaded that has not been checked yet, and does not have a MBS resumeListener.onMediaDataLoaded(KEY, null, data) @@ -226,7 +260,7 @@ class MediaResumeListenerTest : SysuiTestCase() { // But we do not tell it to add new controls verify(mediaDataManager, never()) - .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any()) + .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any()) } @Test @@ -253,8 +287,15 @@ class MediaResumeListenerTest : SysuiTestCase() { // Make sure broadcast receiver is registered resumeListener.setManager(mediaDataManager) - verify(broadcastDispatcher).registerReceiver(eq(resumeListener.userChangeReceiver), - any(), any(), any(), anyInt(), any()) + verify(broadcastDispatcher) + .registerReceiver( + eq(resumeListener.userChangeReceiver), + any(), + any(), + any(), + anyInt(), + any() + ) // When we get an unlock event val intent = Intent(Intent.ACTION_USER_UNLOCKED) @@ -264,8 +305,8 @@ class MediaResumeListenerTest : SysuiTestCase() { verify(resumeBrowser, times(3)).findRecentMedia() // Then since the mock service found media, the manager should be informed - verify(mediaDataManager, times(3)).addResumptionControls(anyInt(), - any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) + verify(mediaDataManager, times(3)) + .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) } @Test @@ -304,12 +345,14 @@ class MediaResumeListenerTest : SysuiTestCase() { // Then we save an update with the current time verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor))) - componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex()) - .dropLastWhile { it.isEmpty() }.forEach { - val result = it.split("/") - assertThat(result.size).isEqualTo(3) - assertThat(result[2].toLong()).isEqualTo(currentTime) - } + componentCaptor.value + .split(ResumeMediaBrowser.DELIMITER.toRegex()) + .dropLastWhile { it.isEmpty() } + .forEach { + val result = it.split("/") + assertThat(result.size).isEqualTo(3) + assertThat(result[2].toLong()).isEqualTo(currentTime) + } verify(sharedPrefsEditor, times(1)).apply() } @@ -328,8 +371,16 @@ class MediaResumeListenerTest : SysuiTestCase() { val lastPlayed = clock.currentTimeMillis() val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) - val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, - tunerService, resumeBrowserFactory, dumpManager, clock) + val resumeListener = + MediaResumeListener( + mockContext, + broadcastDispatcher, + executor, + tunerService, + resumeBrowserFactory, + dumpManager, + clock + ) resumeListener.setManager(mediaDataManager) mediaDataManager.addListener(resumeListener) @@ -339,8 +390,8 @@ class MediaResumeListenerTest : SysuiTestCase() { // We add its resume controls verify(resumeBrowser, times(1)).findRecentMedia() - verify(mediaDataManager, times(1)).addResumptionControls(anyInt(), - any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) + verify(mediaDataManager, times(1)) + .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) } @Test @@ -349,8 +400,16 @@ class MediaResumeListenerTest : SysuiTestCase() { val lastPlayed = clock.currentTimeMillis() - RESUME_MEDIA_TIMEOUT - 100 val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) - val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, - tunerService, resumeBrowserFactory, dumpManager, clock) + val resumeListener = + MediaResumeListener( + mockContext, + broadcastDispatcher, + executor, + tunerService, + resumeBrowserFactory, + dumpManager, + clock + ) resumeListener.setManager(mediaDataManager) mediaDataManager.addListener(resumeListener) @@ -360,8 +419,8 @@ class MediaResumeListenerTest : SysuiTestCase() { // We do not try to add resume controls verify(resumeBrowser, times(0)).findRecentMedia() - verify(mediaDataManager, times(0)).addResumptionControls(anyInt(), - any(), any(), any(), any(), any(), any()) + verify(mediaDataManager, times(0)) + .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any()) } @Test @@ -380,8 +439,16 @@ class MediaResumeListenerTest : SysuiTestCase() { val lastPlayed = currentTime - 1000 val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) - val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, - tunerService, resumeBrowserFactory, dumpManager, clock) + val resumeListener = + MediaResumeListener( + mockContext, + broadcastDispatcher, + executor, + tunerService, + resumeBrowserFactory, + dumpManager, + clock + ) resumeListener.setManager(mediaDataManager) mediaDataManager.addListener(resumeListener) @@ -391,12 +458,14 @@ class MediaResumeListenerTest : SysuiTestCase() { // Then we store the new lastPlayed time verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor))) - componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex()) - .dropLastWhile { it.isEmpty() }.forEach { - val result = it.split("/") - assertThat(result.size).isEqualTo(3) - assertThat(result[2].toLong()).isEqualTo(currentTime) - } + componentCaptor.value + .split(ResumeMediaBrowser.DELIMITER.toRegex()) + .dropLastWhile { it.isEmpty() } + .forEach { + val result = it.split("/") + assertThat(result.size).isEqualTo(3) + assertThat(result[2].toLong()).isEqualTo(currentTime) + } verify(sharedPrefsEditor, times(1)).apply() } @@ -417,9 +486,7 @@ class MediaResumeListenerTest : SysuiTestCase() { setUpMbsWithValidResolveInfo() // Set up mocks to return with an error - whenever(resumeBrowser.testConnection()).thenAnswer { - callbackCaptor.value.onError() - } + whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() } resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data) executor.runAllReady() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt index dafaa6b93696..a04cfd46588b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.resume import android.content.ComponentName import android.content.Context @@ -37,8 +37,8 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.reset import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations private const val PACKAGE_NAME = "package" private const val CLASS_NAME = "class" @@ -47,7 +47,9 @@ private const val MEDIA_ID = "media ID" private const val ROOT = "media browser root" private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() + private fun <T> eq(value: T): T = Mockito.eq(value) ?: value + private fun <T> any(): T = Mockito.any<T>() @SmallTest @@ -57,10 +59,8 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { private lateinit var resumeBrowser: TestableResumeMediaBrowser private val component = ComponentName(PACKAGE_NAME, CLASS_NAME) - private val description = MediaDescription.Builder() - .setTitle(TITLE) - .setMediaId(MEDIA_ID) - .build() + private val description = + MediaDescription.Builder().setTitle(TITLE).setMediaId(MEDIA_ID).build() @Mock lateinit var callback: ResumeMediaBrowser.Callback @Mock lateinit var listener: MediaResumeListener @@ -81,19 +81,20 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(browserFactory.create(any(), capture(connectionCallback), any())) - .thenReturn(browser) + .thenReturn(browser) whenever(mediaController.transportControls).thenReturn(transportControls) whenever(mediaController.sessionToken).thenReturn(token) - resumeBrowser = TestableResumeMediaBrowser( - context, - callback, - component, - browserFactory, - logger, - mediaController - ) + resumeBrowser = + TestableResumeMediaBrowser( + context, + callback, + component, + browserFactory, + logger, + mediaController + ) } @Test @@ -329,30 +330,20 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { verify(oldBrowser).disconnect() } - /** - * Helper function to mock a failed connection - */ + /** Helper function to mock a failed connection */ private fun setupBrowserFailed() { - whenever(browser.connect()).thenAnswer { - connectionCallback.value.onConnectionFailed() - } + whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnectionFailed() } } - /** - * Helper function to mock a successful connection only - */ + /** Helper function to mock a successful connection only */ private fun setupBrowserConnection() { - whenever(browser.connect()).thenAnswer { - connectionCallback.value.onConnected() - } + whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnected() } whenever(browser.isConnected()).thenReturn(true) whenever(browser.getRoot()).thenReturn(ROOT) whenever(browser.sessionToken).thenReturn(token) } - /** - * Helper function to mock a successful connection, but no media results - */ + /** Helper function to mock a successful connection, but no media results */ private fun setupBrowserConnectionNoResults() { setupBrowserConnection() whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer { @@ -360,9 +351,7 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { } } - /** - * Helper function to mock a successful connection, but no playable results - */ + /** Helper function to mock a successful connection, but no playable results */ private fun setupBrowserConnectionNotPlayable() { setupBrowserConnection() @@ -373,9 +362,7 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { } } - /** - * Helper function to mock a successful connection with playable media - */ + /** Helper function to mock a successful connection with playable media */ private fun setupBrowserConnectionValidMedia() { setupBrowserConnection() @@ -387,9 +374,7 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { } } - /** - * Override so media controller use is testable - */ + /** Override so media controller use is testable */ private class TestableResumeMediaBrowser( context: Context, callback: Callback, @@ -403,4 +388,4 @@ public class ResumeMediaBrowserTest : SysuiTestCase() { return fakeController } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/AnimationBindHandlerTest.kt index e4cab1810822..99f56b16ab8b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/AnimationBindHandlerTest.kt @@ -14,26 +14,26 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui -import org.mockito.Mockito.`when` as whenever import android.graphics.drawable.Animatable2 import android.graphics.drawable.Drawable import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner import android.testing.TestableLooper import com.android.systemui.SysuiTestCase -import junit.framework.Assert.assertTrue import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.times import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit @SmallTest @@ -56,8 +56,7 @@ class AnimationBindHandlerTest : SysuiTestCase() { handler = AnimationBindHandler() } - @After - fun tearDown() {} + @After fun tearDown() {} @Test fun registerNoAnimations_executeCallbackImmediately() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt index f56d42ec3fb4..5bb74e5a31f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.ValueAnimator import android.graphics.Color @@ -22,6 +22,8 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.models.GutsViewHolder +import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.monet.ColorScheme import junit.framework.Assert.assertEquals import org.junit.After @@ -67,21 +69,18 @@ class ColorSchemeTransitionTest : SysuiTestCase() { animatingColorTransitionFactory = { _, _, _ -> mockAnimatingTransition } whenever(extractColor.invoke(colorScheme)).thenReturn(TARGET_COLOR) - colorSchemeTransition = ColorSchemeTransition( - context, mediaViewHolder, animatingColorTransitionFactory - ) + colorSchemeTransition = + ColorSchemeTransition(context, mediaViewHolder, animatingColorTransitionFactory) - colorTransition = object : AnimatingColorTransition( - DEFAULT_COLOR, extractColor, applyColor - ) { - override fun buildAnimator(): ValueAnimator { - return valueAnimator + colorTransition = + object : AnimatingColorTransition(DEFAULT_COLOR, extractColor, applyColor) { + override fun buildAnimator(): ValueAnimator { + return valueAnimator + } } - } } - @After - fun tearDown() {} + @After fun tearDown() {} @Test fun testColorTransition_nullColorScheme_keepsDefault() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerTest.kt index c41fac71a179..20260069c943 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.provider.Settings import android.test.suitebuilder.annotation.SmallTest @@ -48,17 +48,12 @@ import org.mockito.junit.MockitoJUnit @TestableLooper.RunWithLooper class KeyguardMediaControllerTest : SysuiTestCase() { - @Mock - private lateinit var mediaHost: MediaHost - @Mock - private lateinit var bypassController: KeyguardBypassController - @Mock - private lateinit var statusBarStateController: SysuiStatusBarStateController - @Mock - private lateinit var configurationController: ConfigurationController + @Mock private lateinit var mediaHost: MediaHost + @Mock private lateinit var bypassController: KeyguardBypassController + @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController + @Mock private lateinit var configurationController: ConfigurationController - @JvmField @Rule - val mockito = MockitoJUnit.rule() + @JvmField @Rule val mockito = MockitoJUnit.rule() private val mediaContainerView: MediaContainerView = MediaContainerView(context, null) private val hostView = UniqueObjectHostView(context) @@ -76,15 +71,16 @@ class KeyguardMediaControllerTest : SysuiTestCase() { hostView.layoutParams = FrameLayout.LayoutParams(100, 100) testableLooper = TestableLooper.get(this) fakeHandler = FakeHandler(testableLooper.looper) - keyguardMediaController = KeyguardMediaController( - mediaHost, - bypassController, - statusBarStateController, - context, - settings, - fakeHandler, - configurationController, - ) + keyguardMediaController = + KeyguardMediaController( + mediaHost, + bypassController, + statusBarStateController, + context, + settings, + fakeHandler, + configurationController, + ) keyguardMediaController.attachSinglePaneContainer(mediaContainerView) keyguardMediaController.useSplitShade = false } @@ -153,8 +149,10 @@ class KeyguardMediaControllerTest : SysuiTestCase() { keyguardMediaController.attachSplitShadeContainer(splitShadeContainer) keyguardMediaController.useSplitShade = true - assertTrue("HostView wasn't attached to the split pane container", - splitShadeContainer.childCount == 1) + assertTrue( + "HostView wasn't attached to the split pane container", + splitShadeContainer.childCount == 1 + ) } @Test @@ -162,8 +160,10 @@ class KeyguardMediaControllerTest : SysuiTestCase() { val splitShadeContainer = FrameLayout(context) keyguardMediaController.attachSplitShadeContainer(splitShadeContainer) - assertTrue("HostView wasn't attached to the single pane container", - mediaContainerView.childCount == 1) + assertTrue( + "HostView wasn't attached to the single pane container", + mediaContainerView.childCount == 1 + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt new file mode 100644 index 000000000000..c8e8943689c9 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt @@ -0,0 +1,645 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui + +import android.app.PendingIntent +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.logging.InstanceId +import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.PAGINATION_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER +import com.android.systemui.media.controls.ui.MediaHierarchyManager.Companion.LOCATION_QS +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener +import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.time.FakeSystemClock +import javax.inject.Provider +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +private val DATA = MediaTestUtils.emptyMediaData + +private val SMARTSPACE_KEY = "smartspace" + +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner::class) +class MediaCarouselControllerTest : SysuiTestCase() { + + @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel> + @Mock lateinit var panel: MediaControlPanel + @Mock lateinit var visualStabilityProvider: VisualStabilityProvider + @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager + @Mock lateinit var mediaHostState: MediaHostState + @Mock lateinit var activityStarter: ActivityStarter + @Mock @Main private lateinit var executor: DelayableExecutor + @Mock lateinit var mediaDataManager: MediaDataManager + @Mock lateinit var configurationController: ConfigurationController + @Mock lateinit var falsingCollector: FalsingCollector + @Mock lateinit var falsingManager: FalsingManager + @Mock lateinit var dumpManager: DumpManager + @Mock lateinit var logger: MediaUiEventLogger + @Mock lateinit var debugLogger: MediaCarouselControllerLogger + @Mock lateinit var mediaViewController: MediaViewController + @Mock lateinit var smartspaceMediaData: SmartspaceMediaData + @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> + @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener> + + private val clock = FakeSystemClock() + private lateinit var mediaCarouselController: MediaCarouselController + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + mediaCarouselController = + MediaCarouselController( + context, + mediaControlPanelFactory, + visualStabilityProvider, + mediaHostStatesManager, + activityStarter, + clock, + executor, + mediaDataManager, + configurationController, + falsingCollector, + falsingManager, + dumpManager, + logger, + debugLogger + ) + verify(mediaDataManager).addListener(capture(listener)) + verify(visualStabilityProvider) + .addPersistentReorderingAllowedListener(capture(visualStabilityCallback)) + whenever(mediaControlPanelFactory.get()).thenReturn(panel) + whenever(panel.mediaViewController).thenReturn(mediaViewController) + whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData) + MediaPlayerData.clear() + } + + @Test + fun testPlayerOrdering() { + // Test values: key, data, last active time + val playingLocal = + Triple( + "playing local", + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ), + 4500L + ) + + val playingCast = + Triple( + "playing cast", + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, + resumption = false + ), + 5000L + ) + + val pausedLocal = + Triple( + "paused local", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ), + 1000L + ) + + val pausedCast = + Triple( + "paused cast", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, + resumption = false + ), + 2000L + ) + + val playingRcn = + Triple( + "playing RCN", + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, + resumption = false + ), + 5000L + ) + + val pausedRcn = + Triple( + "paused RCN", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, + resumption = false + ), + 5000L + ) + + val active = + Triple( + "active", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true + ), + 250L + ) + + val resume1 = + Triple( + "resume 1", + DATA.copy( + active = false, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true + ), + 500L + ) + + val resume2 = + Triple( + "resume 2", + DATA.copy( + active = false, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true + ), + 1000L + ) + + val activeMoreRecent = + Triple( + "active more recent", + DATA.copy( + active = false, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true, + lastActive = 2L + ), + 1000L + ) + + val activeLessRecent = + Triple( + "active less recent", + DATA.copy( + active = false, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true, + lastActive = 1L + ), + 1000L + ) + // Expected ordering for media players: + // Actively playing local sessions + // Actively playing cast sessions + // Paused local and cast sessions, by last active + // RCNs + // Resume controls, by last active + + val expected = + listOf( + playingLocal, + playingCast, + pausedCast, + pausedLocal, + playingRcn, + pausedRcn, + active, + resume2, + resume1 + ) + + expected.forEach { + clock.setCurrentTimeMillis(it.third) + MediaPlayerData.addMediaPlayer( + it.first, + it.second.copy(notificationKey = it.first), + panel, + clock, + isSsReactivated = false + ) + } + + for ((index, key) in MediaPlayerData.playerKeys().withIndex()) { + assertEquals(expected.get(index).first, key.data.notificationKey) + } + + for ((index, key) in MediaPlayerData.visiblePlayerKeys().withIndex()) { + assertEquals(expected.get(index).first, key.data.notificationKey) + } + } + + @Test + fun testOrderWithSmartspace_prioritized() { + testPlayerOrdering() + + // If smartspace is prioritized + MediaPlayerData.addMediaRecommendation( + SMARTSPACE_KEY, + EMPTY_SMARTSPACE_MEDIA_DATA, + panel, + true, + clock + ) + + // Then it should be shown immediately after any actively playing controls + assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) + } + + @Test + fun testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayers() { + testPlayerOrdering() + + // If smartspace is prioritized + listener.value.onSmartspaceMediaDataLoaded( + SMARTSPACE_KEY, + EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), + true + ) + + // Then it should be shown immediately after any actively playing controls + assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) + assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(2).isSsMediaRec) + } + + @Test + fun testOrderWithSmartspace_notPrioritized() { + testPlayerOrdering() + + // If smartspace is not prioritized + MediaPlayerData.addMediaRecommendation( + SMARTSPACE_KEY, + EMPTY_SMARTSPACE_MEDIA_DATA, + panel, + false, + clock + ) + + // Then it should be shown at the end of the carousel's active entries + val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1 + assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec) + } + + @Test + fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() { + testPlayerOrdering() + // playing paused player + listener.value.onMediaDataLoaded( + "paused local", + "paused local", + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + listener.value.onMediaDataLoaded( + "playing local", + "playing local", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true + ) + ) + + assertEquals( + MediaPlayerData.getMediaPlayerIndex("paused local"), + mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex + ) + // paused player order should stays the same in visibleMediaPLayer map. + // paused player order should be first in mediaPlayer map. + assertEquals( + MediaPlayerData.visiblePlayerKeys().elementAt(3), + MediaPlayerData.playerKeys().elementAt(0) + ) + } + @Test + fun testSwipeDismiss_logged() { + mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke() + + verify(logger).logSwipeDismiss() + } + + @Test + fun testSettingsButton_logged() { + mediaCarouselController.settingsButton.callOnClick() + + verify(logger).logCarouselSettings() + } + + @Test + fun testLocationChangeQs_logged() { + mediaCarouselController.onDesiredLocationChanged( + MediaHierarchyManager.LOCATION_QS, + mediaHostState, + animate = false + ) + verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS) + } + + @Test + fun testLocationChangeQqs_logged() { + mediaCarouselController.onDesiredLocationChanged( + MediaHierarchyManager.LOCATION_QQS, + mediaHostState, + animate = false + ) + verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS) + } + + @Test + fun testLocationChangeLockscreen_logged() { + mediaCarouselController.onDesiredLocationChanged( + MediaHierarchyManager.LOCATION_LOCKSCREEN, + mediaHostState, + animate = false + ) + verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN) + } + + @Test + fun testLocationChangeDream_logged() { + mediaCarouselController.onDesiredLocationChanged( + MediaHierarchyManager.LOCATION_DREAM_OVERLAY, + mediaHostState, + animate = false + ) + verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY) + } + + @Test + fun testRecommendationRemoved_logged() { + val packageName = "smartspace package" + val instanceId = InstanceId.fakeInstanceId(123) + + val smartspaceData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = packageName, instanceId = instanceId) + MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock) + mediaCarouselController.removePlayer(SMARTSPACE_KEY) + + verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!)) + } + + @Test + fun testMediaLoaded_ScrollToActivePlayer() { + listener.value.onMediaDataLoaded( + "playing local", + null, + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + listener.value.onMediaDataLoaded( + "paused local", + null, + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + // adding a media recommendation card. + listener.value.onSmartspaceMediaDataLoaded( + SMARTSPACE_KEY, + EMPTY_SMARTSPACE_MEDIA_DATA, + false + ) + mediaCarouselController.shouldScrollToKey = true + // switching between media players. + listener.value.onMediaDataLoaded( + "playing local", + "playing local", + DATA.copy( + active = true, + isPlaying = false, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = true + ) + ) + listener.value.onMediaDataLoaded( + "paused local", + "paused local", + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + + assertEquals( + MediaPlayerData.getMediaPlayerIndex("paused local"), + mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex + ) + } + + @Test + fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() { + listener.value.onSmartspaceMediaDataLoaded( + SMARTSPACE_KEY, + EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true), + false + ) + listener.value.onMediaDataLoaded( + "playing local", + null, + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false + ) + ) + + var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local") + assertEquals( + playerIndex, + mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex + ) + assertEquals(playerIndex, 0) + + // Replaying the same media player one more time. + // And check that the card stays in its position. + mediaCarouselController.shouldScrollToKey = true + listener.value.onMediaDataLoaded( + "playing local", + null, + DATA.copy( + active = true, + isPlaying = true, + playbackLocation = MediaData.PLAYBACK_LOCAL, + resumption = false, + packageName = "PACKAGE_NAME" + ) + ) + playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local") + assertEquals(playerIndex, 0) + } + + @Test + fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() { + var result = false + mediaCarouselController.updateHostVisibility = { result = true } + + whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true) + listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) + + assertEquals(true, result) + } + + @Test + fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() { + var result = false + mediaCarouselController.updateHostVisibility = { result = true } + + whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false) + listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) + assertEquals(false, result) + + visualStabilityCallback.value.onReorderingAllowed() + assertEquals(true, result) + } + + @Test + fun testGetCurrentVisibleMediaContentIntent() { + val clickIntent1 = mock(PendingIntent::class.java) + val player1 = Triple("player1", DATA.copy(clickIntent = clickIntent1), 1000L) + clock.setCurrentTimeMillis(player1.third) + MediaPlayerData.addMediaPlayer( + player1.first, + player1.second.copy(notificationKey = player1.first), + panel, + clock, + isSsReactivated = false + ) + + assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1) + + val clickIntent2 = mock(PendingIntent::class.java) + val player2 = Triple("player2", DATA.copy(clickIntent = clickIntent2), 2000L) + clock.setCurrentTimeMillis(player2.third) + MediaPlayerData.addMediaPlayer( + player2.first, + player2.second.copy(notificationKey = player2.first), + panel, + clock, + isSsReactivated = false + ) + + // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is + // added to the front because it was active more recently. + assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) + + val clickIntent3 = mock(PendingIntent::class.java) + val player3 = Triple("player3", DATA.copy(clickIntent = clickIntent3), 500L) + clock.setCurrentTimeMillis(player3.third) + MediaPlayerData.addMediaPlayer( + player3.first, + player3.second.copy(notificationKey = player3.first), + panel, + clock, + isSsReactivated = false + ) + + // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is + // added to the end because it was active less recently. + assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) + } + + @Test + fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() { + val delta = 0.0001F + val paginationSquishMiddle = + TRANSFORM_BEZIER.getInterpolation( + (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION + ) + val paginationSquishEnd = + TRANSFORM_BEZIER.getInterpolation( + (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION + ) + whenever(mediaHostStatesManager.mediaHostStates) + .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState)) + whenever(mediaHostState.visible).thenReturn(true) + mediaCarouselController.currentEndLocation = LOCATION_QS + whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle) + mediaCarouselController.updatePageIndicatorAlpha() + assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta) + + whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd) + mediaCarouselController.updatePageIndicatorAlpha() + assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index 7de5719c03ec..584305334b6f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.animation.Animator import android.animation.AnimatorSet @@ -59,7 +59,20 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.bluetooth.BroadcastDialogController import com.android.systemui.broadcast.BroadcastSender -import com.android.systemui.media.MediaControlPanel.KEY_SMARTSPACE_APP_NAME +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.GutsViewHolder +import com.android.systemui.media.controls.models.player.MediaAction +import com.android.systemui.media.controls.models.player.MediaButton +import com.android.systemui.media.controls.models.player.MediaData +import com.android.systemui.media.controls.models.player.MediaDeviceData +import com.android.systemui.media.controls.models.player.MediaViewHolder +import com.android.systemui.media.controls.models.player.SeekBarViewModel +import com.android.systemui.media.controls.models.recommendation.KEY_SMARTSPACE_APP_NAME +import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder +import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData +import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA +import com.android.systemui.media.controls.pipeline.MediaDataManager +import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.dialog.MediaOutputDialogFactory import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -164,8 +177,8 @@ public class MediaControlPanelTest : SysuiTestCase() { private lateinit var session: MediaSession private lateinit var device: MediaDeviceData - private val disabledDevice = MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null, - showBroadcastButton = false) + private val disabledDevice = + MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null, showBroadcastButton = false) private lateinit var mediaData: MediaData private val clock = FakeSystemClock() @Mock private lateinit var logger: MediaUiEventLogger @@ -212,24 +225,27 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE) context.setMockPackageManager(packageManager) - player = object : MediaControlPanel( - context, - bgExecutor, - mainExecutor, - activityStarter, - broadcastSender, - mediaViewController, - seekBarViewModel, - Lazy { mediaDataManager }, - mediaOutputDialogFactory, - mediaCarouselController, - falsingManager, - clock, - logger, - keyguardStateController, - activityIntentHelper, - lockscreenUserManager, - broadcastDialogController) { + player = + object : + MediaControlPanel( + context, + bgExecutor, + mainExecutor, + activityStarter, + broadcastSender, + mediaViewController, + seekBarViewModel, + Lazy { mediaDataManager }, + mediaOutputDialogFactory, + mediaCarouselController, + falsingManager, + clock, + logger, + keyguardStateController, + activityIntentHelper, + lockscreenUserManager, + broadcastDialogController + ) { override fun loadAnimator( animId: Int, otionInterpolator: Interpolator, @@ -250,18 +266,20 @@ public class MediaControlPanelTest : SysuiTestCase() { // Set valid recommendation data val extras = Bundle() extras.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME) - val intent = Intent().apply { - putExtras(extras) - setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } + val intent = + Intent().apply { + putExtras(extras) + setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } whenever(smartspaceAction.intent).thenReturn(intent) whenever(smartspaceAction.extras).thenReturn(extras) - smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( - packageName = PACKAGE, - instanceId = instanceId, - recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction), - cardAction = smartspaceAction - ) + smartspaceData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + packageName = PACKAGE, + instanceId = instanceId, + recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction), + cardAction = smartspaceAction + ) } private fun initGutsViewHolderMocks() { @@ -279,36 +297,39 @@ public class MediaControlPanelTest : SysuiTestCase() { } private fun initDeviceMediaData(shouldShowBroadcastButton: Boolean, name: String) { - device = MediaDeviceData(true, null, name, null, - showBroadcastButton = shouldShowBroadcastButton) + device = + MediaDeviceData(true, null, name, null, showBroadcastButton = shouldShowBroadcastButton) // Create media session - val metadataBuilder = MediaMetadata.Builder().apply { - putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) - putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) - } - val playbackBuilder = PlaybackState.Builder().apply { - setState(PlaybackState.STATE_PAUSED, 6000L, 1f) - setActions(PlaybackState.ACTION_PLAY) - } - session = MediaSession(context, SESSION_KEY).apply { - setMetadata(metadataBuilder.build()) - setPlaybackState(playbackBuilder.build()) - } + val metadataBuilder = + MediaMetadata.Builder().apply { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + } + val playbackBuilder = + PlaybackState.Builder().apply { + setState(PlaybackState.STATE_PAUSED, 6000L, 1f) + setActions(PlaybackState.ACTION_PLAY) + } + session = + MediaSession(context, SESSION_KEY).apply { + setMetadata(metadataBuilder.build()) + setPlaybackState(playbackBuilder.build()) + } session.setActive(true) - mediaData = MediaTestUtils.emptyMediaData.copy( + mediaData = + MediaTestUtils.emptyMediaData.copy( artist = ARTIST, song = TITLE, packageName = PACKAGE, token = session.sessionToken, device = device, - instanceId = instanceId) + instanceId = instanceId + ) } - /** - * Initialize elements in media view holder - */ + /** Initialize elements in media view holder */ private fun initMediaViewHolderMocks() { whenever(seekBarViewModel.progress).thenReturn(seekBarData) @@ -349,7 +370,8 @@ public class MediaControlPanelTest : SysuiTestCase() { action1.id, action2.id, action3.id, - action4.id) + action4.id + ) } whenever(viewHolder.player).thenReturn(view) @@ -394,9 +416,7 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier) } - /** - * Initialize elements for the recommendation view holder - */ + /** Initialize elements for the recommendation view holder */ private fun initRecommendationViewHolderMocks() { recTitle1 = TextView(context) recTitle2 = TextView(context) @@ -419,9 +439,8 @@ public class MediaControlPanelTest : SysuiTestCase() { .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3)) whenever(recommendationViewHolder.mediaTitles) .thenReturn(listOf(recTitle1, recTitle2, recTitle3)) - whenever(recommendationViewHolder.mediaSubtitles).thenReturn( - listOf(recSubtitle1, recSubtitle2, recSubtitle3) - ) + whenever(recommendationViewHolder.mediaSubtitles) + .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3)) whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder) @@ -453,12 +472,13 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindSemanticActions() { val icon = context.getDrawable(android.R.drawable.ic_media_play) val bg = context.getDrawable(R.drawable.qs_media_round_button_background) - val semanticActions = MediaButton( - playOrPause = MediaAction(icon, Runnable {}, "play", bg), - nextOrCustom = MediaAction(icon, Runnable {}, "next", bg), - custom0 = MediaAction(icon, null, "custom 0", bg), - custom1 = MediaAction(icon, null, "custom 1", bg) - ) + val semanticActions = + MediaButton( + playOrPause = MediaAction(icon, Runnable {}, "play", bg), + nextOrCustom = MediaAction(icon, Runnable {}, "next", bg), + custom0 = MediaAction(icon, null, "custom 0", bg), + custom1 = MediaAction(icon, null, "custom 1", bg) + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) @@ -501,15 +521,16 @@ public class MediaControlPanelTest : SysuiTestCase() { val bg = context.getDrawable(R.drawable.qs_media_round_button_background) // Setup button state: no prev or next button and their slots reserved - val semanticActions = MediaButton( - playOrPause = MediaAction(icon, Runnable {}, "play", bg), - nextOrCustom = null, - prevOrCustom = null, - custom0 = MediaAction(icon, null, "custom 0", bg), - custom1 = MediaAction(icon, null, "custom 1", bg), - false, - true - ) + val semanticActions = + MediaButton( + playOrPause = MediaAction(icon, Runnable {}, "play", bg), + nextOrCustom = null, + prevOrCustom = null, + custom0 = MediaAction(icon, null, "custom 0", bg), + custom1 = MediaAction(icon, null, "custom 1", bg), + false, + true + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -530,15 +551,16 @@ public class MediaControlPanelTest : SysuiTestCase() { val bg = context.getDrawable(R.drawable.qs_media_round_button_background) // Setup button state: no prev or next button and their slots reserved - val semanticActions = MediaButton( - playOrPause = MediaAction(icon, Runnable {}, "play", bg), - nextOrCustom = null, - prevOrCustom = null, - custom0 = MediaAction(icon, null, "custom 0", bg), - custom1 = MediaAction(icon, null, "custom 1", bg), - true, - false - ) + val semanticActions = + MediaButton( + playOrPause = MediaAction(icon, Runnable {}, "play", bg), + nextOrCustom = null, + prevOrCustom = null, + custom0 = MediaAction(icon, null, "custom 0", bg), + custom1 = MediaAction(icon, null, "custom 1", bg), + true, + false + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -646,10 +668,11 @@ public class MediaControlPanelTest : SysuiTestCase() { useRealConstraintSets() val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - playOrPause = MediaAction(icon, Runnable {}, "play", null), - nextOrCustom = MediaAction(icon, Runnable {}, "next", null) - ) + val semanticActions = + MediaButton( + playOrPause = MediaAction(icon, Runnable {}, "play", null), + nextOrCustom = MediaAction(icon, Runnable {}, "next", null) + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -719,9 +742,8 @@ public class MediaControlPanelTest : SysuiTestCase() { useRealConstraintSets() val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - nextOrCustom = MediaAction(icon, Runnable {}, "next", null) - ) + val semanticActions = + MediaButton(nextOrCustom = MediaAction(icon, Runnable {}, "next", null)) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -736,10 +758,11 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bind_notScrubbing_scrubbingViewsGone() { val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - prevOrCustom = MediaAction(icon, {}, "prev", null), - nextOrCustom = MediaAction(icon, {}, "next", null) - ) + val semanticActions = + MediaButton( + prevOrCustom = MediaAction(icon, {}, "prev", null), + nextOrCustom = MediaAction(icon, {}, "next", null) + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -770,10 +793,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() { val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - prevOrCustom = null, - nextOrCustom = MediaAction(icon, {}, "next", null) - ) + val semanticActions = + MediaButton(prevOrCustom = null, nextOrCustom = MediaAction(icon, {}, "next", null)) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) @@ -790,10 +811,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() { val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - prevOrCustom = MediaAction(icon, {}, "prev", null), - nextOrCustom = null - ) + val semanticActions = + MediaButton(prevOrCustom = MediaAction(icon, {}, "prev", null), nextOrCustom = null) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) @@ -810,10 +829,11 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun setIsScrubbing_true_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() { val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - prevOrCustom = MediaAction(icon, {}, "prev", null), - nextOrCustom = MediaAction(icon, {}, "next", null) - ) + val semanticActions = + MediaButton( + prevOrCustom = MediaAction(icon, {}, "prev", null), + nextOrCustom = MediaAction(icon, {}, "next", null) + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) @@ -832,10 +852,11 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun setIsScrubbing_trueThenFalse_scrubbingTimeGoneAtEnd() { val icon = context.getDrawable(android.R.drawable.ic_media_play) - val semanticActions = MediaButton( - prevOrCustom = MediaAction(icon, {}, "prev", null), - nextOrCustom = MediaAction(icon, {}, "next", null) - ) + val semanticActions = + MediaButton( + prevOrCustom = MediaAction(icon, {}, "prev", null), + nextOrCustom = MediaAction(icon, {}, "next", null) + ) val state = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -859,18 +880,20 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindNotificationActions() { val icon = context.getDrawable(android.R.drawable.ic_media_play) val bg = context.getDrawable(R.drawable.qs_media_round_button_background) - val actions = listOf( - MediaAction(icon, Runnable {}, "previous", bg), - MediaAction(icon, Runnable {}, "play", bg), - MediaAction(icon, null, "next", bg), - MediaAction(icon, null, "custom 0", bg), - MediaAction(icon, Runnable {}, "custom 1", bg) - ) - val state = mediaData.copy( - actions = actions, - actionsToShowInCompact = listOf(1, 2), - semanticActions = null - ) + val actions = + listOf( + MediaAction(icon, Runnable {}, "previous", bg), + MediaAction(icon, Runnable {}, "play", bg), + MediaAction(icon, null, "next", bg), + MediaAction(icon, null, "custom 0", bg), + MediaAction(icon, Runnable {}, "custom 1", bg) + ) + val state = + mediaData.copy( + actions = actions, + actionsToShowInCompact = listOf(1, 2), + semanticActions = null + ) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) @@ -918,15 +941,12 @@ public class MediaControlPanelTest : SysuiTestCase() { val icon = context.getDrawable(R.drawable.ic_media_play) val bg = context.getDrawable(R.drawable.ic_media_play_container) - val semanticActions0 = MediaButton( - playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null) - ) - val semanticActions1 = MediaButton( - playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null) - ) - val semanticActions2 = MediaButton( - playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null) - ) + val semanticActions0 = + MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)) + val semanticActions1 = + MediaButton(playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null)) + val semanticActions2 = + MediaButton(playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null)) val state0 = mediaData.copy(semanticActions = semanticActions0) val state1 = mediaData.copy(semanticActions = semanticActions1) val state2 = mediaData.copy(semanticActions = semanticActions2) @@ -1089,11 +1109,10 @@ public class MediaControlPanelTest : SysuiTestCase() { val mockAvd0 = mock(AnimatedVectorDrawable::class.java) whenever(mockAvd0.mutate()).thenReturn(mockAvd0) - val semanticActions0 = MediaButton( - playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null) - ) - val state = mediaData.copy(resumption = true, semanticActions = semanticActions0, - isPlaying = false) + val semanticActions0 = + MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)) + val state = + mediaData.copy(resumption = true, semanticActions = semanticActions0, isPlaying = false) player.attachPlayer(viewHolder) player.bindPlayer(state, PACKAGE) assertThat(seamlessText.getText()).isEqualTo(APP_NAME) @@ -1432,9 +1451,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionPlayPauseClick_isLogged() { - val semanticActions = MediaButton( - playOrPause = MediaAction(null, Runnable {}, "play", null) - ) + val semanticActions = + MediaButton(playOrPause = MediaAction(null, Runnable {}, "play", null)) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -1446,9 +1464,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionPrevClick_isLogged() { - val semanticActions = MediaButton( - prevOrCustom = MediaAction(null, Runnable {}, "previous", null) - ) + val semanticActions = + MediaButton(prevOrCustom = MediaAction(null, Runnable {}, "previous", null)) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -1460,9 +1477,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionNextClick_isLogged() { - val semanticActions = MediaButton( - nextOrCustom = MediaAction(null, Runnable {}, "next", null) - ) + val semanticActions = + MediaButton(nextOrCustom = MediaAction(null, Runnable {}, "next", null)) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -1474,9 +1490,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionCustom0Click_isLogged() { - val semanticActions = MediaButton( - custom0 = MediaAction(null, Runnable {}, "custom 0", null) - ) + val semanticActions = + MediaButton(custom0 = MediaAction(null, Runnable {}, "custom 0", null)) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -1488,9 +1503,8 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionCustom1Click_isLogged() { - val semanticActions = MediaButton( - custom1 = MediaAction(null, Runnable {}, "custom 1", null) - ) + val semanticActions = + MediaButton(custom1 = MediaAction(null, Runnable {}, "custom 1", null)) val data = mediaData.copy(semanticActions = semanticActions) player.attachPlayer(viewHolder) @@ -1502,13 +1516,14 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionCustom2Click_isLogged() { - val actions = listOf( - MediaAction(null, Runnable {}, "action 0", null), - MediaAction(null, Runnable {}, "action 1", null), - MediaAction(null, Runnable {}, "action 2", null), - MediaAction(null, Runnable {}, "action 3", null), - MediaAction(null, Runnable {}, "action 4", null) - ) + val actions = + listOf( + MediaAction(null, Runnable {}, "action 0", null), + MediaAction(null, Runnable {}, "action 1", null), + MediaAction(null, Runnable {}, "action 2", null), + MediaAction(null, Runnable {}, "action 3", null), + MediaAction(null, Runnable {}, "action 4", null) + ) val data = mediaData.copy(actions = actions) player.attachPlayer(viewHolder) @@ -1520,13 +1535,14 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionCustom3Click_isLogged() { - val actions = listOf( - MediaAction(null, Runnable {}, "action 0", null), - MediaAction(null, Runnable {}, "action 1", null), - MediaAction(null, Runnable {}, "action 2", null), - MediaAction(null, Runnable {}, "action 3", null), - MediaAction(null, Runnable {}, "action 4", null) - ) + val actions = + listOf( + MediaAction(null, Runnable {}, "action 0", null), + MediaAction(null, Runnable {}, "action 1", null), + MediaAction(null, Runnable {}, "action 2", null), + MediaAction(null, Runnable {}, "action 3", null), + MediaAction(null, Runnable {}, "action 4", null) + ) val data = mediaData.copy(actions = actions) player.attachPlayer(viewHolder) @@ -1538,13 +1554,14 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun actionCustom4Click_isLogged() { - val actions = listOf( - MediaAction(null, Runnable {}, "action 0", null), - MediaAction(null, Runnable {}, "action 1", null), - MediaAction(null, Runnable {}, "action 2", null), - MediaAction(null, Runnable {}, "action 3", null), - MediaAction(null, Runnable {}, "action 4", null) - ) + val actions = + listOf( + MediaAction(null, Runnable {}, "action 0", null), + MediaAction(null, Runnable {}, "action 1", null), + MediaAction(null, Runnable {}, "action 2", null), + MediaAction(null, Runnable {}, "action 3", null), + MediaAction(null, Runnable {}, "action 4", null) + ) val data = mediaData.copy(actions = actions) player.attachPlayer(viewHolder) @@ -1608,8 +1625,7 @@ public class MediaControlPanelTest : SysuiTestCase() { // THEN it shows without dismissing keyguard first captor.value.onClick(viewHolder.player) - verify(activityStarter).startActivity(eq(clickIntent), eq(true), - nullable(), eq(true)) + verify(activityStarter).startActivity(eq(clickIntent), eq(true), nullable(), eq(true)) } @Test @@ -1697,20 +1713,22 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindRecommendation_listHasTooFewRecs_notDisplayed() { player.attachRecommendation(recommendationViewHolder) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("subtitle1") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("subtitle2") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "title1") + .setSubtitle("subtitle1") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "title2") + .setSubtitle("subtitle2") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + ) ) - ) player.bindRecommendation(data) @@ -1722,30 +1740,32 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() { player.attachRecommendation(recommendationViewHolder) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("subtitle1") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("subtitle2") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "empty icon 1") - .setSubtitle("subtitle2") - .setIcon(null) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "empty icon 2") - .setSubtitle("subtitle2") - .setIcon(null) - .setExtras(Bundle.EMPTY) - .build(), + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "title1") + .setSubtitle("subtitle1") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "title2") + .setSubtitle("subtitle2") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "empty icon 1") + .setSubtitle("subtitle2") + .setIcon(null) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "empty icon 2") + .setSubtitle("subtitle2") + .setIcon(null) + .setExtras(Bundle.EMPTY) + .build(), + ) ) - ) player.bindRecommendation(data) @@ -1765,25 +1785,27 @@ public class MediaControlPanelTest : SysuiTestCase() { val subtitle3 = "Subtitle3" val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", title1) - .setSubtitle(subtitle1) - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", title2) - .setSubtitle(subtitle2) - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", title3) - .setSubtitle(subtitle3) - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", title1) + .setSubtitle(subtitle1) + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", title2) + .setSubtitle(subtitle2) + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id3", title3) + .setSubtitle(subtitle3) + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) assertThat(recTitle1.text).isEqualTo(title1) @@ -1798,15 +1820,17 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindRecommendation_noTitle_subtitleNotShown() { player.attachRecommendation(recommendationViewHolder) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("fake subtitle") - .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "") + .setSubtitle("fake subtitle") + .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) assertThat(recSubtitle1.text).isEqualTo("") @@ -1818,25 +1842,27 @@ public class MediaControlPanelTest : SysuiTestCase() { player.attachRecommendation(recommendationViewHolder) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("fake subtitle") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("fake subtitle") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "") - .setSubtitle("fake subtitle") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "") + .setSubtitle("fake subtitle") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "title2") + .setSubtitle("fake subtitle") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id3", "") + .setSubtitle("fake subtitle") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.VISIBLE) @@ -1850,25 +1876,27 @@ public class MediaControlPanelTest : SysuiTestCase() { player.attachRecommendation(recommendationViewHolder) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "title3") - .setSubtitle("subtitle3") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "") + .setSubtitle("") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "title2") + .setSubtitle("") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id3", "title3") + .setSubtitle("subtitle3") + .setIcon(icon) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.VISIBLE) @@ -1880,25 +1908,27 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindRecommendation_noneHaveSubtitles_subtitleViewsGone() { useRealConstraintSets() player.attachRecommendation(recommendationViewHolder) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("") - .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("") - .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "title3") - .setSubtitle("") - .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata)) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "title1") + .setSubtitle("") + .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "title2") + .setSubtitle("") + .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id3", "title3") + .setSubtitle("") + .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata)) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) @@ -1911,25 +1941,27 @@ public class MediaControlPanelTest : SysuiTestCase() { fun bindRecommendation_noneHaveTitles_titleAndSubtitleViewsGone() { useRealConstraintSets() player.attachRecommendation(recommendationViewHolder) - val data = smartspaceData.copy( - recommendations = listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("subtitle1") - .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "") - .setSubtitle("subtitle2") - .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "") - .setSubtitle("subtitle3") - .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata)) - .setExtras(Bundle.EMPTY) - .build() + val data = + smartspaceData.copy( + recommendations = + listOf( + SmartspaceAction.Builder("id1", "") + .setSubtitle("subtitle1") + .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id2", "") + .setSubtitle("subtitle2") + .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) + .setExtras(Bundle.EMPTY) + .build(), + SmartspaceAction.Builder("id3", "") + .setSubtitle("subtitle3") + .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata)) + .setExtras(Bundle.EMPTY) + .build() + ) ) - ) player.bindRecommendation(data) @@ -1942,20 +1974,23 @@ public class MediaControlPanelTest : SysuiTestCase() { } private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener = - withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) } + withArgCaptor { + verify(seekBarViewModel).setScrubbingChangeListener(capture()) + } - private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = - withArgCaptor { verify(seekBarViewModel).setEnabledChangeListener(capture()) } + private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = withArgCaptor { + verify(seekBarViewModel).setEnabledChangeListener(capture()) + } /** - * Update our test to use real ConstraintSets instead of mocks. + * Update our test to use real ConstraintSets instead of mocks. * - * Some item visibilities, such as the seekbar visibility, are dependent on other action's - * visibilities. If we use mocks for the ConstraintSets, then action visibility changes are - * just thrown away instead of being saved for reference later. This method sets us up to use - * ConstraintSets so that we do save visibility changes. + * Some item visibilities, such as the seekbar visibility, are dependent on other action's + * visibilities. If we use mocks for the ConstraintSets, then action visibility changes are just + * thrown away instead of being saved for reference later. This method sets us up to use + * ConstraintSets so that we do save visibility changes. * - * TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests? + * TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests? */ private fun useRealConstraintSets() { expandedSet = ConstraintSet() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 954b4386b71d..071604dc5790 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.graphics.Rect import android.provider.Settings @@ -84,10 +84,8 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private lateinit var statusBarCallback: ArgumentCaptor<(StatusBarStateController.StateListener)> @Captor private lateinit var dreamOverlayCallback: - ArgumentCaptor<(DreamOverlayStateController.Callback)> - @JvmField - @Rule - val mockito = MockitoJUnit.rule() + ArgumentCaptor<(DreamOverlayStateController.Callback)> + @JvmField @Rule val mockito = MockitoJUnit.rule() private lateinit var mediaHierarchyManager: MediaHierarchyManager private lateinit var mediaFrame: ViewGroup private val configurationController = FakeConfigurationController() @@ -98,13 +96,15 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Before fun setup() { - context.getOrCreateTestableResources().addOverride( - R.bool.config_use_split_notification_shade, false) + context + .getOrCreateTestableResources() + .addOverride(R.bool.config_use_split_notification_shade, false) mediaFrame = FrameLayout(context) testableLooper = TestableLooper.get(this) fakeHandler = FakeHandler(testableLooper.looper) whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame) - mediaHierarchyManager = MediaHierarchyManager( + mediaHierarchyManager = + MediaHierarchyManager( context, statusBarStateController, keyguardStateController, @@ -116,7 +116,8 @@ class MediaHierarchyManagerTest : SysuiTestCase() { wakefulnessLifecycle, notifPanelEvents, settings, - fakeHandler,) + fakeHandler, + ) verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture()) verify(statusBarStateController).addCallback(statusBarCallback.capture()) verify(dreamOverlayStateController).addCallback(dreamOverlayCallback.capture()) @@ -125,7 +126,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) whenever(mediaCarouselController.mediaCarouselScrollHandler) - .thenReturn(mediaCarouselScrollHandler) + .thenReturn(mediaCarouselScrollHandler) val observer = wakefullnessObserver.value assertNotNull("lifecycle observer wasn't registered", observer) observer.onFinishedWakingUp() @@ -151,30 +152,53 @@ class MediaHierarchyManagerTest : SysuiTestCase() { fun testBlockedWhenScreenTurningOff() { // Let's set it onto QS: mediaHierarchyManager.qsExpansion = 1.0f - verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) + verify(mediaCarouselController) + .onDesiredLocationChanged( + ArgumentMatchers.anyInt(), + any(MediaHostState::class.java), + anyBoolean(), + anyLong(), + anyLong() + ) val observer = wakefullnessObserver.value assertNotNull("lifecycle observer wasn't registered", observer) observer.onStartedGoingToSleep() clearInvocations(mediaCarouselController) mediaHierarchyManager.qsExpansion = 0.0f verify(mediaCarouselController, times(0)) - .onDesiredLocationChanged(ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) + .onDesiredLocationChanged( + ArgumentMatchers.anyInt(), + any(MediaHostState::class.java), + anyBoolean(), + anyLong(), + anyLong() + ) } @Test fun testAllowedWhenNotTurningOff() { // Let's set it onto QS: mediaHierarchyManager.qsExpansion = 1.0f - verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) + verify(mediaCarouselController) + .onDesiredLocationChanged( + ArgumentMatchers.anyInt(), + any(MediaHostState::class.java), + anyBoolean(), + anyLong(), + anyLong() + ) val observer = wakefullnessObserver.value assertNotNull("lifecycle observer wasn't registered", observer) clearInvocations(mediaCarouselController) mediaHierarchyManager.qsExpansion = 0.0f - verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), - any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) + verify(mediaCarouselController) + .onDesiredLocationChanged( + ArgumentMatchers.anyInt(), + any(MediaHostState::class.java), + anyBoolean(), + anyLong(), + anyLong() + ) } @Test @@ -183,22 +207,26 @@ class MediaHierarchyManagerTest : SysuiTestCase() { // Let's transition all the way to full shade mediaHierarchyManager.setTransitionToFullShadeAmount(100000f) - verify(mediaCarouselController).onDesiredLocationChanged( - eq(MediaHierarchyManager.LOCATION_QQS), - any(MediaHostState::class.java), - eq(false), - anyLong(), - anyLong()) + verify(mediaCarouselController) + .onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_QQS), + any(MediaHostState::class.java), + eq(false), + anyLong(), + anyLong() + ) clearInvocations(mediaCarouselController) // Let's go back to the lock screen mediaHierarchyManager.setTransitionToFullShadeAmount(0.0f) - verify(mediaCarouselController).onDesiredLocationChanged( - eq(MediaHierarchyManager.LOCATION_LOCKSCREEN), - any(MediaHostState::class.java), - eq(false), - anyLong(), - anyLong()) + verify(mediaCarouselController) + .onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_LOCKSCREEN), + any(MediaHostState::class.java), + eq(false), + anyLong(), + anyLong() + ) // Let's make sure alpha is set mediaHierarchyManager.setTransitionToFullShadeAmount(2.0f) @@ -302,7 +330,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { val expectedTranslation = LOCKSCREEN_TOP - QS_TOP assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()) - .isEqualTo(expectedTranslation) + .isEqualTo(expectedTranslation) } @Test @@ -343,27 +371,31 @@ class MediaHierarchyManagerTest : SysuiTestCase() { fun testDream() { goToDream() setMediaDreamComplicationEnabled(true) - verify(mediaCarouselController).onDesiredLocationChanged( + verify(mediaCarouselController) + .onDesiredLocationChanged( eq(MediaHierarchyManager.LOCATION_DREAM_OVERLAY), nullable(), eq(false), anyLong(), - anyLong()) + anyLong() + ) clearInvocations(mediaCarouselController) setMediaDreamComplicationEnabled(false) - verify(mediaCarouselController).onDesiredLocationChanged( + verify(mediaCarouselController) + .onDesiredLocationChanged( eq(MediaHierarchyManager.LOCATION_QQS), any(MediaHostState::class.java), eq(false), anyLong(), - anyLong()) + anyLong() + ) } private fun enableSplitShade() { - context.getOrCreateTestableResources().addOverride( - R.bool.config_use_split_notification_shade, true - ) + context + .getOrCreateTestableResources() + .addOverride(R.bool.config_use_split_notification_shade, true) configurationController.notifyConfigurationChanged() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt index 6e38d26411ee..32b822d798f8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt @@ -14,11 +14,13 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -33,13 +35,10 @@ import org.mockito.junit.MockitoJUnit @RunWith(AndroidTestingRunner::class) public class MediaPlayerDataTest : SysuiTestCase() { - @Mock - private lateinit var playerIsPlaying: MediaControlPanel + @Mock private lateinit var playerIsPlaying: MediaControlPanel private var systemClock: FakeSystemClock = FakeSystemClock() - @JvmField - @Rule - val mockito = MockitoJUnit.rule() + @JvmField @Rule val mockito = MockitoJUnit.rule() companion object { val LOCAL = MediaData.PLAYBACK_LOCAL @@ -61,10 +60,20 @@ public class MediaPlayerDataTest : SysuiTestCase() { val playerIsRemote = mock(MediaControlPanel::class.java) val dataIsRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION) - MediaPlayerData.addMediaPlayer("2", dataIsRemote, playerIsRemote, systemClock, - isSsReactivated = false) - MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + "2", + dataIsRemote, + playerIsRemote, + systemClock, + isSsReactivated = false + ) + MediaPlayerData.addMediaPlayer( + "1", + dataIsPlaying, + playerIsPlaying, + systemClock, + isSsReactivated = false + ) val players = MediaPlayerData.players() assertThat(players).hasSize(2) @@ -79,22 +88,42 @@ public class MediaPlayerDataTest : SysuiTestCase() { val playerIsPlaying2 = mock(MediaControlPanel::class.java) var dataIsPlaying2 = createMediaData("app2", !PLAYING, LOCAL, !RESUMPTION) - MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + "1", + dataIsPlaying1, + playerIsPlaying1, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) - MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + "2", + dataIsPlaying2, + playerIsPlaying2, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) dataIsPlaying1 = createMediaData("app1", !PLAYING, LOCAL, !RESUMPTION) dataIsPlaying2 = createMediaData("app2", PLAYING, LOCAL, !RESUMPTION) - MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + "1", + dataIsPlaying1, + playerIsPlaying1, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) - MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + "2", + dataIsPlaying2, + playerIsPlaying2, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) val players = MediaPlayerData.players() @@ -122,26 +151,60 @@ public class MediaPlayerDataTest : SysuiTestCase() { val dataUndetermined = createMediaData("app6", UNDETERMINED, LOCAL, RESUMPTION) MediaPlayerData.addMediaPlayer( - "3", dataIsStoppedAndLocal, playerIsStoppedAndLocal, systemClock, - isSsReactivated = false) + "3", + dataIsStoppedAndLocal, + playerIsStoppedAndLocal, + systemClock, + isSsReactivated = false + ) MediaPlayerData.addMediaPlayer( - "5", dataIsStoppedAndRemote, playerIsStoppedAndRemote, systemClock, - isSsReactivated = false) - MediaPlayerData.addMediaPlayer("4", dataCanResume, playerCanResume, systemClock, - isSsReactivated = false) - MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock, - isSsReactivated = false) + "5", + dataIsStoppedAndRemote, + playerIsStoppedAndRemote, + systemClock, + isSsReactivated = false + ) MediaPlayerData.addMediaPlayer( - "2", dataIsPlayingAndRemote, playerIsPlayingAndRemote, systemClock, - isSsReactivated = false) - MediaPlayerData.addMediaPlayer("6", dataUndetermined, playerUndetermined, systemClock, - isSsReactivated = false) + "4", + dataCanResume, + playerCanResume, + systemClock, + isSsReactivated = false + ) + MediaPlayerData.addMediaPlayer( + "1", + dataIsPlaying, + playerIsPlaying, + systemClock, + isSsReactivated = false + ) + MediaPlayerData.addMediaPlayer( + "2", + dataIsPlayingAndRemote, + playerIsPlayingAndRemote, + systemClock, + isSsReactivated = false + ) + MediaPlayerData.addMediaPlayer( + "6", + dataUndetermined, + playerUndetermined, + systemClock, + isSsReactivated = false + ) val players = MediaPlayerData.players() assertThat(players).hasSize(6) - assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote, - playerIsStoppedAndRemote, playerIsStoppedAndLocal, playerUndetermined, - playerCanResume).inOrder() + assertThat(players) + .containsExactly( + playerIsPlaying, + playerIsPlayingAndRemote, + playerIsStoppedAndRemote, + playerIsStoppedAndLocal, + playerUndetermined, + playerCanResume + ) + .inOrder() } @Test @@ -153,13 +216,23 @@ public class MediaPlayerDataTest : SysuiTestCase() { assertThat(MediaPlayerData.players()).hasSize(0) - MediaPlayerData.addMediaPlayer(keyA, data, playerIsPlaying, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + keyA, + data, + playerIsPlaying, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) assertThat(MediaPlayerData.players()).hasSize(1) - MediaPlayerData.addMediaPlayer(keyB, data, playerIsPlaying, systemClock, - isSsReactivated = false) + MediaPlayerData.addMediaPlayer( + keyB, + data, + playerIsPlaying, + systemClock, + isSsReactivated = false + ) systemClock.advanceTime(1) assertThat(MediaPlayerData.players()).hasSize(2) @@ -177,12 +250,13 @@ public class MediaPlayerDataTest : SysuiTestCase() { isPlaying: Boolean?, location: Int, resumption: Boolean - ) = MediaTestUtils.emptyMediaData.copy( - app = app, - packageName = "package: $app", - playbackLocation = location, - resumption = resumption, - notificationKey = "key: $app", - isPlaying = isPlaying - ) + ) = + MediaTestUtils.emptyMediaData.copy( + app = app, + packageName = "package: $app", + playbackLocation = location, + resumption = resumption, + notificationKey = "key: $app", + isPlaying = isPlaying + ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt new file mode 100644 index 000000000000..6b7615557d83 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.View +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY +import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER +import com.android.systemui.util.animation.MeasurementInput +import com.android.systemui.util.animation.TransitionLayout +import com.android.systemui.util.animation.TransitionViewState +import com.android.systemui.util.animation.WidgetState +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.floatThat +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner::class) +class MediaViewControllerTest : SysuiTestCase() { + private val mediaHostStateHolder = MediaHost.MediaHostStateHolder() + private val mediaHostStatesManager = MediaHostStatesManager() + private val configurationController = + com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context) + private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0) + private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0) + @Mock lateinit var logger: MediaViewLogger + @Mock private lateinit var mockViewState: TransitionViewState + @Mock private lateinit var mockCopiedState: TransitionViewState + @Mock private lateinit var detailWidgetState: WidgetState + @Mock private lateinit var controlWidgetState: WidgetState + @Mock private lateinit var mediaTitleWidgetState: WidgetState + @Mock private lateinit var mediaContainerWidgetState: WidgetState + + val delta = 0.0001F + + private lateinit var mediaViewController: MediaViewController + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + mediaViewController = + MediaViewController(context, configurationController, mediaHostStatesManager, logger) + } + + @Test + fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() { + mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) + player.measureState = TransitionViewState().apply { this.height = 100 } + mediaHostStateHolder.expansion = 1f + val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + mediaHostStateHolder.measurementInput = + MeasurementInput(widthMeasureSpec, heightMeasureSpec) + + // Test no squish + mediaHostStateHolder.squishFraction = 1f + assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100) + + // Test half squish + mediaHostStateHolder.squishFraction = 0.5f + assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50) + } + + @Test + fun testObtainViewState_applySquishFraction_toRecommendationTransitionViewState_height() { + mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION) + recommendation.measureState = TransitionViewState().apply { this.height = 100 } + mediaHostStateHolder.expansion = 1f + val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + mediaHostStateHolder.measurementInput = + MeasurementInput(widthMeasureSpec, heightMeasureSpec) + + // Test no squish + mediaHostStateHolder.squishFraction = 1f + assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100) + + // Test half squish + mediaHostStateHolder.squishFraction = 0.5f + assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50) + } + + @Test + fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() { + whenever(mockViewState.copy()).thenReturn(mockCopiedState) + whenever(mockCopiedState.widgetStates) + .thenReturn( + mutableMapOf( + R.id.media_progress_bar to controlWidgetState, + R.id.header_artist to detailWidgetState + ) + ) + + val detailSquishMiddle = + TRANSFORM_BEZIER.getInterpolation( + (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, detailSquishMiddle) + verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } + + val detailSquishEnd = + TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION) + mediaViewController.squishViewState(mockViewState, detailSquishEnd) + verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } + + val controlSquishMiddle = + TRANSFORM_BEZIER.getInterpolation( + (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, controlSquishMiddle) + verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } + + val controlSquishEnd = + TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION) + mediaViewController.squishViewState(mockViewState, controlSquishEnd) + verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } + } + + @Test + fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() { + whenever(mockViewState.copy()).thenReturn(mockCopiedState) + whenever(mockCopiedState.widgetStates) + .thenReturn( + mutableMapOf( + R.id.media_title1 to mediaTitleWidgetState, + R.id.media_cover1_container to mediaContainerWidgetState + ) + ) + + val containerSquishMiddle = + TRANSFORM_BEZIER.getInterpolation( + (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, containerSquishMiddle) + verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } + + val containerSquishEnd = + TRANSFORM_BEZIER.getInterpolation( + (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, containerSquishEnd) + verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } + + val titleSquishMiddle = + TRANSFORM_BEZIER.getInterpolation( + (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, titleSquishMiddle) + verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } + + val titleSquishEnd = + TRANSFORM_BEZIER.getInterpolation( + (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION + ) + mediaViewController.squishViewState(mockViewState, titleSquishEnd) + verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MetadataAnimationHandlerTest.kt index 311aa9649911..323b7818ed3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MetadataAnimationHandlerTest.kt @@ -14,9 +14,8 @@ * limitations under the License. */ -package com.android.systemui.media +package com.android.systemui.media.controls.ui -import org.mockito.Mockito.`when` as whenever import android.animation.Animator import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner @@ -29,10 +28,11 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.times import org.mockito.Mockito.mock import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit @SmallTest @@ -55,8 +55,7 @@ class MetadataAnimationHandlerTest : SysuiTestCase() { handler = MetadataAnimationHandler(exitAnimator, enterAnimator) } - @After - fun tearDown() {} + @After fun tearDown() {} @Test fun firstBind_startsAnimationSet() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/SquigglyProgressTest.kt index d087b0fe4413..d6cff81c0aaa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/SquigglyProgressTest.kt @@ -1,4 +1,20 @@ -package com.android.systemui.media +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui import android.graphics.Canvas import android.graphics.Color @@ -107,7 +123,6 @@ class SquigglyProgressTest : SysuiTestCase() { val (wavePaint, linePaint) = paintCaptor.getAllValues() assertThat(wavePaint.color).isEqualTo(tint) - assertThat(linePaint.color).isEqualTo( - ColorUtils.setAlphaComponent(tint, DISABLED_ALPHA)) + assertThat(linePaint.color).isEqualTo(ColorUtils.setAlphaComponent(tint, DISABLED_ALPHA)) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java index 29188da46562..ce885c0bba2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java @@ -25,7 +25,7 @@ import android.widget.FrameLayout; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.media.MediaHost; +import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.util.animation.UniqueObjectHostView; import org.junit.Before; diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java index 2f52950a9ee4..ed928a35a20e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java @@ -33,8 +33,8 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dreams.complication.DreamMediaEntryComplication; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.media.MediaData; -import com.android.systemui.media.MediaDataManager; +import com.android.systemui.media.controls.models.player.MediaData; +import com.android.systemui.media.controls.pipeline.MediaDataManager; import org.junit.Before; import org.junit.Test; @@ -73,7 +73,7 @@ public class MediaDreamSentinelTest extends SysuiTestCase { @Test public void testOnMediaDataLoaded_complicationAddition() { - final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager, + final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager, mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags); sentinel.start(); @@ -94,7 +94,7 @@ public class MediaDreamSentinelTest extends SysuiTestCase { @Test public void testOnMediaDataRemoved_complicationRemoval() { - final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager, + final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager, mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags); sentinel.start(); @@ -114,7 +114,7 @@ public class MediaDreamSentinelTest extends SysuiTestCase { @Test public void testOnMediaDataLoaded_complicationRemoval() { - final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager, + final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager, mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags); sentinel.start(); @@ -139,7 +139,7 @@ public class MediaDreamSentinelTest extends SysuiTestCase { public void testOnMediaDataLoaded_mediaComplicationDisabled_doesNotAddComplication() { when(mFeatureFlags.isEnabled(DREAM_MEDIA_COMPLICATION)).thenReturn(false); - final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager, + final MediaDreamSentinel sentinel = new MediaDreamSentinel(mMediaDataManager, mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags); sentinel.start(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt index 2a130535c657..d82819397f08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt @@ -64,6 +64,7 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { context, FakeExecutor(FakeSystemClock()), ) + mediaTttCommandLineHelper.start() } @Test(expected = IllegalStateException::class) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt index 1078cdaa57c4..e009e8651f2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt @@ -19,9 +19,9 @@ package com.android.systemui.media.taptotransfer.common import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogcatEchoTracker import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt index 37f6434ea069..6a4c0f60466d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt @@ -19,11 +19,12 @@ package com.android.systemui.media.taptotransfer.common import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable -import android.widget.FrameLayout import androidx.test.filters.SmallTest -import com.android.internal.widget.CachingIconView import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -64,6 +65,34 @@ class MediaTttUtilsTest : SysuiTestCase() { } @Test + fun getIconFromPackageName_nullPackageName_returnsDefault() { + val icon = MediaTttUtils.getIconFromPackageName(context, appPackageName = null, logger) + + val expectedDesc = + ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name) + .loadContentDescription(context) + assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc) + } + + @Test + fun getIconFromPackageName_invalidPackageName_returnsDefault() { + val icon = MediaTttUtils.getIconFromPackageName(context, "fakePackageName", logger) + + val expectedDesc = + ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name) + .loadContentDescription(context) + assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc) + } + + @Test + fun getIconFromPackageName_validPackageName_returnsAppInfo() { + val icon = MediaTttUtils.getIconFromPackageName(context, PACKAGE_NAME, logger) + + assertThat(icon) + .isEqualTo(Icon.Loaded(appIconFromPackageName, ContentDescription.Loaded(APP_NAME))) + } + + @Test fun getIconInfoFromPackageName_nullPackageName_returnsDefault() { val iconInfo = MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null, logger) @@ -90,48 +119,6 @@ class MediaTttUtilsTest : SysuiTestCase() { assertThat(iconInfo.drawable).isEqualTo(appIconFromPackageName) assertThat(iconInfo.contentDescription).isEqualTo(APP_NAME) } - - @Test - fun setIcon_viewHasIconAndContentDescription() { - val view = CachingIconView(context) - val icon = context.getDrawable(R.drawable.ic_celebration)!! - val contentDescription = "Happy birthday!" - - MediaTttUtils.setIcon(view, icon, contentDescription) - - assertThat(view.drawable).isEqualTo(icon) - assertThat(view.contentDescription).isEqualTo(contentDescription) - } - - @Test - fun setIcon_iconSizeNull_viewSizeDoesNotChange() { - val view = CachingIconView(context) - val size = 456 - view.layoutParams = FrameLayout.LayoutParams(size, size) - - MediaTttUtils.setIcon(view, context.getDrawable(R.drawable.ic_cake)!!, "desc") - - assertThat(view.layoutParams.width).isEqualTo(size) - assertThat(view.layoutParams.height).isEqualTo(size) - } - - @Test - fun setIcon_iconSizeProvided_viewSizeUpdates() { - val view = CachingIconView(context) - val size = 456 - view.layoutParams = FrameLayout.LayoutParams(size, size) - - val newSize = 40 - MediaTttUtils.setIcon( - view, - context.getDrawable(R.drawable.ic_cake)!!, - "desc", - iconSize = newSize - ) - - assertThat(view.layoutParams.width).isEqualTo(newSize) - assertThat(view.layoutParams.height).isEqualTo(newSize) - } } private const val PACKAGE_NAME = "com.android.systemui" diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt index d41ad48676b4..8c3ae3d01f1d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt @@ -34,6 +34,7 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController @@ -41,6 +42,7 @@ import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -48,6 +50,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.never +import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -69,8 +72,12 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { @Mock private lateinit var configurationController: ConfigurationController @Mock + private lateinit var mediaTttFlags: MediaTttFlags + @Mock private lateinit var powerManager: PowerManager @Mock + private lateinit var viewUtil: ViewUtil + @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var commandQueue: CommandQueue @@ -82,6 +89,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true) fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) @@ -104,8 +112,11 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { configurationController, powerManager, Handler.getMain(), - receiverUiEventLogger + mediaTttFlags, + receiverUiEventLogger, + viewUtil, ) + controllerReceiver.start() val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) verify(commandQueue).addCallback(callbackCaptor.capture()) @@ -113,6 +124,30 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { } @Test + fun commandQueueCallback_flagOff_noCallbackAdded() { + reset(commandQueue) + whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(false) + + controllerReceiver = MediaTttChipControllerReceiver( + commandQueue, + context, + logger, + windowManager, + FakeExecutor(FakeSystemClock()), + accessibilityManager, + configurationController, + powerManager, + Handler.getMain(), + mediaTttFlags, + receiverUiEventLogger, + viewUtil, + ) + controllerReceiver.start() + + verify(commandQueue, never()).addCallback(any()) + } + + @Test fun commandQueueCallback_closeToSender_triggersChip() { val appName = "FakeAppName" commandQueueCallback.updateMediaTapToTransferReceiverDisplay( @@ -212,35 +247,27 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { } @Test - fun updateView_isAppIcon_usesAppIconSize() { + fun updateView_isAppIcon_usesAppIconPadding() { controllerReceiver.displayView(getChipReceiverInfo(packageName = PACKAGE_NAME)) - val chipView = getChipView() - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) - ) - - val expectedSize = - context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver) - assertThat(chipView.getAppIconView().measuredWidth).isEqualTo(expectedSize) - assertThat(chipView.getAppIconView().measuredHeight).isEqualTo(expectedSize) + val chipView = getChipView() + assertThat(chipView.getAppIconView().paddingLeft).isEqualTo(0) + assertThat(chipView.getAppIconView().paddingRight).isEqualTo(0) + assertThat(chipView.getAppIconView().paddingTop).isEqualTo(0) + assertThat(chipView.getAppIconView().paddingBottom).isEqualTo(0) } @Test - fun updateView_notAppIcon_usesGenericIconSize() { + fun updateView_notAppIcon_usesGenericIconPadding() { controllerReceiver.displayView(getChipReceiverInfo(packageName = null)) - val chipView = getChipView() - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) - ) - - val expectedSize = - context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_size_receiver) - assertThat(chipView.getAppIconView().measuredWidth).isEqualTo(expectedSize) - assertThat(chipView.getAppIconView().measuredHeight).isEqualTo(expectedSize) + val chipView = getChipView() + val expectedPadding = + context.resources.getDimensionPixelSize(R.dimen.media_ttt_generic_icon_padding) + assertThat(chipView.getAppIconView().paddingLeft).isEqualTo(expectedPadding) + assertThat(chipView.getAppIconView().paddingRight).isEqualTo(expectedPadding) + assertThat(chipView.getAppIconView().paddingTop).isEqualTo(expectedPadding) + assertThat(chipView.getAppIconView().paddingBottom).isEqualTo(expectedPadding) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt deleted file mode 100644 index ff0faf98fe1e..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt +++ /dev/null @@ -1,786 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.taptotransfer.sender - -import android.app.StatusBarManager -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.media.MediaRoute2Info -import android.os.PowerManager -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import android.view.accessibility.AccessibilityManager -import android.widget.ImageView -import android.widget.TextView -import androidx.test.filters.SmallTest -import com.android.internal.logging.testing.UiEventLoggerFake -import com.android.internal.statusbar.IUndoMediaTransferCallback -import com.android.systemui.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.media.taptotransfer.common.MediaTttLogger -import com.android.systemui.statusbar.CommandQueue -import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever -import org.mockito.MockitoAnnotations - -@SmallTest -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper -class MediaTttChipControllerSenderTest : SysuiTestCase() { - private lateinit var controllerSender: MediaTttChipControllerSender - - @Mock - private lateinit var packageManager: PackageManager - @Mock - private lateinit var applicationInfo: ApplicationInfo - @Mock - private lateinit var logger: MediaTttLogger - @Mock - private lateinit var accessibilityManager: AccessibilityManager - @Mock - private lateinit var configurationController: ConfigurationController - @Mock - private lateinit var powerManager: PowerManager - @Mock - private lateinit var windowManager: WindowManager - @Mock - private lateinit var commandQueue: CommandQueue - private lateinit var commandQueueCallback: CommandQueue.Callbacks - private lateinit var fakeAppIconDrawable: Drawable - private lateinit var fakeClock: FakeSystemClock - private lateinit var fakeExecutor: FakeExecutor - private lateinit var uiEventLoggerFake: UiEventLoggerFake - private lateinit var senderUiEventLogger: MediaTttSenderUiEventLogger - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - - fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! - whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) - whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) - whenever(packageManager.getApplicationInfo( - eq(PACKAGE_NAME), any<PackageManager.ApplicationInfoFlags>() - )).thenReturn(applicationInfo) - context.setMockPackageManager(packageManager) - - fakeClock = FakeSystemClock() - fakeExecutor = FakeExecutor(fakeClock) - - uiEventLoggerFake = UiEventLoggerFake() - senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake) - - whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) - - controllerSender = MediaTttChipControllerSender( - commandQueue, - context, - logger, - windowManager, - fakeExecutor, - accessibilityManager, - configurationController, - powerManager, - senderUiEventLogger - ) - - val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) - verify(commandQueue).addCallback(callbackCaptor.capture()) - commandQueueCallback = callbackCaptor.value!! - } - - @Test - fun commandQueueCallback_almostCloseToStartCast_triggersCorrectChip() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, - routeInfo, - null - ) - - assertThat(getChipView().getChipText()).isEqualTo( - almostCloseToStartCast().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id - ) - } - - @Test - fun commandQueueCallback_almostCloseToEndCast_triggersCorrectChip() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, - routeInfo, - null - ) - - assertThat(getChipView().getChipText()).isEqualTo( - almostCloseToEndCast().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id - ) - } - - @Test - fun commandQueueCallback_transferToReceiverTriggered_triggersCorrectChip() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, - routeInfo, - null - ) - - assertThat(getChipView().getChipText()).isEqualTo( - transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id - ) - } - - @Test - fun commandQueueCallback_transferToThisDeviceTriggered_triggersCorrectChip() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, - routeInfo, - null - ) - - assertThat(getChipView().getChipText()).isEqualTo( - transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id - ) - } - - @Test - fun commandQueueCallback_transferToReceiverSucceeded_triggersCorrectChip() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, - routeInfo, - null - ) - - assertThat(getChipView().getChipText()).isEqualTo( - transferToReceiverSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id - ) - } - - @Test - fun commandQueueCallback_transferToThisDeviceSucceeded_triggersCorrectChip() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, - routeInfo, - null - ) - - assertThat(getChipView().getChipText()).isEqualTo( - transferToThisDeviceSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id - ) - } - - @Test - fun commandQueueCallback_transferToReceiverFailed_triggersCorrectChip() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED, - routeInfo, - null - ) - - assertThat(getChipView().getChipText()).isEqualTo( - transferToReceiverFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id - ) - } - - @Test - fun commandQueueCallback_transferToThisDeviceFailed_triggersCorrectChip() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED, - routeInfo, - null - ) - - assertThat(getChipView().getChipText()).isEqualTo( - transferToThisDeviceFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id - ) - } - - @Test - fun commandQueueCallback_farFromReceiver_noChipShown() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, - routeInfo, - null - ) - - verify(windowManager, never()).addView(any(), any()) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER.id - ) - } - - @Test - fun commandQueueCallback_almostCloseThenFarFromReceiver_chipShownThenHidden() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, - routeInfo, - null - ) - - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, - routeInfo, - null - ) - - val viewCaptor = ArgumentCaptor.forClass(View::class.java) - verify(windowManager).addView(viewCaptor.capture(), any()) - verify(windowManager).removeView(viewCaptor.value) - } - - @Test - fun commandQueueCallback_invalidStateParam_noChipShown() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - 100, - routeInfo, - null - ) - - verify(windowManager, never()).addView(any(), any()) - } - - @Test - fun receivesNewStateFromCommandQueue_isLogged() { - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, - routeInfo, - null - ) - - verify(logger).logStateChange(any(), any(), any()) - } - - @Test - fun almostCloseToStartCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() { - val state = almostCloseToStartCast() - controllerSender.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun almostCloseToEndCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() { - val state = almostCloseToEndCast() - controllerSender.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToReceiverTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() { - val state = transferToReceiverTriggered() - controllerSender.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToThisDeviceTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() { - val state = transferToThisDeviceTriggered() - controllerSender.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToReceiverSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() { - val state = transferToReceiverSucceeded() - controllerSender.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToReceiverSucceeded_nullUndoRunnable_noUndo() { - controllerSender.displayView(transferToReceiverSucceeded(undoCallback = null)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToReceiverSucceeded_withUndoRunnable_undoWithClick() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - controllerSender.displayView(transferToReceiverSucceeded(undoCallback)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue() - } - - @Test - fun transferToReceiverSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() { - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - controllerSender.displayView(transferToReceiverSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() - } - - @Test - fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - controllerSender.displayView(transferToReceiverSucceeded(undoCallback)) - - getChipView().getUndoButton().performClick() - - assertThat(getChipView().getChipText()).isEqualTo( - transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id - ) - } - - @Test - fun transferToThisDeviceSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() { - val state = transferToThisDeviceSucceeded() - controllerSender.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToThisDeviceSucceeded_nullUndoRunnable_noUndo() { - controllerSender.displayView(transferToThisDeviceSucceeded(undoCallback = null)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - } - - @Test - fun transferToThisDeviceSucceeded_withUndoRunnable_undoWithClick() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - controllerSender.displayView(transferToThisDeviceSucceeded(undoCallback)) - - val chipView = getChipView() - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE) - assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue() - } - - @Test - fun transferToThisDeviceSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() { - var undoCallbackCalled = false - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() { - undoCallbackCalled = true - } - } - - controllerSender.displayView(transferToThisDeviceSucceeded(undoCallback)) - getChipView().getUndoButton().performClick() - - assertThat(undoCallbackCalled).isTrue() - } - - @Test - fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToReceiverTriggered() { - val undoCallback = object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - controllerSender.displayView(transferToThisDeviceSucceeded(undoCallback)) - - getChipView().getUndoButton().performClick() - - assertThat(getChipView().getChipText()).isEqualTo( - transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( - MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id - ) - } - - @Test - fun transferToReceiverFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { - val state = transferToReceiverFailed() - controllerSender.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(getChipView().getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) - } - - @Test - fun transferToThisDeviceFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { - val state = transferToThisDeviceFailed() - controllerSender.displayView(state) - - val chipView = getChipView() - assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(getChipView().getChipText()).isEqualTo( - state.state.getChipTextString(context, OTHER_DEVICE_NAME) - ) - assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) - assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) - assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) - } - - @Test - fun changeFromAlmostCloseToStartToTransferTriggered_loadingIconAppears() { - controllerSender.displayView(almostCloseToStartCast()) - controllerSender.displayView(transferToReceiverTriggered()) - - assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE) - } - - @Test - fun changeFromTransferTriggeredToTransferSucceeded_loadingIconDisappears() { - controllerSender.displayView(transferToReceiverTriggered()) - controllerSender.displayView(transferToReceiverSucceeded()) - - assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE) - } - - @Test - fun changeFromTransferTriggeredToTransferSucceeded_undoButtonAppears() { - controllerSender.displayView(transferToReceiverTriggered()) - controllerSender.displayView( - transferToReceiverSucceeded( - object : IUndoMediaTransferCallback.Stub() { - override fun onUndoTriggered() {} - } - ) - ) - - assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE) - } - - @Test - fun changeFromTransferSucceededToAlmostCloseToStart_undoButtonDisappears() { - controllerSender.displayView(transferToReceiverSucceeded()) - controllerSender.displayView(almostCloseToStartCast()) - - assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE) - } - - @Test - fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() { - controllerSender.displayView(transferToReceiverTriggered()) - controllerSender.displayView(transferToReceiverFailed()) - - assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE) - } - - @Test - fun transferToReceiverTriggeredThenRemoveView_viewStillDisplayed() { - controllerSender.displayView(transferToReceiverTriggered()) - fakeClock.advanceTime(1000L) - - controllerSender.removeView("fakeRemovalReason") - fakeExecutor.runAllReady() - - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) - } - - @Test - fun transferToReceiverTriggeredThenFarFromReceiver_viewStillDisplayed() { - controllerSender.displayView(transferToReceiverTriggered()) - - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, - routeInfo, - null - ) - fakeExecutor.runAllReady() - - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) - } - - @Test - fun transferToReceiverTriggeredThenRemoveView_eventuallyTimesOut() { - controllerSender.displayView(transferToReceiverTriggered()) - - controllerSender.removeView("fakeRemovalReason") - fakeClock.advanceTime(TIMEOUT + 1L) - - verify(windowManager).removeView(any()) - } - - @Test - fun transferToThisDeviceTriggeredThenRemoveView_viewStillDisplayed() { - controllerSender.displayView(transferToThisDeviceTriggered()) - fakeClock.advanceTime(1000L) - - controllerSender.removeView("fakeRemovalReason") - fakeExecutor.runAllReady() - - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) - } - - @Test - fun transferToThisDeviceTriggeredThenRemoveView_eventuallyTimesOut() { - controllerSender.displayView(transferToThisDeviceTriggered()) - - controllerSender.removeView("fakeRemovalReason") - fakeClock.advanceTime(TIMEOUT + 1L) - - verify(windowManager).removeView(any()) - } - - @Test - fun transferToThisDeviceTriggeredThenFarFromReceiver_viewStillDisplayed() { - controllerSender.displayView(transferToThisDeviceTriggered()) - - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, - routeInfo, - null - ) - fakeExecutor.runAllReady() - - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) - } - - @Test - fun transferToReceiverSucceededThenRemoveView_viewStillDisplayed() { - controllerSender.displayView(transferToReceiverSucceeded()) - - controllerSender.removeView("fakeRemovalReason") - fakeExecutor.runAllReady() - - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) - } - - @Test - fun transferToReceiverSucceededThenRemoveView_eventuallyTimesOut() { - controllerSender.displayView(transferToReceiverSucceeded()) - - controllerSender.removeView("fakeRemovalReason") - fakeClock.advanceTime(TIMEOUT + 1L) - - verify(windowManager).removeView(any()) - } - - @Test - fun transferToReceiverSucceededThenFarFromReceiver_viewStillDisplayed() { - controllerSender.displayView(transferToReceiverSucceeded()) - - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, - routeInfo, - null - ) - fakeExecutor.runAllReady() - - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) - } - - @Test - fun transferToThisDeviceSucceededThenRemoveView_viewStillDisplayed() { - controllerSender.displayView(transferToThisDeviceSucceeded()) - - controllerSender.removeView("fakeRemovalReason") - fakeExecutor.runAllReady() - - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) - } - - @Test - fun transferToThisDeviceSucceededThenRemoveView_eventuallyTimesOut() { - controllerSender.displayView(transferToThisDeviceSucceeded()) - - controllerSender.removeView("fakeRemovalReason") - fakeClock.advanceTime(TIMEOUT + 1L) - - verify(windowManager).removeView(any()) - } - - @Test - fun transferToThisDeviceSucceededThenFarFromReceiver_viewStillDisplayed() { - controllerSender.displayView(transferToThisDeviceSucceeded()) - - commandQueueCallback.updateMediaTapToTransferSenderDisplay( - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, - routeInfo, - null - ) - fakeExecutor.runAllReady() - - verify(windowManager, never()).removeView(any()) - verify(logger).logRemovalBypass(any(), any()) - } - - private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon) - - private fun ViewGroup.getChipText(): String = - (this.requireViewById<TextView>(R.id.text)).text as String - - private fun ViewGroup.getLoadingIconVisibility(): Int = - this.requireViewById<View>(R.id.loading).visibility - - private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.undo) - - private fun ViewGroup.getFailureIcon(): View = this.requireViewById(R.id.failure_icon) - - private fun getChipView(): ViewGroup { - val viewCaptor = ArgumentCaptor.forClass(View::class.java) - verify(windowManager).addView(viewCaptor.capture(), any()) - return viewCaptor.value as ViewGroup - } - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToStartCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun almostCloseToEndCast() = - ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceTriggered() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToReceiverFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) - - /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferToThisDeviceFailed() = - ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) -} - -private const val APP_NAME = "Fake app name" -private const val OTHER_DEVICE_NAME = "My Tablet" -private const val PACKAGE_NAME = "com.android.systemui" -private const val TIMEOUT = 10000 - -private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) - .addFeature("feature") - .setClientPackageName(PACKAGE_NAME) - .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt new file mode 100644 index 000000000000..fdeb3f5eb857 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -0,0 +1,691 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.taptotransfer.sender + +import android.app.StatusBarManager +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.media.MediaRoute2Info +import android.os.PowerManager +import android.os.VibrationEffect +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.view.accessibility.AccessibilityManager +import android.widget.ImageView +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.internal.statusbar.IUndoMediaTransferCallback +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.Text.Companion.loadText +import com.android.systemui.media.taptotransfer.MediaTttFlags +import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.view.ViewUtil +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class MediaTttSenderCoordinatorTest : SysuiTestCase() { + + // Note: This tests are a bit like integration tests because they use a real instance of + // [ChipbarCoordinator] and verify that the coordinator displays the correct view, based on + // the inputs from [MediaTttSenderCoordinator]. + + private lateinit var underTest: MediaTttSenderCoordinator + + @Mock private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var applicationInfo: ApplicationInfo + @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var falsingManager: FalsingManager + @Mock private lateinit var falsingCollector: FalsingCollector + @Mock private lateinit var logger: MediaTttLogger + @Mock private lateinit var mediaTttFlags: MediaTttFlags + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var powerManager: PowerManager + @Mock private lateinit var viewUtil: ViewUtil + @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var vibratorHelper: VibratorHelper + private lateinit var chipbarCoordinator: ChipbarCoordinator + private lateinit var commandQueueCallback: CommandQueue.Callbacks + private lateinit var fakeAppIconDrawable: Drawable + private lateinit var fakeClock: FakeSystemClock + private lateinit var fakeExecutor: FakeExecutor + private lateinit var uiEventLoggerFake: UiEventLoggerFake + private lateinit var uiEventLogger: MediaTttSenderUiEventLogger + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true) + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) + + fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!! + whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) + whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable) + whenever( + packageManager.getApplicationInfo( + eq(PACKAGE_NAME), + any<PackageManager.ApplicationInfoFlags>() + ) + ) + .thenReturn(applicationInfo) + context.setMockPackageManager(packageManager) + + fakeClock = FakeSystemClock() + fakeExecutor = FakeExecutor(fakeClock) + + uiEventLoggerFake = UiEventLoggerFake() + uiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake) + + chipbarCoordinator = + FakeChipbarCoordinator( + context, + logger, + windowManager, + fakeExecutor, + accessibilityManager, + configurationController, + powerManager, + falsingManager, + falsingCollector, + viewUtil, + vibratorHelper, + ) + chipbarCoordinator.start() + + underTest = + MediaTttSenderCoordinator( + chipbarCoordinator, + commandQueue, + context, + logger, + mediaTttFlags, + uiEventLogger, + ) + underTest.start() + + val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) + verify(commandQueue).addCallback(callbackCaptor.capture()) + commandQueueCallback = callbackCaptor.value!! + } + + @Test + fun commandQueueCallback_flagOff_noCallbackAdded() { + reset(commandQueue) + whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(false) + underTest = + MediaTttSenderCoordinator( + chipbarCoordinator, + commandQueue, + context, + logger, + mediaTttFlags, + uiEventLogger, + ) + underTest.start() + + verify(commandQueue, never()).addCallback(any()) + } + + @Test + fun commandQueueCallback_almostCloseToStartCast_triggersCorrectChip() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, + routeInfo, + null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) + } + + @Test + fun commandQueueCallback_almostCloseToEndCast_triggersCorrectChip() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, + routeInfo, + null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) + } + + @Test + fun commandQueueCallback_transferToReceiverTriggered_triggersCorrectChip() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, + routeInfo, + null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) + } + + @Test + fun commandQueueCallback_transferToThisDeviceTriggered_triggersCorrectChip() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, + routeInfo, + null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) + } + + @Test + fun commandQueueCallback_transferToReceiverSucceeded_triggersCorrectChip() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id) + verify(vibratorHelper, never()).vibrate(any<VibrationEffect>()) + } + + @Test + fun transferToReceiverSucceeded_nullUndoCallback_noUndo() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun transferToReceiverSucceeded_withUndoRunnable_undoVisible() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() + } + + @Test + fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { + var undoCallbackCalled = false + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + }, + ) + + getChipbarView().getUndoButton().performClick() + + // Event index 1 since initially displaying the succeeded chip would also log an event + assertThat(uiEventLoggerFake.eventId(1)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id) + assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + } + + @Test + fun commandQueueCallback_transferToThisDeviceSucceeded_triggersCorrectChip() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id) + verify(vibratorHelper, never()).vibrate(any<VibrationEffect>()) + } + + @Test + fun transferToThisDeviceSucceeded_nullUndoCallback_noUndo() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun transferToThisDeviceSucceeded_withUndoRunnable_undoVisible() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue() + } + + @Test + fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { + var undoCallbackCalled = false + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + }, + ) + + getChipbarView().getUndoButton().performClick() + + // Event index 1 since initially displaying the succeeded chip would also log an event + assertThat(uiEventLoggerFake.eventId(1)) + .isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id + ) + assertThat(undoCallbackCalled).isTrue() + assertThat(getChipbarView().getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + } + + @Test + fun commandQueueCallback_transferToReceiverFailed_triggersCorrectChip() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED, + routeInfo, + null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) + } + + @Test + fun commandQueueCallback_transferToThisDeviceFailed_triggersCorrectChip() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED, + routeInfo, + null + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText()) + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id) + verify(vibratorHelper).vibrate(any<VibrationEffect>()) + } + + @Test + fun commandQueueCallback_farFromReceiver_noChipShown() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + + verify(windowManager, never()).addView(any(), any()) + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER.id) + } + + @Test + fun commandQueueCallback_almostCloseThenFarFromReceiver_chipShownThenHidden() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, + routeInfo, + null + ) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + + val viewCaptor = ArgumentCaptor.forClass(View::class.java) + verify(windowManager).addView(viewCaptor.capture(), any()) + verify(windowManager).removeView(viewCaptor.value) + } + + @Test + fun commandQueueCallback_invalidStateParam_noChipShown() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay(100, routeInfo, null) + + verify(windowManager, never()).addView(any(), any()) + } + + @Test + fun receivesNewStateFromCommandQueue_isLogged() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, + routeInfo, + null + ) + + verify(logger).logStateChange(any(), any(), any()) + } + + @Test + fun transferToReceiverTriggeredThenFarFromReceiver_viewStillDisplayedButStillTimesOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, + routeInfo, + null + ) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + verify(windowManager).removeView(any()) + } + + @Test + fun transferToThisDeviceTriggeredThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, + routeInfo, + null + ) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + verify(windowManager).removeView(any()) + } + + @Test + fun transferToReceiverSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + null + ) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + verify(windowManager).removeView(any()) + } + + @Test + fun transferToThisDeviceSucceededThenFarFromReceiver_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + null + ) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + verify(windowManager).removeView(any()) + } + + @Test + fun transferToReceiverSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText()) + + // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should + // verify that the new state it triggers operates just like any other state. + getChipbarView().getUndoButton().performClick() + fakeExecutor.runAllReady() + + // Verify that the click updated us to the triggered state + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText()) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + // Verify that we didn't remove the chipbar because it's in the triggered state + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + // Verify we eventually remove the chipbar + verify(windowManager).removeView(any()) + } + + @Test + fun transferToThisDeviceSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + routeInfo, + object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() {} + }, + ) + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText()) + + // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should + // verify that the new state it triggers operates just like any other state. + getChipbarView().getUndoButton().performClick() + fakeExecutor.runAllReady() + + // Verify that the click updated us to the triggered state + assertThat(chipbarView.getChipText()) + .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + routeInfo, + null + ) + fakeExecutor.runAllReady() + + // Verify that we didn't remove the chipbar because it's in the triggered state + verify(windowManager, never()).removeView(any()) + verify(logger).logRemovalBypass(any(), any()) + + fakeClock.advanceTime(TIMEOUT + 1L) + + // Verify we eventually remove the chipbar + verify(windowManager).removeView(any()) + } + + private fun getChipbarView(): ViewGroup { + val viewCaptor = ArgumentCaptor.forClass(View::class.java) + verify(windowManager).addView(viewCaptor.capture(), any()) + return viewCaptor.value as ViewGroup + } + + private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.start_icon) + + private fun ViewGroup.getChipText(): String = + (this.requireViewById<TextView>(R.id.text)).text as String + + private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading) + + private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error) + + private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button) + + private fun ChipStateSender.getExpectedStateText(): String? { + return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context) + } +} + +private const val APP_NAME = "Fake app name" +private const val OTHER_DEVICE_NAME = "My Tablet" +private const val PACKAGE_NAME = "com.android.systemui" +private const val TIMEOUT = 10000 + +private val routeInfo = + MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME) + .addFeature("feature") + .setClientPackageName(PACKAGE_NAME) + .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt index 00b1f3268bae..19d2d334b884 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt @@ -25,6 +25,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { private val controller = MediaProjectionAppSelectorController( taskListProvider, + view, scope, appSelectorComponentName ) @@ -33,7 +34,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { fun initNoRecentTasks_bindsEmptyList() { taskListProvider.tasks = emptyList() - controller.init(view) + controller.init() verify(view).bind(emptyList()) } @@ -44,7 +45,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { createRecentTask(taskId = 1) ) - controller.init(view) + controller.init() verify(view).bind( listOf( @@ -62,7 +63,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { ) taskListProvider.tasks = tasks - controller.init(view) + controller.init() verify(view).bind( listOf( @@ -84,7 +85,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { ) taskListProvider.tasks = tasks - controller.init(view) + controller.init() verify(view).bind( listOf( diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt new file mode 100644 index 000000000000..464acb68fb07 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.mediaprojection.appselector.view + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Rect +import android.util.DisplayMetrics.DENSITY_DEFAULT +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener +import com.android.systemui.statusbar.policy.FakeConfigurationController +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlin.math.min +import org.junit.Before +import org.junit.Test + +@SmallTest +class TaskPreviewSizeProviderTest : SysuiTestCase() { + + private val mockContext: Context = mock() + private val resources: Resources = mock() + private val windowManager: WindowManager = mock() + private val sizeUpdates = arrayListOf<Rect>() + private val testConfigurationController = FakeConfigurationController() + + @Before + fun setup() { + whenever(mockContext.getSystemService(eq(WindowManager::class.java))) + .thenReturn(windowManager) + whenever(mockContext.resources).thenReturn(resources) + } + + @Test + fun size_phoneDisplay_thumbnailSizeIsSmallerAndProportionalToTheScreenSize() { + givenDisplay(width = 400, height = 600, isTablet = false) + + val size = createSizeProvider().size + + assertThat(size).isEqualTo(Rect(0, 0, 100, 150)) + } + + @Test + fun size_tabletDisplay_thumbnailSizeProportionalToTheScreenSizeExcludingTaskbar() { + givenDisplay(width = 400, height = 600, isTablet = true) + givenTaskbarSize(20) + + val size = createSizeProvider().size + + assertThat(size).isEqualTo(Rect(0, 0, 97, 140)) + } + + @Test + fun size_phoneDisplayAndRotate_emitsSizeUpdate() { + givenDisplay(width = 400, height = 600, isTablet = false) + createSizeProvider() + + givenDisplay(width = 600, height = 400, isTablet = false) + testConfigurationController.onConfigurationChanged(Configuration()) + + assertThat(sizeUpdates).containsExactly(Rect(0, 0, 150, 100)) + } + + @Test + fun size_phoneDisplayAndRotateConfigurationChange_returnsUpdatedSize() { + givenDisplay(width = 400, height = 600, isTablet = false) + val sizeProvider = createSizeProvider() + + givenDisplay(width = 600, height = 400, isTablet = false) + testConfigurationController.onConfigurationChanged(Configuration()) + + assertThat(sizeProvider.size).isEqualTo(Rect(0, 0, 150, 100)) + } + + private fun givenTaskbarSize(size: Int) { + whenever(resources.getDimensionPixelSize(eq(R.dimen.taskbar_frame_height))).thenReturn(size) + } + + private fun givenDisplay(width: Int, height: Int, isTablet: Boolean = false) { + val bounds = Rect(0, 0, width, height) + val windowMetrics = WindowMetrics(bounds, null) + whenever(windowManager.maximumWindowMetrics).thenReturn(windowMetrics) + whenever(windowManager.currentWindowMetrics).thenReturn(windowMetrics) + + val minDimension = min(width, height) + + // Calculate DPI so the smallest width is either considered as tablet or as phone + val targetSmallestWidthDpi = + if (isTablet) SMALLEST_WIDTH_DPI_TABLET else SMALLEST_WIDTH_DPI_PHONE + val densityDpi = minDimension * DENSITY_DEFAULT / targetSmallestWidthDpi + + val configuration = Configuration(context.resources.configuration) + configuration.densityDpi = densityDpi + whenever(resources.configuration).thenReturn(configuration) + } + + private fun createSizeProvider(): TaskPreviewSizeProvider { + val listener = + object : TaskPreviewSizeListener { + override fun onTaskSizeChanged(size: Rect) { + sizeUpdates.add(size) + } + } + + return TaskPreviewSizeProvider(mockContext, windowManager, testConfigurationController) + .also { it.addCallback(listener) } + } + + private companion object { + private const val SMALLEST_WIDTH_DPI_TABLET = 800 + private const val SMALLEST_WIDTH_DPI_PHONE = 400 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java index 0badd861787d..1bc4719c70b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java @@ -147,6 +147,18 @@ public class ColorSchemeTest extends SysuiTestCase { } @Test + public void testMonochromatic() { + int colorInt = 0xffB3588A; // H350 C50 T50 + ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */, + Style.MONOCHROMATIC /* style */); + int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2); + Assert.assertTrue( + Color.red(neutralMid) == Color.green(neutralMid) + && Color.green(neutralMid) == Color.blue(neutralMid) + ); + } + + @Test @SuppressWarnings("ResultOfMethodCallIgnored") public void testToString() { new ColorScheme(Color.TRANSPARENT, false /* darkTheme */).toString(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java index 80731037481a..6c03730e056e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java @@ -39,7 +39,6 @@ import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.keyguard.KeyguardViewController; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.AccessibilityButtonModeObserver; import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver; @@ -49,6 +48,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.CentralSurfaces; +import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.Before; import org.junit.Test; @@ -113,7 +113,7 @@ public class NavBarHelperTest extends SysuiTestCase { mNavBarHelper = new NavBarHelper(mContext, mAccessibilityManager, mAccessibilityButtonModeObserver, mAccessibilityButtonTargetObserver, mSystemActions, mOverviewProxyService, mAssistManagerLazy, - () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardViewController.class), + () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardStateController.class), mNavigationModeController, mUserTracker, mDumpManager); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java index b0cf0612b2d2..9bf27a26a682 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java @@ -19,6 +19,8 @@ package com.android.systemui.navigationbar; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -30,20 +32,25 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.content.res.Configuration; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.util.SparseArray; import androidx.test.filters.SmallTest; +import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.systemui.Dependency; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.model.SysUiState; import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.AutoHideController; import com.android.systemui.statusbar.phone.LightBarController; @@ -72,11 +79,14 @@ public class NavigationBarControllerTest extends SysuiTestCase { private NavigationBarController mNavigationBarController; private NavigationBar mDefaultNavBar; private NavigationBar mSecondaryNavBar; + private StaticMockitoSession mMockitoSession; @Mock private CommandQueue mCommandQueue; @Mock private NavigationBarComponent.Factory mNavigationBarFactory; + @Mock + TaskbarDelegate mTaskbarDelegate; @Before public void setUp() { @@ -90,7 +100,7 @@ public class NavigationBarControllerTest extends SysuiTestCase { Dependency.get(Dependency.MAIN_HANDLER), mock(ConfigurationController.class), mock(NavBarHelper.class), - mock(TaskbarDelegate.class), + mTaskbarDelegate, mNavigationBarFactory, mock(StatusBarKeyguardViewManager.class), mock(DumpManager.class), @@ -100,6 +110,7 @@ public class NavigationBarControllerTest extends SysuiTestCase { Optional.of(mock(BackAnimation.class)), mock(FeatureFlags.class))); initializeNavigationBars(); + mMockitoSession = mockitoSession().mockStatic(Utilities.class).startMocking(); } private void initializeNavigationBars() { @@ -120,6 +131,7 @@ public class NavigationBarControllerTest extends SysuiTestCase { mNavigationBarController = null; mDefaultNavBar = null; mSecondaryNavBar = null; + mMockitoSession.finishMocking(); } @Test @@ -268,4 +280,22 @@ public class NavigationBarControllerTest extends SysuiTestCase { public void test3ButtonTaskbarFlagDisabledNoRegister() { verify(mCommandQueue, never()).addCallback(any(TaskbarDelegate.class)); } + + + @Test + public void testConfigurationChange_taskbarNotInitialized() { + Configuration configuration = mContext.getResources().getConfiguration(); + when(Utilities.isTablet(any())).thenReturn(true); + mNavigationBarController.onConfigChanged(configuration); + verify(mTaskbarDelegate, never()).onConfigurationChanged(configuration); + } + + @Test + public void testConfigurationChange_taskbarInitialized() { + Configuration configuration = mContext.getResources().getConfiguration(); + when(Utilities.isTablet(any())).thenReturn(true); + when(mTaskbarDelegate.isInitialized()).thenReturn(true); + mNavigationBarController.onConfigChanged(configuration); + verify(mTaskbarDelegate, times(1)).onConfigurationChanged(configuration); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java index 51f0953771cb..6adce7a827b6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java @@ -72,7 +72,6 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; -import com.android.keyguard.KeyguardViewController; import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestableContext; import com.android.systemui.accessibility.AccessibilityButtonModeObserver; @@ -81,6 +80,7 @@ import com.android.systemui.accessibility.SystemActions; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; +import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.buttons.ButtonDispatcher; import com.android.systemui.navigationbar.buttons.DeadZone; @@ -194,10 +194,12 @@ public class NavigationBarTest extends SysuiTestCase { @Mock private CentralSurfaces mCentralSurfaces; @Mock - private KeyguardViewController mKeyguardViewController; + private KeyguardStateController mKeyguardStateController; @Mock private UserContextProvider mUserContextProvider; @Mock + private WakefulnessLifecycle mWakefulnessLifecycle; + @Mock private Resources mResources; private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake(); @@ -240,7 +242,7 @@ public class NavigationBarTest extends SysuiTestCase { mock(AccessibilityButtonTargetsObserver.class), mSystemActions, mOverviewProxyService, () -> mock(AssistManager.class), () -> Optional.of(mCentralSurfaces), - mKeyguardViewController, mock(NavigationModeController.class), + mKeyguardStateController, mock(NavigationModeController.class), mock(UserTracker.class), mock(DumpManager.class))); mNavigationBar = createNavBar(mContext); mExternalDisplayNavigationBar = createNavBar(mSysuiTestableContextExternal); @@ -380,7 +382,7 @@ public class NavigationBarTest extends SysuiTestCase { // Verify navbar didn't alter and showing back icon when the keyguard is showing without // requesting IME insets visible. - doReturn(true).when(mKeyguardViewController).isShowing(); + doReturn(true).when(mKeyguardStateController).isShowing(); mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE, BACK_DISPOSITION_DEFAULT, true); assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0); @@ -475,7 +477,8 @@ public class NavigationBarTest extends SysuiTestCase { mNavigationBarTransitions, mEdgeBackGestureHandler, Optional.of(mock(BackAnimation.class)), - mUserContextProvider)); + mUserContextProvider, + mWakefulnessLifecycle)); } private void processAllMessages() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java index 4e9b2325b899..c377c374148f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java @@ -46,6 +46,7 @@ import com.android.settingslib.fuelgauge.Estimate; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.power.PowerUI.WarningsUI; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -84,6 +85,7 @@ public class PowerUITest extends SysuiTestCase { private PowerUI mPowerUI; @Mock private EnhancedEstimates mEnhancedEstimates; @Mock private PowerManager mPowerManager; + @Mock private WakefulnessLifecycle mWakefulnessLifecycle; @Mock private IThermalService mThermalServiceMock; private IThermalEventListener mUsbThermalEventListener; private IThermalEventListener mSkinThermalEventListener; @@ -680,7 +682,7 @@ public class PowerUITest extends SysuiTestCase { private void createPowerUi() { mPowerUI = new PowerUI( mContext, mBroadcastDispatcher, mCommandQueue, mCentralSurfacesOptionalLazy, - mMockWarnings, mEnhancedEstimates, mPowerManager); + mMockWarnings, mEnhancedEstimates, mWakefulnessLifecycle, mPowerManager); mPowerUI.mThermalService = mThermalServiceMock; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt index e2c6ff996199..d6db62aa2f72 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt @@ -20,7 +20,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogcatEchoTracker import com.android.systemui.statusbar.DisableFlagsLogger import com.google.common.truth.Truth.assertThat import org.junit.Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java index 5d5918de3d9e..cd7a949443c9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java @@ -14,6 +14,9 @@ package com.android.systemui.qs; +import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; +import static com.android.systemui.statusbar.StatusBarState.SHADE; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertTrue; @@ -47,15 +50,15 @@ import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.Flags; -import com.android.systemui.media.MediaHost; +import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSFragmentComponent; import com.android.systemui.qs.external.TileServiceRequestController; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; @@ -93,7 +96,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { @Mock private QSPanel.QSTileLayout mQsTileLayout; @Mock private QSPanel.QSTileLayout mQQsTileLayout; @Mock private QSAnimator mQSAnimator; - @Mock private StatusBarStateController mStatusBarStateController; + @Mock private SysuiStatusBarStateController mStatusBarStateController; @Mock private QSSquishinessController mSquishinessController; private View mQsFragmentView; @@ -158,7 +161,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { public void transitionToFullShade_onKeyguard_noBouncer_setsAlphaUsingLinearInterpolator() { QSFragment fragment = resumeAndGetFragment(); - setStatusBarState(StatusBarState.KEYGUARD); + setStatusBarState(KEYGUARD); when(mQSPanelController.isBouncerInTransit()).thenReturn(false); boolean isTransitioningToFullShade = true; float transitionProgress = 0.5f; @@ -174,7 +177,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { public void transitionToFullShade_onKeyguard_bouncerActive_setsAlphaUsingBouncerInterpolator() { QSFragment fragment = resumeAndGetFragment(); - setStatusBarState(StatusBarState.KEYGUARD); + setStatusBarState(KEYGUARD); when(mQSPanelController.isBouncerInTransit()).thenReturn(true); boolean isTransitioningToFullShade = true; float transitionProgress = 0.5f; @@ -262,6 +265,27 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { } @Test + public void setQsExpansion_inSplitShade_whenTransitioningToKeyguard_setsAlphaBasedOnShadeTransitionProgress() { + QSFragment fragment = resumeAndGetFragment(); + enableSplitShade(); + when(mStatusBarStateController.getState()).thenReturn(SHADE); + when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD); + boolean isTransitioningToFullShade = false; + float transitionProgress = 0; + float squishinessFraction = 0f; + + fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress, + squishinessFraction); + + // trigger alpha refresh with non-zero expansion and fraction values + fragment.setQsExpansion(/* expansion= */ 1, /* panelExpansionFraction= */1, + /* proposedTranslation= */ 0, /* squishinessFraction= */ 1); + + // alpha should follow lockscreen to shade progress, not panel expansion fraction + assertThat(mQsFragmentView.getAlpha()).isEqualTo(transitionProgress); + } + + @Test public void getQsMinExpansionHeight_notInSplitShade_returnsHeaderHeight() { QSFragment fragment = resumeAndGetFragment(); disableSplitShade(); @@ -402,6 +426,19 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { verify(mQSPanelController).setListening(eq(true), anyBoolean()); } + @Test + public void passCorrectExpansionState_inSplitShade() { + QSFragment fragment = resumeAndGetFragment(); + enableSplitShade(); + clearInvocations(mQSPanelController); + + fragment.setExpanded(true); + verify(mQSPanelController).setExpanded(true); + + fragment.setExpanded(false); + verify(mQSPanelController).setExpanded(false); + } + @Override protected Fragment instantiate(Context context, String className, Bundle arguments) { MockitoAnnotations.initMocks(this); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java index 3cad2a005882..caf8321949ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java @@ -44,7 +44,7 @@ import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.MediaHost; +import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTileView; import com.android.systemui.qs.customize.QSCustomizerController; @@ -277,7 +277,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { // Then the layout changes assertThat(mController.shouldUseHorizontalLayout()).isTrue(); - verify(mHorizontalLayoutListener).run(); // not invoked + verify(mHorizontalLayoutListener).run(); // When it is rotated back to portrait mConfiguration.orientation = Configuration.ORIENTATION_PORTRAIT; @@ -300,4 +300,24 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { verify(mQSTile).refreshState(); verify(mOtherTile, never()).refreshState(); } + + @Test + public void configurationChange_onlySplitShadeConfigChanges_horizontalLayoutStatusUpdated() { + // Preconditions for horizontal layout + when(mMediaHost.getVisible()).thenReturn(true); + when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(false); + mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE; + mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener); + mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration); + assertThat(mController.shouldUseHorizontalLayout()).isTrue(); + reset(mHorizontalLayoutListener); + + // Only split shade status changes + when(mResources.getBoolean(R.bool.config_use_split_notification_shade)).thenReturn(true); + mController.mOnConfigurationChangedListener.onConfigurationChange(mConfiguration); + + // Horizontal layout is updated accordingly. + assertThat(mController.shouldUseHorizontalLayout()).isFalse(); + verify(mHorizontalLayoutListener).run(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt index 5eb9a9862340..3c867ab32725 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt @@ -6,8 +6,9 @@ import com.android.internal.logging.MetricsLogger import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.media.MediaHost -import com.android.systemui.media.MediaHostState +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.media.controls.ui.MediaHostState import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.customize.QSCustomizerController @@ -52,6 +53,7 @@ class QSPanelControllerTest : SysuiTestCase() { @Mock private lateinit var tile: QSTile @Mock private lateinit var otherTile: QSTile @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager + @Mock private lateinit var featureFlags: FeatureFlags private lateinit var controller: QSPanelController @@ -82,7 +84,8 @@ class QSPanelControllerTest : SysuiTestCase() { brightnessControllerFactory, brightnessSliderFactory, falsingManager, - statusBarKeyguardViewManager + statusBarKeyguardViewManager, + featureFlags ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt index 2db58be15665..7c930b196d68 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt @@ -159,6 +159,32 @@ class QSPanelTest : SysuiTestCase() { } @Test + fun testTopPadding_notCombinedHeaders() { + qsPanel.setUsingCombinedHeaders(false) + val padding = 10 + val paddingCombined = 100 + context.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_top, padding) + context.orCreateTestableResources.addOverride( + R.dimen.qs_panel_padding_top_combined_headers, paddingCombined) + + qsPanel.updatePadding() + assertThat(qsPanel.paddingTop).isEqualTo(padding) + } + + @Test + fun testTopPadding_combinedHeaders() { + qsPanel.setUsingCombinedHeaders(true) + val padding = 10 + val paddingCombined = 100 + context.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_top, padding) + context.orCreateTestableResources.addOverride( + R.dimen.qs_panel_padding_top_combined_headers, paddingCombined) + + qsPanel.updatePadding() + assertThat(qsPanel.paddingTop).isEqualTo(paddingCombined) + } + + @Test fun testSetSquishinessFraction_noCrash() { qsPanel.addView(qsPanel.mTileLayout as View, 0) qsPanel.addView(FrameLayout(context)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java index 1c686c66e31e..5e9c1aaad309 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java @@ -22,7 +22,6 @@ import static junit.framework.Assert.assertNotNull; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -52,6 +51,7 @@ import android.text.SpannableStringBuilder; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import android.widget.TextView; import com.android.systemui.R; @@ -97,6 +97,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { private static final int DEFAULT_ICON_ID = R.drawable.ic_info_outline; private ViewGroup mRootView; + private ViewGroup mSecurityFooterView; private TextView mFooterText; private TestableImageView mPrimaryFooterIcon; private QSSecurityFooter mFooter; @@ -121,21 +122,26 @@ public class QSSecurityFooterTest extends SysuiTestCase { Looper looper = mTestableLooper.getLooper(); Handler mainHandler = new Handler(looper); when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class)); - mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext) + mSecurityFooterView = (ViewGroup) new LayoutInflaterBuilder(mContext) .replace("ImageView", TestableImageView.class) .build().inflate(R.layout.quick_settings_security_footer, null, false); mFooterUtils = new QSSecurityFooterUtils(getContext(), getContext().getSystemService(DevicePolicyManager.class), mUserTracker, mainHandler, mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator); - mFooter = new QSSecurityFooter(mRootView, mainHandler, mSecurityController, looper, - mBroadcastDispatcher, mFooterUtils); - mFooterText = mRootView.findViewById(R.id.footer_text); - mPrimaryFooterIcon = mRootView.findViewById(R.id.primary_footer_icon); + mFooter = new QSSecurityFooter(mSecurityFooterView, mainHandler, mSecurityController, + looper, mBroadcastDispatcher, mFooterUtils); + mFooterText = mSecurityFooterView.findViewById(R.id.footer_text); + mPrimaryFooterIcon = mSecurityFooterView.findViewById(R.id.primary_footer_icon); when(mSecurityController.getDeviceOwnerComponentOnAnyUser()) .thenReturn(DEVICE_OWNER_COMPONENT); when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT)) .thenReturn(DEVICE_OWNER_TYPE_DEFAULT); + + // mSecurityFooterView must have a ViewGroup parent so that + // DialogLaunchAnimator.Controller.fromView() does not return null. + mRootView = new FrameLayout(mContext); + mRootView.addView(mSecurityFooterView); ViewUtils.attachView(mRootView); mFooter.init(); @@ -153,7 +159,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertEquals(View.GONE, mRootView.getVisibility()); + assertEquals(View.GONE, mSecurityFooterView.getVisibility()); } @Test @@ -165,7 +171,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { TestableLooper.get(this).processAllMessages(); assertEquals(mContext.getString(R.string.quick_settings_disclosure_management), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -181,7 +187,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { assertEquals(mContext.getString(R.string.quick_settings_disclosure_named_management, MANAGING_ORGANIZATION), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -200,7 +206,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { assertEquals(mContext.getString( R.string.quick_settings_financed_disclosure_named_management, MANAGING_ORGANIZATION), mFooterText.getText()); - assertEquals(View.VISIBLE, mRootView.getVisibility()); + assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility()); assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility()); assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource()); } @@ -217,7 +223,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertEquals(View.GONE, mRootView.getVisibility()); + assertEquals(View.GONE, mSecurityFooterView.getVisibility()); } @Test @@ -227,8 +233,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertFalse(mRootView.isClickable()); - assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertFalse(mSecurityFooterView.isClickable()); + assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -241,8 +247,9 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertTrue(mRootView.isClickable()); - assertEquals(View.VISIBLE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertTrue(mSecurityFooterView.isClickable()); + assertEquals(View.VISIBLE, + mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -254,8 +261,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { mFooter.refreshState(); TestableLooper.get(this).processAllMessages(); - assertFalse(mRootView.isClickable()); - assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility()); + assertFalse(mSecurityFooterView.isClickable()); + assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility()); } @Test @@ -734,11 +741,11 @@ public class QSSecurityFooterTest extends SysuiTestCase { @Test public void testDialogUsesDialogLauncher() { when(mSecurityController.isDeviceManaged()).thenReturn(true); - mFooter.onClick(mRootView); + mFooter.onClick(mSecurityFooterView); mTestableLooper.processAllMessages(); - verify(mDialogLaunchAnimator).showFromView(any(), eq(mRootView), any()); + verify(mDialogLaunchAnimator).show(any(), any()); } @Test @@ -775,7 +782,7 @@ public class QSSecurityFooterTest extends SysuiTestCase { ArgumentCaptor<AlertDialog> dialogCaptor = ArgumentCaptor.forClass(AlertDialog.class); mTestableLooper.processAllMessages(); - verify(mDialogLaunchAnimator).showFromView(dialogCaptor.capture(), any(), any()); + verify(mDialogLaunchAnimator).show(dialogCaptor.capture(), any()); AlertDialog dialog = dialogCaptor.getValue(); dialog.create(); @@ -817,8 +824,8 @@ public class QSSecurityFooterTest extends SysuiTestCase { verify(mBroadcastDispatcher).registerReceiverWithHandler(captor.capture(), any(), any(), any()); - // Pretend view is not visible temporarily - mRootView.onVisibilityAggregated(false); + // Pretend view is not attached anymore. + mRootView.removeView(mSecurityFooterView); captor.getValue().onReceive(mContext, new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG)); mTestableLooper.processAllMessages(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java index 3c58b6fc1354..c452872a527e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java @@ -52,6 +52,7 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.dump.DumpManager; +import com.android.systemui.dump.nano.SystemUIProtoDump; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.QSFactory; import com.android.systemui.plugins.qs.QSTile; @@ -114,8 +115,6 @@ public class QSTileHostTest extends SysuiTestCase { @Mock private DumpManager mDumpManager; @Mock - private QSTile.State mMockState; - @Mock private CentralSurfaces mCentralSurfaces; @Mock private QSLogger mQSLogger; @@ -195,7 +194,6 @@ public class QSTileHostTest extends SysuiTestCase { } private void setUpTileFactory() { - when(mMockState.toString()).thenReturn(MOCK_STATE_STRING); // Only create this kind of tiles when(mDefaultFactory.createTile(anyString())).thenAnswer( invocation -> { @@ -209,7 +207,11 @@ public class QSTileHostTest extends SysuiTestCase { } else if ("na".equals(spec)) { return new NotAvailableTile(mQSTileHost); } else if (CUSTOM_TILE_SPEC.equals(spec)) { - return mCustomTile; + QSTile tile = mCustomTile; + QSTile.State s = mock(QSTile.State.class); + s.spec = spec; + when(mCustomTile.getState()).thenReturn(s); + return tile; } else if ("internet".equals(spec) || "wifi".equals(spec) || "cell".equals(spec)) { @@ -647,7 +649,7 @@ public class QSTileHostTest extends SysuiTestCase { @Test public void testSetTileRemoved_removedBySystem() { int user = mUserTracker.getUserId(); - saveSetting("spec1" + CUSTOM_TILE_SPEC); + saveSetting("spec1," + CUSTOM_TILE_SPEC); // This will be done by TileServiceManager mQSTileHost.setTileAdded(CUSTOM_TILE, user, true); @@ -658,6 +660,27 @@ public class QSTileHostTest extends SysuiTestCase { .getBoolean(CUSTOM_TILE.flattenToString(), false)); } + @Test + public void testProtoDump_noTiles() { + SystemUIProtoDump proto = new SystemUIProtoDump(); + mQSTileHost.dumpProto(proto, new String[0]); + + assertEquals(0, proto.tiles.length); + } + + @Test + public void testTilesInOrder() { + saveSetting("spec1," + CUSTOM_TILE_SPEC); + + SystemUIProtoDump proto = new SystemUIProtoDump(); + mQSTileHost.dumpProto(proto, new String[0]); + + assertEquals(2, proto.tiles.length); + assertEquals("spec1", proto.tiles[0].getSpec()); + assertEquals(CUSTOM_TILE.getPackageName(), proto.tiles[1].getComponentName().packageName); + assertEquals(CUSTOM_TILE.getClassName(), proto.tiles[1].getComponentName().className); + } + private SharedPreferences getSharedPreferenecesForUser(int user) { return mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user); } @@ -707,12 +730,9 @@ public class QSTileHostTest extends SysuiTestCase { @Override public State newTileState() { - return mMockState; - } - - @Override - public State getState() { - return mMockState; + State s = mock(QSTile.State.class); + when(s.toString()).thenReturn(MOCK_STATE_STRING); + return s; } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt index 6af8e4904a1e..f53e997a331c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt @@ -23,8 +23,8 @@ import com.android.internal.logging.MetricsLogger import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.media.MediaHost -import com.android.systemui.media.MediaHostState +import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.media.controls.ui.MediaHostState import com.android.systemui.plugins.qs.QSTile import com.android.systemui.plugins.qs.QSTileView import com.android.systemui.qs.customize.QSCustomizerController diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt new file mode 100644 index 000000000000..629c663943db --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt @@ -0,0 +1,104 @@ +package com.android.systemui.qs + +import android.content.ComponentName +import android.service.quicksettings.Tile +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.external.CustomTile +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class TileStateToProtoTest : SysuiTestCase() { + + companion object { + private const val TEST_LABEL = "label" + private const val TEST_SUBTITLE = "subtitle" + private const val TEST_SPEC = "spec" + private val TEST_COMPONENT = ComponentName("test_pkg", "test_cls") + } + + @Test + fun platformTile_INACTIVE() { + val state = + QSTile.State().apply { + spec = TEST_SPEC + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_INACTIVE + } + val proto = state.toProto() + + assertThat(proto).isNotNull() + assertThat(proto?.hasSpec()).isTrue() + assertThat(proto?.spec).isEqualTo(TEST_SPEC) + assertThat(proto?.hasComponentName()).isFalse() + assertThat(proto?.label).isEqualTo(TEST_LABEL) + assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE) + assertThat(proto?.state).isEqualTo(Tile.STATE_INACTIVE) + assertThat(proto?.hasBooleanState()).isFalse() + } + + @Test + fun componentTile_UNAVAILABLE() { + val state = + QSTile.State().apply { + spec = CustomTile.toSpec(TEST_COMPONENT) + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_UNAVAILABLE + } + val proto = state.toProto() + + assertThat(proto).isNotNull() + assertThat(proto?.hasSpec()).isFalse() + assertThat(proto?.hasComponentName()).isTrue() + val componentName = proto?.componentName + assertThat(componentName?.packageName).isEqualTo(TEST_COMPONENT.packageName) + assertThat(componentName?.className).isEqualTo(TEST_COMPONENT.className) + assertThat(proto?.label).isEqualTo(TEST_LABEL) + assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE) + assertThat(proto?.state).isEqualTo(Tile.STATE_UNAVAILABLE) + assertThat(proto?.hasBooleanState()).isFalse() + } + + @Test + fun booleanState_ACTIVE() { + val state = + QSTile.BooleanState().apply { + spec = TEST_SPEC + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_ACTIVE + value = true + } + val proto = state.toProto() + + assertThat(proto).isNotNull() + assertThat(proto?.hasSpec()).isTrue() + assertThat(proto?.spec).isEqualTo(TEST_SPEC) + assertThat(proto?.hasComponentName()).isFalse() + assertThat(proto?.label).isEqualTo(TEST_LABEL) + assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE) + assertThat(proto?.state).isEqualTo(Tile.STATE_ACTIVE) + assertThat(proto?.hasBooleanState()).isTrue() + assertThat(proto?.booleanState).isTrue() + } + + @Test + fun noSpec_returnsNull() { + val state = + QSTile.State().apply { + label = TEST_LABEL + secondaryLabel = TEST_SUBTITLE + state = Tile.STATE_ACTIVE + } + val proto = state.toProto() + + assertThat(proto).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt index 3c258077c29d..2c2ddbb9b8c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt @@ -23,13 +23,13 @@ import android.os.UserHandle import android.provider.Settings import android.testing.AndroidTestingRunner import android.testing.TestableLooper -import android.view.View import androidx.test.filters.SmallTest import com.android.internal.logging.nano.MetricsProto import com.android.internal.logging.testing.FakeMetricsLogger import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.globalactions.GlobalActionsDialogLite @@ -70,13 +70,13 @@ class FooterActionsInteractorTest : SysuiTestCase() { val underTest = utils.footerActionsInteractor(qsSecurityFooterUtils = qsSecurityFooterUtils) val quickSettingsContext = mock<Context>() - underTest.showDeviceMonitoringDialog(quickSettingsContext) - verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null) - val view = mock<View>() - whenever(view.context).thenReturn(quickSettingsContext) - underTest.showDeviceMonitoringDialog(view) + underTest.showDeviceMonitoringDialog(quickSettingsContext, null) verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null) + + val expandable = mock<Expandable>() + underTest.showDeviceMonitoringDialog(quickSettingsContext, expandable) + verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, expandable) } @Test @@ -85,8 +85,8 @@ class FooterActionsInteractorTest : SysuiTestCase() { val underTest = utils.footerActionsInteractor(uiEventLogger = uiEventLogger) val globalActionsDialogLite = mock<GlobalActionsDialogLite>() - val view = mock<View>() - underTest.showPowerMenuDialog(globalActionsDialogLite, view) + val expandable = mock<Expandable>() + underTest.showPowerMenuDialog(globalActionsDialogLite, expandable) // Event is logged. val logs = uiEventLogger.logs @@ -99,7 +99,7 @@ class FooterActionsInteractorTest : SysuiTestCase() { .showOrHideDialog( /* keyguardShowing= */ false, /* isDeviceProvisioned= */ true, - view, + expandable, ) } @@ -167,11 +167,11 @@ class FooterActionsInteractorTest : SysuiTestCase() { userSwitchDialogController = userSwitchDialogController, ) - val view = mock<View>() - underTest.showUserSwitcher(view) + val expandable = mock<Expandable>() + underTest.showUserSwitcher(context, expandable) // Dialog is shown. - verify(userSwitchDialogController).showDialog(view) + verify(userSwitchDialogController).showDialog(context, expandable) } @Test @@ -184,12 +184,9 @@ class FooterActionsInteractorTest : SysuiTestCase() { activityStarter = activityStarter, ) - // The clicked view. The context is necessary because it's used to build the intent, that - // we check below. - val view = mock<View>() - whenever(view.context).thenReturn(context) - - underTest.showUserSwitcher(view) + // The clicked expandable. + val expandable = mock<Expandable>() + underTest.showUserSwitcher(context, expandable) // Dialog is shown. val intentCaptor = argumentCaptor<Intent>() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt index 2a4996f259dc..760bb9bec559 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt @@ -192,16 +192,6 @@ class FooterActionsViewModelTest : SysuiTestCase() { // UserManager change. assertThat(iconTint()).isNull() - // Trigger a user info change: there should now be a tint. - userInfoController.updateInfo { userAccount = "doe" } - assertThat(iconTint()) - .isEqualTo( - Utils.getColorAttrDefaultColor( - context, - android.R.attr.colorForeground, - ) - ) - // Make sure we don't tint the icon if it is a user image (and not the default image), even // in guest mode. userInfoController.updateInfo { this.picture = mock<UserIconDrawable>() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt index da52a9b1a3c2..3131f60893c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt @@ -30,9 +30,11 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.qs.QSUserSwitcherEvent +import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.user.data.source.UserRecord import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -40,20 +42,27 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest class UserDetailViewAdapterTest : SysuiTestCase() { - @Mock private lateinit var mUserSwitcherController: UserSwitcherController - @Mock private lateinit var mParent: ViewGroup - @Mock private lateinit var mUserDetailItemView: UserDetailItemView - @Mock private lateinit var mOtherView: View - @Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView - @Mock private lateinit var mLayoutInflater: LayoutInflater + @Mock + private lateinit var mUserSwitcherController: UserSwitcherController + @Mock + private lateinit var mParent: ViewGroup + @Mock + private lateinit var mUserDetailItemView: UserDetailItemView + @Mock + private lateinit var mOtherView: View + @Mock + private lateinit var mInflatedUserDetailItemView: UserDetailItemView + @Mock + private lateinit var mLayoutInflater: LayoutInflater private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake() private lateinit var adapter: UserDetailView.Adapter private lateinit var uiEventLogger: UiEventLoggerFake @@ -66,10 +75,12 @@ class UserDetailViewAdapterTest : SysuiTestCase() { mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, mLayoutInflater) `when`(mLayoutInflater.inflate(anyInt(), any(ViewGroup::class.java), anyBoolean())) - .thenReturn(mInflatedUserDetailItemView) + .thenReturn(mInflatedUserDetailItemView) `when`(mParent.context).thenReturn(mContext) - adapter = UserDetailView.Adapter(mContext, mUserSwitcherController, uiEventLogger, - falsingManagerFake) + adapter = UserDetailView.Adapter( + mContext, mUserSwitcherController, uiEventLogger, + falsingManagerFake + ) mPicture = UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.ic_avatar_user)) } @@ -139,6 +150,20 @@ class UserDetailViewAdapterTest : SysuiTestCase() { clickableTest(false, false, mUserDetailItemView, true) } + @Test + fun testManageUsersIsNotAvailable() { + assertNull(adapter.users.find { it.isManageUsers }) + } + + @Test + fun clickDismissDialog() { + val shower: UserSwitchDialogController.DialogShower = + mock(UserSwitchDialogController.DialogShower::class.java) + adapter.injectDialogShower(shower) + adapter.onUserListItemClicked(createUserRecord(current = true, guest = false), shower) + verify(shower).dismiss() + } + private fun createUserRecord(current: Boolean, guest: Boolean) = UserRecord( UserInfo(0 /* id */, "name", 0 /* flags */), diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt index 9d908fdfb976..0a34810f4d3f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt @@ -20,12 +20,12 @@ import android.content.DialogInterface import android.content.Intent import android.provider.Settings import android.testing.AndroidTestingRunner -import android.view.View import android.widget.Button import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.animation.Expandable import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PseudoGridView @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -63,7 +64,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { @Mock private lateinit var userDetailViewAdapter: UserDetailView.Adapter @Mock - private lateinit var launchView: View + private lateinit var launchExpandable: Expandable @Mock private lateinit var neutralButton: Button @Mock @@ -79,7 +80,6 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - `when`(launchView.context).thenReturn(mContext) `when`(dialog.context).thenReturn(mContext) controller = UserSwitchDialogController( @@ -94,32 +94,34 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { @Test fun showDialog_callsDialogShow() { - controller.showDialog(launchView) - verify(dialogLaunchAnimator).showFromView(eq(dialog), eq(launchView), any(), anyBoolean()) + val launchController = mock<DialogLaunchAnimator.Controller>() + `when`(launchExpandable.dialogLaunchController(any())).thenReturn(launchController) + controller.showDialog(context, launchExpandable) + verify(dialogLaunchAnimator).show(eq(dialog), eq(launchController), anyBoolean()) verify(uiEventLogger).log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN) } @Test fun dialog_showForAllUsers() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setShowForAllUsers(true) } @Test fun dialog_cancelOnTouchOutside() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setCanceledOnTouchOutside(true) } @Test fun adapterAndGridLinked() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(userDetailViewAdapter).linkToViewGroup(any<PseudoGridView>()) } @Test fun doneButtonLogsCorrectly() { - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog).setPositiveButton(anyInt(), capture(clickCaptor)) @@ -132,7 +134,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun clickSettingsButton_noFalsing_opensSettings() { `when`(falsingManager.isFalseTap(anyInt())).thenReturn(false) - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog) .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */) @@ -153,7 +155,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun clickSettingsButton_Falsing_notOpensSettings() { `when`(falsingManager.isFalseTap(anyInt())).thenReturn(true) - controller.showDialog(launchView) + controller.showDialog(context, launchExpandable) verify(dialog) .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt new file mode 100644 index 000000000000..b6a595b0077a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.Mockito.`when` as whenever + +@SmallTest +class ActionIntentCreatorTest : SysuiTestCase() { + + @Test + fun testCreateShareIntent() { + val uri = Uri.parse("content://fake") + val subject = "Example subject" + + val output = ActionIntentCreator.createShareIntent(uri, subject) + + assertThat(output.action).isEqualTo(Intent.ACTION_CHOOSER) + assertFlagsSet( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_GRANT_READ_URI_PERMISSION, + output.flags + ) + + val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) + assertThat(wrappedIntent?.action).isEqualTo(Intent.ACTION_SEND) + assertThat(wrappedIntent?.data).isEqualTo(uri) + assertThat(wrappedIntent?.type).isEqualTo("image/png") + assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isEqualTo(subject) + assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)) + .isEqualTo(uri) + } + + @Test + fun testCreateShareIntent_noSubject() { + val uri = Uri.parse("content://fake") + val output = ActionIntentCreator.createShareIntent(uri, null) + val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) + assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isNull() + } + + @Test + fun testCreateEditIntent() { + val uri = Uri.parse("content://fake") + val context = mock<Context>() + + val output = ActionIntentCreator.createEditIntent(uri, context) + + assertThat(output.action).isEqualTo(Intent.ACTION_EDIT) + assertThat(output.data).isEqualTo(uri) + assertThat(output.type).isEqualTo("image/png") + assertThat(output.component).isNull() + val expectedFlags = + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TASK + assertFlagsSet(expectedFlags, output.flags) + } + + @Test + fun testCreateEditIntent_withEditor() { + val uri = Uri.parse("content://fake") + val context = mock<Context>() + var component = ComponentName("com.android.foo", "com.android.foo.Something") + + whenever(context.getString(eq(R.string.config_screenshotEditor))) + .thenReturn(component.flattenToString()) + + val output = ActionIntentCreator.createEditIntent(uri, context) + + assertThat(output.component).isEqualTo(component) + } + + private fun assertFlagsSet(expected: Int, observed: Int) { + assertThat(observed and expected).isEqualTo(expected) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DraggableConstraintLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DraggableConstraintLayoutTest.java new file mode 100644 index 000000000000..c6ce51a28dd3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DraggableConstraintLayoutTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.MotionEvent; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class DraggableConstraintLayoutTest extends SysuiTestCase { + + @Mock + DraggableConstraintLayout.SwipeDismissCallbacks mCallbacks; + + private DraggableConstraintLayout mDraggableConstraintLayout; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mDraggableConstraintLayout = new DraggableConstraintLayout(mContext, null, 0); + } + + @Test + public void test_dismissDoesNotCallSwipeInitiated() { + mDraggableConstraintLayout.setCallbacks(mCallbacks); + + mDraggableConstraintLayout.dismiss(); + + verify(mCallbacks, never()).onSwipeDismissInitiated(any()); + } + + @Test + public void test_onTouchCallsOnInteraction() { + mDraggableConstraintLayout.setCallbacks(mCallbacks); + + mDraggableConstraintLayout.onInterceptTouchEvent( + MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0)); + + verify(mCallbacks).onInteraction(); + } + + @Test + public void test_callbacksNotSet() { + // just test that it doesn't throw an NPE + mDraggableConstraintLayout.onInterceptTouchEvent( + MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0)); + mDraggableConstraintLayout.onInterceptHoverEvent( + MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0, 0, 0)); + mDraggableConstraintLayout.dismiss(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java index 7d563399ee1c..4c44dacab1a2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java @@ -33,6 +33,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.os.Build; import android.os.ParcelFileDescriptor; +import android.os.Process; import android.provider.MediaStore; import android.testing.AndroidTestingRunner; @@ -97,7 +98,8 @@ public class ImageExporterTest extends SysuiTestCase { Bitmap original = createCheckerBitmap(10, 10, 10); ListenableFuture<ImageExporter.Result> direct = - exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME); + exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME, + Process.myUserHandle()); assertTrue("future should be done", direct.isDone()); assertFalse("future should not be canceled", direct.isCancelled()); ImageExporter.Result result = direct.get(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt index 073c23cec569..46a502acba16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt @@ -28,7 +28,6 @@ import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE -import android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler.bundleToHardwareBitmap import com.android.internal.util.ScreenshotHelper.ScreenshotRequest @@ -100,13 +99,14 @@ class RequestProcessorTest { policy.getDefaultDisplayId(), DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)) - val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD) + val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER) val processor = RequestProcessor(imageCapture, policy, flags, scope) val processedRequest = processor.process(request) // Request has topComponent added, but otherwise unchanged. assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN) + assertThat(processedRequest.source).isEqualTo(SCREENSHOT_OTHER) assertThat(processedRequest.topComponent).isEqualTo(component) } @@ -140,66 +140,6 @@ class RequestProcessorTest { } @Test - fun testSelectedRegionScreenshot_workProfilePolicyDisabled() = runBlocking { - flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false) - - val request = ScreenshotRequest(TAKE_SCREENSHOT_SELECTED_REGION, SCREENSHOT_KEY_CHORD) - val processor = RequestProcessor(imageCapture, policy, flags, scope) - - val processedRequest = processor.process(request) - - // No changes - assertThat(processedRequest).isEqualTo(request) - } - - @Test - fun testSelectedRegionScreenshot() = runBlocking { - flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true) - - val request = ScreenshotRequest(TAKE_SCREENSHOT_SELECTED_REGION, SCREENSHOT_KEY_CHORD) - val processor = RequestProcessor(imageCapture, policy, flags, scope) - - policy.setManagedProfile(USER_ID, false) - policy.setDisplayContentInfo(policy.getDefaultDisplayId(), - DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)) - - val processedRequest = processor.process(request) - - // Request has topComponent added, but otherwise unchanged. - assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN) - assertThat(processedRequest.topComponent).isEqualTo(component) - } - - @Test - fun testSelectedRegionScreenshot_managedProfile() = runBlocking { - flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true) - - // Provide a fake task bitmap when asked - val bitmap = makeHardwareBitmap(100, 100) - imageCapture.image = bitmap - - val request = ScreenshotRequest(TAKE_SCREENSHOT_SELECTED_REGION, SCREENSHOT_KEY_CHORD) - val processor = RequestProcessor(imageCapture, policy, flags, scope) - - // Indicate that the primary content belongs to a manged profile - policy.setManagedProfile(USER_ID, true) - policy.setDisplayContentInfo(policy.getDefaultDisplayId(), - DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)) - - val processedRequest = processor.process(request) - - // Expect a task snapshot is taken, overriding the selected region mode - assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_PROVIDED_IMAGE) - assertThat(bitmap.equalsHardwareBitmapBundle(processedRequest.bitmapBundle)).isTrue() - assertThat(processedRequest.boundsInScreen).isEqualTo(bounds) - assertThat(processedRequest.insets).isEqualTo(Insets.NONE) - assertThat(processedRequest.taskId).isEqualTo(TASK_ID) - assertThat(imageCapture.requestedTaskId).isEqualTo(TASK_ID) - assertThat(processedRequest.userId).isEqualTo(USER_ID) - assertThat(processedRequest.topComponent).isEqualTo(component) - } - - @Test fun testProvidedImageScreenshot_workProfilePolicyDisabled() = runBlocking { flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java index 69b7b88b0524..8c9404e336ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java @@ -180,7 +180,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { data.finisher = null; data.mActionsReadyListener = null; SaveImageInBackgroundTask task = - new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data, + new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data, ActionTransition::new, mSmartActionsProvider); Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(), @@ -208,7 +208,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { data.finisher = null; data.mActionsReadyListener = null; SaveImageInBackgroundTask task = - new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data, + new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data, ActionTransition::new, mSmartActionsProvider); Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(), @@ -236,7 +236,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { data.finisher = null; data.mActionsReadyListener = null; SaveImageInBackgroundTask task = - new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data, + new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data, ActionTransition::new, mSmartActionsProvider); Notification.Action deleteAction = task.createDeleteAction(mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt index 002ef2962b03..3a4da86b8045 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt @@ -33,7 +33,6 @@ import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE -import android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.util.ScreenshotHelper @@ -175,28 +174,6 @@ class TakeScreenshotServiceTest : SysuiTestCase() { } @Test - fun takeScreenshotPartial() { - val request = ScreenshotRequest( - TAKE_SCREENSHOT_SELECTED_REGION, - SCREENSHOT_KEY_CHORD, - /* topComponent = */ null) - - service.handleRequest(request, { /* onSaved */ }, callback) - - verify(controller, times(1)).takeScreenshotPartial( - /* topComponent = */ isNull(), - /* onSavedListener = */ any(), - /* requestCallback = */ any()) - - assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1) - val logEvent = eventLogger.get(0) - - assertEquals("Expected SCREENSHOT_REQUESTED UiEvent", - logEvent.eventId, SCREENSHOT_REQUESTED_KEY_CHORD.id) - assertEquals("Expected empty package name in UiEvent", "", eventLogger.get(0).packageName) - } - - @Test fun takeScreenshotProvidedImage() { val bounds = Rect(50, 50, 150, 150) val bitmap = makeHardwareBitmap(100, 100) diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt new file mode 100644 index 000000000000..1130bda9b881 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.settings.brightness + +import android.content.Intent +import android.graphics.Rect +import android.os.Handler +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.View +import android.view.ViewGroup +import androidx.test.filters.SmallTest +import androidx.test.rule.ActivityTestRule +import androidx.test.runner.intercepting.SingleActivityFactory +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.util.mockito.any +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class BrightnessDialogTest : SysuiTestCase() { + + @Mock private lateinit var brightnessSliderControllerFactory: BrightnessSliderController.Factory + @Mock private lateinit var backgroundHandler: Handler + @Mock private lateinit var brightnessSliderController: BrightnessSliderController + + @Rule + @JvmField + var activityRule = + ActivityTestRule( + object : SingleActivityFactory<TestDialog>(TestDialog::class.java) { + override fun create(intent: Intent?): TestDialog { + return TestDialog( + fakeBroadcastDispatcher, + brightnessSliderControllerFactory, + backgroundHandler + ) + } + }, + false, + false + ) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + `when`(brightnessSliderControllerFactory.create(any(), any())) + .thenReturn(brightnessSliderController) + `when`(brightnessSliderController.rootView).thenReturn(View(context)) + + activityRule.launchActivity(null) + } + + @After + fun tearDown() { + activityRule.finishActivity() + } + + @Test + fun testGestureExclusion() { + val frame = activityRule.activity.requireViewById<View>(R.id.brightness_mirror_container) + + val lp = frame.layoutParams as ViewGroup.MarginLayoutParams + val horizontalMargin = + activityRule.activity.resources.getDimensionPixelSize( + R.dimen.notification_side_paddings + ) + assertThat(lp.leftMargin).isEqualTo(horizontalMargin) + assertThat(lp.rightMargin).isEqualTo(horizontalMargin) + + assertThat(frame.systemGestureExclusionRects.size).isEqualTo(1) + val exclusion = frame.systemGestureExclusionRects[0] + assertThat(exclusion) + .isEqualTo(Rect(-horizontalMargin, 0, frame.width + horizontalMargin, frame.height)) + } + + class TestDialog( + broadcastDispatcher: BroadcastDispatcher, + brightnessSliderControllerFactory: BrightnessSliderController.Factory, + backgroundHandler: Handler + ) : BrightnessDialog(broadcastDispatcher, brightnessSliderControllerFactory, backgroundHandler) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt index c76d9e7a2b20..14a3bc147808 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt @@ -645,6 +645,65 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() { verify(animator).start() } + @Test + fun privacyChipParentVisibleFromStart() { + verify(privacyIconsController).onParentVisible() + } + + @Test + fun privacyChipParentVisibleAlways() { + controller.largeScreenActive = true + controller.largeScreenActive = false + controller.largeScreenActive = true + + verify(privacyIconsController, never()).onParentInvisible() + } + + @Test + fun clockPivotYInCenter() { + val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java) + verify(clock).addOnLayoutChangeListener(capture(captor)) + var height = 100 + val width = 50 + + clock.executeLayoutChange(0, 0, width, height, captor.value) + verify(clock).pivotY = height.toFloat() / 2 + + height = 150 + clock.executeLayoutChange(0, 0, width, height, captor.value) + verify(clock).pivotY = height.toFloat() / 2 + } + + private fun View.executeLayoutChange( + left: Int, + top: Int, + right: Int, + bottom: Int, + listener: View.OnLayoutChangeListener + ) { + val oldLeft = this.left + val oldTop = this.top + val oldRight = this.right + val oldBottom = this.bottom + whenever(this.left).thenReturn(left) + whenever(this.top).thenReturn(top) + whenever(this.right).thenReturn(right) + whenever(this.bottom).thenReturn(bottom) + whenever(this.height).thenReturn(bottom - top) + whenever(this.width).thenReturn(right - left) + listener.onLayoutChange( + this, + oldLeft, + oldTop, + oldRight, + oldBottom, + left, + top, + right, + bottom + ) + } + private fun createWindowInsets( topCutout: Rect? = Rect() ): WindowInsets { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index b40d5ac69d7b..02f28a235b95 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -18,8 +18,12 @@ package com.android.systemui.shade; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; +import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED; +import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN; +import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPENING; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED; @@ -30,11 +34,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -73,6 +79,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.internal.util.CollectionUtils; import com.android.internal.util.LatencyTracker; +import com.android.keyguard.FaceAuthApiRequestReason; import com.android.keyguard.KeyguardClockSwitch; import com.android.keyguard.KeyguardClockSwitchController; import com.android.keyguard.KeyguardStatusView; @@ -90,7 +97,6 @@ import com.android.systemui.biometrics.AuthController; import com.android.systemui.camera.CameraGestureHelper; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; -import com.android.systemui.controls.dagger.ControlsComponent; import com.android.systemui.doze.DozeLog; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; @@ -99,14 +105,15 @@ import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; -import com.android.systemui.media.KeyguardMediaController; -import com.android.systemui.media.MediaDataManager; -import com.android.systemui.media.MediaHierarchyManager; +import com.android.systemui.media.controls.pipeline.MediaDataManager; +import com.android.systemui.media.controls.ui.KeyguardMediaController; +import com.android.systemui.media.controls.ui.MediaHierarchyManager; import com.android.systemui.model.SysUiState; +import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; -import com.android.systemui.qrcodescanner.controller.QRCodeScannerController; +import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSFragment; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.shade.transition.ShadeTransitionController; @@ -153,7 +160,6 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager; import com.android.systemui.statusbar.phone.TapAgainViewController; import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -163,7 +169,6 @@ import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.time.SystemClock; -import com.android.systemui.wallet.controller.QuickAccessWalletController; import com.android.wm.shell.animation.FlingAnimationUtils; import org.junit.After; @@ -171,6 +176,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; @@ -249,17 +256,15 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private KeyguardMediaController mKeyguardMediaController; @Mock private PrivacyDotViewController mPrivacyDotViewController; @Mock private NavigationModeController mNavigationModeController; + @Mock private NavigationBarController mNavigationBarController; @Mock private LargeScreenShadeHeaderController mLargeScreenShadeHeaderController; @Mock private ContentResolver mContentResolver; @Mock private TapAgainViewController mTapAgainViewController; @Mock private KeyguardIndicationController mKeyguardIndicationController; @Mock private FragmentService mFragmentService; @Mock private FragmentHostManager mFragmentHostManager; - @Mock private QuickAccessWalletController mQuickAccessWalletController; - @Mock private QRCodeScannerController mQrCodeScannerController; @Mock private NotificationRemoteInputManager mNotificationRemoteInputManager; @Mock private RecordingController mRecordingController; - @Mock private ControlsComponent mControlsComponent; @Mock private LockscreenGestureLogger mLockscreenGestureLogger; @Mock private DumpManager mDumpManager; @Mock private InteractionJankMonitor mInteractionJankMonitor; @@ -280,6 +285,10 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private ViewTreeObserver mViewTreeObserver; @Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel; @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; + @Mock private MotionEvent mDownMotionEvent; + @Captor + private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener> + mEmptySpaceClickListenerCaptor; private NotificationPanelViewController.TouchHandler mTouchHandler; private ConfigurationController mConfigurationController; @@ -294,8 +303,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { private final FalsingManagerFake mFalsingManager = new FalsingManagerFake(); private final Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty(); private final DisplayMetrics mDisplayMetrics = new DisplayMetrics(); - private final PanelExpansionStateManager mPanelExpansionStateManager = - new PanelExpansionStateManager(); + private final ShadeExpansionStateManager mShadeExpansionStateManager = + new ShadeExpansionStateManager(); private FragmentHostManager.FragmentListener mFragmentListener; @Before @@ -371,6 +380,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { NotificationWakeUpCoordinator coordinator = new NotificationWakeUpCoordinator( + mDumpManager, mock(HeadsUpManagerPhone.class), new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager, mInteractionJankMonitor), @@ -386,6 +396,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mConfigurationController, mStatusBarStateController, mFalsingManager, + mShadeExpansionStateManager, mLockscreenShadeTransitionController, new FalsingCollectorFake(), mDumpManager); @@ -423,6 +434,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mView.getViewTreeObserver()).thenReturn(mViewTreeObserver); when(mView.getParent()).thenReturn(mViewParent); when(mQs.getHeader()).thenReturn(mQsHeader); + when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN); + when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); mMainHandler = new Handler(Looper.getMainLooper()); NotificationPanelViewController.PanelEventsEmitter panelEventsEmitter = @@ -466,13 +479,14 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mPrivacyDotViewController, mTapAgainViewController, mNavigationModeController, + mNavigationBarController, mFragmentService, mContentResolver, mRecordingController, mLargeScreenShadeHeaderController, mScreenOffAnimationController, mLockscreenGestureLogger, - mPanelExpansionStateManager, + mShadeExpansionStateManager, mNotificationRemoteInputManager, mSysUIUnfoldComponent, mInteractionJankMonitor, @@ -510,6 +524,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { .addCallback(mNotificationPanelViewController.mStatusBarStateListener); mNotificationPanelViewController .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class)); + verify(mNotificationStackScrollLayoutController) + .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture()); } @After @@ -714,6 +730,72 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { } @Test + public void test_pulsing_onTouchEvent_noTracking() { + // GIVEN device is pulsing + mNotificationPanelViewController.setPulsing(true); + + // WHEN touch DOWN & MOVE events received + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 200f /* y */, + 0 /* metaState */)); + + // THEN touch is NOT tracked (since the device is pulsing) + assertThat(mNotificationPanelViewController.isTracking()).isFalse(); + } + + @Test + public void test_onTouchEvent_startTracking() { + // GIVEN device is NOT pulsing + mNotificationPanelViewController.setPulsing(false); + + // WHEN touch DOWN & MOVE events received + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 200f /* y */, + 0 /* metaState */)); + + // THEN touch is tracked + assertThat(mNotificationPanelViewController.isTracking()).isTrue(); + } + + @Test + public void testOnTouchEvent_expansionResumesAfterBriefTouch() { + // Start shade collapse with swipe up + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + onTouchEvent(MotionEvent.obtain(0L /* downTime */, + 0L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + assertThat(mNotificationPanelViewController.getClosing()).isTrue(); + assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + + // simulate touch that does not exceed touch slop + onTouchEvent(MotionEvent.obtain(2L /* downTime */, + 2L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + mNotificationPanelViewController.setTouchSlopExceeded(false); + + onTouchEvent(MotionEvent.obtain(2L /* downTime */, + 2L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */, + 0 /* metaState */)); + + // fling should still be called after a touch that does not exceed touch slop + assertThat(mNotificationPanelViewController.getClosing()).isTrue(); + assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue(); + } + + @Test public void handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() { when(mCommandQueue.panelsEnabled()).thenReturn(false); @@ -1249,14 +1331,10 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() { enableSplitShade(/* enabled= */ true); - // set panel state to CLOSED - mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0, - /* expanded= */ false, /* tracking= */ false, /* dragDownPxAmount= */ 0); + mShadeExpansionStateManager.updateState(STATE_CLOSED); assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); - // change panel state to OPENING - mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0.5f, - /* expanded= */ true, /* tracking= */ true, /* dragDownPxAmount= */ 100); + mShadeExpansionStateManager.updateState(STATE_OPENING); assertThat(mNotificationPanelViewController.mQsExpandImmediate).isTrue(); } @@ -1264,15 +1342,23 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void testQsNotToBeImmediatelyExpandedWhenGoingFromUnlockedToLocked() { enableSplitShade(/* enabled= */ true); - // set panel state to CLOSED - mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0, - /* expanded= */ false, /* tracking= */ false, /* dragDownPxAmount= */ 0); + mShadeExpansionStateManager.updateState(STATE_CLOSED); - // go to lockscreen, which also sets fraction to 1.0f and makes shade "expanded" mStatusBarStateController.setState(KEYGUARD); - mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1, - /* expanded= */ true, /* tracking= */ true, /* dragDownPxAmount= */ 0); + // going to lockscreen would trigger STATE_OPENING + mShadeExpansionStateManager.updateState(STATE_OPENING); + + assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); + } + @Test + public void testQsImmediateResetsWhenPanelOpensOrCloses() { + mNotificationPanelViewController.mQsExpandImmediate = true; + mShadeExpansionStateManager.updateState(STATE_OPEN); + assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); + + mNotificationPanelViewController.mQsExpandImmediate = true; + mShadeExpansionStateManager.updateState(STATE_CLOSED); assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); } @@ -1293,7 +1379,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void testPanelClosedWhenClosingQsInSplitShade() { - mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1, + mShadeExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1, /* expanded= */ true, /* tracking= */ false, /* dragDownPxAmount= */ 0); enableSplitShade(/* enabled= */ true); mNotificationPanelViewController.setExpandedFraction(1f); @@ -1305,7 +1391,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void testPanelStaysOpenWhenClosingQs() { - mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1, + mShadeExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1, /* expanded= */ true, /* tracking= */ false, /* dragDownPxAmount= */ 0); mNotificationPanelViewController.setExpandedFraction(1f); @@ -1500,6 +1586,103 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { ); } + @Test + public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(false, false); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mUpdateMonitor).requestFaceAuth(true, + FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); + } + + @Test + public void onEmptySpaceClicked_notDozingAndFaceDetectionIsNotRunning_startsUnlockAnimation() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(false, false); + when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(false); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mNotificationStackScrollLayoutController).setUnlockHintRunning(true); + } + + @Test + public void onEmptySpaceClicked_notDozingAndFaceDetectionIsRunning_doesNotStartUnlockHint() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(false, false); + when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(true); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mNotificationStackScrollLayoutController, never()).setUnlockHintRunning(true); + } + + @Test + public void onEmptySpaceClicked_whenDozingAndOnKeyguard_doesNotRequestFaceAuth() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(KEYGUARD); + mNotificationPanelViewController.setDozing(true, false); + + // This sets the dozing state that is read when onMiddleClicked is eventually invoked. + mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString()); + } + + @Test + public void onEmptySpaceClicked_whenStatusBarShadeLocked_doesNotRequestFaceAuth() { + StatusBarStateController.StateListener statusBarStateListener = + mNotificationPanelViewController.mStatusBarStateListener; + statusBarStateListener.onStateChanged(SHADE_LOCKED); + + mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); + + verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString()); + + } + + /** + * When shade is flinging to close and this fling is not intercepted, + * {@link AmbientState#setIsClosing(boolean)} should be called before + * {@link NotificationStackScrollLayoutController#onExpansionStopped()} + * to ensure scrollY can be correctly set to be 0 + */ + @Test + public void onShadeFlingClosingEnd_mAmbientStateSetClose_thenOnExpansionStopped() { + // Given: Shade is expanded + mNotificationPanelViewController.notifyExpandingFinished(); + mNotificationPanelViewController.setIsClosing(false); + + // When: Shade flings to close not canceled + mNotificationPanelViewController.notifyExpandingStarted(); + mNotificationPanelViewController.setIsClosing(true); + mNotificationPanelViewController.onFlingEnd(false); + + // Then: AmbientState's mIsClosing should be set to false + // before mNotificationStackScrollLayoutController.onExpansionStopped() is called + // to ensure NotificationStackScrollLayout.resetScrollPosition() -> resetScrollPosition + // -> setOwnScrollY(0) can set scrollY to 0 when shade is closed + InOrder inOrder = inOrder(mAmbientState, mNotificationStackScrollLayoutController); + inOrder.verify(mAmbientState).setIsClosing(false); + inOrder.verify(mNotificationStackScrollLayoutController).onExpansionStopped(); + } + private static MotionEvent createMotionEvent(int x, int y, int action) { return MotionEvent.obtain( /* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt index 12ef036d89d0..bdafc7df33bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt @@ -66,6 +66,8 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { @Mock private lateinit var largeScreenShadeHeaderController: LargeScreenShadeHeaderController @Mock + private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager + @Mock private lateinit var featureFlags: FeatureFlags @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> @@ -96,6 +98,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { navigationModeController, overviewProxyService, largeScreenShadeHeaderController, + shadeExpansionStateManager, featureFlags, delayableExecutor ) @@ -380,6 +383,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { navigationModeController, overviewProxyService, largeScreenShadeHeaderController, + shadeExpansionStateManager, featureFlags, delayableExecutor ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index ad3d3d2958cb..95cf9d60b511 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -88,6 +88,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Mock private KeyguardStateController mKeyguardStateController; @Mock private ScreenOffAnimationController mScreenOffAnimationController; @Mock private AuthController mAuthController; + @Mock private ShadeExpansionStateManager mShadeExpansionStateManager; @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters; private NotificationShadeWindowControllerImpl mNotificationShadeWindowController; @@ -103,7 +104,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController, mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController, mColorExtractor, mDumpManager, mKeyguardStateController, - mScreenOffAnimationController, mAuthController) { + mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager) { @Override protected boolean isDebuggable() { return false; diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 481e4e9992d4..db7e017e379e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -41,7 +41,6 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.phone.PhoneStatusBarViewController import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager import com.android.systemui.statusbar.window.StatusBarWindowStateController import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -117,7 +116,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { notificationShadeDepthController, view, notificationPanelViewController, - PanelExpansionStateManager(), + ShadeExpansionStateManager(), stackScrollLayoutController, statusBarKeyguardViewManager, statusBarWindowStateController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java index 4a7dec912895..26a0770a7bba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java @@ -51,7 +51,6 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.tuner.TunerService; @@ -118,7 +117,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { mNotificationShadeDepthController, mView, mNotificationPanelViewController, - new PanelExpansionStateManager(), + new ShadeExpansionStateManager(), mNotificationStackScrollLayoutController, mStatusBarKeyguardViewManager, mStatusBarWindowStateController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt index c4f80492b084..a601b678c905 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeExpansionStateManagerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar.phone.panelstate +package com.android.systemui.shade import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -23,26 +23,30 @@ import org.junit.Before import org.junit.Test @SmallTest -class PanelExpansionStateManagerTest : SysuiTestCase() { +class ShadeExpansionStateManagerTest : SysuiTestCase() { - private lateinit var panelExpansionStateManager: PanelExpansionStateManager + private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager @Before fun setUp() { - panelExpansionStateManager = PanelExpansionStateManager() + shadeExpansionStateManager = ShadeExpansionStateManager() } @Test fun onPanelExpansionChanged_listenerNotified() { - val listener = TestPanelExpansionListener() - panelExpansionStateManager.addExpansionListener(listener) + val listener = TestShadeExpansionListener() + shadeExpansionStateManager.addExpansionListener(listener) val fraction = 0.6f val expanded = true val tracking = true val dragDownAmount = 1234f - panelExpansionStateManager.onPanelExpansionChanged( - fraction, expanded, tracking, dragDownAmount) + shadeExpansionStateManager.onPanelExpansionChanged( + fraction, + expanded, + tracking, + dragDownAmount + ) assertThat(listener.fraction).isEqualTo(fraction) assertThat(listener.expanded).isEqualTo(expanded) @@ -56,11 +60,15 @@ class PanelExpansionStateManagerTest : SysuiTestCase() { val expanded = true val tracking = true val dragDownAmount = 1234f - panelExpansionStateManager.onPanelExpansionChanged( - fraction, expanded, tracking, dragDownAmount) - val listener = TestPanelExpansionListener() + shadeExpansionStateManager.onPanelExpansionChanged( + fraction, + expanded, + tracking, + dragDownAmount + ) + val listener = TestShadeExpansionListener() - panelExpansionStateManager.addExpansionListener(listener) + shadeExpansionStateManager.addExpansionListener(listener) assertThat(listener.fraction).isEqualTo(fraction) assertThat(listener.expanded).isEqualTo(expanded) @@ -70,10 +78,10 @@ class PanelExpansionStateManagerTest : SysuiTestCase() { @Test fun updateState_listenerNotified() { - val listener = TestPanelStateListener() - panelExpansionStateManager.addStateListener(listener) + val listener = TestShadeStateListener() + shadeExpansionStateManager.addStateListener(listener) - panelExpansionStateManager.updateState(STATE_OPEN) + shadeExpansionStateManager.updateState(STATE_OPEN) assertThat(listener.state).isEqualTo(STATE_OPEN) } @@ -84,48 +92,64 @@ class PanelExpansionStateManagerTest : SysuiTestCase() { @Test fun onPEC_fractionLessThanOne_expandedTrue_trackingFalse_becomesStateOpening() { - val listener = TestPanelStateListener() - panelExpansionStateManager.addStateListener(listener) + val listener = TestShadeStateListener() + shadeExpansionStateManager.addStateListener(listener) - panelExpansionStateManager.onPanelExpansionChanged( - fraction = 0.5f, expanded = true, tracking = false, dragDownPxAmount = 0f) + shadeExpansionStateManager.onPanelExpansionChanged( + fraction = 0.5f, + expanded = true, + tracking = false, + dragDownPxAmount = 0f + ) assertThat(listener.state).isEqualTo(STATE_OPENING) } @Test fun onPEC_fractionLessThanOne_expandedTrue_trackingTrue_becomesStateOpening() { - val listener = TestPanelStateListener() - panelExpansionStateManager.addStateListener(listener) + val listener = TestShadeStateListener() + shadeExpansionStateManager.addStateListener(listener) - panelExpansionStateManager.onPanelExpansionChanged( - fraction = 0.5f, expanded = true, tracking = true, dragDownPxAmount = 0f) + shadeExpansionStateManager.onPanelExpansionChanged( + fraction = 0.5f, + expanded = true, + tracking = true, + dragDownPxAmount = 0f + ) assertThat(listener.state).isEqualTo(STATE_OPENING) } @Test fun onPEC_fractionLessThanOne_expandedFalse_trackingFalse_becomesStateClosed() { - val listener = TestPanelStateListener() - panelExpansionStateManager.addStateListener(listener) + val listener = TestShadeStateListener() + shadeExpansionStateManager.addStateListener(listener) // Start out on a different state - panelExpansionStateManager.updateState(STATE_OPEN) + shadeExpansionStateManager.updateState(STATE_OPEN) - panelExpansionStateManager.onPanelExpansionChanged( - fraction = 0.5f, expanded = false, tracking = false, dragDownPxAmount = 0f) + shadeExpansionStateManager.onPanelExpansionChanged( + fraction = 0.5f, + expanded = false, + tracking = false, + dragDownPxAmount = 0f + ) assertThat(listener.state).isEqualTo(STATE_CLOSED) } @Test fun onPEC_fractionLessThanOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() { - val listener = TestPanelStateListener() - panelExpansionStateManager.addStateListener(listener) + val listener = TestShadeStateListener() + shadeExpansionStateManager.addStateListener(listener) // Start out on a different state - panelExpansionStateManager.updateState(STATE_OPEN) + shadeExpansionStateManager.updateState(STATE_OPEN) - panelExpansionStateManager.onPanelExpansionChanged( - fraction = 0.5f, expanded = false, tracking = true, dragDownPxAmount = 0f) + shadeExpansionStateManager.onPanelExpansionChanged( + fraction = 0.5f, + expanded = false, + tracking = true, + dragDownPxAmount = 0f + ) assertThat(listener.state).isEqualTo(STATE_OPEN) } @@ -134,11 +158,15 @@ class PanelExpansionStateManagerTest : SysuiTestCase() { @Test fun onPEC_fractionOne_expandedTrue_trackingFalse_becomesStateOpeningThenStateOpen() { - val listener = TestPanelStateListener() - panelExpansionStateManager.addStateListener(listener) + val listener = TestShadeStateListener() + shadeExpansionStateManager.addStateListener(listener) - panelExpansionStateManager.onPanelExpansionChanged( - fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f) + shadeExpansionStateManager.onPanelExpansionChanged( + fraction = 1f, + expanded = true, + tracking = false, + dragDownPxAmount = 0f + ) assertThat(listener.previousState).isEqualTo(STATE_OPENING) assertThat(listener.state).isEqualTo(STATE_OPEN) @@ -146,50 +174,62 @@ class PanelExpansionStateManagerTest : SysuiTestCase() { @Test fun onPEC_fractionOne_expandedTrue_trackingTrue_becomesStateOpening() { - val listener = TestPanelStateListener() - panelExpansionStateManager.addStateListener(listener) + val listener = TestShadeStateListener() + shadeExpansionStateManager.addStateListener(listener) - panelExpansionStateManager.onPanelExpansionChanged( - fraction = 1f, expanded = true, tracking = true, dragDownPxAmount = 0f) + shadeExpansionStateManager.onPanelExpansionChanged( + fraction = 1f, + expanded = true, + tracking = true, + dragDownPxAmount = 0f + ) assertThat(listener.state).isEqualTo(STATE_OPENING) } @Test fun onPEC_fractionOne_expandedFalse_trackingFalse_becomesStateClosed() { - val listener = TestPanelStateListener() - panelExpansionStateManager.addStateListener(listener) + val listener = TestShadeStateListener() + shadeExpansionStateManager.addStateListener(listener) // Start out on a different state - panelExpansionStateManager.updateState(STATE_OPEN) + shadeExpansionStateManager.updateState(STATE_OPEN) - panelExpansionStateManager.onPanelExpansionChanged( - fraction = 1f, expanded = false, tracking = false, dragDownPxAmount = 0f) + shadeExpansionStateManager.onPanelExpansionChanged( + fraction = 1f, + expanded = false, + tracking = false, + dragDownPxAmount = 0f + ) assertThat(listener.state).isEqualTo(STATE_CLOSED) } @Test fun onPEC_fractionOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() { - val listener = TestPanelStateListener() - panelExpansionStateManager.addStateListener(listener) + val listener = TestShadeStateListener() + shadeExpansionStateManager.addStateListener(listener) // Start out on a different state - panelExpansionStateManager.updateState(STATE_OPEN) + shadeExpansionStateManager.updateState(STATE_OPEN) - panelExpansionStateManager.onPanelExpansionChanged( - fraction = 1f, expanded = false, tracking = true, dragDownPxAmount = 0f) + shadeExpansionStateManager.onPanelExpansionChanged( + fraction = 1f, + expanded = false, + tracking = true, + dragDownPxAmount = 0f + ) assertThat(listener.state).isEqualTo(STATE_OPEN) } /* ***** end [PanelExpansionStateManager.onPanelExpansionChanged] test cases ******/ - class TestPanelExpansionListener : PanelExpansionListener { + class TestShadeExpansionListener : ShadeExpansionListener { var fraction: Float = 0f var expanded: Boolean = false var tracking: Boolean = false var dragDownAmountPx: Float = 0f - override fun onPanelExpansionChanged(event: PanelExpansionChangeEvent) { + override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) { this.fraction = event.fraction this.expanded = event.expanded this.tracking = event.tracking @@ -197,7 +237,7 @@ class PanelExpansionStateManagerTest : SysuiTestCase() { } } - class TestPanelStateListener : PanelStateListener { + class TestShadeStateListener : ShadeStateListener { @PanelState var previousState: Int = STATE_CLOSED @PanelState var state: Int = STATE_CLOSED diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt index 6be76a6ac969..84f86561d073 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt @@ -5,13 +5,13 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.shade.STATE_CLOSED +import com.android.systemui.shade.STATE_OPEN +import com.android.systemui.shade.STATE_OPENING +import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.ScrimController -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent -import com.android.systemui.statusbar.phone.panelstate.STATE_CLOSED -import com.android.systemui.statusbar.phone.panelstate.STATE_OPEN -import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.HeadsUpManager import org.junit.Before @@ -148,7 +148,7 @@ class ScrimShadeTransitionControllerTest : SysuiTestCase() { companion object { val EXPANSION_EVENT = - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 0.5f, expanded = true, tracking = true, dragDownPxAmount = 10f) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt index b6f8326394fa..7cac854c0853 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt @@ -7,12 +7,12 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.qs.QS import com.android.systemui.shade.NotificationPanelViewController +import com.android.systemui.shade.STATE_OPENING +import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager -import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING import com.android.systemui.statusbar.policy.FakeConfigurationController import org.junit.Before import org.junit.Test @@ -40,7 +40,7 @@ class ShadeTransitionControllerTest : SysuiTestCase() { private lateinit var controller: ShadeTransitionController private val configurationController = FakeConfigurationController() - private val panelExpansionStateManager = PanelExpansionStateManager() + private val shadeExpansionStateManager = ShadeExpansionStateManager() @Before fun setUp() { @@ -49,7 +49,7 @@ class ShadeTransitionControllerTest : SysuiTestCase() { controller = ShadeTransitionController( configurationController, - panelExpansionStateManager, + shadeExpansionStateManager, dumpManager, context, splitShadeOverScrollerFactory = { _, _ -> splitShadeOverScroller }, @@ -166,7 +166,7 @@ class ShadeTransitionControllerTest : SysuiTestCase() { } private fun startPanelExpansion() { - panelExpansionStateManager.onPanelExpansionChanged( + shadeExpansionStateManager.onPanelExpansionChanged( DEFAULT_EXPANSION_EVENT.fraction, DEFAULT_EXPANSION_EVENT.expanded, DEFAULT_EXPANSION_EVENT.tracking, @@ -194,7 +194,7 @@ class ShadeTransitionControllerTest : SysuiTestCase() { companion object { private const val DEFAULT_DRAG_DOWN_AMOUNT = 123f private val DEFAULT_EXPANSION_EVENT = - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 0.5f, expanded = true, tracking = true, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt index aafd871f72f3..0e48b4835dfe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/SplitShadeOverScrollerTest.kt @@ -7,11 +7,11 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.qs.QS +import com.android.systemui.shade.STATE_CLOSED +import com.android.systemui.shade.STATE_OPEN +import com.android.systemui.shade.STATE_OPENING import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.phone.ScrimController -import com.android.systemui.statusbar.phone.panelstate.STATE_CLOSED -import com.android.systemui.statusbar.phone.panelstate.STATE_OPEN -import com.android.systemui.statusbar.phone.panelstate.STATE_OPENING import com.android.systemui.statusbar.policy.FakeConfigurationController import org.junit.Before import org.junit.Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt index a4a89a4c67f2..7a74b1229c5c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt @@ -27,8 +27,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @@ -42,8 +42,8 @@ class UnfoldConstantTranslateAnimatorTest : SysuiTestCase() { private val viewsIdToRegister = setOf( - ViewIdToTranslate(LEFT_VIEW_ID, Direction.LEFT), - ViewIdToTranslate(RIGHT_VIEW_ID, Direction.RIGHT)) + ViewIdToTranslate(START_VIEW_ID, Direction.START), + ViewIdToTranslate(END_VIEW_ID, Direction.END)) @Before fun setup() { @@ -66,41 +66,62 @@ class UnfoldConstantTranslateAnimatorTest : SysuiTestCase() { } @Test - fun onTransition_oneMovesLeft() { + fun onTransition_oneMovesStartWithLTR() { // GIVEN one view with a matching id val view = View(context) - whenever(parent.findViewById<View>(LEFT_VIEW_ID)).thenReturn(view) + whenever(parent.findViewById<View>(START_VIEW_ID)).thenReturn(view) - moveAndValidate(listOf(view to LEFT)) + moveAndValidate(listOf(view to START), View.LAYOUT_DIRECTION_LTR) } @Test - fun onTransition_oneMovesLeftAndOneMovesRightMultipleTimes() { + fun onTransition_oneMovesStartWithRTL() { + // GIVEN one view with a matching id + val view = View(context) + whenever(parent.findViewById<View>(START_VIEW_ID)).thenReturn(view) + + whenever(parent.getLayoutDirection()).thenReturn(View.LAYOUT_DIRECTION_RTL) + moveAndValidate(listOf(view to START), View.LAYOUT_DIRECTION_RTL) + } + + @Test + fun onTransition_oneMovesStartAndOneMovesEndMultipleTimes() { // GIVEN two views with a matching id val leftView = View(context) val rightView = View(context) - whenever(parent.findViewById<View>(LEFT_VIEW_ID)).thenReturn(leftView) - whenever(parent.findViewById<View>(RIGHT_VIEW_ID)).thenReturn(rightView) + whenever(parent.findViewById<View>(START_VIEW_ID)).thenReturn(leftView) + whenever(parent.findViewById<View>(END_VIEW_ID)).thenReturn(rightView) - moveAndValidate(listOf(leftView to LEFT, rightView to RIGHT)) - moveAndValidate(listOf(leftView to LEFT, rightView to RIGHT)) + moveAndValidate(listOf(leftView to START, rightView to END), View.LAYOUT_DIRECTION_LTR) + moveAndValidate(listOf(leftView to START, rightView to END), View.LAYOUT_DIRECTION_LTR) } - private fun moveAndValidate(list: List<Pair<View, Int>>) { + private fun moveAndValidate(list: List<Pair<View, Int>>, layoutDirection: Int) { // Compare values as ints because -0f != 0f // WHEN the transition starts progressProvider.onTransitionStarted() progressProvider.onTransitionProgress(0f) + val rtlMultiplier = if (layoutDirection == View.LAYOUT_DIRECTION_LTR) { + 1 + } else { + -1 + } list.forEach { (view, direction) -> - assertEquals((-MAX_TRANSLATION * direction).toInt(), view.translationX.toInt()) + assertEquals( + (-MAX_TRANSLATION * direction * rtlMultiplier).toInt(), + view.translationX.toInt() + ) } // WHEN the transition progresses, translation is updated progressProvider.onTransitionProgress(.5f) list.forEach { (view, direction) -> - assertEquals((-MAX_TRANSLATION / 2f * direction).toInt(), view.translationX.toInt()) + assertEquals( + (-MAX_TRANSLATION / 2f * direction * rtlMultiplier).toInt(), + view.translationX.toInt() + ) } // WHEN the transition ends, translation is completed @@ -110,12 +131,12 @@ class UnfoldConstantTranslateAnimatorTest : SysuiTestCase() { } companion object { - private val LEFT = Direction.LEFT.multiplier.toInt() - private val RIGHT = Direction.RIGHT.multiplier.toInt() + private val START = Direction.START.multiplier.toInt() + private val END = Direction.END.multiplier.toInt() private const val MAX_TRANSLATION = 42f - private const val LEFT_VIEW_ID = 1 - private const val RIGHT_VIEW_ID = 2 + private const val START_VIEW_ID = 1 + private const val END_VIEW_ID = 2 } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt index eb34561d15a0..cc45cf88fa18 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.animation.TextAnimator +import com.android.systemui.util.mockito.any import org.junit.Before import org.junit.Rule import org.junit.Test @@ -55,7 +56,7 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.animateAppearOnLockscreen() clockView.measure(50, 50) - verify(mockTextAnimator).glyphFilter = null + verify(mockTextAnimator).glyphFilter = any() verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, false, 350L, null, 0L, null) verifyNoMoreInteractions(mockTextAnimator) } @@ -66,7 +67,7 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.measure(50, 50) clockView.animateAppearOnLockscreen() - verify(mockTextAnimator, times(2)).glyphFilter = null + verify(mockTextAnimator, times(2)).glyphFilter = any() verify(mockTextAnimator).setTextStyle(100, -1.0f, 200, false, 0L, null, 0L, null) verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, true, 350L, null, 0L, null) verifyNoMoreInteractions(mockTextAnimator) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt index 8be138a3b2be..70cbc64c79f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt @@ -19,10 +19,11 @@ import android.content.ContentResolver import android.content.Context import android.graphics.drawable.Drawable import android.os.Handler +import android.os.UserHandle import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.plugins.Clock +import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockId import com.android.systemui.plugins.ClockMetadata import com.android.systemui.plugins.ClockProviderPlugin @@ -48,8 +49,8 @@ class ClockRegistryTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() @Mock private lateinit var mockContext: Context @Mock private lateinit var mockPluginManager: PluginManager - @Mock private lateinit var mockClock: Clock - @Mock private lateinit var mockDefaultClock: Clock + @Mock private lateinit var mockClock: ClockController + @Mock private lateinit var mockDefaultClock: ClockController @Mock private lateinit var mockThumbnail: Drawable @Mock private lateinit var mockHandler: Handler @Mock private lateinit var mockContentResolver: ContentResolver @@ -60,7 +61,7 @@ class ClockRegistryTest : SysuiTestCase() { private var settingValue: String = "" companion object { - private fun failFactory(): Clock { + private fun failFactory(): ClockController { fail("Unexpected call to createClock") return null!! } @@ -73,17 +74,17 @@ class ClockRegistryTest : SysuiTestCase() { private class FakeClockPlugin : ClockProviderPlugin { private val metadata = mutableListOf<ClockMetadata>() - private val createCallbacks = mutableMapOf<ClockId, () -> Clock>() + private val createCallbacks = mutableMapOf<ClockId, () -> ClockController>() private val thumbnailCallbacks = mutableMapOf<ClockId, () -> Drawable?>() override fun getClocks() = metadata - override fun createClock(id: ClockId): Clock = createCallbacks[id]!!() + override fun createClock(id: ClockId): ClockController = createCallbacks[id]!!() override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!() fun addClock( id: ClockId, name: String, - create: () -> Clock = ::failFactory, + create: () -> ClockController = ::failFactory, getThumbnail: () -> Drawable? = ::failThumbnail ): FakeClockPlugin { metadata.add(ClockMetadata(id, name)) @@ -104,13 +105,14 @@ class ClockRegistryTest : SysuiTestCase() { mockContext, mockPluginManager, mockHandler, - fakeDefaultProvider + isEnabled = true, + userHandle = UserHandle.USER_ALL, + defaultClockProvider = fakeDefaultProvider ) { override var currentClockId: ClockId get() = settingValue set(value) { settingValue = value } } - registry.isEnabled = true verify(mockPluginManager) .addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java), eq(true)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt index 2b4a109282ce..539a54b731ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.shared.clocks import android.content.res.Resources +import android.graphics.Color import android.graphics.drawable.Drawable import android.testing.AndroidTestingRunner import android.util.TypedValue @@ -25,7 +26,7 @@ import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.shared.clocks.DefaultClock.Companion.DOZE_COLOR +import com.android.systemui.shared.clocks.DefaultClockController.Companion.DOZE_COLOR import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock @@ -88,17 +89,20 @@ class DefaultClockProviderTest : SysuiTestCase() { // Default clock provider must always provide the default clock val clock = provider.createClock(DEFAULT_CLOCK_ID) assertNotNull(clock) - assertEquals(clock.smallClock, mockSmallClockView) - assertEquals(clock.largeClock, mockLargeClockView) + assertEquals(mockSmallClockView, clock.smallClock.view) + assertEquals(mockLargeClockView, clock.largeClock.view) } @Test fun defaultClock_initialize() { val clock = provider.createClock(DEFAULT_CLOCK_ID) + verify(mockSmallClockView).setColors(Color.MAGENTA, Color.MAGENTA) + verify(mockLargeClockView).setColors(Color.MAGENTA, Color.MAGENTA) + clock.initialize(resources, 0f, 0f) - verify(mockSmallClockView, times(2)).setColors(eq(DOZE_COLOR), anyInt()) - verify(mockLargeClockView, times(2)).setColors(eq(DOZE_COLOR), anyInt()) + verify(mockSmallClockView).setColors(eq(DOZE_COLOR), anyInt()) + verify(mockLargeClockView).setColors(eq(DOZE_COLOR), anyInt()) verify(mockSmallClockView).onTimeZoneChanged(notNull()) verify(mockLargeClockView).onTimeZoneChanged(notNull()) verify(mockSmallClockView).refreshTime() @@ -147,10 +151,14 @@ class DefaultClockProviderTest : SysuiTestCase() { @Test fun defaultClock_events_onColorPaletteChanged() { val clock = provider.createClock(DEFAULT_CLOCK_ID) - clock.events.onColorPaletteChanged(resources, true, true) - verify(mockSmallClockView, times(2)).setColors(eq(DOZE_COLOR), anyInt()) - verify(mockLargeClockView, times(2)).setColors(eq(DOZE_COLOR), anyInt()) + verify(mockSmallClockView).setColors(Color.MAGENTA, Color.MAGENTA) + verify(mockLargeClockView).setColors(Color.MAGENTA, Color.MAGENTA) + + clock.events.onColorPaletteChanged(resources) + + verify(mockSmallClockView).setColors(eq(DOZE_COLOR), anyInt()) + verify(mockLargeClockView).setColors(eq(DOZE_COLOR), anyInt()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/navigationbar/RegionSamplingHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/navigationbar/RegionSamplingHelperTest.kt index 8bc438bce5cc..5fc09c7d5563 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/navigationbar/RegionSamplingHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/navigationbar/RegionSamplingHelperTest.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.shared.navigationbar + import android.graphics.Rect import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -24,15 +25,23 @@ import android.view.ViewRootImpl import androidx.concurrent.futures.DirectExecutor import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock -import org.mockito.Mockito.* -import org.mockito.junit.MockitoJUnit +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit @RunWith(AndroidTestingRunner::class) @SmallTest @@ -99,4 +108,39 @@ class RegionSamplingHelperTest : SysuiTestCase() { regionSamplingHelper.stopAndDestroy() verify(compositionListener).unregister(any()) } -}
\ No newline at end of file + + @Test + fun testCompositionSamplingListener_has_nonEmptyRect() { + // simulate race condition + val fakeExecutor = FakeExecutor(FakeSystemClock()) // pass in as backgroundExecutor + val fakeSamplingCallback = mock(RegionSamplingHelper.SamplingCallback::class.java) + + whenever(fakeSamplingCallback.isSamplingEnabled).thenReturn(true) + whenever(wrappedSurfaceControl.isValid).thenReturn(true) + + regionSamplingHelper = object : RegionSamplingHelper(sampledView, fakeSamplingCallback, + DirectExecutor.INSTANCE, fakeExecutor, compositionListener) { + override fun wrap(stopLayerControl: SurfaceControl?): SurfaceControl { + return wrappedSurfaceControl + } + } + regionSamplingHelper.setWindowVisible(true) + regionSamplingHelper.start(Rect(0, 0, 100, 100)) + + // make sure background task is enqueued + assertThat(fakeExecutor.numPending()).isEqualTo(1) + + // make sure regionSamplingHelper will have empty Rect + whenever(fakeSamplingCallback.getSampledRegion(any())).thenReturn(Rect(0, 0, 0, 0)) + regionSamplingHelper.onLayoutChange(sampledView, 0, 0, 0, 0, 0, 0, 0, 0) + + // resume running of background thread + fakeExecutor.runAllReady() + + // grab Rect passed into compositionSamplingListener and make sure it's not empty + val argumentGrabber = argumentCaptor<Rect>() + verify(compositionListener).register(any(), anyInt(), eq(wrappedSurfaceControl), + argumentGrabber.capture()) + assertThat(argumentGrabber.value.isEmpty).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java index cf5fa87272c7..4478039912c8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java @@ -16,20 +16,21 @@ package com.android.systemui.shared.system; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.view.RemoteAnimationTarget.MODE_CHANGING; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_STANDARD; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CHANGING; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -40,6 +41,7 @@ import android.app.ActivityManager; import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -68,17 +70,20 @@ public class RemoteTransitionTest extends SysuiTestCase { TransitionInfo combined = new TransitionInfoBuilder(TRANSIT_CLOSE) .addChange(TRANSIT_CHANGE, FLAG_SHOW_WALLPAPER, createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_STANDARD)) + // Embedded TaskFragment should be excluded when animated with Task. + .addChange(TRANSIT_CLOSE, FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY, null /* taskInfo */) .addChange(TRANSIT_CLOSE, 0 /* flags */, createTaskInfo(2 /* taskId */, ACTIVITY_TYPE_STANDARD)) .addChange(TRANSIT_OPEN, FLAG_IS_WALLPAPER, null /* taskInfo */) - .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */).build(); + .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */) + .build(); // Check apps extraction - RemoteAnimationTargetCompat[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined, + RemoteAnimationTarget[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined, mock(SurfaceControl.Transaction.class), null /* leashes */); assertEquals(2, wrapped.length); int changeLayer = -1; int closeLayer = -1; - for (RemoteAnimationTargetCompat t : wrapped) { + for (RemoteAnimationTarget t : wrapped) { if (t.mode == MODE_CHANGING) { changeLayer = t.prefixOrderIndex; } else if (t.mode == MODE_CLOSING) { @@ -91,14 +96,14 @@ public class RemoteTransitionTest extends SysuiTestCase { assertTrue(closeLayer < changeLayer); // Check wallpaper extraction - RemoteAnimationTargetCompat[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined, + RemoteAnimationTarget[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined, true /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */); assertEquals(1, wallps.length); assertTrue(wallps[0].prefixOrderIndex < closeLayer); assertEquals(MODE_OPENING, wallps[0].mode); // Check non-apps extraction - RemoteAnimationTargetCompat[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined, + RemoteAnimationTarget[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined, false /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */); assertEquals(1, nonApps.length); assertTrue(nonApps[0].prefixOrderIndex < closeLayer); @@ -115,9 +120,9 @@ public class RemoteTransitionTest extends SysuiTestCase { change.setTaskInfo(createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_HOME)); change.setEndAbsBounds(endBounds); change.setEndRelOffset(0, 0); - final RemoteAnimationTargetCompat wrapped = new RemoteAnimationTargetCompat(change, - 0 /* order */, tinfo, mock(SurfaceControl.Transaction.class)); - assertEquals(ACTIVITY_TYPE_HOME, wrapped.activityType); + RemoteAnimationTarget wrapped = RemoteAnimationTargetCompat.newTarget( + change, 0 /* order */, tinfo, mock(SurfaceControl.Transaction.class), null); + assertEquals(ACTIVITY_TYPE_HOME, wrapped.windowConfiguration.getActivityType()); assertEquals(new Rect(0, 0, 100, 140), wrapped.localBounds); assertEquals(endBounds, wrapped.screenSpaceBounds); assertTrue(wrapped.isTranslucent); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt index f9e279eb5871..b761647e24e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt @@ -3,26 +3,25 @@ package com.android.systemui.shared.system import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat +import java.lang.Thread.UncaughtExceptionHandler import org.junit.Assert.assertThrows import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.mockito.Mock -import org.mockito.Mockito.only import org.mockito.Mockito.any +import org.mockito.Mockito.only import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import java.lang.Thread.UncaughtExceptionHandler @SmallTest class UncaughtExceptionPreHandlerTest : SysuiTestCase() { private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager - @Mock - private lateinit var mockHandler: UncaughtExceptionHandler + @Mock private lateinit var mockHandler: UncaughtExceptionHandler - @Mock - private lateinit var mockHandler2: UncaughtExceptionHandler + @Mock private lateinit var mockHandler2: UncaughtExceptionHandler @Before fun setUp() { @@ -55,6 +54,7 @@ class UncaughtExceptionPreHandlerTest : SysuiTestCase() { } @Test + @Ignore fun registerHandler_toleratesHandlersThatThrow() { `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException()) preHandlerManager.registerHandler(mockHandler2) @@ -71,4 +71,4 @@ class UncaughtExceptionPreHandlerTest : SysuiTestCase() { preHandlerManager.registerHandler(mockHandler) } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt index 8cb530c355bd..5fc0ffe42f55 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt @@ -4,7 +4,7 @@ import android.testing.AndroidTestingRunner import android.util.DisplayMetrics import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.phone.LSShadeTransitionLogger import com.android.systemui.statusbar.phone.LockscreenGestureLogger diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index 8643e86acef2..3d11ced6207d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -10,7 +10,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.WakefulnessLifecycle -import com.android.systemui.media.MediaHierarchyManager +import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QS import com.android.systemui.shade.NotificationPanelViewController diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java deleted file mode 100644 index 5432a74035b7..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar; - -import static org.junit.Assert.assertFalse; - -import android.os.Handler; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; - -import androidx.test.filters.SmallTest; - -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.Dependency; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.shade.ShadeController; -import com.android.systemui.statusbar.notification.logging.NotificationLogger; -import com.android.systemui.statusbar.notification.row.NotificationGutsManager; -import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener; -import com.android.systemui.statusbar.notification.stack.NotificationListContainer; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Verifies that particular sets of dependencies don't have dependencies on others. For example, - * code managing notifications shouldn't directly depend on CentralSurfaces, since there are - * platforms which want to manage notifications, but don't use CentralSurfaces. - */ -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -public class NonPhoneDependencyTest extends SysuiTestCase { - @Mock private NotificationPresenter mPresenter; - @Mock private NotificationListContainer mListContainer; - @Mock private RemoteInputController.Delegate mDelegate; - @Mock private NotificationRemoteInputManager.Callback mRemoteInputManagerCallback; - @Mock private OnSettingsClickListener mOnSettingsClickListener; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mDependency.injectMockDependency(KeyguardUpdateMonitor.class); - mDependency.injectTestDependency(Dependency.MAIN_HANDLER, - new Handler(TestableLooper.get(this).getLooper())); - } - - @Ignore("Causes binder calls which fail") - @Test - public void testNotificationManagementCodeHasNoDependencyOnStatusBarWindowManager() { - mDependency.injectMockDependency(ShadeController.class); - NotificationGutsManager gutsManager = Dependency.get(NotificationGutsManager.class); - NotificationLogger notificationLogger = Dependency.get(NotificationLogger.class); - NotificationMediaManager mediaManager = Dependency.get(NotificationMediaManager.class); - NotificationRemoteInputManager remoteInputManager = - Dependency.get(NotificationRemoteInputManager.class); - NotificationLockscreenUserManager lockscreenUserManager = - Dependency.get(NotificationLockscreenUserManager.class); - gutsManager.setUpWithPresenter(mPresenter, mListContainer, - mOnSettingsClickListener); - notificationLogger.setUpWithContainer(mListContainer); - mediaManager.setUpWithPresenter(mPresenter); - remoteInputManager.setUpWithCallback(mRemoteInputManagerCallback, - mDelegate); - lockscreenUserManager.setUpWithPresenter(mPresenter); - - TestableLooper.get(this).processAllMessages(); - assertFalse(mDependency.hasInstantiatedDependency(NotificationShadeWindowController.class)); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 853d1df4b9bc..bdafa4893c9e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -52,6 +52,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; @@ -88,6 +89,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { @Mock private NotificationClickNotifier mClickNotifier; @Mock + private OverviewProxyService mOverviewProxyService; + @Mock private KeyguardManager mKeyguardManager; @Mock private DeviceProvisionedController mDeviceProvisionedController; @@ -344,6 +347,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { (() -> mVisibilityProvider), (() -> mNotifCollection), mClickNotifier, + (() -> mOverviewProxyService), NotificationLockscreenUserManagerTest.this.mKeyguardManager, mStatusBarStateController, Handler.createAsync(Looper.myLooper()), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index 6446fb5d8c81..77b1e3740907 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -28,10 +28,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.ScrimController -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.WallpaperController @@ -137,7 +137,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun onPanelExpansionChanged_apliesBlur_ifShade() { notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)) verify(shadeAnimation).animateTo(eq(maxBlur)) } @@ -145,7 +145,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun onPanelExpansionChanged_animatesBlurIn_ifShade() { notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 0.01f, expanded = false, tracking = false, dragDownPxAmount = 0f)) verify(shadeAnimation).animateTo(eq(maxBlur)) } @@ -155,7 +155,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { onPanelExpansionChanged_animatesBlurIn_ifShade() clearInvocations(shadeAnimation) notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 0f, expanded = false, tracking = false, dragDownPxAmount = 0f)) verify(shadeAnimation).animateTo(eq(0)) } @@ -163,7 +163,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun onPanelExpansionChanged_animatesBlurOut_ifFlick() { val event = - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f) onPanelExpansionChanged_apliesBlur_ifShade() clearInvocations(shadeAnimation) @@ -184,7 +184,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { onPanelExpansionChanged_animatesBlurOut_ifFlick() clearInvocations(shadeAnimation) notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 0.6f, expanded = true, tracking = true, dragDownPxAmount = 0f)) verify(shadeAnimation).animateTo(eq(maxBlur)) } @@ -192,7 +192,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun onPanelExpansionChanged_respectsMinPanelPullDownFraction() { val event = - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 0.5f, expanded = true, tracking = true, dragDownPxAmount = 0f) notificationShadeDepthController.panelPullDownMinFraction = 0.5f notificationShadeDepthController.onPanelExpansionChanged(event) @@ -220,7 +220,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { statusBarState = StatusBarState.KEYGUARD notificationShadeDepthController.qsPanelExpansion = 1f notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)) notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false)) @@ -231,7 +231,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { statusBarState = StatusBarState.KEYGUARD notificationShadeDepthController.qsPanelExpansion = 0.25f notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)) notificationShadeDepthController.updateBlurCallback.doFrame(0) verify(wallpaperController) @@ -243,7 +243,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { enableSplitShade() notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)) notificationShadeDepthController.updateBlurCallback.doFrame(0) @@ -255,7 +255,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { disableSplitShade() notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)) notificationShadeDepthController.updateBlurCallback.doFrame(0) @@ -269,7 +269,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { val expanded = true val tracking = false val dragDownPxAmount = 0f - val event = PanelExpansionChangeEvent(rawFraction, expanded, tracking, dragDownPxAmount) + val event = ShadeExpansionChangeEvent(rawFraction, expanded, tracking, dragDownPxAmount) val inOrder = Mockito.inOrder(wallpaperController) notificationShadeDepthController.onPanelExpansionChanged(event) @@ -333,7 +333,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun updateBlurCallback_setsBlur_whenExpanded() { notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)) `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat()) notificationShadeDepthController.updateBlurCallback.doFrame(0) @@ -343,7 +343,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun updateBlurCallback_ignoreShadeBlurUntilHidden_overridesZoom() { notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)) `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat()) notificationShadeDepthController.blursDisabledForAppLaunch = true @@ -361,7 +361,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun ignoreBlurForUnlock_ignores() { notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)) `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat()) @@ -378,7 +378,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Test fun ignoreBlurForUnlock_doesNotIgnore() { notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)) `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat()) @@ -410,7 +410,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { `when`(brightnessSpring.ratio).thenReturn(1f) // And shade is blurred notificationShadeDepthController.onPanelExpansionChanged( - PanelExpansionChangeEvent( + ShadeExpansionChangeEvent( fraction = 1f, expanded = true, tracking = false, dragDownPxAmount = 0f)) `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt index 44cbe51a30ac..fbb8ebfb3e3b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager @@ -56,6 +57,7 @@ class PulseExpansionHandlerTest : SysuiTestCase() { private val configurationController: ConfigurationController = mock() private val statusBarStateController: StatusBarStateController = mock() private val falsingManager: FalsingManager = mock() + private val shadeExpansionStateManager: ShadeExpansionStateManager = mock() private val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock() private val falsingCollector: FalsingCollector = mock() private val dumpManager: DumpManager = mock() @@ -65,7 +67,8 @@ class PulseExpansionHandlerTest : SysuiTestCase() { fun setUp() { whenever(expandableView.collapsedHeight).thenReturn(collapsedHeight) - pulseExpansionHandler = PulseExpansionHandler( + pulseExpansionHandler = + PulseExpansionHandler( mContext, wakeUpCoordinator, bypassController, @@ -74,10 +77,11 @@ class PulseExpansionHandlerTest : SysuiTestCase() { configurationController, statusBarStateController, falsingManager, + shadeExpansionStateManager, lockscreenShadeTransitionController, falsingCollector, dumpManager - ) + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java index f8a0d2fc415c..9c65fac1af45 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java @@ -70,7 +70,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.telephony.TelephonyListenerManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java index ed8a3e16cdd1..4bed4a19b3d9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java @@ -38,7 +38,7 @@ import android.testing.TestableLooper.RunWithLooper; import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.DataUsageController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.CarrierConfigTracker; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java index a76676e01c15..d5f5105036d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java @@ -43,7 +43,7 @@ import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.DataUsageController; import com.android.systemui.R; import com.android.systemui.dump.DumpManager; -import com.android.systemui.log.LogBuffer; +import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.CarrierConfigTracker; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java index b719c7f9e54e..a6381d13f7da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java @@ -32,7 +32,6 @@ import android.testing.TestableLooper; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.Assert; @@ -58,8 +57,6 @@ public class DynamicPrivacyControllerTest extends SysuiTestCase { mDynamicPrivacyController = new DynamicPrivacyController( mLockScreenUserManager, mKeyguardStateController, mock(StatusBarStateController.class)); - mDynamicPrivacyController.setStatusBarKeyguardViewManager( - mock(StatusBarKeyguardViewManager.class)); mDynamicPrivacyController.addListener(mListener); // Disable dynamic privacy by default allowNotificationsInPublic(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java index 4b458f5a9123..dda7fadde2d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java @@ -31,8 +31,8 @@ public class GroupEntryBuilder { private long mCreationTime = 0; @Nullable private GroupEntry mParent = GroupEntry.ROOT_ENTRY; private NotifSection mNotifSection; - private NotificationEntry mSummary = null; - private List<NotificationEntry> mChildren = new ArrayList<>(); + @Nullable private NotificationEntry mSummary = null; + private final List<NotificationEntry> mChildren = new ArrayList<>(); /** Builds a new instance of GroupEntry */ public GroupEntry build() { @@ -41,7 +41,9 @@ public class GroupEntryBuilder { ge.getAttachState().setSection(mNotifSection); ge.setSummary(mSummary); - mSummary.setParent(ge); + if (mSummary != null) { + mSummary.setParent(ge); + } for (NotificationEntry child : mChildren) { ge.addChild(child); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java index 851517e1e35b..3b05321e1a6b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java @@ -1498,45 +1498,8 @@ public class NotifCollectionTest extends SysuiTestCase { } @Test - public void testMissingRankingWhenRemovalFeatureIsDisabled() { + public void testMissingRanking() { // GIVEN a pipeline with one two notifications - when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(false); - String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key; - String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key; - NotificationEntry entry1 = mCollectionListener.getEntry(key1); - NotificationEntry entry2 = mCollectionListener.getEntry(key2); - clearInvocations(mCollectionListener); - - // GIVEN the message for removing key1 gets does not reach NotifCollection - Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1); - // WHEN the message for removing key2 arrives - mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL); - - // THEN only entry2 gets removed - verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL)); - verify(mCollectionListener).onEntryCleanUp(eq(entry2)); - verify(mCollectionListener).onRankingApplied(); - verifyNoMoreInteractions(mCollectionListener); - verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any()); - verify(mLogger, never()).logRecoveredRankings(any(), anyInt()); - clearInvocations(mCollectionListener, mLogger); - - // WHEN a ranking update includes key1 again - mNoMan.setRanking(key1, ranking1); - mNoMan.issueRankingUpdate(); - - // VERIFY that we do nothing but log the 'recovery' - verify(mCollectionListener).onRankingUpdate(any()); - verify(mCollectionListener).onRankingApplied(); - verifyNoMoreInteractions(mCollectionListener); - verify(mLogger, never()).logMissingRankings(any(), anyInt(), any()); - verify(mLogger).logRecoveredRankings(eq(List.of(key1)), eq(0)); - } - - @Test - public void testMissingRankingWhenRemovalFeatureIsEnabled() { - // GIVEN a pipeline with one two notifications - when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(true); String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key; String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key; NotificationEntry entry1 = mCollectionListener.getEntry(key1); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java index 9f214097ea55..09f8a10f88c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.collection; import static com.android.systemui.statusbar.notification.collection.ListDumper.dumpTree; +import static com.android.systemui.statusbar.notification.collection.ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS; import static com.google.common.truth.Truth.assertThat; @@ -33,10 +34,12 @@ import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -45,6 +48,7 @@ import android.os.SystemClock; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.ArrayMap; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -125,10 +129,15 @@ public class ShadeListBuilderTest extends SysuiTestCase { private Map<String, Integer> mNextIdMap = new ArrayMap<>(); private int mNextRank = 0; + private Log.TerribleFailureHandler mOldWtfHandler = null; + private Log.TerribleFailure mLastWtf = null; + private int mWtfCount = 0; + @Before public void setUp() { MockitoAnnotations.initMocks(this); allowTestableLooperAsMainThread(); + when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true); mListBuilder = new ShadeListBuilder( mDumpManager, @@ -1748,14 +1757,17 @@ public class ShadeListBuilderTest extends SysuiTestCase { mListBuilder.addPreGroupFilter(filter); mListBuilder.addOnBeforeTransformGroupsListener(listener); + interceptWtfs(); + // WHEN we try to run the pipeline and the filter is invalidated exactly // MAX_CONSECUTIVE_REENTRANT_REBUILDS times, addNotif(0, PACKAGE_2); - invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS); + invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); dispatchBuild(); - runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); - // THEN an exception is NOT thrown. + // THEN an exception is NOT thrown directly, but a WTF IS logged. + expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS); } @Test(expected = IllegalStateException.class) @@ -1767,18 +1779,24 @@ public class ShadeListBuilderTest extends SysuiTestCase { mListBuilder.addPreGroupFilter(filter); mListBuilder.addOnBeforeTransformGroupsListener(listener); + interceptWtfs(); + // WHEN we try to run the pipeline and the filter is invalidated more than // MAX_CONSECUTIVE_REENTRANT_REBUILDS times, addNotif(0, PACKAGE_2); - invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); + invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); dispatchBuild(); - runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + try { + runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + } finally { + expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS); + } // THEN an exception IS thrown. } @Test - public void testNonConsecutiveOutOfOrderInvalidationDontThrowAfterTooManyRuns() { + public void testNonConsecutiveOutOfOrderInvalidationsDontThrowAfterTooManyRuns() { // GIVEN a PreGroupNotifFilter that gets invalidated during the grouping stage, NotifFilter filter = new PackageFilter(PACKAGE_1); CountingInvalidator invalidator = new CountingInvalidator(filter); @@ -1786,17 +1804,22 @@ public class ShadeListBuilderTest extends SysuiTestCase { mListBuilder.addPreGroupFilter(filter); mListBuilder.addOnBeforeTransformGroupsListener(listener); - // WHEN we try to run the pipeline and the filter is invalidated at least - // MAX_CONSECUTIVE_REENTRANT_REBUILDS times, + interceptWtfs(); + + // WHEN we try to run the pipeline and the filter is invalidated + // MAX_CONSECUTIVE_REENTRANT_REBUILDS times, the pipeline runs for a non-reentrant reason, + // and then the filter is invalidated MAX_CONSECUTIVE_REENTRANT_REBUILDS times again, addNotif(0, PACKAGE_2); - invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS); + invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); dispatchBuild(); - runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); - invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS); + runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); + // Note: dispatchBuild itself triggers a non-reentrant pipeline run. dispatchBuild(); - runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); - // THEN an exception is NOT thrown. + // THEN an exception is NOT thrown, but WTFs ARE logged. + expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS * 2); } @Test @@ -1808,14 +1831,18 @@ public class ShadeListBuilderTest extends SysuiTestCase { mListBuilder.addPromoter(promoter); mListBuilder.addOnBeforeSortListener(listener); + interceptWtfs(); + // WHEN we try to run the pipeline and the promoter is invalidated exactly // MAX_CONSECUTIVE_REENTRANT_REBUILDS times, addNotif(0, PACKAGE_1); - invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS); + invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); dispatchBuild(); - runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + + // THEN an exception is NOT thrown directly, but a WTF IS logged. + expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS); - // THEN an exception is NOT thrown. } @Test(expected = IllegalStateException.class) @@ -1827,12 +1854,18 @@ public class ShadeListBuilderTest extends SysuiTestCase { mListBuilder.addPromoter(promoter); mListBuilder.addOnBeforeSortListener(listener); + interceptWtfs(); + // WHEN we try to run the pipeline and the promoter is invalidated more than // MAX_CONSECUTIVE_REENTRANT_REBUILDS times, addNotif(0, PACKAGE_1); - invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); + invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); dispatchBuild(); - runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + try { + runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + } finally { + expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS); + } // THEN an exception IS thrown. } @@ -1846,14 +1879,17 @@ public class ShadeListBuilderTest extends SysuiTestCase { mListBuilder.setComparators(singletonList(comparator)); mListBuilder.addOnBeforeRenderListListener(listener); + interceptWtfs(); + // WHEN we try to run the pipeline and the comparator is invalidated exactly // MAX_CONSECUTIVE_REENTRANT_REBUILDS times, addNotif(0, PACKAGE_2); - invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS); + invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); dispatchBuild(); - runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); - // THEN an exception is NOT thrown. + // THEN an exception is NOT thrown directly, but a WTF IS logged. + expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS); } @Test(expected = IllegalStateException.class) @@ -1865,12 +1901,14 @@ public class ShadeListBuilderTest extends SysuiTestCase { mListBuilder.setComparators(singletonList(comparator)); mListBuilder.addOnBeforeRenderListListener(listener); + interceptWtfs(); + // WHEN we try to run the pipeline and the comparator is invalidated more than // MAX_CONSECUTIVE_REENTRANT_REBUILDS times, addNotif(0, PACKAGE_2); - invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); + invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); dispatchBuild(); - runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); // THEN an exception IS thrown. } @@ -1884,14 +1922,17 @@ public class ShadeListBuilderTest extends SysuiTestCase { mListBuilder.addFinalizeFilter(filter); mListBuilder.addOnBeforeRenderListListener(listener); + interceptWtfs(); + // WHEN we try to run the pipeline and the PreRenderFilter is invalidated exactly // MAX_CONSECUTIVE_REENTRANT_REBUILDS times, addNotif(0, PACKAGE_2); - invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS); + invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); dispatchBuild(); - runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); - // THEN an exception is NOT thrown. + // THEN an exception is NOT thrown directly, but a WTF IS logged. + expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS); } @Test(expected = IllegalStateException.class) @@ -1903,33 +1944,143 @@ public class ShadeListBuilderTest extends SysuiTestCase { mListBuilder.addFinalizeFilter(filter); mListBuilder.addOnBeforeRenderListListener(listener); + interceptWtfs(); + // WHEN we try to run the pipeline and the PreRenderFilter is invalidated more than // MAX_CONSECUTIVE_REENTRANT_REBUILDS times, addNotif(0, PACKAGE_2); - invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); + invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); dispatchBuild(); - runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + try { + runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); + } finally { + expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS); + } // THEN an exception IS thrown. } + private void interceptWtfs() { + assertNull(mOldWtfHandler); + + mLastWtf = null; + mWtfCount = 0; + + mOldWtfHandler = Log.setWtfHandler((tag, e, system) -> { + Log.e("ShadeListBuilderTest", "Observed WTF: " + e); + mLastWtf = e; + mWtfCount++; + }); + } + + private void expectNoWtfs() { + assertNull(expectWtfs(0)); + } + + private Log.TerribleFailure expectWtf() { + return expectWtfs(1); + } + + private Log.TerribleFailure expectWtfs(int expectedWtfCount) { + assertNotNull(mOldWtfHandler); + + Log.setWtfHandler(mOldWtfHandler); + mOldWtfHandler = null; + + Log.TerribleFailure wtf = mLastWtf; + int wtfCount = mWtfCount; + + mLastWtf = null; + mWtfCount = 0; + + assertEquals(expectedWtfCount, wtfCount); + return wtf; + } + + @Test + public void testActiveOrdering_withLegacyStability() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false); + assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change + assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X + assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change + assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X + assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap + } + + @Test + public void testStableOrdering_withLegacyStability() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false); + mStabilityManager.setAllowEntryReordering(false); + assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change + assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X + assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change + assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X + assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap + } + @Test public void testStableOrdering() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); mStabilityManager.setAllowEntryReordering(false); - assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG"); // X - assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG"); // no change - assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG"); // Z and X - assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG"); // Z and X + gap - verify(mStabilityManager, times(4)).onEntryReorderSuppressed(); + // No input or output + assertOrder("", "", "", true); + // Remove everything + assertOrder("ABCDEFG", "", "", true); + // Literally no changes + assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); + + // No stable order + assertOrder("", "ABCDEFG", "ABCDEFG", true); + + // F moved after A, and... + assertOrder("ABCDEFG", "AFBCDEG", "ABCDEFG", false); // No other changes + assertOrder("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false); // Insert X before F + assertOrder("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false); // Insert X after F + assertOrder("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false); // Insert X where F was + + // B moved after F, and... + assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // No other changes + assertOrder("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false); // Insert X before B + assertOrder("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false); // Insert X after B + assertOrder("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false); // Insert X where B was + + // Swap F and B, and... + assertOrder("ABCDEFG", "AFCDEBG", "ABCDEFG", false); // No other changes + assertOrder("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false); // Insert X before F + assertOrder("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false); // Insert X after F + assertOrder("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false); // Insert X between CD (or: ABCXDEFG) + assertOrder("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false); // Insert X between DE (or: ABCDEFXG) + assertOrder("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false); // Insert X before B + assertOrder("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false); // Insert X after B + + // Remove a bunch of entries at once + assertOrder("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true); + + // Remove a bunch of entries and scramble + assertOrder("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false); + + // Add a bunch of entries at once + assertOrder("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true); + + // Add a bunch of entries and reverse originals + // NOTE: Some of these don't have obviously correct answers + assertOrder("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false); // appended + assertOrder("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false); // prepended + assertOrder("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false); // closer to back: append + assertOrder("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false); // closer to front: prepend + assertOrder("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false); // split new entries + + // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout + assertOrder("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false); } @Test public void testActiveOrdering() { - assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG"); // X - assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG"); // no change - assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG"); // Z and X - assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG"); // Z and X + gap - verify(mStabilityManager, never()).onEntryReorderSuppressed(); + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); + assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X + assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change + assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X + assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap } @Test @@ -1981,6 +2132,52 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test + public void stableOrderingDisregardedWithSectionChange() { + when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true); + // GIVEN the first sectioner's packages can be changed from run-to-run + List<String> mutableSectionerPackages = new ArrayList<>(); + mutableSectionerPackages.add(PACKAGE_1); + mListBuilder.setSectioners(asList( + new PackageSectioner(mutableSectionerPackages, null), + new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null))); + mStabilityManager.setAllowEntryReordering(false); + + // WHEN the list is originally built with reordering disabled (and section changes allowed) + addNotif(0, PACKAGE_1).setRank(4); + addNotif(1, PACKAGE_1).setRank(5); + addNotif(2, PACKAGE_2).setRank(1); + addNotif(3, PACKAGE_2).setRank(2); + addNotif(4, PACKAGE_3).setRank(3); + dispatchBuild(); + + // VERIFY the order and that entry reordering has not been suppressed + verifyBuiltList( + notif(0), + notif(1), + notif(2), + notif(3), + notif(4) + ); + verify(mStabilityManager, never()).onEntryReorderSuppressed(); + + // WHEN the first section now claims PACKAGE_3 notifications + mutableSectionerPackages.add(PACKAGE_3); + dispatchBuild(); + + // VERIFY the re-sectioned notification is inserted at #1 of the first section, which + // is the correct position based on its rank, rather than #3 in the new section simply + // because it was #3 in its previous section. + verifyBuiltList( + notif(4), + notif(0), + notif(1), + notif(2), + notif(3) + ); + verify(mStabilityManager, never()).onEntryReorderSuppressed(); + } + + @Test public void testStableChildOrdering() { // WHEN the list is originally built with reordering disabled mStabilityManager.setAllowEntryReordering(false); @@ -2031,6 +2228,85 @@ public class ShadeListBuilderTest extends SysuiTestCase { ); } + @Test + public void groupRevertingToSummaryDoesNotRetainStablePositionWithLegacyIndexLogic() { + when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(false); + + // GIVEN a notification group is on screen + mStabilityManager.setAllowEntryReordering(false); + + // WHEN the list is originally built with reordering disabled (and section changes allowed) + addNotif(0, PACKAGE_1).setRank(2); + addNotif(1, PACKAGE_1).setRank(3); + addGroupSummary(2, PACKAGE_1, "group").setRank(4); + addGroupChild(3, PACKAGE_1, "group").setRank(5); + addGroupChild(4, PACKAGE_1, "group").setRank(6); + dispatchBuild(); + + verifyBuiltList( + notif(0), + notif(1), + group( + summary(2), + child(3), + child(4) + ) + ); + + // WHEN the notification summary rank increases and children removed + setNewRank(notif(2).entry, 1); + mEntrySet.remove(4); + mEntrySet.remove(3); + dispatchBuild(); + + // VERIFY the summary (incorrectly) moves to the top of the section where it is ranked, + // despite visual stability being active + verifyBuiltList( + notif(2), + notif(0), + notif(1) + ); + } + + @Test + public void groupRevertingToSummaryRetainsStablePosition() { + when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true); + + // GIVEN a notification group is on screen + mStabilityManager.setAllowEntryReordering(false); + + // WHEN the list is originally built with reordering disabled (and section changes allowed) + addNotif(0, PACKAGE_1).setRank(2); + addNotif(1, PACKAGE_1).setRank(3); + addGroupSummary(2, PACKAGE_1, "group").setRank(4); + addGroupChild(3, PACKAGE_1, "group").setRank(5); + addGroupChild(4, PACKAGE_1, "group").setRank(6); + dispatchBuild(); + + verifyBuiltList( + notif(0), + notif(1), + group( + summary(2), + child(3), + child(4) + ) + ); + + // WHEN the notification summary rank increases and children removed + setNewRank(notif(2).entry, 1); + mEntrySet.remove(4); + mEntrySet.remove(3); + dispatchBuild(); + + // VERIFY the summary stays in the same location on rebuild + verifyBuiltList( + notif(0), + notif(1), + notif(2) + ); + } + private static void setNewRank(NotificationEntry entry, int rank) { entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build()); } @@ -2174,26 +2450,35 @@ public class ShadeListBuilderTest extends SysuiTestCase { return addGroupChildWithTag(index, packageId, groupId, null); } - private void assertOrder(String visible, String active, String expected) { + private void assertOrder(String visible, String active, String expected, + boolean isOrderedCorrectly) { StringBuilder differenceSb = new StringBuilder(); + NotifSection section = new NotifSection(mock(NotifSectioner.class), 0); for (char c : active.toCharArray()) { if (visible.indexOf(c) < 0) differenceSb.append(c); } String difference = differenceSb.toString(); + int globalIndex = 0; for (int i = 0; i < visible.length(); i++) { - addNotif(i, String.valueOf(visible.charAt(i))) - .setRank(active.indexOf(visible.charAt(i))) + final char c = visible.charAt(i); + // Skip notifications which aren't active anymore + if (!active.contains(String.valueOf(c))) continue; + addNotif(globalIndex++, String.valueOf(c)) + .setRank(active.indexOf(c)) + .setSection(section) .setStableIndex(i); - } - for (int i = 0; i < difference.length(); i++) { - addNotif(i + visible.length(), String.valueOf(difference.charAt(i))) - .setRank(active.indexOf(difference.charAt(i))) + for (char c : difference.toCharArray()) { + addNotif(globalIndex++, String.valueOf(c)) + .setRank(active.indexOf(c)) + .setSection(section) .setStableIndex(-1); } + clearInvocations(mStabilityManager); + dispatchBuild(); StringBuilder resultSb = new StringBuilder(); for (int i = 0; i < expected.length(); i++) { @@ -2203,6 +2488,9 @@ public class ShadeListBuilderTest extends SysuiTestCase { assertEquals("visible [" + visible + "] active [" + active + "]", expected, resultSb.toString()); mEntrySet.clear(); + + verify(mStabilityManager, isOrderedCorrectly ? never() : times(1)) + .onEntryReorderSuppressed(); } private int nextId(String packageName) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 2ee31265ff7b..3ff7639e9262 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -47,6 +47,8 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.time.FakeSystemClock +import java.util.ArrayList +import java.util.function.Consumer import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -57,10 +59,8 @@ import org.mockito.BDDMockito.given import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations -import java.util.ArrayList -import java.util.function.Consumer import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @@ -337,6 +337,40 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { } @Test + fun testOnEntryUpdated_toAlert() { + // GIVEN that an entry is posted that should not heads up + setShouldHeadsUp(mEntry, false) + mCollectionListener.onEntryAdded(mEntry) + + // WHEN it's updated to heads up + setShouldHeadsUp(mEntry) + mCollectionListener.onEntryUpdated(mEntry) + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification alerts + finishBind(mEntry) + verify(mHeadsUpManager).showNotification(mEntry) + } + + @Test + fun testOnEntryUpdated_toNotAlert() { + // GIVEN that an entry is posted that should heads up + setShouldHeadsUp(mEntry) + mCollectionListener.onEntryAdded(mEntry) + + // WHEN it's updated to not heads up + setShouldHeadsUp(mEntry, false) + mCollectionListener.onEntryUpdated(mEntry) + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is never bound or shown + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + verify(mHeadsUpManager, never()).showNotification(any()) + } + + @Test fun testOnEntryRemovedRemovesHeadsUpNotification() { // GIVEN the current HUN is mEntry addHUN(mEntry) @@ -637,8 +671,88 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(mHeadsUpManager, never()).showNotification(mGroupChild2) } + @Test + fun testOnRankingApplied_newEntryShouldAlert() { + // GIVEN that mEntry has never interrupted in the past, and now should + // and is new enough to do so + assertFalse(mEntry.hasInterrupted()) + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis()) + setShouldHeadsUp(mEntry) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) + + // WHEN a ranking applied update occurs + mCollectionListener.onRankingApplied() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is shown + finishBind(mEntry) + verify(mHeadsUpManager).showNotification(mEntry) + } + + @Test + fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() { + // GIVEN that mEntry has alerted in the past, even if it's new + mEntry.setInterruption() + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis()) + setShouldHeadsUp(mEntry) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) + + // WHEN a ranking applied update occurs + mCollectionListener.onRankingApplied() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is never bound or shown + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + verify(mHeadsUpManager, never()).showNotification(any()) + } + + @Test + fun testOnRankingApplied_entryUpdatedToHun() { + // GIVEN that mEntry is added in a state where it should not HUN + setShouldHeadsUp(mEntry, false) + mCollectionListener.onEntryAdded(mEntry) + + // and it is then updated such that it should now HUN + setShouldHeadsUp(mEntry) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) + + // WHEN a ranking applied update occurs + mCollectionListener.onRankingApplied() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is shown + finishBind(mEntry) + verify(mHeadsUpManager).showNotification(mEntry) + } + + @Test + fun testOnRankingApplied_entryUpdatedButTooOld() { + // GIVEN that mEntry is added in a state where it should not HUN + setShouldHeadsUp(mEntry, false) + mCollectionListener.onEntryAdded(mEntry) + + // and it was actually added 10s ago + mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis() - 10000) + + // WHEN it is updated to HUN and then a ranking update occurs + setShouldHeadsUp(mEntry) + whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry)) + mCollectionListener.onRankingApplied() + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is never bound or shown + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + verify(mHeadsUpManager, never()).showNotification(any()) + } + private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) { whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should) + whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any())) + .thenReturn(should) } private fun finishBind(entry: NotificationEntry) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java index e1e5051751bb..590c902ba687 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java @@ -35,7 +35,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.SysuiTestCase; -import com.android.systemui.media.MediaFeatureFlag; +import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index f4adf6927e31..b6b0b7738997 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -181,7 +181,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testInflatesNewNotification() { // WHEN there is a new notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); // THEN we inflate it @@ -194,7 +194,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testRebindsInflatedNotificationsOnUpdate() { // GIVEN an inflated notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -213,7 +213,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testEntrySmartReplyAdditionWillRebindViews() { // GIVEN an inflated notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -232,7 +232,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testEntryChangedToMinimizedSectionWillRebindViews() { // GIVEN an inflated notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any()); assertFalse(mParamsCaptor.getValue().isLowPriority()); @@ -254,7 +254,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { public void testMinimizedEntryMovedIntoGroupWillRebindViews() { // GIVEN an inflated, minimized notification setSectionIsLowPriority(true); - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any()); assertTrue(mParamsCaptor.getValue().isLowPriority()); @@ -275,7 +275,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testEntryRankChangeWillNotRebindViews() { // GIVEN an inflated notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -294,7 +294,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Test public void testDoesntFilterInflatedNotifs() { // GIVEN an inflated notification - mCollectionListener.onEntryAdded(mEntry); + mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); @@ -330,9 +330,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mCollectionListener.onEntryInit(entry); } - mCollectionListener.onEntryAdded(summary); + mCollectionListener.onEntryInit(summary); for (NotificationEntry entry : children) { - mCollectionListener.onEntryAdded(entry); + mCollectionListener.onEntryInit(entry); } mBeforeFilterListener.onBeforeFinalizeFilter(List.of(groupEntry)); @@ -393,6 +393,70 @@ public class PreparationCoordinatorTest extends SysuiTestCase { } @Test + public void testNullGroupSummary() { + // GIVEN a newly-posted group with a summary and two children + final GroupEntry group = new GroupEntryBuilder() + .setCreationTime(400) + .setSummary(getNotificationEntryBuilder().setId(1).build()) + .addChild(getNotificationEntryBuilder().setId(2).build()) + .addChild(getNotificationEntryBuilder().setId(3).build()) + .build(); + fireAddEvents(List.of(group)); + final NotificationEntry child0 = group.getChildren().get(0); + final NotificationEntry child1 = group.getChildren().get(1); + mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group)); + + // WHEN the summary is pruned + new GroupEntryBuilder() + .setCreationTime(400) + .addChild(child0) + .addChild(child1) + .build(); + + // WHEN all of the children (but not the summary) finish inflating + mNotifInflater.invokeInflateCallbackForEntry(child0); + mNotifInflater.invokeInflateCallbackForEntry(child1); + + // THEN the entire group is not filtered out + assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401)); + assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401)); + } + + @Test + public void testPartiallyInflatedGroupsAreNotFilteredOutIfSummaryReinflate() { + // GIVEN a newly-posted group with a summary and two children + final String groupKey = "test_reinflate_group"; + final int summaryId = 1; + final GroupEntry group = new GroupEntryBuilder() + .setKey(groupKey) + .setCreationTime(400) + .setSummary(getNotificationEntryBuilder().setId(summaryId).setImportance(1).build()) + .addChild(getNotificationEntryBuilder().setId(2).build()) + .addChild(getNotificationEntryBuilder().setId(3).build()) + .build(); + fireAddEvents(List.of(group)); + final NotificationEntry summary = group.getSummary(); + final NotificationEntry child0 = group.getChildren().get(0); + final NotificationEntry child1 = group.getChildren().get(1); + mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group)); + + // WHEN all of the children (but not the summary) finish inflating + mNotifInflater.invokeInflateCallbackForEntry(child0); + mNotifInflater.invokeInflateCallbackForEntry(child1); + mNotifInflater.invokeInflateCallbackForEntry(summary); + + // WHEN the summary is updated and starts re-inflating + summary.setRanking(new RankingBuilder(summary.getRanking()).setImportance(4).build()); + fireUpdateEvents(summary); + mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group)); + + // THEN the entire group is still not filtered out + assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401)); + assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401)); + assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401)); + } + + @Test public void testCompletedInflatedGroupsAreReleased() { // GIVEN a newly-posted group with a summary and two children final GroupEntry group = new GroupEntryBuilder() @@ -412,7 +476,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mNotifInflater.invokeInflateCallbackForEntry(child1); mNotifInflater.invokeInflateCallbackForEntry(summary); - // THEN the entire group is still filtered out + // THEN the entire group is no longer filtered out assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401)); assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401)); assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401)); @@ -494,7 +558,11 @@ public class PreparationCoordinatorTest extends SysuiTestCase { private void fireAddEvents(NotificationEntry entry) { mCollectionListener.onEntryInit(entry); - mCollectionListener.onEntryAdded(entry); + mCollectionListener.onEntryInit(entry); + } + + private void fireUpdateEvents(NotificationEntry entry) { + mCollectionListener.onEntryUpdated(entry); } private static final String TEST_MESSAGE = "TEST_MESSAGE"; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt new file mode 100644 index 000000000000..1cdd023dd01c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection.listbuilder + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.util.Log +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class SemiStableSortTest : SysuiTestCase() { + + var shuffleInput: Boolean = false + var testStabilizeTo: Boolean = false + var sorter: SemiStableSort? = null + + @Before + fun setUp() { + shuffleInput = false + sorter = null + } + + private fun stringStabilizeTo( + stableOrder: String, + activeOrder: String, + ): Pair<String, Boolean> { + val actives = activeOrder.toMutableList() + val result = mutableListOf<Char>() + return (sorter ?: SemiStableSort()) + .stabilizeTo( + actives, + { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } }, + result, + ) + .let { ordered -> result.joinToString("") to ordered } + } + + private fun stringSort( + stableOrder: String, + activeOrder: String, + ): Pair<String, Boolean> { + val actives = activeOrder.toMutableList() + if (shuffleInput) { + actives.shuffle() + } + return (sorter ?: SemiStableSort()) + .sort( + actives, + { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } }, + compareBy { activeOrder.indexOf(it) }, + ) + .let { ordered -> actives.joinToString("") to ordered } + } + + private fun testCase( + stableOrder: String, + activeOrder: String, + expected: String, + expectOrdered: Boolean, + ) { + val (mergeResult, ordered) = + if (testStabilizeTo) stringStabilizeTo(stableOrder, activeOrder) + else stringSort(stableOrder, activeOrder) + val resultPass = expected == mergeResult + val orderedPass = ordered == expectOrdered + val pass = resultPass && orderedPass + val resultSuffix = + if (resultPass) "result=$expected" else "expected=$expected got=$mergeResult" + val orderedSuffix = + if (orderedPass) "ordered=$ordered" else "expected ordered to be $expectOrdered" + val readableResult = "stable=$stableOrder active=$activeOrder $resultSuffix $orderedSuffix" + Log.d("SemiStableSortTest", "${if (pass) "PASS" else "FAIL"}: $readableResult") + if (!pass) { + throw AssertionError("Test case failed: $readableResult") + } + } + + private fun runAllTestCases() { + // No input or output + testCase("", "", "", true) + // Remove everything + testCase("ABCDEFG", "", "", true) + // Literally no changes + testCase("ABCDEFG", "ABCDEFG", "ABCDEFG", true) + + // No stable order + testCase("", "ABCDEFG", "ABCDEFG", true) + + // F moved after A, and... + testCase("ABCDEFG", "AFBCDEG", "ABCDEFG", false) // No other changes + testCase("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false) // Insert X before F + testCase("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false) // Insert X after F + testCase("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false) // Insert X where F was + + // B moved after F, and... + testCase("ABCDEFG", "ACDEFBG", "ABCDEFG", false) // No other changes + testCase("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false) // Insert X before B + testCase("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false) // Insert X after B + testCase("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false) // Insert X where B was + + // Swap F and B, and... + testCase("ABCDEFG", "AFCDEBG", "ABCDEFG", false) // No other changes + testCase("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false) // Insert X before F + testCase("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false) // Insert X after F + testCase("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false) // Insert X between CD (Alt: ABCXDEFG) + testCase("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false) // Insert X between DE (Alt: ABCDEFXG) + testCase("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false) // Insert X before B + testCase("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false) // Insert X after B + + // Remove a bunch of entries at once + testCase("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true) + + // Remove a bunch of entries and scramble + testCase("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false) + + // Add a bunch of entries at once + testCase("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true) + + // Add a bunch of entries and reverse originals + // NOTE: Some of these don't have obviously correct answers + testCase("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false) // appended + testCase("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false) // prepended + testCase("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false) // closer to back: append + testCase("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false) // closer to front: prepend + testCase("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false) // split new entries + + // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout + testCase("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false) + } + + @Test + fun testSort() { + testStabilizeTo = false + shuffleInput = false + sorter = null + runAllTestCases() + } + + @Test + fun testSortWithSingleInstance() { + testStabilizeTo = false + shuffleInput = false + sorter = SemiStableSort() + runAllTestCases() + } + + @Test + fun testSortWithShuffledInput() { + testStabilizeTo = false + shuffleInput = true + sorter = null + runAllTestCases() + } + + @Test + fun testStabilizeTo() { + testStabilizeTo = true + sorter = null + runAllTestCases() + } + + @Test + fun testStabilizeToWithSingleInstance() { + testStabilizeTo = true + sorter = SemiStableSort() + runAllTestCases() + } + + @Test + fun testIsSorted() { + val intCmp = Comparator<Int> { x, y -> Integer.compare(x, y) } + SemiStableSort.apply { + assertTrue(emptyList<Int>().isSorted(intCmp)) + assertTrue(listOf(1).isSorted(intCmp)) + assertTrue(listOf(1, 2).isSorted(intCmp)) + assertTrue(listOf(1, 2, 3).isSorted(intCmp)) + assertTrue(listOf(1, 2, 3, 4).isSorted(intCmp)) + assertTrue(listOf(1, 2, 3, 4, 5).isSorted(intCmp)) + assertTrue(listOf(1, 1, 1, 1, 1).isSorted(intCmp)) + assertTrue(listOf(1, 1, 2, 2, 3, 3).isSorted(intCmp)) + assertFalse(listOf(2, 1).isSorted(intCmp)) + assertFalse(listOf(2, 1, 2).isSorted(intCmp)) + assertFalse(listOf(1, 2, 1).isSorted(intCmp)) + assertFalse(listOf(1, 2, 3, 2, 5).isSorted(intCmp)) + assertFalse(listOf(5, 2, 3, 4, 5).isSorted(intCmp)) + assertFalse(listOf(1, 2, 3, 4, 1).isSorted(intCmp)) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt new file mode 100644 index 000000000000..20369546d68a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection.listbuilder + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper.getContiguousSubLists +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class ShadeListBuilderHelperTest : SysuiTestCase() { + + @Test + fun testGetContiguousSubLists() { + assertThat(getContiguousSubLists("AAAAAA".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A', 'A', 'A', 'A'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABBB".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B', 'B', 'B'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABAA".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B'), + listOf('A', 'A'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABAA".toList(), minLength = 2) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('A', 'A'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList()) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B', 'B', 'B', 'B'), + listOf('C', 'C'), + listOf('D'), + listOf('E', 'E', 'E'), + ) + .inOrder() + assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList(), minLength = 2) { it }) + .containsExactly( + listOf('A', 'A', 'A'), + listOf('B', 'B', 'B', 'B'), + listOf('C', 'C'), + listOf('E', 'E', 'E'), + ) + .inOrder() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java index 3f641df376ed..ca6598726a85 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java @@ -91,6 +91,8 @@ public class HeadsUpViewBinderTest extends SysuiTestCase { verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); + when(mBindStage.tryGetStageParams(eq(mEntry))).thenReturn(new RowContentBindParams()); + mViewBinder.unbindHeadsUpView(mEntry); verify(mLogger).entryContentViewMarkedFreeable(eq(mEntry)); verifyNoMoreInteractions(mLogger); @@ -139,6 +141,8 @@ public class HeadsUpViewBinderTest extends SysuiTestCase { verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); + when(mBindStage.tryGetStageParams(eq(mEntry))).thenReturn(new RowContentBindParams()); + mViewBinder.unbindHeadsUpView(mEntry); verify(mLogger).currentOngoingBindingAborted(eq(mEntry)); verify(mLogger).entryContentViewMarkedFreeable(eq(mEntry)); @@ -150,4 +154,30 @@ public class HeadsUpViewBinderTest extends SysuiTestCase { verifyNoMoreInteractions(mLogger); clearInvocations(mLogger); } + + @Test + public void testLoggingForLateUnbindFlow() { + AtomicReference<NotifBindPipeline.BindCallback> callback = new AtomicReference<>(); + when(mBindStage.requestRebind(any(), any())).then(i -> { + callback.set(i.getArgument(1)); + return new CancellationSignal(); + }); + + mViewBinder.bindHeadsUpView(mEntry, null); + verify(mLogger).startBindingHun(eq(mEntry)); + verifyNoMoreInteractions(mLogger); + clearInvocations(mLogger); + + callback.get().onBindFinished(mEntry); + verify(mLogger).entryBoundSuccessfully(eq(mEntry)); + verifyNoMoreInteractions(mLogger); + clearInvocations(mLogger); + + when(mBindStage.tryGetStageParams(eq(mEntry))).thenReturn(null); + + mViewBinder.unbindHeadsUpView(mEntry); + verify(mLogger).entryBindStageParamsNullOnUnbind(eq(mEntry)); + verifyNoMoreInteractions(mLogger); + clearInvocations(mLogger); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java index d59cc54dfe98..8b7b4dea155f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProviderTest.java @@ -29,6 +29,7 @@ import static com.android.systemui.statusbar.notification.collection.EntryUtilKt import static com.android.systemui.util.mockito.KotlinMockitoHelpersKt.argThat; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -305,15 +306,59 @@ public class KeyguardNotificationVisibilityProviderTest extends SysuiTestCase { } @Test - public void hideSilentNotificationsPerUserSetting() { - when(mKeyguardStateController.isShowing()).thenReturn(true); + public void hideSilentOnLockscreenSetting() { + // GIVEN an 'unfiltered-keyguard-showing' state and notifications shown on lockscreen + setupUnfilteredState(mEntry); mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true); + + // WHEN the show silent notifs on lockscreen setting is false mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, false); + + // WHEN the notification is not high priority and not ambient + mEntry = new NotificationEntryBuilder() + .setImportance(IMPORTANCE_LOW) + .build(); + when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false); + + // THEN filter out the entry + assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry)); + } + + @Test + public void showSilentOnLockscreenSetting() { + // GIVEN an 'unfiltered-keyguard-showing' state and notifications shown on lockscreen + setupUnfilteredState(mEntry); + mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true); + + // WHEN the show silent notifs on lockscreen setting is true + mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, true); + + // WHEN the notification is not high priority and not ambient + mEntry = new NotificationEntryBuilder() + .setImportance(IMPORTANCE_LOW) + .build(); + when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false); + + // THEN do not filter out the entry + assertFalse(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry)); + } + + @Test + public void defaultSilentOnLockscreenSettingIsHide() { + // GIVEN an 'unfiltered-keyguard-showing' state and notifications shown on lockscreen + setupUnfilteredState(mEntry); + mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, true); + + // WHEN the notification is not high priority and not ambient mEntry = new NotificationEntryBuilder() .setUser(new UserHandle(NOTIF_USER_ID)) .setImportance(IMPORTANCE_LOW) .build(); when(mHighPriorityProvider.isHighPriority(any())).thenReturn(false); + + // WhHEN the show silent notifs on lockscreen setting is unset + assertNull(mFakeSettings.getString(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS)); + assertTrue(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry)); } @@ -431,25 +476,6 @@ public class KeyguardNotificationVisibilityProviderTest extends SysuiTestCase { } @Test - public void showSilentOnLockscreenSetting() { - // GIVEN an 'unfiltered-keyguard-showing' state - setupUnfilteredState(mEntry); - - // WHEN the notification is not high priority and not ambient - mEntry.setRanking(new RankingBuilder() - .setKey(mEntry.getKey()) - .setImportance(IMPORTANCE_LOW) - .build()); - when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false); - - // WHEN the show silent notifs on lockscreen setting is true - mFakeSettings.putBool(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, true); - - // THEN do not filter out the entry - assertFalse(mKeyguardNotificationVisibilityProvider.shouldHideNotification(mEntry)); - } - - @Test public void notificationVisibilityPublic() { // GIVEN a VISIBILITY_PUBLIC notification NotificationEntryBuilder entryBuilder = new NotificationEntryBuilder() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java index 46f630b7db63..ea311da3e20b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java @@ -51,12 +51,14 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; +import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -97,6 +99,7 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { NotifPipelineFlags mFlags; @Mock KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider; + UiEventLoggerFake mUiEventLoggerFake; @Mock PendingIntent mPendingIntent; @@ -107,6 +110,8 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(false); + mUiEventLoggerFake = new UiEventLoggerFake(); + mNotifInterruptionStateProvider = new NotificationInterruptStateProviderImpl( mContext.getContentResolver(), @@ -120,7 +125,8 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { mLogger, mMockHandler, mFlags, - mKeyguardNotificationVisibilityProvider); + mKeyguardNotificationVisibilityProvider, + mUiEventLoggerFake); mNotifInterruptionStateProvider.mUseHeadsUp = true; } @@ -442,6 +448,13 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { verify(mLogger, never()).logNoFullscreen(any(), any()); verify(mLogger).logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN"); verify(mLogger, never()).logFullscreen(any(), any()); + + assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1); + UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0); + assertThat(fakeUiEvent.eventId).isEqualTo( + NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR.getId()); + assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid()); + assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName()); } @Test @@ -600,6 +613,13 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { verify(mLogger, never()).logNoFullscreen(any(), any()); verify(mLogger).logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard"); verify(mLogger, never()).logFullscreen(any(), any()); + + assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1); + UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0); + assertThat(fakeUiEvent.eventId).isEqualTo( + NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD.getId()); + assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid()); + assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName()); } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt new file mode 100644 index 000000000000..f69839b7087c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt @@ -0,0 +1,305 @@ +/* + * + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.app.PendingIntent +import android.app.Person +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.testing.AndroidTestingRunner +import android.widget.RemoteViews +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.NotificationUtils +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class NotificationMemoryMeterTest : SysuiTestCase() { + + @Test + fun currentNotificationMemoryUse_plainNotification() { + val notification = createBasicNotification().build() + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) + assertNotificationObjectSizes( + memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3316, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)) + val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build() + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = 0, + extras = 3316, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_customViewNotification_marksTrue() { + val notification = + createBasicNotification() + .setCustomContentView( + RemoteViews(context.packageName, android.R.layout.list_content) + ) + .build() + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3384, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = true, + ) + } + + @Test + fun currentNotificationMemoryUse_notificationWithDataIcon_calculatesCorrectly() { + val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444) + val notification = + createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build() + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = 444444, + largeIcon = 0, + extras = 3212, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_bigPictureStyle() { + val bigPicture = + Icon.createWithBitmap(Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888)) + val bigPictureIcon = + Icon.createWithAdaptiveBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888)) + val notification = + createBasicNotification() + .setStyle( + Notification.BigPictureStyle() + .bigPicture(bigPicture) + .bigLargeIcon(bigPictureIcon) + ) + .build() + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 4092, + bigPicture = bigPicture.bitmap.allocationByteCount, + extender = 0, + style = "BigPictureStyle", + styleIcon = bigPictureIcon.bitmap.allocationByteCount, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_callingStyle() { + val personIcon = + Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888)) + val person = Person.Builder().setIcon(personIcon).setName("Person").build() + val fakeIntent = + PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) + val notification = + createBasicNotification() + .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent)) + .build() + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 4084, + bigPicture = 0, + extender = 0, + style = "CallStyle", + styleIcon = personIcon.bitmap.allocationByteCount, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_messagingStyle() { + val personIcon = + Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888)) + val person = Person.Builder().setIcon(personIcon).setName("Person").build() + val message = Notification.MessagingStyle.Message("Message!", 4323, person) + val historicPersonIcon = + Icon.createWithBitmap(Bitmap.createBitmap(348, 382, Bitmap.Config.ARGB_8888)) + val historicPerson = + Person.Builder().setIcon(historicPersonIcon).setName("Historic person").build() + val historicMessage = + Notification.MessagingStyle.Message("Historic message!", 5848, historicPerson) + + val notification = + createBasicNotification() + .setStyle( + Notification.MessagingStyle(person) + .addMessage(message) + .addHistoricMessage(historicMessage) + ) + .build() + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 5024, + bigPicture = 0, + extender = 0, + style = "MessagingStyle", + styleIcon = + personIcon.bitmap.allocationByteCount + + historicPersonIcon.bitmap.allocationByteCount, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_carExtender() { + val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888) + val extender = Notification.CarExtender().setLargeIcon(carIcon) + val notification = createBasicNotification().extend(extender).build() + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3612, + bigPicture = 0, + extender = 556656, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_tvWearExtender() { + val tvExtender = Notification.TvExtender().setChannel("channel2") + val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888) + val wearExtender = Notification.WearableExtender().setBackground(wearBackground) + val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build() + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification)) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3820, + bigPicture = 0, + extender = 388 + wearBackground.allocationByteCount, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + private fun createBasicNotification(): Notification.Builder { + val smallIcon = + Icon.createWithBitmap(Bitmap.createBitmap(250, 250, Bitmap.Config.ARGB_8888)) + val largeIcon = + Icon.createWithBitmap(Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888)) + return Notification.Builder(context) + .setSmallIcon(smallIcon) + .setLargeIcon(largeIcon) + .setContentTitle("This is a title") + .setContentText("This is content text.") + } + + /** This will generate a nicer error message than comparing objects */ + private fun assertNotificationObjectSizes( + memoryUse: NotificationMemoryUsage, + smallIcon: Int, + largeIcon: Int, + extras: Int, + bigPicture: Int, + extender: Int, + style: String?, + styleIcon: Int, + hasCustomView: Boolean, + ) { + assertThat(memoryUse.packageName).isEqualTo("test_pkg") + assertThat(memoryUse.notificationKey) + .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0")) + assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon) + assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon) + assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture) + if (style == null) { + assertThat(memoryUse.objectUsage.style).isNull() + } else { + assertThat(memoryUse.objectUsage.style).isEqualTo(style) + } + assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon) + assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView) + } + + private fun getUseObject( + singleItemUseList: List<NotificationMemoryUsage>, + ): NotificationMemoryUsage { + assertThat(singleItemUseList).hasSize(1) + return singleItemUseList[0] + } + + private fun createNotificationEntry( + notification: Notification, + ): NotificationEntry = + NotificationEntryBuilder().setTag("test").setNotification(notification).build() +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt new file mode 100644 index 000000000000..3a16fb33388b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt @@ -0,0 +1,148 @@ +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.widget.RemoteViews +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.android.systemui.tests.R +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class NotificationMemoryViewWalkerTest : SysuiTestCase() { + + private lateinit var testHelper: NotificationTestHelper + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + testHelper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + } + + @Test + fun testViewWalker_nullRow_returnsEmptyView() { + val result = NotificationMemoryViewWalker.getViewUsage(null) + assertThat(result).isNotNull() + assertThat(result).isEmpty() + } + + @Test + fun testViewWalker_plainNotification() { + val row = testHelper.createRow() + val result = NotificationMemoryViewWalker.getViewUsage(row) + assertThat(result).hasSize(5) + assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0)) + } + + @Test + fun testViewWalker_bigPictureNotification() { + val bigPicture = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888) + val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888)) + val largeIcon = Icon.createWithBitmap(Bitmap.createBitmap(60, 60, Bitmap.Config.ARGB_8888)) + val row = + testHelper.createRow( + Notification.Builder(mContext) + .setContentText("Test") + .setContentTitle("title") + .setSmallIcon(icon) + .setLargeIcon(largeIcon) + .setStyle(Notification.BigPictureStyle().bigPicture(bigPicture)) + .build() + ) + val result = NotificationMemoryViewWalker.getViewUsage(row) + assertThat(result).hasSize(5) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_EXPANDED_VIEW, + icon.bitmap.allocationByteCount, + largeIcon.bitmap.allocationByteCount, + 0, + bigPicture.allocationByteCount, + 0, + bigPicture.allocationByteCount + + icon.bitmap.allocationByteCount + + largeIcon.bitmap.allocationByteCount + ) + ) + + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_CONTRACTED_VIEW, + icon.bitmap.allocationByteCount, + largeIcon.bitmap.allocationByteCount, + 0, + 0, + 0, + icon.bitmap.allocationByteCount + largeIcon.bitmap.allocationByteCount + ) + ) + // Due to deduplication, this should all be 0. + assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + } + + @Test + fun testViewWalker_customView() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888)) + val bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888) + + val views = RemoteViews(mContext.packageName, R.layout.custom_view_dark) + views.setImageViewBitmap(R.id.custom_view_dark_image, bitmap) + val row = + testHelper.createRow( + Notification.Builder(mContext) + .setContentText("Test") + .setContentTitle("title") + .setSmallIcon(icon) + .setCustomContentView(views) + .setCustomBigContentView(views) + .build() + ) + val result = NotificationMemoryViewWalker.getViewUsage(row) + assertThat(result).hasSize(5) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_CONTRACTED_VIEW, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + bitmap.allocationByteCount, + bitmap.allocationByteCount + icon.bitmap.allocationByteCount + ) + ) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_EXPANDED_VIEW, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + bitmap.allocationByteCount, + bitmap.allocationByteCount + icon.bitmap.allocationByteCount + ) + ) + // Due to deduplication, this should all be 0. + assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java index 7e97629e82e2..dae0aa229dbf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java @@ -40,6 +40,10 @@ public class NotificationPanelLoggerFake implements NotificationPanelLogger { NotificationPanelLogger.toNotificationProto(visibleNotifications))); } + @Override + public void logNotificationDrag(NotificationEntry draggedNotification) { + } + public static class CallRecord { public boolean isLockscreen; public Notifications.NotificationList list; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java index 922e93d06efc..ed2afe753a5e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java @@ -40,6 +40,8 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.shade.ShadeController; +import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger; +import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerFake; import com.android.systemui.statusbar.policy.HeadsUpManager; import org.junit.Before; @@ -63,6 +65,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { private NotificationMenuRowPlugin.MenuItem mMenuItem = mock(NotificationMenuRowPlugin.MenuItem.class); private ShadeController mShadeController = mock(ShadeController.class); + private NotificationPanelLogger mNotificationPanelLogger = mock(NotificationPanelLogger.class); @Before public void setUp() throws Exception { @@ -82,7 +85,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem); mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager, - mShadeController); + mShadeController, mNotificationPanelLogger); } @Test @@ -96,6 +99,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { mRow.doDragCallback(0, 0); verify(controller).startDragAndDrop(mRow); verify(mHeadsUpManager, times(1)).releaseAllImmediately(); + verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any()); } @Test @@ -107,6 +111,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { verify(controller).startDragAndDrop(mRow); verify(mShadeController).animateCollapsePanels(eq(0), eq(true), eq(false), anyFloat()); + verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any()); } @Test @@ -124,6 +129,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { // Verify that we never start the actual drag since there is no content verify(mRow, never()).startDragAndDrop(any(), any(), any(), anyInt()); + verify(mNotificationPanelLogger, never()).logNotificationDrag(any()); } private ExpandableNotificationRowDragController createSpyController() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 8375e7cceb28..5394d88ad103 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -51,7 +51,7 @@ import androidx.test.filters.SmallTest; import androidx.test.filters.Suppress; import com.android.systemui.SysuiTestCase; -import com.android.systemui.media.MediaFeatureFlag; +import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.collection.NotificationEntry; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java deleted file mode 100644 index 682ff1fc8c52..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2014 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.statusbar.notification.row; - -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.view.NotificationHeaderView; -import android.view.View; -import android.view.ViewPropertyAnimator; - -import androidx.test.annotation.UiThreadTest; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.internal.R; -import com.android.internal.widget.NotificationExpandButton; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.media.dialog.MediaOutputDialogFactory; -import com.android.systemui.statusbar.notification.FeedbackIcon; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class NotificationContentViewTest extends SysuiTestCase { - - NotificationContentView mView; - - @Before - @UiThreadTest - public void setup() { - mDependency.injectMockDependency(MediaOutputDialogFactory.class); - - mView = new NotificationContentView(mContext, null); - ExpandableNotificationRow row = new ExpandableNotificationRow(mContext, null); - ExpandableNotificationRow mockRow = spy(row); - doReturn(10).when(mockRow).getIntrinsicHeight(); - - mView.setContainingNotification(mockRow); - mView.setHeights(10, 20, 30); - - mView.setContractedChild(createViewWithHeight(10)); - mView.setExpandedChild(createViewWithHeight(20)); - mView.setHeadsUpChild(createViewWithHeight(30)); - - mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); - } - - private View createViewWithHeight(int height) { - View view = new View(mContext, null); - view.setMinimumHeight(height); - return view; - } - - @Test - @UiThreadTest - public void testSetFeedbackIcon() { - View mockContracted = mock(NotificationHeaderView.class); - when(mockContracted.findViewById(com.android.internal.R.id.feedback)) - .thenReturn(mockContracted); - when(mockContracted.getContext()).thenReturn(mContext); - View mockExpanded = mock(NotificationHeaderView.class); - when(mockExpanded.findViewById(com.android.internal.R.id.feedback)) - .thenReturn(mockExpanded); - when(mockExpanded.getContext()).thenReturn(mContext); - View mockHeadsUp = mock(NotificationHeaderView.class); - when(mockHeadsUp.findViewById(com.android.internal.R.id.feedback)) - .thenReturn(mockHeadsUp); - when(mockHeadsUp.getContext()).thenReturn(mContext); - - mView.setContractedChild(mockContracted); - mView.setExpandedChild(mockExpanded); - mView.setHeadsUpChild(mockHeadsUp); - - mView.setFeedbackIcon(new FeedbackIcon(R.drawable.ic_feedback_alerted, - R.string.notification_feedback_indicator_alerted)); - - verify(mockContracted, times(1)).setVisibility(View.VISIBLE); - verify(mockExpanded, times(1)).setVisibility(View.VISIBLE); - verify(mockHeadsUp, times(1)).setVisibility(View.VISIBLE); - } - - @Test - @UiThreadTest - public void testExpandButtonFocusIsCalled() { - View mockContractedEB = mock(NotificationExpandButton.class); - View mockContracted = mock(NotificationHeaderView.class); - when(mockContracted.animate()).thenReturn(mock(ViewPropertyAnimator.class)); - when(mockContracted.findViewById(com.android.internal.R.id.expand_button)).thenReturn( - mockContractedEB); - when(mockContracted.getContext()).thenReturn(mContext); - - View mockExpandedEB = mock(NotificationExpandButton.class); - View mockExpanded = mock(NotificationHeaderView.class); - when(mockExpanded.animate()).thenReturn(mock(ViewPropertyAnimator.class)); - when(mockExpanded.findViewById(com.android.internal.R.id.expand_button)).thenReturn( - mockExpandedEB); - when(mockExpanded.getContext()).thenReturn(mContext); - - View mockHeadsUpEB = mock(NotificationExpandButton.class); - View mockHeadsUp = mock(NotificationHeaderView.class); - when(mockHeadsUp.animate()).thenReturn(mock(ViewPropertyAnimator.class)); - when(mockHeadsUp.findViewById(com.android.internal.R.id.expand_button)).thenReturn( - mockHeadsUpEB); - when(mockHeadsUp.getContext()).thenReturn(mContext); - - // Set up all 3 child forms - mView.setContractedChild(mockContracted); - mView.setExpandedChild(mockExpanded); - mView.setHeadsUpChild(mockHeadsUp); - - // This is required to call requestAccessibilityFocus() - mView.setFocusOnVisibilityChange(); - - // The following will initialize the view and switch from not visible to expanded. - // (heads-up is actually an alternate form of contracted, hence this enters expanded state) - mView.setHeadsUp(true); - - verify(mockContractedEB, times(0)).requestAccessibilityFocus(); - verify(mockExpandedEB, times(1)).requestAccessibilityFocus(); - verify(mockHeadsUpEB, times(0)).requestAccessibilityFocus(); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt new file mode 100644 index 000000000000..562b4dfb35ef --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import android.content.res.Resources +import android.os.UserHandle +import android.service.notification.StatusBarNotification +import android.testing.AndroidTestingRunner +import android.view.NotificationHeaderView +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.internal.widget.NotificationActionListLayout +import com.android.internal.widget.NotificationExpandButton +import com.android.systemui.SysuiTestCase +import com.android.systemui.media.dialog.MediaOutputDialogFactory +import com.android.systemui.statusbar.notification.FeedbackIcon +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations.initMocks + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class NotificationContentViewTest : SysuiTestCase() { + private lateinit var view: NotificationContentView + + @Mock private lateinit var mPeopleNotificationIdentifier: PeopleNotificationIdentifier + + private val notificationContentMargin = + mContext.resources.getDimensionPixelSize(R.dimen.notification_content_margin) + + @Before + fun setup() { + initMocks(this) + + mDependency.injectMockDependency(MediaOutputDialogFactory::class.java) + + view = spy(NotificationContentView(mContext, /* attrs= */ null)) + val row = ExpandableNotificationRow(mContext, /* attrs= */ null) + row.entry = createMockNotificationEntry(false) + val spyRow = spy(row) + doReturn(10).whenever(spyRow).intrinsicHeight + + with(view) { + initialize(mPeopleNotificationIdentifier, mock(), mock(), mock()) + setContainingNotification(spyRow) + setHeights(/* smallHeight= */ 10, /* headsUpMaxHeight= */ 20, /* maxHeight= */ 30) + contractedChild = createViewWithHeight(10) + expandedChild = createViewWithHeight(20) + headsUpChild = createViewWithHeight(30) + measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + layout(0, 0, view.measuredWidth, view.measuredHeight) + } + } + + private fun createViewWithHeight(height: Int) = + View(mContext, /* attrs= */ null).apply { minimumHeight = height } + + @Test + fun testSetFeedbackIcon() { + // Given: contractedChild, enpandedChild, and headsUpChild being set + val mockContracted = createMockNotificationHeaderView() + val mockExpanded = createMockNotificationHeaderView() + val mockHeadsUp = createMockNotificationHeaderView() + + with(view) { + contractedChild = mockContracted + expandedChild = mockExpanded + headsUpChild = mockHeadsUp + } + + // When: FeedBackIcon is set + view.setFeedbackIcon( + FeedbackIcon( + R.drawable.ic_feedback_alerted, + R.string.notification_feedback_indicator_alerted + ) + ) + + // Then: contractedChild, enpandedChild, and headsUpChild should be set to be visible + verify(mockContracted).visibility = View.VISIBLE + verify(mockExpanded).visibility = View.VISIBLE + verify(mockHeadsUp).visibility = View.VISIBLE + } + + private fun createMockNotificationHeaderView() = + mock<NotificationHeaderView>().apply { + whenever(this.findViewById<View>(R.id.feedback)).thenReturn(this) + whenever(this.context).thenReturn(mContext) + } + + @Test + fun testExpandButtonFocusIsCalled() { + val mockContractedEB = mock<NotificationExpandButton>() + val mockContracted = createMockNotificationHeaderView(mockContractedEB) + + val mockExpandedEB = mock<NotificationExpandButton>() + val mockExpanded = createMockNotificationHeaderView(mockExpandedEB) + + val mockHeadsUpEB = mock<NotificationExpandButton>() + val mockHeadsUp = createMockNotificationHeaderView(mockHeadsUpEB) + + // Set up all 3 child forms + view.contractedChild = mockContracted + view.expandedChild = mockExpanded + view.headsUpChild = mockHeadsUp + + // This is required to call requestAccessibilityFocus() + view.setFocusOnVisibilityChange() + + // The following will initialize the view and switch from not visible to expanded. + // (heads-up is actually an alternate form of contracted, hence this enters expanded state) + view.setHeadsUp(true) + verify(mockContractedEB, never()).requestAccessibilityFocus() + verify(mockExpandedEB).requestAccessibilityFocus() + verify(mockHeadsUpEB, never()).requestAccessibilityFocus() + } + + private fun createMockNotificationHeaderView(mockExpandedEB: NotificationExpandButton) = + mock<NotificationHeaderView>().apply { + whenever(this.animate()).thenReturn(mock()) + whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB) + whenever(this.context).thenReturn(mContext) + } + + @Test + fun testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() { + val mockContracted = mock<NotificationHeaderView>() + + val mockExpandedActions = mock<NotificationActionListLayout>() + val mockExpanded = mock<NotificationHeaderView>() + whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions) + + val mockHeadsUpActions = mock<NotificationActionListLayout>() + val mockHeadsUp = mock<NotificationHeaderView>() + whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions) + + with(view) { + contractedChild = mockContracted + expandedChild = mockExpanded + headsUpChild = mockHeadsUp + } + + view.setRemoteInputVisible(true) + + verify(mockContracted, never()).findViewById<View>(0) + verify(mockExpandedActions).importantForAccessibility = + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + verify(mockHeadsUpActions).importantForAccessibility = + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + } + + @Test + fun testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() { + val mockContracted = mock<NotificationHeaderView>() + + val mockExpandedActions = mock<NotificationActionListLayout>() + val mockExpanded = mock<NotificationHeaderView>() + whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions) + + val mockHeadsUpActions = mock<NotificationActionListLayout>() + val mockHeadsUp = mock<NotificationHeaderView>() + whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions) + + with(view) { + contractedChild = mockContracted + expandedChild = mockExpanded + headsUpChild = mockHeadsUp + } + + view.setRemoteInputVisible(false) + + verify(mockContracted, never()).findViewById<View>(0) + verify(mockExpandedActions).importantForAccessibility = + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + verify(mockHeadsUpActions).importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO + } + + @Test + fun setExpandedChild_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() { + // Given: bottom margin of actionListMarginTarget is notificationContentMargin + // Bubble button should not be shown for the given NotificationEntry + val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false) + val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) + val actionListMarginTarget = + spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) + val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) + whenever( + mockExpandedChild.findViewById<LinearLayout>( + R.id.notification_action_list_margin_target + ) + ) + .thenReturn(actionListMarginTarget) + view.setContainingNotification(mockContainingNotification) + + // When: call NotificationContentView.setExpandedChild() to set the expandedChild + view.expandedChild = mockExpandedChild + + // Then: bottom margin of actionListMarginTarget should not change, + // still be notificationContentMargin + assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) + } + + @Test + fun setExpandedChild_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() { + // Given: bottom margin of actionListMarginTarget is notificationContentMargin + // Bubble button should be shown for the given NotificationEntry + val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ true) + val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) + val actionListMarginTarget = + spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) + val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) + whenever( + mockExpandedChild.findViewById<LinearLayout>( + R.id.notification_action_list_margin_target + ) + ) + .thenReturn(actionListMarginTarget) + view.setContainingNotification(mockContainingNotification) + + // When: call NotificationContentView.setExpandedChild() to set the expandedChild + view.expandedChild = mockExpandedChild + + // Then: bottom margin of actionListMarginTarget should be set to 0 + assertEquals(0, getMarginBottom(actionListMarginTarget)) + } + + @Test + fun onNotificationUpdated_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() { + // Given: bottom margin of actionListMarginTarget is notificationContentMargin + val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false) + val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) + val actionListMarginTarget = + spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) + val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) + whenever( + mockExpandedChild.findViewById<LinearLayout>( + R.id.notification_action_list_margin_target + ) + ) + .thenReturn(actionListMarginTarget) + view.setContainingNotification(mockContainingNotification) + view.expandedChild = mockExpandedChild + assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) + + // When: call NotificationContentView.onNotificationUpdated() to update the + // NotificationEntry, which should not show bubble button + view.onNotificationUpdated(createMockNotificationEntry(/* showButton= */ false)) + + // Then: bottom margin of actionListMarginTarget should not change, still be 20 + assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) + } + + @Test + fun onNotificationUpdated_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() { + // Given: bottom margin of actionListMarginTarget is notificationContentMargin + val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false) + val mockContainingNotification = createMockContainingNotification(mockNotificationEntry) + val actionListMarginTarget = + spy(createLinearLayoutWithBottomMargin(notificationContentMargin)) + val mockExpandedChild = createMockExpandedChild(mockNotificationEntry) + whenever( + mockExpandedChild.findViewById<LinearLayout>( + R.id.notification_action_list_margin_target + ) + ) + .thenReturn(actionListMarginTarget) + view.setContainingNotification(mockContainingNotification) + view.expandedChild = mockExpandedChild + assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) + + // When: call NotificationContentView.onNotificationUpdated() to update the + // NotificationEntry, which should show bubble button + view.onNotificationUpdated(createMockNotificationEntry(true)) + + // Then: bottom margin of actionListMarginTarget should not change, still be 20 + assertEquals(0, getMarginBottom(actionListMarginTarget)) + } + + private fun createMockContainingNotification(notificationEntry: NotificationEntry) = + mock<ExpandableNotificationRow>().apply { + whenever(this.entry).thenReturn(notificationEntry) + whenever(this.context).thenReturn(mContext) + whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {}) + } + + private fun createMockNotificationEntry(showButton: Boolean) = + mock<NotificationEntry>().apply { + whenever(mPeopleNotificationIdentifier.getPeopleNotificationType(this)) + .thenReturn(PeopleNotificationIdentifier.TYPE_FULL_PERSON) + whenever(this.bubbleMetadata).thenReturn(mock()) + val sbnMock: StatusBarNotification = mock() + val userMock: UserHandle = mock() + whenever(this.sbn).thenReturn(sbnMock) + whenever(sbnMock.user).thenReturn(userMock) + doReturn(showButton).whenever(view).shouldShowBubbleButton(this) + } + + private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout { + val outerLayout = LinearLayout(mContext) + val innerLayout = LinearLayout(mContext) + outerLayout.addView(innerLayout) + val mlp = innerLayout.layoutParams as ViewGroup.MarginLayoutParams + mlp.setMargins(0, 0, 0, bottomMargin) + return innerLayout + } + + private fun createMockExpandedChild(notificationEntry: NotificationEntry) = + mock<ExpandableNotificationRow>().apply { + whenever(this.findViewById<ImageView>(R.id.bubble_button)).thenReturn(mock()) + whenever(this.findViewById<View>(R.id.actions_container)).thenReturn(mock()) + whenever(this.entry).thenReturn(notificationEntry) + whenever(this.context).thenReturn(mContext) + + val resourcesMock: Resources = mock() + whenever(resourcesMock.configuration).thenReturn(mock()) + whenever(this.resources).thenReturn(resourcesMock) + } + + private fun getMarginBottom(layout: LinearLayout): Int = + (layout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 9abdeb900c67..421f918a135e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -52,7 +52,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.TestableDependency; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; -import com.android.systemui.media.MediaFeatureFlag; +import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationMediaManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java index ad3bd711c23f..7c99568ee75f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java @@ -21,6 +21,10 @@ import static com.android.systemui.statusbar.notification.row.NotificationRowCon import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNotSame; +import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -31,6 +35,7 @@ import static org.mockito.Mockito.verify; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.util.Log; import androidx.test.filters.SmallTest; @@ -100,6 +105,67 @@ public class RowContentBindStageTest extends SysuiTestCase { verify(mBinder).unbindContent(eq(mEntry), any(), eq(flags)); } + class CountingWtfHandler implements Log.TerribleFailureHandler { + private Log.TerribleFailureHandler mOldHandler = null; + private int mWtfCount = 0; + + public void register() { + mOldHandler = Log.setWtfHandler(this); + } + + public void unregister() { + Log.setWtfHandler(mOldHandler); + mOldHandler = null; + } + + @Override + public void onTerribleFailure(String tag, Log.TerribleFailure what, boolean system) { + mWtfCount++; + } + + public int getWtfCount() { + return mWtfCount; + } + } + + @Test + public void testGetStageParamsAfterCleanUp() { + // GIVEN an entry whose params have already been deleted. + RowContentBindParams originalParams = mRowContentBindStage.getStageParams(mEntry); + mRowContentBindStage.deleteStageParams(mEntry); + + // WHEN a caller calls getStageParams. + CountingWtfHandler countingWtfHandler = new CountingWtfHandler(); + countingWtfHandler.register(); + + RowContentBindParams blankParams = mRowContentBindStage.getStageParams(mEntry); + + countingWtfHandler.unregister(); + + // THEN getStageParams logs a WTF and returns blank params created to avoid a crash. + assertEquals(1, countingWtfHandler.getWtfCount()); + assertNotNull(blankParams); + assertNotSame(originalParams, blankParams); + } + + @Test + public void testTryGetStageParamsAfterCleanUp() { + // GIVEN an entry whose params have already been deleted. + mRowContentBindStage.deleteStageParams(mEntry); + + // WHEN a caller calls getStageParams. + CountingWtfHandler countingWtfHandler = new CountingWtfHandler(); + countingWtfHandler.register(); + + RowContentBindParams nullParams = mRowContentBindStage.tryGetStageParams(mEntry); + + countingWtfHandler.unregister(); + + // THEN getStageParams does NOT log a WTF and returns null to indicate missing params. + assertEquals(0, countingWtfHandler.getWtfCount()); + assertNull(nullParams); + } + @Test public void testRebindAllContentViews() { // GIVEN a view with content bound. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt index 11798a7a4f96..87f4c323b7cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt @@ -361,6 +361,22 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.isOnKeyguard).isFalse() } // endregion + + // region mIsClosing + @Test + fun isClosing_whenShadeClosing_shouldReturnTrue() { + sut.setIsClosing(true) + + assertThat(sut.isClosing).isTrue() + } + + @Test + fun isClosing_whenShadeFinishClosing_shouldReturnFalse() { + sut.setIsClosing(false) + + assertThat(sut.isClosing).isFalse() + } + // endregion } // region Arrange helper methods. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java index a95a49c31adf..8c8b64424814 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java @@ -147,8 +147,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mFirst, mSecond), createSection(null, null) }); - Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f); } @Test @@ -170,13 +170,13 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { when(testHelper.getStatusBarStateController().isDozing()).thenReturn(true); row.setHeadsUp(true); mRoundnessManager.updateView(entry.getRow(), false); - Assert.assertEquals(1f, row.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1f, row.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1f, row.getBottomRoundness(), 0.0f); + Assert.assertEquals(1f, row.getTopRoundness(), 0.0f); row.setHeadsUp(false); mRoundnessManager.updateView(entry.getRow(), false); - Assert.assertEquals(mSmallRadiusRatio, row.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, row.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, row.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, row.getTopRoundness(), 0.0f); } @Test @@ -185,8 +185,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mFirst, mFirst), createSection(null, mSecond) }); - Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f); } @Test @@ -195,8 +195,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mFirst, mFirst), createSection(mSecond, null) }); - Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mSecond.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mSecond.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mSecond.getTopRoundness(), 0.0f); } @Test @@ -205,8 +205,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mFirst, null), createSection(null, null) }); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -215,8 +215,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f); } @Test @@ -226,8 +226,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -238,8 +238,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -250,8 +250,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -262,8 +262,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f); } @Test @@ -274,8 +274,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -286,8 +286,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mSecond, mSecond), createSection(null, null) }); - Assert.assertEquals(0.5f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(0.5f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(0.5f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(0.5f, mFirst.getTopRoundness(), 0.0f); } @Test @@ -298,8 +298,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(null, null) }); mFirst.setHeadsUpAnimatingAway(true); - Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f); } @@ -312,8 +312,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { }); mFirst.setHeadsUpAnimatingAway(true); mFirst.setHeadsUpAnimatingAway(false); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f); - Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f); + Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java index 9d848e87b0a0..ecc02246ea1d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java @@ -30,7 +30,7 @@ import android.view.ViewGroup; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.media.KeyguardMediaController; +import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 8be9eb57aa9b..90061b078afe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -45,7 +45,8 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.dump.DumpManager; -import com.android.systemui.media.KeyguardMediaController; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -127,6 +128,8 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private NotificationStackScrollLogger mLogger; @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator; @Mock private ShadeTransitionController mShadeTransitionController; + @Mock private FeatureFlags mFeatureFlags; + @Mock private NotificationTargetsHelper mNotificationTargetsHelper; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor; @@ -174,7 +177,9 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mJankMonitor, mStackLogger, mLogger, - mNotificationStackSizeCalculator + mNotificationStackSizeCalculator, + mFeatureFlags, + mNotificationTargetsHelper ); when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(true); 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 6ae021b48f66..91aecd8cf753 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 @@ -55,6 +55,7 @@ import android.view.ViewGroup; import androidx.test.annotation.UiThreadTest; import androidx.test.filters.SmallTest; +import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.systemui.ExpandHelper; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; @@ -162,7 +163,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mStackScroller.setCentralSurfaces(mCentralSurfaces); mStackScroller.setEmptyShadeView(mEmptyShadeView); when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true); - when(mStackScrollLayoutController.getNoticationRoundessManager()) + when(mStackScrollLayoutController.getNotificationRoundnessManager()) .thenReturn(mNotificationRoundnessManager); mStackScroller.setController(mStackScrollLayoutController); @@ -179,6 +180,40 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + public void testUpdateStackHeight_qsExpansionGreaterThanZero() { + final float expansionFraction = 0.2f; + final float overExpansion = 50f; + + mStackScroller.setQsExpansionFraction(1f); + mAmbientState.setExpansionFraction(expansionFraction); + mAmbientState.setOverExpansion(overExpansion); + when(mAmbientState.isBouncerInTransit()).thenReturn(true); + + + mStackScroller.setExpandedHeight(100f); + + float expected = MathUtils.lerp(0, overExpansion, + BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(expansionFraction)); + assertThat(mAmbientState.getStackY()).isEqualTo(expected); + } + + @Test + public void testUpdateStackHeight_qsExpansionZero() { + final float expansionFraction = 0.2f; + final float overExpansion = 50f; + + mStackScroller.setQsExpansionFraction(0f); + mAmbientState.setExpansionFraction(expansionFraction); + mAmbientState.setOverExpansion(overExpansion); + when(mAmbientState.isBouncerInTransit()).thenReturn(true); + + mStackScroller.setExpandedHeight(100f); + + float expected = MathUtils.lerp(0, overExpansion, expansionFraction); + assertThat(mAmbientState.getStackY()).isEqualTo(expected); + } + + @Test public void testUpdateStackHeight_withDozeAmount_whenDozeChanging() { final float dozeAmount = 0.5f; mAmbientState.setDozeAmount(dozeAmount); @@ -693,6 +728,57 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { verify(mNotificationStackSizeCalculator).computeHeight(any(), anyInt(), anyFloat()); } + @Test + public void testSetOwnScrollY_shadeNotClosing_scrollYChanges() { + // Given: shade is not closing, scrollY is 0 + mAmbientState.setScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + mAmbientState.setIsClosing(false); + + // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1 + mStackScroller.setOwnScrollY(1); + + // Then: scrollY should be set to 1 + assertEquals(1, mAmbientState.getScrollY()); + + // Reset scrollY back to 0 to avoid interfering with other tests + mStackScroller.setOwnScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + } + + @Test + public void testSetOwnScrollY_shadeClosing_scrollYDoesNotChange() { + // Given: shade is closing, scrollY is 0 + mAmbientState.setScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + mAmbientState.setIsClosing(true); + + // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1 + mStackScroller.setOwnScrollY(1); + + // Then: scrollY should not change, it should still be 0 + assertEquals(0, mAmbientState.getScrollY()); + + // Reset scrollY and mAmbientState.mIsClosing to avoid interfering with other tests + mAmbientState.setIsClosing(false); + mStackScroller.setOwnScrollY(0); + assertEquals(0, mAmbientState.getScrollY()); + } + + @Test + public void onShadeFlingClosingEnd_scrollYShouldBeSetToZero() { + // Given: mAmbientState.mIsClosing is set to be true + // mIsExpanded is set to be false + mAmbientState.setIsClosing(true); + mStackScroller.setIsExpanded(false); + + // When: onExpansionStopped is called + mStackScroller.onExpansionStopped(); + + // Then: mAmbientState.scrollY should be set to be 0 + assertEquals(mAmbientState.getScrollY(), 0); + } + private void setBarStateForTest(int state) { // Can't inject this through the listener or we end up on the actual implementation // rather than the mock because the spy just coppied the anonymous inner /shruggie. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java index 1305d79e4648..4ea1c7100557 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java @@ -18,10 +18,13 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -51,10 +54,15 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + /** * Tests for {@link NotificationSwipeHelper}. */ @@ -74,7 +82,11 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { private Runnable mFalsingCheck; private FeatureFlags mFeatureFlags; - @Rule public MockitoRule mockito = MockitoJUnit.rule(); + private static final int FAKE_ROW_WIDTH = 20; + private static final int FAKE_ROW_HEIGHT = 20; + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); @Before public void setUp() throws Exception { @@ -444,8 +456,8 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { doReturn(5f).when(mEvent).getRawX(); doReturn(10f).when(mEvent).getRawY(); - doReturn(20).when(mView).getWidth(); - doReturn(20).when(mView).getHeight(); + doReturn(FAKE_ROW_WIDTH).when(mView).getWidth(); + doReturn(FAKE_ROW_HEIGHT).when(mView).getHeight(); Answer answer = (Answer) invocation -> { int[] arr = invocation.getArgument(0); @@ -472,8 +484,8 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { doReturn(5f).when(mEvent).getRawX(); doReturn(10f).when(mEvent).getRawY(); - doReturn(20).when(mNotificationRow).getWidth(); - doReturn(20).when(mNotificationRow).getActualHeight(); + doReturn(FAKE_ROW_WIDTH).when(mNotificationRow).getWidth(); + doReturn(FAKE_ROW_HEIGHT).when(mNotificationRow).getActualHeight(); Answer answer = (Answer) invocation -> { int[] arr = invocation.getArgument(0); @@ -491,4 +503,56 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { assertFalse("Touch is not within the view", mSwipeHelper.isTouchInView(mEvent, mNotificationRow)); } + + @Test + public void testContentAlphaRemainsUnchangedWhenNotificationIsNotDismissible() { + doReturn(FAKE_ROW_WIDTH).when(mNotificationRow).getMeasuredWidth(); + + mSwipeHelper.onTranslationUpdate(mNotificationRow, 12, false); + + verify(mNotificationRow, never()).setContentAlpha(anyFloat()); + } + + @Test + public void testContentAlphaRemainsUnchangedWhenFeatureFlagIsDisabled() { + + // Returning true prevents alpha fade. In an unmocked scenario the callback is instantiated + // within NotificationStackScrollLayoutController and returns the inverted value of the + // feature flag + doReturn(true).when(mCallback).updateSwipeProgress(any(), anyBoolean(), anyFloat()); + doReturn(FAKE_ROW_WIDTH).when(mNotificationRow).getMeasuredWidth(); + + mSwipeHelper.onTranslationUpdate(mNotificationRow, 12, true); + + verify(mNotificationRow, never()).setContentAlpha(anyFloat()); + } + + @Test + public void testContentAlphaFadeAnimationSpecs() { + // The alpha fade should be linear from 1f to 0f as translation progresses from 0 to 60% of + // view-width, and 0f at translations higher than that. + doReturn(FAKE_ROW_WIDTH).when(mNotificationRow).getMeasuredWidth(); + + List<Integer> translations = Arrays.asList( + -FAKE_ROW_WIDTH * 2, + -FAKE_ROW_WIDTH, + (int) (-FAKE_ROW_WIDTH * 0.3), + 0, + (int) (FAKE_ROW_WIDTH * 0.3), + (int) (FAKE_ROW_WIDTH * 0.6), + FAKE_ROW_WIDTH, + FAKE_ROW_WIDTH * 2); + List<Float> expectedAlphas = translations.stream().map(translation -> + mSwipeHelper.getSwipeAlpha(Math.abs((float) translation / FAKE_ROW_WIDTH))) + .collect(Collectors.toList()); + + for (Integer translation : translations) { + mSwipeHelper.onTranslationUpdate(mNotificationRow, translation, true); + } + + ArgumentCaptor<Float> capturedValues = ArgumentCaptor.forClass(Float.class); + verify(mNotificationRow, times(translations.size())).setContentAlpha( + capturedValues.capture()); + assertEquals(expectedAlphas, capturedValues.getAllValues()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt new file mode 100644 index 000000000000..a2e92305bf27 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt @@ -0,0 +1,107 @@ +package com.android.systemui.statusbar.notification.stack + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.android.systemui.util.mockito.mock +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for {@link NotificationTargetsHelper}. */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class NotificationTargetsHelperTest : SysuiTestCase() { + lateinit var notificationTestHelper: NotificationTestHelper + private val sectionsManager: NotificationSectionsManager = mock() + private val stackScrollLayout: NotificationStackScrollLayout = mock() + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + notificationTestHelper = + NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + } + + private fun notificationTargetsHelper( + notificationGroupCorner: Boolean = true, + ) = + NotificationTargetsHelper( + FakeFeatureFlags().apply { + set(Flags.NOTIFICATION_GROUP_CORNER, notificationGroupCorner) + } + ) + + @Test + fun targetsForFirstNotificationInGroup() { + val children = notificationTestHelper.createGroup(3).childrenContainer + val swiped = children.attachedChildren[0] + + val actual = + notificationTargetsHelper() + .findRoundableTargets( + viewSwiped = swiped, + stackScrollLayout = stackScrollLayout, + sectionsManager = sectionsManager, + ) + + val expected = + RoundableTargets( + before = children.notificationHeaderWrapper, // group header + swiped = swiped, + after = children.attachedChildren[1], + ) + assertEquals(expected, actual) + } + + @Test + fun targetsForMiddleNotificationInGroup() { + val children = notificationTestHelper.createGroup(3).childrenContainer + val swiped = children.attachedChildren[1] + + val actual = + notificationTargetsHelper() + .findRoundableTargets( + viewSwiped = swiped, + stackScrollLayout = stackScrollLayout, + sectionsManager = sectionsManager, + ) + + val expected = + RoundableTargets( + before = children.attachedChildren[0], + swiped = swiped, + after = children.attachedChildren[2], + ) + assertEquals(expected, actual) + } + + @Test + fun targetsForLastNotificationInGroup() { + val children = notificationTestHelper.createGroup(3).childrenContainer + val swiped = children.attachedChildren[2] + + val actual = + notificationTargetsHelper() + .findRoundableTargets( + viewSwiped = swiped, + stackScrollLayout = stackScrollLayout, + sectionsManager = sectionsManager, + ) + + val expected = + RoundableTargets( + before = children.attachedChildren[1], + swiped = swiped, + after = null, + ) + assertEquals(expected, actual) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt new file mode 100644 index 000000000000..da543d4454b8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.notification.stack + +import android.testing.AndroidTestingRunner +import android.util.Log +import android.util.Log.TerribleFailureHandler +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import kotlin.math.log2 +import kotlin.math.sqrt +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ViewStateTest : SysuiTestCase() { + private val viewState = ViewState() + + private var wtfHandler: TerribleFailureHandler? = null + private var wtfCount = 0 + + @Suppress("DIVISION_BY_ZERO") + @Test + fun testWtfs() { + interceptWtfs() + + // Setting valid values doesn't cause any wtfs. + viewState.alpha = 0.1f + viewState.xTranslation = 0f + viewState.yTranslation = 10f + viewState.zTranslation = 20f + viewState.scaleX = 0.5f + viewState.scaleY = 0.25f + + expectWtfs(0) + + // Setting NaN values leads to wtfs being logged, and the value not being changed. + viewState.alpha = 0.0f / 0.0f + expectWtfs(1) + Assert.assertEquals(viewState.alpha, 0.1f) + + viewState.xTranslation = Float.NaN + expectWtfs(2) + Assert.assertEquals(viewState.xTranslation, 0f) + + viewState.yTranslation = log2(-10.0).toFloat() + expectWtfs(3) + Assert.assertEquals(viewState.yTranslation, 10f) + + viewState.zTranslation = sqrt(-1.0).toFloat() + expectWtfs(4) + Assert.assertEquals(viewState.zTranslation, 20f) + + viewState.scaleX = Float.POSITIVE_INFINITY + Float.NEGATIVE_INFINITY + expectWtfs(5) + Assert.assertEquals(viewState.scaleX, 0.5f) + + viewState.scaleY = Float.POSITIVE_INFINITY * 0 + expectWtfs(6) + Assert.assertEquals(viewState.scaleY, 0.25f) + } + + private fun interceptWtfs() { + wtfCount = 0 + wtfHandler = + Log.setWtfHandler { _: String?, e: Log.TerribleFailure, _: Boolean -> + Log.e("ViewStateTest", "Observed WTF: $e") + wtfCount++ + } + } + + private fun expectWtfs(expectedWtfCount: Int) { + Assert.assertNotNull(wtfHandler) + Assert.assertEquals(expectedWtfCount, wtfCount) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index cd0cc33df1a9..6fa217415044 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -100,8 +100,6 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { @Mock private AuthController mAuthController; @Mock - private DozeParameters mDozeParameters; - @Mock private MetricsLogger mMetricsLogger; @Mock private NotificationMediaManager mNotificationMediaManager; @@ -127,7 +125,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(this); TestableResources res = getContext().getOrCreateTestableResources(); - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true); when(mKeyguardStateController.isFaceAuthEnabled()).thenReturn(true); when(mKeyguardStateController.isUnlocked()).thenReturn(false); @@ -139,7 +137,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mBiometricUnlockController = new BiometricUnlockController(mDozeScrimController, mKeyguardViewMediator, mScrimController, mShadeController, mNotificationShadeWindowController, mKeyguardStateController, mHandler, - mUpdateMonitor, res.getResources(), mKeyguardBypassController, mDozeParameters, + mUpdateMonitor, res.getResources(), mKeyguardBypassController, mMetricsLogger, mDumpManager, mPowerManager, mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle, mAuthController, mStatusBarStateController, mKeyguardUnlockAnimationController, @@ -177,7 +175,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { public void onBiometricAuthenticated_whenFingerprintAndNotInteractive_wakeAndUnlock() { reset(mUpdateMonitor); reset(mStatusBarKeyguardViewManager); - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); when(mDozeScrimController.isPulsing()).thenReturn(true); // the value of isStrongBiometric doesn't matter here since we only care about the returned @@ -194,7 +192,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { public void onBiometricAuthenticated_whenDeviceIsAlreadyUnlocked_wakeAndUnlock() { reset(mUpdateMonitor); reset(mStatusBarKeyguardViewManager); - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false); + when(mKeyguardStateController.isShowing()).thenReturn(false); when(mKeyguardStateController.isUnlocked()).thenReturn(true); when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); when(mDozeScrimController.isPulsing()).thenReturn(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index f510e48de5a5..57557821cca6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -80,6 +80,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; import com.android.internal.jank.InteractionJankMonitor; +import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.logging.testing.FakeMetricsLogger; import com.android.internal.statusbar.IStatusBarService; @@ -98,7 +99,8 @@ import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -116,6 +118,7 @@ import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeControllerImpl; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.KeyguardIndicationController; @@ -150,7 +153,6 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -233,6 +235,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private NavigationBarController mNavigationBarController; @Mock private AccessibilityFloatingMenuController mAccessibilityFloatingMenuController; @Mock private SysuiColorExtractor mColorExtractor; + private WakefulnessLifecycle mWakefulnessLifecycle; @Mock private ColorExtractor.GradientColors mGradientColors; @Mock private PulseExpansionHandler mPulseExpansionHandler; @Mock private NotificationWakeUpCoordinator mNotificationWakeUpCoordinator; @@ -271,7 +274,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private OngoingCallController mOngoingCallController; @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; @Mock private LockscreenShadeTransitionController mLockscreenTransitionController; - @Mock private FeatureFlags mFeatureFlags; @Mock private NotificationVisibilityProvider mVisibilityProvider; @Mock private WallpaperManager mWallpaperManager; @Mock private IWallpaperManager mIWallpaperManager; @@ -296,9 +298,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); - private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); - private FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock); - private InitController mInitController = new InitController(); + private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); + private final FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock); + private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); + private final InitController mInitController = new InitController(); private final DumpManager mDumpManager = new DumpManager(); @Before @@ -322,7 +325,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mock(NotificationInterruptLogger.class), new Handler(TestableLooper.get(this).getLooper()), mock(NotifPipelineFlags.class), - mock(KeyguardNotificationVisibilityProvider.class)); + mock(KeyguardNotificationVisibilityProvider.class), + mock(UiEventLogger.class)); mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class)); mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class)); @@ -363,10 +367,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { return null; }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any()); - WakefulnessLifecycle wakefulnessLifecycle = + mWakefulnessLifecycle = new WakefulnessLifecycle(mContext, mIWallpaperManager, mDumpManager); - wakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN); - wakefulnessLifecycle.dispatchFinishedWakingUp(); + mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN); + mWakefulnessLifecycle.dispatchFinishedWakingUp(); when(mGradientColors.supportsDarkText()).thenReturn(true); when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors); @@ -413,7 +417,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mNotificationGutsManager, notificationLogger, mNotificationInterruptStateProvider, - new PanelExpansionStateManager(), + new ShadeExpansionStateManager(), mKeyguardViewMediator, new DisplayMetrics(), mMetricsLogger, @@ -425,7 +429,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mBatteryController, mColorExtractor, new ScreenLifecycle(mDumpManager), - wakefulnessLifecycle, + mWakefulnessLifecycle, mStatusBarStateController, Optional.of(mBubbles), mDeviceProvisionedController, @@ -486,7 +490,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { when(mKeyguardViewMediator.registerCentralSurfaces( any(CentralSurfacesImpl.class), any(NotificationPanelViewController.class), - any(PanelExpansionStateManager.class), + any(ShadeExpansionStateManager.class), any(BiometricUnlockController.class), any(ViewGroup.class), any(KeyguardBypassController.class))) @@ -504,6 +508,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mCentralSurfaces.mKeyguardIndicationController = mKeyguardIndicationController; mCentralSurfaces.mBarService = mBarService; mCentralSurfaces.mStackScroller = mStackScroller; + mCentralSurfaces.mGestureWakeLock = mPowerManager.newWakeLock( + PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "sysui:GestureWakeLock"); mCentralSurfaces.startKeyguard(); mInitController.executePostInitTasks(); notificationLogger.setUpWithContainer(mNotificationListContainer); @@ -516,32 +522,32 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Test public void executeRunnableDismissingKeyguard_nullRunnable_showingAndOccluded() { - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true); - when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isOccluded()).thenReturn(true); mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false); } @Test public void executeRunnableDismissingKeyguard_nullRunnable_showing() { - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true); - when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false); + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isOccluded()).thenReturn(false); mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false); } @Test public void executeRunnableDismissingKeyguard_nullRunnable_notShowing() { - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false); + when(mKeyguardStateController.isShowing()).thenReturn(false); + when(mKeyguardStateController.isOccluded()).thenReturn(false); mCentralSurfaces.executeRunnableDismissingKeyguard(null, null, false, false, false); } @Test public void executeRunnableDismissingKeyguard_dreaming_notShowing() throws RemoteException { - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false); + when(mKeyguardStateController.isShowing()).thenReturn(false); + when(mKeyguardStateController.isOccluded()).thenReturn(false); when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(true); mCentralSurfaces.executeRunnableDismissingKeyguard(() -> {}, @@ -555,8 +561,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Test public void executeRunnableDismissingKeyguard_notDreaming_notShowing() throws RemoteException { - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false); + when(mKeyguardStateController.isShowing()).thenReturn(false); + when(mKeyguardStateController.isOccluded()).thenReturn(false); when(mKeyguardUpdateMonitor.isDreaming()).thenReturn(false); mCentralSurfaces.executeRunnableDismissingKeyguard(() -> {}, @@ -571,10 +577,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Test public void lockscreenStateMetrics_notShowing() { // uninteresting state, except that fingerprint must be non-zero - when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false); + when(mKeyguardStateController.isOccluded()).thenReturn(false); when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); // interesting state - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false); + when(mKeyguardStateController.isShowing()).thenReturn(false); when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false); when(mKeyguardStateController.isMethodSecure()).thenReturn(false); mCentralSurfaces.onKeyguardViewManagerStatesUpdated(); @@ -589,10 +595,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Test public void lockscreenStateMetrics_notShowing_secure() { // uninteresting state, except that fingerprint must be non-zero - when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false); + when(mKeyguardStateController.isOccluded()).thenReturn(false); when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); // interesting state - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(false); + when(mKeyguardStateController.isShowing()).thenReturn(false); when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false); when(mKeyguardStateController.isMethodSecure()).thenReturn(true); @@ -608,10 +614,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Test public void lockscreenStateMetrics_isShowing() { // uninteresting state, except that fingerprint must be non-zero - when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false); + when(mKeyguardStateController.isOccluded()).thenReturn(false); when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); // interesting state - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false); when(mKeyguardStateController.isMethodSecure()).thenReturn(false); @@ -627,10 +633,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Test public void lockscreenStateMetrics_isShowing_secure() { // uninteresting state, except that fingerprint must be non-zero - when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false); + when(mKeyguardStateController.isOccluded()).thenReturn(false); when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); // interesting state - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false); when(mKeyguardStateController.isMethodSecure()).thenReturn(true); @@ -646,10 +652,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Test public void lockscreenStateMetrics_isShowingBouncer() { // uninteresting state, except that fingerprint must be non-zero - when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(false); + when(mKeyguardStateController.isOccluded()).thenReturn(false); when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); // interesting state - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true); when(mKeyguardStateController.isMethodSecure()).thenReturn(true); @@ -1017,6 +1023,60 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test + public void collapseShade_callsAnimateCollapsePanels_whenExpanded() { + // GIVEN the shade is expanded + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + + // WHEN collapseShade is called + mCentralSurfaces.collapseShade(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController).animateCollapsePanels(); + } + + @Test + public void collapseShade_doesNotCallAnimateCollapsePanels_whenCollapsed() { + // GIVEN the shade is collapsed + mCentralSurfaces.setPanelExpanded(false); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + + // WHEN collapseShade is called + mCentralSurfaces.collapseShade(); + + // VERIFY that animateCollapsePanels is NOT called + verify(mShadeController, never()).animateCollapsePanels(); + } + + @Test + public void collapseShadeForBugReport_callsAnimateCollapsePanels_whenFlagDisabled() { + // GIVEN the shade is expanded & flag enabled + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, false); + + // WHEN collapseShadeForBugreport is called + mCentralSurfaces.collapseShadeForBugreport(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController).animateCollapsePanels(); + } + + @Test + public void collapseShadeForBugReport_doesNotCallAnimateCollapsePanels_whenFlagEnabled() { + // GIVEN the shade is expanded & flag enabled + mCentralSurfaces.setPanelExpanded(true); + mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE); + mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, true); + + // WHEN collapseShadeForBugreport is called + mCentralSurfaces.collapseShadeForBugreport(); + + // VERIFY that animateCollapsePanels is called + verify(mShadeController, never()).animateCollapsePanels(); + } + + @Test public void deviceStateChange_unfolded_shadeOpen_setsLeaveOpenOnKeyguardHide() { when(mKeyguardStateController.isShowing()).thenReturn(false); setFoldedStates(FOLD_STATE_FOLDED); @@ -1053,9 +1113,9 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test - public void startActivityDismissingKeyguard_isShowingandIsOccluded() { - when(mStatusBarKeyguardViewManager.isShowing()).thenReturn(true); - when(mStatusBarKeyguardViewManager.isOccluded()).thenReturn(true); + public void startActivityDismissingKeyguard_isShowingAndIsOccluded() { + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isOccluded()).thenReturn(true); mCentralSurfaces.startActivityDismissingKeyguard( new Intent(), /* onlyProvisioned = */false, @@ -1068,6 +1128,55 @@ public class CentralSurfacesImplTest extends SysuiTestCase { assertThat(onDismissActionCaptor.getValue().onDismiss()).isFalse(); } + @Test + public void testKeyguardHideDelayedIfOcclusionAnimationRunning() { + // Show the keyguard and verify we've done so. + setKeyguardShowingAndOccluded(true /* showing */, false /* occluded */); + verify(mStatusBarStateController).setState(StatusBarState.KEYGUARD); + + // Request to hide the keyguard, but while the occlude animation is playing. We should delay + // this hide call, since we're playing the occlude animation over the keyguard and thus want + // it to remain visible. + when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true); + setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */); + verify(mStatusBarStateController, never()).setState(StatusBarState.SHADE); + + // Once the animation ends, verify that the keyguard is actually hidden. + when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(false); + setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */); + verify(mStatusBarStateController).setState(StatusBarState.SHADE); + } + + @Test + public void testKeyguardHideNotDelayedIfOcclusionAnimationNotRunning() { + // Show the keyguard and verify we've done so. + setKeyguardShowingAndOccluded(true /* showing */, false /* occluded */); + verify(mStatusBarStateController).setState(StatusBarState.KEYGUARD); + + // Hide the keyguard while the occlusion animation is not running. Verify that we + // immediately hide the keyguard. + when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(false); + setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */); + verify(mStatusBarStateController).setState(StatusBarState.SHADE); + } + + /** + * Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard} + * to reconfigure the keyguard to reflect the requested showing/occluded states. + */ + private void setKeyguardShowingAndOccluded(boolean showing, boolean occluded) { + when(mStatusBarStateController.isKeyguardRequested()).thenReturn(showing); + when(mKeyguardStateController.isOccluded()).thenReturn(occluded); + + // If we want to show the keyguard, make sure that we think we're awake and not unlocking. + if (showing) { + when(mBiometricUnlockController.isWakeAndUnlock()).thenReturn(false); + mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN); + } + + mCentralSurfaces.updateIsKeyguard(false /* forceStateChange */); + } + private void setDeviceState(int state) { ArgumentCaptor<DeviceStateManager.DeviceStateCallback> callbackCaptor = ArgumentCaptor.forClass(DeviceStateManager.DeviceStateCallback.class); @@ -1102,7 +1211,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { NotificationInterruptLogger logger, Handler mainHandler, NotifPipelineFlags flags, - KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) { + KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, + UiEventLogger uiEventLogger) { super( contentResolver, powerManager, @@ -1115,7 +1225,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { logger, mainHandler, flags, - keyguardNotificationVisibilityProvider + keyguardNotificationVisibilityProvider, + uiEventLogger ); mUseHeadsUp = true; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java index 9de9db1d39e7..996851e218f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java @@ -40,7 +40,6 @@ import com.android.systemui.assist.AssistManager; import com.android.systemui.biometrics.AuthController; import com.android.systemui.doze.DozeHost; import com.android.systemui.doze.DozeLog; -import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.NotificationShadeWindowViewController; @@ -73,7 +72,6 @@ public class DozeServiceHostTest extends SysuiTestCase { @Mock private HeadsUpManagerPhone mHeadsUpManager; @Mock private ScrimController mScrimController; @Mock private DozeScrimController mDozeScrimController; - @Mock private KeyguardViewMediator mKeyguardViewMediator; @Mock private StatusBarStateControllerImpl mStatusBarStateController; @Mock private BatteryController mBatteryController; @Mock private DeviceProvisionedController mDeviceProvisionedController; @@ -101,7 +99,7 @@ public class DozeServiceHostTest extends SysuiTestCase { mDozeServiceHost = new DozeServiceHost(mDozeLog, mPowerManager, mWakefullnessLifecycle, mStatusBarStateController, mDeviceProvisionedController, mHeadsUpManager, mBatteryController, mScrimController, () -> mBiometricUnlockController, - mKeyguardViewMediator, () -> mAssistManager, mDozeScrimController, + () -> mAssistManager, mDozeScrimController, mKeyguardUpdateMonitor, mPulseExpansionHandler, mNotificationShadeWindowController, mNotificationWakeUpCoordinator, mAuthController, mNotificationIconAreaController); @@ -132,19 +130,11 @@ public class DozeServiceHostTest extends SysuiTestCase { verify(mStatusBarStateController).setIsDozing(eq(false)); } - @Test public void testPulseWhileDozing_updatesScrimController() { mCentralSurfaces.setBarStateForTest(StatusBarState.KEYGUARD); mCentralSurfaces.showKeyguardImpl(); - // Keep track of callback to be able to stop the pulse -// DozeHost.PulseCallback[] pulseCallback = new DozeHost.PulseCallback[1]; -// doAnswer(invocation -> { -// pulseCallback[0] = invocation.getArgument(0); -// return null; -// }).when(mDozeScrimController).pulse(any(), anyInt()); - // Starting a pulse should change the scrim controller to the pulsing state mDozeServiceHost.pulseWhileDozing(new DozeHost.PulseCallback() { @Override @@ -210,4 +200,17 @@ public class DozeServiceHostTest extends SysuiTestCase { } } } + + @Test + public void testStopPulsing_setPendingPulseToFalse() { + // GIVEN a pending pulse + mDozeServiceHost.setPulsePending(true); + + // WHEN pulsing is stopped + mDozeServiceHost.stopPulsing(); + + // THEN isPendingPulse=false, pulseOutNow is called + assertFalse(mDozeServiceHost.isPulsePending()); + verify(mDozeScrimController).pulseOutNow(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java new file mode 100644 index 000000000000..a986777afa22 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/FakeKeyguardStateController.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone; + +import com.android.systemui.statusbar.policy.KeyguardStateController; + +/** + * Mock implementation of KeyguardStateController which tracks showing and occluded states + * based on {@link #notifyKeyguardState(boolean showing, boolean occluded)}}. + */ +public class FakeKeyguardStateController implements KeyguardStateController { + private boolean mShowing; + private boolean mOccluded; + private boolean mCanDismissLockScreen; + + @Override + public void notifyKeyguardState(boolean showing, boolean occluded) { + mShowing = showing; + mOccluded = occluded; + } + + @Override + public boolean isShowing() { + return mShowing; + } + + @Override + public boolean isOccluded() { + return mOccluded; + } + + public void setCanDismissLockScreen(boolean canDismissLockScreen) { + mCanDismissLockScreen = canDismissLockScreen; + } + + @Override + public boolean canDismissLockScreen() { + return mCanDismissLockScreen; + } + + @Override + public boolean isBouncerShowing() { + return false; + } + + @Override + public boolean isKeyguardScreenRotationAllowed() { + return false; + } + + @Override + public boolean isMethodSecure() { + return true; + } + + @Override + public boolean isTrusted() { + return false; + } + + @Override + public boolean isKeyguardGoingAway() { + return false; + } + + @Override + public boolean isKeyguardFadingAway() { + return false; + } + + @Override + public boolean isLaunchTransitionFadingAway() { + return false; + } + + @Override + public long getKeyguardFadingAwayDuration() { + return 0; + } + + @Override + public long getKeyguardFadingAwayDelay() { + return 0; + } + + @Override + public long calculateGoingToFullShadeDelay() { + return 0; + } + + @Override + public float getDismissAmount() { + return 0f; + } + + @Override + public boolean isDismissingFromSwipe() { + return false; + } + + @Override + public boolean isFlingingToDismissKeyguard() { + return false; + } + + @Override + public boolean isFlingingToDismissKeyguardDuringSwipeGesture() { + return false; + } + + @Override + public boolean isSnappingKeyguardBackAfterSwipe() { + return false; + } + + @Override + public void notifyPanelFlingStart(boolean dismiss) { + } + + @Override + public void notifyPanelFlingEnd() { + } + + @Override + public void addCallback(Callback listener) { + } + + @Override + public void removeCallback(Callback listener) { + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index cfaa4707ef76..6ec5cf82a81e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -47,6 +47,7 @@ import androidx.test.filters.SmallTest; import com.android.keyguard.CarrierTextController; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.battery.BatteryMeterViewController; @@ -123,6 +124,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { private StatusBarUserInfoTracker mStatusBarUserInfoTracker; @Mock private SecureSettings mSecureSettings; @Mock private CommandQueue mCommandQueue; + @Mock private KeyguardLogger mLogger; private TestNotificationPanelViewStateProvider mNotificationPanelViewStateProvider; private KeyguardStatusBarView mKeyguardStatusBarView; @@ -172,7 +174,8 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mStatusBarUserInfoTracker, mSecureSettings, mCommandQueue, - mFakeExecutor + mFakeExecutor, + mLogger ); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 4d1a52c494ab..a5deaa45bf0f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.statusbar.phone.ScrimController.KEYGUARD_SCRIM_ALPHA; import static com.android.systemui.statusbar.phone.ScrimController.OPAQUE; import static com.android.systemui.statusbar.phone.ScrimController.SEMI_TRANSPARENT; import static com.android.systemui.statusbar.phone.ScrimController.TRANSPARENT; @@ -58,6 +59,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.scrim.ScrimView; import com.android.systemui.statusbar.policy.FakeConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -117,6 +119,7 @@ public class ScrimControllerTest extends SysuiTestCase { // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The // event-dispatch-on-registration pattern caused some of these unit tests to fail.) @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + @Mock private KeyguardViewMediator mKeyguardViewMediator; private static class AnimatorListener implements Animator.AnimatorListener { private int mNumStarts; @@ -230,7 +233,8 @@ public class ScrimControllerTest extends SysuiTestCase { mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, - mStatusBarKeyguardViewManager); + mStatusBarKeyguardViewManager, + mKeyguardViewMediator); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -239,6 +243,8 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.setWallpaperSupportsAmbientMode(false); mScrimController.transitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); + + mScrimController.setLaunchingAffordanceWithPreview(false); } @After @@ -852,7 +858,8 @@ public class ScrimControllerTest extends SysuiTestCase { mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, - mStatusBarKeyguardViewManager); + mStatusBarKeyguardViewManager, + mKeyguardViewMediator); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -1592,6 +1599,30 @@ public class ScrimControllerTest extends SysuiTestCase { assertScrimAlpha(mScrimBehind, 0); } + @Test + public void keyguardAlpha_whenUnlockedForOcclusion_ifPlayingOcclusionAnimation() { + mScrimController.transitionTo(ScrimState.KEYGUARD); + + when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true); + + mScrimController.transitionTo(ScrimState.UNLOCKED); + finishAnimationsImmediately(); + + assertScrimAlpha(mNotificationsScrim, (int) (KEYGUARD_SCRIM_ALPHA * 255f)); + } + + @Test + public void keyguardAlpha_whenUnlockedForLaunch_ifLaunchingAffordance() { + mScrimController.transitionTo(ScrimState.KEYGUARD); + when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true); + mScrimController.setLaunchingAffordanceWithPreview(true); + + mScrimController.transitionTo(ScrimState.UNLOCKED); + finishAnimationsImmediately(); + + assertScrimAlpha(mNotificationsScrim, (int) (KEYGUARD_SCRIM_ALPHA * 255f)); + } + private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) { mScrimController.setRawPanelExpansionFraction(expansion); finishAnimationsImmediately(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java index 34399b80c9f7..9c56c2670c63 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java @@ -44,6 +44,7 @@ import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel; import com.android.systemui.utils.leaks.LeakCheckedTest; @@ -80,6 +81,7 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { StatusBarLocation.HOME, mock(StatusBarPipelineFlags.class), mock(WifiViewModel.class), + mock(MobileUiAdapter.class), mMobileContextProvider, mock(DarkIconDispatcher.class)); testCallOnAdd_forManager(manager); @@ -123,12 +125,14 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, + MobileUiAdapter mobileUiAdapter, MobileContextProvider contextProvider, DarkIconDispatcher darkIconDispatcher) { super(group, location, statusBarPipelineFlags, wifiViewModel, + mobileUiAdapter, contextProvider, darkIconDispatcher); } @@ -169,6 +173,7 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { StatusBarLocation.HOME, mock(StatusBarPipelineFlags.class), mock(WifiViewModel.class), + mock(MobileUiAdapter.class), contextProvider); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index dcce61b86ced..0c35659b458a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.flags.Flags.MODERN_BOUNCER; + import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -26,6 +28,7 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,6 +36,10 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; import android.view.ViewGroup; +import android.view.ViewRootImpl; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; +import android.window.WindowOnBackInvokedDispatcher; import androidx.test.filters.SmallTest; @@ -55,14 +62,13 @@ import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeExpansionChangeEvent; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.google.common.truth.Truth; @@ -71,6 +77,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -82,12 +89,11 @@ import java.util.Optional; @TestableLooper.RunWithLooper public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { - private static final PanelExpansionChangeEvent EXPANSION_EVENT = + private static final ShadeExpansionChangeEvent EXPANSION_EVENT = expansionEvent(/* fraction= */ 0.5f, /* expanded= */ false, /* tracking= */ true); @Mock private ViewMediatorCallback mViewMediatorCallback; @Mock private LockPatternUtils mLockPatternUtils; - @Mock private KeyguardStateController mKeyguardStateController; @Mock private CentralSurfaces mCentralSurfaces; @Mock private ViewGroup mContainer; @Mock private NotificationPanelViewController mNotificationPanelView; @@ -111,11 +117,18 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private BouncerCallbackInteractor mBouncerCallbackInteractor; @Mock private BouncerInteractor mBouncerInteractor; @Mock private BouncerView mBouncerView; -// @Mock private WeakReference<BouncerViewDelegate> mBouncerViewDelegateWeakReference; @Mock private BouncerViewDelegate mBouncerViewDelegate; private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private KeyguardBouncer.BouncerExpansionCallback mBouncerExpansionCallback; + private FakeKeyguardStateController mKeyguardStateController = + spy(new FakeKeyguardStateController()); + + @Mock private ViewRootImpl mViewRootImpl; + @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher; + @Captor + private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback; + @Before public void setUp() { @@ -152,15 +165,21 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mFeatureFlags, mBouncerCallbackInteractor, mBouncerInteractor, - mBouncerView); + mBouncerView) { + @Override + public ViewRootImpl getViewRootImpl() { + return mViewRootImpl; + } + }; + when(mViewRootImpl.getOnBackInvokedDispatcher()) + .thenReturn(mOnBackInvokedDispatcher); mStatusBarKeyguardViewManager.registerCentralSurfaces( mCentralSurfaces, mNotificationPanelView, - new PanelExpansionStateManager(), + new ShadeExpansionStateManager(), mBiometricUnlockController, mNotificationContainer, mBypassController); - when(mKeyguardStateController.isOccluded()).thenReturn(false); mStatusBarKeyguardViewManager.show(null); ArgumentCaptor<KeyguardBouncer.BouncerExpansionCallback> callbackArgumentCaptor = ArgumentCaptor.forClass(KeyguardBouncer.BouncerExpansionCallback.class); @@ -233,7 +252,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test public void onPanelExpansionChanged_showsBouncerWhenSwiping() { - when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false); + mKeyguardStateController.setCanDismissLockScreen(false); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); verify(mBouncer).show(eq(false), eq(false)); @@ -320,13 +339,12 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test - public void setOccluded_onKeyguardOccludedChangedCalledCorrectly() { + public void setOccluded_onKeyguardOccludedChangedCalled() { clearInvocations(mKeyguardStateController); clearInvocations(mKeyguardUpdateMonitor); - // Should be false to start, so no invocations mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */); - verify(mKeyguardStateController, never()).notifyKeyguardState(anyBoolean(), anyBoolean()); + verify(mKeyguardStateController).notifyKeyguardState(true, false); clearInvocations(mKeyguardUpdateMonitor); clearInvocations(mKeyguardStateController); @@ -337,8 +355,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { clearInvocations(mKeyguardUpdateMonitor); clearInvocations(mKeyguardStateController); - mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */); - verify(mKeyguardStateController, never()).notifyKeyguardState(anyBoolean(), anyBoolean()); + mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, false /* animated */); + verify(mKeyguardStateController).notifyKeyguardState(true, false); } @Test @@ -406,7 +424,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mAlternateAuthInterceptor.isShowingAlternateAuthBouncer()).thenReturn(true); assertTrue( "Is showing not accurate when alternative auth showing", - mStatusBarKeyguardViewManager.isShowing()); + mStatusBarKeyguardViewManager.isBouncerShowing()); } @Test @@ -500,13 +518,44 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { Truth.assertThat(mStatusBarKeyguardViewManager.isBouncerInTransit()).isFalse(); } - private static PanelExpansionChangeEvent expansionEvent( + private static ShadeExpansionChangeEvent expansionEvent( float fraction, boolean expanded, boolean tracking) { - return new PanelExpansionChangeEvent( + return new ShadeExpansionChangeEvent( fraction, expanded, tracking, /* dragDownPxAmount= */ 0f); } @Test + public void testPredictiveBackCallback_registration() { + /* verify that a predictive back callback is registered when the bouncer becomes visible */ + mBouncerExpansionCallback.onVisibilityChanged(true); + verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), + mOnBackInvokedCallback.capture()); + + /* verify that the same callback is unregistered when the bouncer becomes invisible */ + mBouncerExpansionCallback.onVisibilityChanged(false); + verify(mOnBackInvokedDispatcher).unregisterOnBackInvokedCallback( + eq(mOnBackInvokedCallback.getValue())); + } + + @Test + public void testPredictiveBackCallback_invocationHidesBouncer() { + mBouncerExpansionCallback.onVisibilityChanged(true); + /* capture the predictive back callback during registration */ + verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), + mOnBackInvokedCallback.capture()); + + when(mBouncer.isShowing()).thenReturn(true); + when(mCentralSurfaces.shouldKeyguardHideImmediately()).thenReturn(true); + /* invoke the back callback directly */ + mOnBackInvokedCallback.getValue().onBackInvoked(); + + /* verify that the bouncer will be hidden as a result of the invocation */ + verify(mCentralSurfaces).setBouncerShowing(eq(false)); + } + + @Test public void testReportBouncerOnDreamWhenVisible() { mBouncerExpansionCallback.onVisibilityChanged(true); verify(mCentralSurfaces).setBouncerShowingOverDream(false); @@ -525,4 +574,11 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mBouncerExpansionCallback.onVisibilityChanged(false); verify(mCentralSurfaces).setBouncerShowingOverDream(false); } + + @Test + public void flag_off_DoesNotCallBouncerInteractor() { + when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(false); + mStatusBarKeyguardViewManager.hideBouncer(false); + verify(mBouncerInteractor, never()).hide(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt index fa7b2599c108..9957c2a7f4a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt @@ -14,8 +14,6 @@ import com.android.internal.statusbar.LetterboxDetails import com.android.internal.view.AppearanceRegion import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.statusbar.SysuiStatusBarStateController import org.junit.Before import org.junit.Test @@ -40,7 +38,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { @Mock private lateinit var lightBarController: LightBarController @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController @Mock private lateinit var letterboxAppearanceCalculator: LetterboxAppearanceCalculator - @Mock private lateinit var featureFlags: FeatureFlags @Mock private lateinit var centralSurfaces: CentralSurfaces private lateinit var sysBarAttrsListener: SystemBarAttributesListener @@ -57,7 +54,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { sysBarAttrsListener = SystemBarAttributesListener( centralSurfaces, - featureFlags, letterboxAppearanceCalculator, statusBarStateController, lightBarController, @@ -74,18 +70,14 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { } @Test - fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToCentralSurfaces() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) - + fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToCentralSurfaces() { changeSysBarAttrs(TEST_APPEARANCE, TEST_LETTERBOX_DETAILS) verify(centralSurfaces).setAppearance(TEST_LETTERBOX_APPEARANCE.appearance) } @Test - fun onSysBarAttrsChanged_flagTrue_noLetterbox_forwardsOriginalAppearanceToCtrlSrfcs() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) - + fun onSysBarAttrsChanged_noLetterbox_forwardsOriginalAppearanceToCtrlSrfcs() { changeSysBarAttrs(TEST_APPEARANCE, arrayOf<LetterboxDetails>()) verify(centralSurfaces).setAppearance(TEST_APPEARANCE) @@ -100,9 +92,7 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { } @Test - fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToStatusBarStateCtrl() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) - + fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToStatusBarStateCtrl() { changeSysBarAttrs(TEST_APPEARANCE, TEST_LETTERBOX_DETAILS) verify(statusBarStateController) @@ -120,9 +110,7 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { } @Test - fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToLightBarController() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) - + fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToLightBarController() { changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS) verify(lightBarController) @@ -135,7 +123,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { @Test fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToStatusBarStateController() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS) reset(centralSurfaces, lightBarController, statusBarStateController) @@ -148,7 +135,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { @Test fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToLightBarController() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS) reset(centralSurfaces, lightBarController, statusBarStateController) @@ -164,7 +150,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { @Test fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToCentralSurfaces() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS) reset(centralSurfaces, lightBarController, statusBarStateController) @@ -175,7 +160,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { @Test fun onStatusBarBoundsChanged_previousCallEmptyLetterbox_doesNothing() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf()) reset(centralSurfaces, lightBarController, statusBarStateController) @@ -184,17 +168,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { verifyZeroInteractions(centralSurfaces, lightBarController, statusBarStateController) } - @Test - fun onStatusBarBoundsChanged_flagFalse_doesNothing() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(false) - changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS) - reset(centralSurfaces, lightBarController, statusBarStateController) - - sysBarAttrsListener.onStatusBarBoundsChanged() - - verifyZeroInteractions(centralSurfaces, lightBarController, statusBarStateController) - } - private fun changeSysBarAttrs(@Appearance appearance: Int) { changeSysBarAttrs(appearance, arrayOf<LetterboxDetails>()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt index 1ee8875eada8..78a4db1224cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt @@ -20,7 +20,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogcatEchoTracker import com.android.systemui.statusbar.DisableFlagsLogger import com.google.common.truth.Truth.assertThat import org.junit.Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index a3c6e9514191..438271c489e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -49,11 +49,12 @@ import com.android.systemui.R; import com.android.systemui.SysuiBaseFragmentTest; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.log.LogBuffer; -import com.android.systemui.log.LogcatEchoTracker; import com.android.systemui.plugins.DarkIconDispatcher; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogcatEchoTracker; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationPanelViewController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.DisableFlagsLogger; import com.android.systemui.statusbar.OperatorNameViewController; @@ -66,7 +67,6 @@ import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarLocationPublisher; import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.CarrierConfigTracker; import com.android.systemui.util.concurrency.FakeExecutor; @@ -441,7 +441,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mAnimationScheduler, mLocationPublisher, mMockNotificationAreaController, - new PanelExpansionStateManager(), + new ShadeExpansionStateManager(), mock(FeatureFlags.class), mStatusBarIconController, mIconManagerFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt index bf432388ad28..eba3b04f3472 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt @@ -20,7 +20,6 @@ import android.content.Intent import android.os.UserHandle import android.testing.AndroidTestingRunner import android.testing.TestableLooper -import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags @@ -34,8 +33,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -91,7 +90,7 @@ class StatusBarUserSwitcherControllerOldImplTest : SysuiTestCase() { fun testStartActivity() { `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(false) statusBarUserSwitcherContainer.callOnClick() - verify(userSwitcherDialogController).showDialog(any(View::class.java)) + verify(userSwitcherDialogController).showDialog(any(), any()) `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(true) statusBarUserSwitcherContainer.callOnClick() verify(activityStarter).startActivity(any(Intent::class.java), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt new file mode 100644 index 000000000000..b7a6c0125cfa --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.data.repository + +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.provider.Settings.Global +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class AirplaneModeRepositoryImplTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeRepositoryImpl + + @Mock private lateinit var logger: ConnectivityPipelineLogger + private lateinit var bgHandler: Handler + private lateinit var scope: CoroutineScope + private lateinit var settings: FakeSettings + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + bgHandler = Handler(Looper.getMainLooper()) + scope = CoroutineScope(IMMEDIATE) + settings = FakeSettings() + settings.userId = UserHandle.USER_ALL + + underTest = + AirplaneModeRepositoryImpl( + bgHandler, + settings, + logger, + scope, + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun isAirplaneMode_initiallyGetsSettingsValue() = + runBlocking(IMMEDIATE) { + settings.putInt(Global.AIRPLANE_MODE_ON, 1) + + underTest = + AirplaneModeRepositoryImpl( + bgHandler, + settings, + logger, + scope, + ) + + val job = underTest.isAirplaneMode.launchIn(this) + + assertThat(underTest.isAirplaneMode.value).isTrue() + + job.cancel() + } + + @Test + fun isAirplaneMode_settingUpdated_valueUpdated() = + runBlocking(IMMEDIATE) { + val job = underTest.isAirplaneMode.launchIn(this) + + settings.putInt(Global.AIRPLANE_MODE_ON, 0) + yield() + assertThat(underTest.isAirplaneMode.value).isFalse() + + settings.putInt(Global.AIRPLANE_MODE_ON, 1) + yield() + assertThat(underTest.isAirplaneMode.value).isTrue() + + settings.putInt(Global.AIRPLANE_MODE_ON, 0) + yield() + assertThat(underTest.isAirplaneMode.value).isFalse() + + job.cancel() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt new file mode 100644 index 000000000000..63bbdfca0071 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class FakeAirplaneModeRepository : AirplaneModeRepository { + private val _isAirplaneMode = MutableStateFlow(false) + override val isAirplaneMode: StateFlow<Boolean> = _isAirplaneMode + + fun setIsAirplaneMode(isAirplaneMode: Boolean) { + _isAirplaneMode.value = isAirplaneMode + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt new file mode 100644 index 000000000000..33a80e1a3dd6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.Before +import org.junit.Test + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +class AirplaneModeInteractorTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeInteractor + + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository + private lateinit var connectivityRepository: FakeConnectivityRepository + + @Before + fun setUp() { + airplaneModeRepository = FakeAirplaneModeRepository() + connectivityRepository = FakeConnectivityRepository() + underTest = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository) + } + + @Test + fun isAirplaneMode_matchesRepo() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isAirplaneMode.onEach { latest = it }.launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + yield() + assertThat(latest).isTrue() + + airplaneModeRepository.setIsAirplaneMode(false) + yield() + assertThat(latest).isFalse() + + airplaneModeRepository.setIsAirplaneMode(true) + yield() + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isForceHidden_repoHasWifiHidden_outputsTrue() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + + var latest: Boolean? = null + val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isForceHidden_repoDoesNotHaveWifiHidden_outputsFalse() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf()) + + var latest: Boolean? = null + val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt new file mode 100644 index 000000000000..76016a121e68 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +class AirplaneModeViewModelTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeViewModel + + @Mock private lateinit var logger: ConnectivityPipelineLogger + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository + private lateinit var connectivityRepository: FakeConnectivityRepository + private lateinit var interactor: AirplaneModeInteractor + private lateinit var scope: CoroutineScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() + connectivityRepository = FakeConnectivityRepository() + interactor = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository) + scope = CoroutineScope(IMMEDIATE) + + underTest = + AirplaneModeViewModel( + interactor, + logger, + scope, + ) + } + + @Test + fun isAirplaneModeIconVisible_notAirplaneMode_outputsFalse() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf()) + airplaneModeRepository.setIsAirplaneMode(false) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isAirplaneModeIconVisible_forceHidden_outputsFalse() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + airplaneModeRepository.setIsAirplaneMode(true) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isAirplaneModeIconVisible_isAirplaneModeAndNotForceHidden_outputsTrue() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf()) + airplaneModeRepository.setIsAirplaneMode(true) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt new file mode 100644 index 000000000000..6ff7b7ccd5e3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository + +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeMobileConnectionRepository : MobileConnectionRepository { + private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel()) + override val subscriptionModelFlow: Flow<MobileSubscriptionModel> = _subscriptionsModelFlow + + fun setMobileSubscriptionModel(model: MobileSubscriptionModel) { + _subscriptionsModelFlow.value = model + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt new file mode 100644 index 000000000000..c88d468f1755 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository + +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import com.android.settingslib.mobile.MobileMappings.Config +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeMobileConnectionsRepository : MobileConnectionsRepository { + private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf()) + override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow + + private val _activeMobileDataSubscriptionId = + MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId + + private val _defaultDataSubRatConfig = MutableStateFlow(Config()) + override val defaultDataSubRatConfig = _defaultDataSubRatConfig + + private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>() + override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it } + } + + fun setSubscriptions(subs: List<SubscriptionInfo>) { + _subscriptionsFlow.value = subs + } + + fun setDefaultDataSubRatConfig(config: Config) { + _defaultDataSubRatConfig.value = config + } + + fun setActiveMobileDataSubscriptionId(subId: Int) { + _activeMobileDataSubscriptionId.value = subId + } + + fun setMobileConnectionRepositoryForId(subId: Int, repo: MobileConnectionRepository) { + subIdRepos[subId] = repo + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt new file mode 100644 index 000000000000..6c495c5c705a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +/** Defaults to `true` */ +class FakeUserSetupRepository : UserSetupRepository { + private val _isUserSetup: MutableStateFlow<Boolean> = MutableStateFlow(true) + override val isUserSetupFlow: Flow<Boolean> = _isUserSetup + + fun setUserSetup(setup: Boolean) { + _isUserSetup.value = setup + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt new file mode 100644 index 000000000000..775e6dbb5e19 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository + +import android.telephony.CellSignalStrengthCdma +import android.telephony.ServiceState +import android.telephony.SignalStrength +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyCallback +import android.telephony.TelephonyCallback.ServiceStateListener +import android.telephony.TelephonyDisplayInfo +import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA +import android.telephony.TelephonyManager +import android.telephony.TelephonyManager.NETWORK_TYPE_LTE +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class MobileConnectionRepositoryTest : SysuiTestCase() { + private lateinit var underTest: MobileConnectionRepositoryImpl + + @Mock private lateinit var subscriptionManager: SubscriptionManager + @Mock private lateinit var telephonyManager: TelephonyManager + @Mock private lateinit var logger: ConnectivityPipelineLogger + + private val scope = CoroutineScope(IMMEDIATE) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) + + underTest = + MobileConnectionRepositoryImpl( + SUB_1_ID, + telephonyManager, + IMMEDIATE, + logger, + scope, + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun testFlowForSubId_default() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(MobileSubscriptionModel()) + + job.cancel() + } + + @Test + fun testFlowForSubId_emergencyOnly() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val serviceState = ServiceState() + serviceState.isEmergencyOnly = true + + getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState) + + assertThat(latest?.isEmergencyOnly).isEqualTo(true) + + job.cancel() + } + + @Test + fun testFlowForSubId_emergencyOnly_toggles() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<ServiceStateListener>() + val serviceState = ServiceState() + serviceState.isEmergencyOnly = true + callback.onServiceStateChanged(serviceState) + serviceState.isEmergencyOnly = false + callback.onServiceStateChanged(serviceState) + + assertThat(latest?.isEmergencyOnly).isEqualTo(false) + + job.cancel() + } + + @Test + fun testFlowForSubId_signalStrengths_levelsUpdate() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() + val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) + + assertThat(latest?.isGsm).isEqualTo(true) + assertThat(latest?.primaryLevel).isEqualTo(1) + assertThat(latest?.cdmaLevel).isEqualTo(2) + + job.cancel() + } + + @Test + fun testFlowForSubId_dataConnectionState() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = + getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(100, 200 /* unused */) + + assertThat(latest?.dataConnectionState).isEqualTo(100) + + job.cancel() + } + + @Test + fun testFlowForSubId_dataActivity() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<TelephonyCallback.DataActivityListener>() + callback.onDataActivity(3) + + assertThat(latest?.dataActivityDirection).isEqualTo(3) + + job.cancel() + } + + @Test + fun testFlowForSubId_carrierNetworkChange() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>() + callback.onCarrierNetworkChange(true) + + assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true) + + job.cancel() + } + + @Test + fun subscriptionFlow_networkType_default() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val type = NETWORK_TYPE_UNKNOWN + val expected = DefaultNetworkType(type) + + assertThat(latest?.resolvedNetworkType).isEqualTo(expected) + + job.cancel() + } + + @Test + fun subscriptionFlow_networkType_updatesUsingDefault() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() + val type = NETWORK_TYPE_LTE + val expected = DefaultNetworkType(type) + val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) } + callback.onDisplayInfoChanged(ti) + + assertThat(latest?.resolvedNetworkType).isEqualTo(expected) + + job.cancel() + } + + @Test + fun subscriptionFlow_networkType_updatesUsingOverride() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() + val type = OVERRIDE_NETWORK_TYPE_LTE_CA + val expected = OverrideNetworkType(type) + val ti = + mock<TelephonyDisplayInfo>().also { + whenever(it.overrideNetworkType).thenReturn(type) + } + callback.onDisplayInfoChanged(ti) + + assertThat(latest?.resolvedNetworkType).isEqualTo(expected) + + job.cancel() + } + + private fun getTelephonyCallbacks(): List<TelephonyCallback> { + val callbackCaptor = argumentCaptor<TelephonyCallback>() + Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) + return callbackCaptor.allValues + } + + private inline fun <reified T> getTelephonyCallbackForType(): T { + val cbs = getTelephonyCallbacks().filterIsInstance<T>() + assertThat(cbs.size).isEqualTo(1) + return cbs[0] + } + + /** Convenience constructor for SignalStrength */ + private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength { + val signalStrength = mock<SignalStrength>() + whenever(signalStrength.isGsm).thenReturn(isGsm) + whenever(signalStrength.level).thenReturn(gsmLevel) + val cdmaStrength = + mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) } + whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java)) + .thenReturn(listOf(cdmaStrength)) + + return signalStrength + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private const val SUB_1_ID = 1 + private val SUB_1 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt new file mode 100644 index 000000000000..326e0d28166f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository + +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyCallback +import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener +import android.telephony.TelephonyManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class MobileConnectionsRepositoryTest : SysuiTestCase() { + private lateinit var underTest: MobileConnectionsRepositoryImpl + + @Mock private lateinit var subscriptionManager: SubscriptionManager + @Mock private lateinit var telephonyManager: TelephonyManager + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + + private val scope = CoroutineScope(IMMEDIATE) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever( + broadcastDispatcher.broadcastFlow( + any(), + nullable(), + ArgumentMatchers.anyInt(), + nullable(), + ) + ) + .thenReturn(flowOf(Unit)) + + underTest = + MobileConnectionsRepositoryImpl( + subscriptionManager, + telephonyManager, + logger, + broadcastDispatcher, + context, + IMMEDIATE, + scope, + mock(), + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun testSubscriptions_initiallyEmpty() = + runBlocking(IMMEDIATE) { + assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>()) + } + + @Test + fun testSubscriptions_listUpdates() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionInfo>? = null + + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) + + job.cancel() + } + + @Test + fun testSubscriptions_removingSub_updatesList() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionInfo>? = null + + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + // WHEN 2 networks show up + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // WHEN one network is removed + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // THEN the subscriptions list represents the newest change + assertThat(latest).isEqualTo(listOf(SUB_2)) + + job.cancel() + } + + @Test + fun testActiveDataSubscriptionId_initialValueIsInvalidId() = + runBlocking(IMMEDIATE) { + assertThat(underTest.activeMobileDataSubscriptionId.value) + .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + } + + @Test + fun testActiveDataSubscriptionId_updates() = + runBlocking(IMMEDIATE) { + var active: Int? = null + + val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this) + + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() + .onActiveDataSubscriptionIdChanged(SUB_2_ID) + + assertThat(active).isEqualTo(SUB_2_ID) + + job.cancel() + } + + @Test + fun testConnectionRepository_validSubId_isCached() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptionsFlow.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_1_ID) + + assertThat(repo1).isSameInstanceAs(repo2) + + job.cancel() + } + + @Test + fun testConnectionCache_clearsInvalidSubscriptions() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptionsFlow.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Get repos to trigger caching + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_2_ID) + + assertThat(underTest.getSubIdRepoCache()) + .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2) + + // SUB_2 disappears + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) + + job.cancel() + } + + @Test + fun testConnectionRepository_invalidSubId_throws() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptionsFlow.launchIn(this) + + assertThrows(IllegalArgumentException::class.java) { + underTest.getRepoForSubId(SUB_1_ID) + } + + job.cancel() + } + + private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { + val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>() + verify(subscriptionManager) + .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) + return callbackCaptor.value!! + } + + private fun getTelephonyCallbacks(): List<TelephonyCallback> { + val callbackCaptor = argumentCaptor<TelephonyCallback>() + verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) + return callbackCaptor.allValues + } + + private inline fun <reified T> getTelephonyCallbackForType(): T { + val cbs = getTelephonyCallbacks().filterIsInstance<T>() + assertThat(cbs.size).isEqualTo(1) + return cbs[0] + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private const val SUB_1_ID = 1 + private val SUB_1 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + + private const val SUB_2_ID = 2 + private val SUB_2 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt new file mode 100644 index 000000000000..91c233a4177d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +class UserSetupRepositoryTest : SysuiTestCase() { + private lateinit var underTest: UserSetupRepository + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + private val scope = CoroutineScope(IMMEDIATE) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + underTest = + UserSetupRepositoryImpl( + deviceProvisionedController, + IMMEDIATE, + scope, + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun testUserSetup_defaultFalse() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + + val job = underTest.isUserSetupFlow.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun testUserSetup_updatesOnChange() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + + val job = underTest.isUserSetupFlow.onEach { latest = it }.launchIn(this) + + whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(true) + val callback = getDeviceProvisionedListener() + callback.onUserSetupChanged() + + assertThat(latest).isTrue() + + job.cancel() + } + + private fun getDeviceProvisionedListener(): DeviceProvisionedListener { + val captor = argumentCaptor<DeviceProvisionedListener>() + verify(deviceProvisionedController).addCallback(captor.capture()) + return captor.value!! + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt new file mode 100644 index 000000000000..cd4dbebcc35c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.domain.interactor + +import android.telephony.CellSignalStrength +import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.TelephonyIcons +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeMobileIconInteractor : MobileIconInteractor { + private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN) + override val networkTypeIconGroup = _iconGroup + + private val _isEmergencyOnly = MutableStateFlow<Boolean>(false) + override val isEmergencyOnly = _isEmergencyOnly + + private val _level = MutableStateFlow<Int>(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + override val level = _level + + private val _numberOfLevels = MutableStateFlow<Int>(4) + override val numberOfLevels = _numberOfLevels + + private val _cutOut = MutableStateFlow<Boolean>(false) + override val cutOut = _cutOut + + fun setIconGroup(group: SignalIcon.MobileIconGroup) { + _iconGroup.value = group + } + + fun setIsEmergencyOnly(emergency: Boolean) { + _isEmergencyOnly.value = emergency + } + + fun setLevel(level: Int) { + _level.value = level + } + + fun setNumberOfLevels(num: Int) { + _numberOfLevels.value = num + } + + fun setCutOut(cutOut: Boolean) { + _cutOut.value = cutOut + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt new file mode 100644 index 000000000000..2bd228603cb0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.domain.interactor + +import android.telephony.SubscriptionInfo +import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO +import android.telephony.TelephonyManager.NETWORK_TYPE_GSM +import android.telephony.TelephonyManager.NETWORK_TYPE_LTE +import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) : + MobileIconsInteractor { + val THREE_G_KEY = mobileMappings.toIconKey(THREE_G) + val LTE_KEY = mobileMappings.toIconKey(LTE) + val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G) + val FIVE_G_OVERRIDE_KEY = mobileMappings.toIconKeyOverride(FIVE_G_OVERRIDE) + + /** + * To avoid a reliance on [MobileMappings], we'll build a simpler map from network type to + * mobile icon. See TelephonyManager.NETWORK_TYPES for a list of types and [TelephonyIcons] for + * the exhaustive set of icons + */ + val TEST_MAPPING: Map<String, MobileIconGroup> = + mapOf( + THREE_G_KEY to TelephonyIcons.THREE_G, + LTE_KEY to TelephonyIcons.LTE, + FOUR_G_KEY to TelephonyIcons.FOUR_G, + FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G, + ) + + private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf()) + override val filteredSubscriptions = _filteredSubscriptions + + private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) + override val defaultMobileIconMapping = _defaultMobileIconMapping + + private val _defaultMobileIconGroup = MutableStateFlow(DEFAULT_ICON) + override val defaultMobileIconGroup = _defaultMobileIconGroup + + private val _isUserSetup = MutableStateFlow(true) + override val isUserSetup = _isUserSetup + + /** Always returns a new fake interactor */ + override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor { + return FakeMobileIconInteractor() + } + + companion object { + val DEFAULT_ICON = TelephonyIcons.G + + // Use [MobileMappings] to define some simple definitions + const val THREE_G = NETWORK_TYPE_GSM + const val LTE = NETWORK_TYPE_LTE + const val FOUR_G = NETWORK_TYPE_UMTS + const val FIVE_G_OVERRIDE = OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt new file mode 100644 index 000000000000..ff44af4c9204 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.domain.interactor + +import android.telephony.CellSignalStrength +import android.telephony.SubscriptionInfo +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN +import androidx.test.filters.SmallTest +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.Before +import org.junit.Test + +@SmallTest +class MobileIconInteractorTest : SysuiTestCase() { + private lateinit var underTest: MobileIconInteractor + private val mobileMappingsProxy = FakeMobileMappingsProxy() + private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy) + private val connectionRepository = FakeMobileConnectionRepository() + + @Before + fun setUp() { + underTest = + MobileIconInteractorImpl( + mobileIconsInteractor.defaultMobileIconMapping, + mobileIconsInteractor.defaultMobileIconGroup, + mobileMappingsProxy, + connectionRepository, + ) + } + + @Test + fun gsm_level_default_unknown() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(isGsm = true), + ) + + var latest: Int? = null + val job = underTest.level.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + + job.cancel() + } + + @Test + fun gsm_usesGsmLevel() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel( + isGsm = true, + primaryLevel = GSM_LEVEL, + cdmaLevel = CDMA_LEVEL + ), + ) + + var latest: Int? = null + val job = underTest.level.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(GSM_LEVEL) + + job.cancel() + } + + @Test + fun cdma_level_default_unknown() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(isGsm = false), + ) + + var latest: Int? = null + val job = underTest.level.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + job.cancel() + } + + @Test + fun cdma_usesCdmaLevel() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel( + isGsm = false, + primaryLevel = GSM_LEVEL, + cdmaLevel = CDMA_LEVEL + ), + ) + + var latest: Int? = null + val job = underTest.level.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(CDMA_LEVEL) + + job.cancel() + } + + @Test + fun iconGroup_three_g() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(TelephonyIcons.THREE_G) + + job.cancel() + } + + @Test + fun iconGroup_updates_on_change() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel( + resolvedNetworkType = DefaultNetworkType(FOUR_G), + ), + ) + yield() + + assertThat(latest).isEqualTo(TelephonyIcons.FOUR_G) + + job.cancel() + } + + @Test + fun iconGroup_5g_override_type() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(resolvedNetworkType = OverrideNetworkType(FIVE_G_OVERRIDE)), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(TelephonyIcons.NR_5G) + + job.cancel() + } + + @Test + fun iconGroup_default_if_no_lookup() = + runBlocking(IMMEDIATE) { + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel( + resolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN), + ), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(FakeMobileIconsInteractor.DEFAULT_ICON) + + job.cancel() + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + + private const val GSM_LEVEL = 1 + private const val CDMA_LEVEL = 2 + + private const val SUB_1_ID = 1 + private val SUB_1 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt new file mode 100644 index 000000000000..b01efd18971f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.domain.interactor + +import android.telephony.SubscriptionInfo +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.util.CarrierConfigTracker +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +class MobileIconsInteractorTest : SysuiTestCase() { + private lateinit var underTest: MobileIconsInteractor + private val userSetupRepository = FakeUserSetupRepository() + private val subscriptionsRepository = FakeMobileConnectionsRepository() + private val mobileMappingsProxy = FakeMobileMappingsProxy() + private val scope = CoroutineScope(IMMEDIATE) + + @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + underTest = + MobileIconsInteractorImpl( + subscriptionsRepository, + carrierConfigTracker, + mobileMappingsProxy, + userSetupRepository, + scope + ) + } + + @After fun tearDown() {} + + @Test + fun filteredSubscriptions_default() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(listOf<SubscriptionInfo>()) + + job.cancel() + } + + @Test + fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() = + runBlocking(IMMEDIATE) { + subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) + + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) + + job.cancel() + } + + @Test + fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_3() = + runBlocking(IMMEDIATE) { + subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) + subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(false) + + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + // Filtered subscriptions should show the active one when the config is false + assertThat(latest).isEqualTo(listOf(SUB_3_OPP)) + + job.cancel() + } + + @Test + fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_4() = + runBlocking(IMMEDIATE) { + subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) + subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID) + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(false) + + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + // Filtered subscriptions should show the active one when the config is false + assertThat(latest).isEqualTo(listOf(SUB_4_OPP)) + + job.cancel() + } + + @Test + fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_active_1() = + runBlocking(IMMEDIATE) { + subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP)) + subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID) + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(true) + + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + // Filtered subscriptions should show the primary (non-opportunistic) if the config is + // true + assertThat(latest).isEqualTo(listOf(SUB_1)) + + job.cancel() + } + + @Test + fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_nonActive_1() = + runBlocking(IMMEDIATE) { + subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP)) + subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(true) + + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + // Filtered subscriptions should show the primary (non-opportunistic) if the config is + // true + assertThat(latest).isEqualTo(listOf(SUB_1)) + + job.cancel() + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + + private const val SUB_1_ID = 1 + private val SUB_1 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + + private const val SUB_2_ID = 2 + private val SUB_2 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + + private const val SUB_3_ID = 3 + private val SUB_3_OPP = + mock<SubscriptionInfo>().also { + whenever(it.subscriptionId).thenReturn(SUB_3_ID) + whenever(it.isOpportunistic).thenReturn(true) + } + + private const val SUB_4_ID = 4 + private val SUB_4_OPP = + mock<SubscriptionInfo>().also { + whenever(it.subscriptionId).thenReturn(SUB_4_ID) + whenever(it.isOpportunistic).thenReturn(true) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt new file mode 100644 index 000000000000..b374abbd5082 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.settingslib.graph.SignalDrawable +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +class MobileIconViewModelTest : SysuiTestCase() { + private lateinit var underTest: MobileIconViewModel + private val interactor = FakeMobileIconInteractor() + @Mock private lateinit var logger: ConnectivityPipelineLogger + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + interactor.apply { + setLevel(1) + setCutOut(false) + setIconGroup(TelephonyIcons.THREE_G) + setIsEmergencyOnly(false) + setNumberOfLevels(4) + } + underTest = MobileIconViewModel(SUB_1_ID, interactor, logger) + } + + @Test + fun iconId_correctLevel_notCutout() = + runBlocking(IMMEDIATE) { + var latest: Int? = null + val job = underTest.iconId.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(SignalDrawable.getState(1, 4, false)) + + job.cancel() + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private const val SUB_1_ID = 1 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt new file mode 100644 index 000000000000..6d8d902615de --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.util + +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.MobileMappings.Config +import com.android.settingslib.mobile.TelephonyIcons + +class FakeMobileMappingsProxy : MobileMappingsProxy { + private var iconMap = mapOf<String, MobileIconGroup>() + private var defaultIcons = TelephonyIcons.THREE_G + + fun setIconMap(map: Map<String, MobileIconGroup>) { + iconMap = map + } + override fun mapIconSets(config: Config): Map<String, MobileIconGroup> = iconMap + fun getIconMap() = iconMap + + fun setDefaultIcons(group: MobileIconGroup) { + defaultIcons = group + } + override fun getDefaultIcons(config: Config): MobileIconGroup = defaultIcons + fun getDefaultIcons(): MobileIconGroup = defaultIcons + + override fun toIconKey(networkType: Int): String { + return networkType.toString() + } + + override fun toIconKeyOverride(networkType: Int): String { + return toIconKey(networkType) + "_override" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt index 0e75c74ef6f5..b32058fca109 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt @@ -22,7 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogcatEchoTracker import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.google.common.truth.Truth.assertThat diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt index f751afc195b2..2f18ce31217e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt @@ -27,6 +27,9 @@ class FakeWifiRepository : WifiRepository { private val _isWifiEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false) override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled + private val _isWifiDefault: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault + private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> = MutableStateFlow(WifiNetworkModel.Inactive) override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork @@ -38,6 +41,10 @@ class FakeWifiRepository : WifiRepository { _isWifiEnabled.value = enabled } + fun setIsWifiDefault(default: Boolean) { + _isWifiDefault.value = default + } + fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) { _wifiNetwork.value = wifiNetworkModel } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt index 0ba0bd623c39..a64a4bd2e57a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt @@ -222,6 +222,83 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @Test + fun isWifiDefault_initiallyGetsDefault() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + assertThat(underTest.isWifiDefault.value).isFalse() + + job.cancel() + } + + @Test + fun isWifiDefault_wifiNetwork_isTrue() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val wifiInfo = mock<WifiInfo>().apply { + whenever(this.ssid).thenReturn(SSID) + } + + getDefaultNetworkCallback().onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo) + ) + + assertThat(underTest.isWifiDefault.value).isTrue() + + job.cancel() + } + + @Test + fun isWifiDefault_cellularVcnNetwork_isTrue() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val capabilities = mock<NetworkCapabilities>().apply { + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO)) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(underTest.isWifiDefault.value).isTrue() + + job.cancel() + } + + @Test + fun isWifiDefault_cellularNotVcnNetwork_isFalse() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + val capabilities = mock<NetworkCapabilities>().apply { + whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(this.transportInfo).thenReturn(mock()) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) + + assertThat(underTest.isWifiDefault.value).isFalse() + + job.cancel() + } + + @Test + fun isWifiDefault_wifiNetworkLost_isFalse() = runBlocking(IMMEDIATE) { + val job = underTest.isWifiDefault.launchIn(this) + + // First, add a network + getDefaultNetworkCallback() + .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) + assertThat(underTest.isWifiDefault.value).isTrue() + + // WHEN the network is lost + getDefaultNetworkCallback().onLost(NETWORK) + + // THEN we update to false + assertThat(underTest.isWifiDefault.value).isFalse() + + job.cancel() + } + + @Test fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) { var latest: WifiNetworkModel? = null val job = underTest @@ -745,6 +822,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { return callbackCaptor.value!! } + private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { + val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>() + verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) + return callbackCaptor.value!! + } + private fun createWifiNetworkCapabilities( wifiInfo: WifiInfo, isValidated: Boolean = true, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt index 39b886af1cb8..71b8bab87d19 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt @@ -178,6 +178,29 @@ class WifiInteractorTest : SysuiTestCase() { } @Test + fun isDefault_matchesRepoIsDefault() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .isDefault + .onEach { latest = it } + .launchIn(this) + + wifiRepository.setIsWifiDefault(true) + yield() + assertThat(latest).isTrue() + + wifiRepository.setIsWifiDefault(false) + yield() + assertThat(latest).isFalse() + + wifiRepository.setIsWifiDefault(true) + yield() + assertThat(latest).isTrue() + + job.cancel() + } + + @Test fun wifiNetwork_matchesRepoWifiNetwork() = runBlocking(IMMEDIATE) { val wifiNetwork = WifiNetworkModel.Active( networkId = 45, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt index 4efb13520ebf..c5841098010a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt @@ -30,6 +30,9 @@ import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository @@ -63,11 +66,13 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor private lateinit var viewModel: WifiViewModel private lateinit var scope: CoroutineScope + private lateinit var airplaneModeViewModel: AirplaneModeViewModel @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule() @@ -77,12 +82,22 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(Dispatchers.Unconfined) + airplaneModeViewModel = AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) viewModel = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt index 929e5294de3d..a1afcd71e3c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt @@ -22,11 +22,14 @@ import androidx.test.filters.SmallTest import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.ContentDescription -import com.android.systemui.statusbar.connectivity.WifiIcons +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS +import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot @@ -64,19 +67,31 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor + private lateinit var airplaneModeViewModel: AirplaneModeViewModel private lateinit var scope: CoroutineScope @Before fun setUp() { MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(IMMEDIATE) + airplaneModeViewModel = + AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) } @After @@ -88,6 +103,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase fun wifiIcon() = runBlocking(IMMEDIATE) { wifiRepository.setIsWifiEnabled(testCase.enabled) + wifiRepository.setIsWifiDefault(testCase.isDefault) connectivityRepository.setForceHiddenIcons( if (testCase.forceHidden) { setOf(ConnectivitySlot.WIFI) @@ -101,6 +117,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase .thenReturn(testCase.hasDataCapabilities) underTest = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, @@ -125,26 +142,24 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase } else { testCase.expected.contentDescription.invoke(context) } - assertThat(iconFlow.value?.contentDescription?.getAsString()) + assertThat(iconFlow.value?.contentDescription?.loadContentDescription(context)) .isEqualTo(expectedContentDescription) job.cancel() } - private fun ContentDescription.getAsString(): String? { - return when (this) { - is ContentDescription.Loaded -> this.description - is ContentDescription.Resource -> context.getString(this.res) - } - } - internal data class Expected( /** The resource that should be used for the icon. */ @DrawableRes val iconResource: Int, /** A function that, given a context, calculates the correct content description string. */ val contentDescription: (Context) -> String, - ) + + /** A human-readable description used for the test names. */ + val description: String, + ) { + override fun toString() = description + } // Note: We use default values for the boolean parameters to reflect a "typical configuration" // for wifi. This allows each TestCase to only define the parameter values that are critical @@ -154,16 +169,27 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase val forceHidden: Boolean = false, val alwaysShowIconWhenEnabled: Boolean = false, val hasDataCapabilities: Boolean = true, + val isDefault: Boolean = false, val network: WifiNetworkModel, /** The expected output. Null if we expect the output to be null. */ val expected: Expected? - ) + ) { + override fun toString(): String { + return "when INPUT(enabled=$enabled, " + + "forceHidden=$forceHidden, " + + "showWhenEnabled=$alwaysShowIconWhenEnabled, " + + "hasDataCaps=$hasDataCapabilities, " + + "isDefault=$isDefault, " + + "network=$network) then " + + "EXPECTED($expected)" + } + } companion object { - @Parameters(name = "{0}") - @JvmStatic - fun data(): Collection<TestCase> = + @Parameters(name = "{0}") @JvmStatic fun data(): Collection<TestCase> = testData + + private val testData: List<TestCase> = listOf( // Enabled = false => no networks shown TestCase( @@ -215,11 +241,12 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase network = WifiNetworkModel.Inactive, expected = Expected( - iconResource = WifiIcons.WIFI_NO_NETWORK, + iconResource = WIFI_NO_NETWORK, contentDescription = { context -> "${context.getString(WIFI_NO_CONNECTION)}," + context.getString(NO_INTERNET) - } + }, + description = "No network icon", ), ), TestCase( @@ -231,7 +258,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase contentDescription = { context -> "${context.getString(WIFI_CONNECTION_STRENGTH[4])}," + context.getString(NO_INTERNET) - } + }, + description = "No internet level 4 icon", ), ), TestCase( @@ -242,7 +270,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase iconResource = WIFI_FULL_ICONS[2], contentDescription = { context -> context.getString(WIFI_CONNECTION_STRENGTH[2]) - } + }, + description = "Full internet level 2 icon", ), ), @@ -252,11 +281,12 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase network = WifiNetworkModel.Inactive, expected = Expected( - iconResource = WifiIcons.WIFI_NO_NETWORK, + iconResource = WIFI_NO_NETWORK, contentDescription = { context -> "${context.getString(WIFI_NO_CONNECTION)}," + context.getString(NO_INTERNET) - } + }, + description = "No network icon", ), ), TestCase( @@ -268,7 +298,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase contentDescription = { context -> "${context.getString(WIFI_CONNECTION_STRENGTH[2])}," + context.getString(NO_INTERNET) - } + }, + description = "No internet level 2 icon", ), ), TestCase( @@ -279,7 +310,48 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase iconResource = WIFI_FULL_ICONS[0], contentDescription = { context -> context.getString(WIFI_CONNECTION_STRENGTH[0]) - } + }, + description = "Full internet level 0 icon", + ), + ), + + // isDefault = true => all Inactive and Active networks shown + TestCase( + isDefault = true, + network = WifiNetworkModel.Inactive, + expected = + Expected( + iconResource = WIFI_NO_NETWORK, + contentDescription = { context -> + "${context.getString(WIFI_NO_CONNECTION)}," + + context.getString(NO_INTERNET) + }, + description = "No network icon", + ), + ), + TestCase( + isDefault = true, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 3), + expected = + Expected( + iconResource = WIFI_NO_INTERNET_ICONS[3], + contentDescription = { context -> + "${context.getString(WIFI_CONNECTION_STRENGTH[3])}," + + context.getString(NO_INTERNET) + }, + description = "No internet level 3 icon", + ), + ), + TestCase( + isDefault = true, + network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 1), + expected = + Expected( + iconResource = WIFI_FULL_ICONS[1], + contentDescription = { context -> + context.getString(WIFI_CONNECTION_STRENGTH[1]) + }, + description = "Full internet level 1 icon", ), ), @@ -309,7 +381,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase iconResource = WIFI_FULL_ICONS[4], contentDescription = { context -> context.getString(WIFI_CONNECTION_STRENGTH[4]) - } + }, + description = "Full internet level 4 icon", ), ), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index 3169eef83f07..7d2c56098584 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -20,8 +20,12 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository @@ -55,19 +59,31 @@ class WifiViewModelTest : SysuiTestCase() { @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor + private lateinit var airplaneModeViewModel: AirplaneModeViewModel private lateinit var scope: CoroutineScope @Before fun setUp() { MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(IMMEDIATE) + airplaneModeViewModel = AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) + createAndSetViewModel() } @@ -76,6 +92,8 @@ class WifiViewModelTest : SysuiTestCase() { scope.cancel() } + // See [WifiViewModelIconParameterizedTest] for additional view model tests. + // Note on testing: [WifiViewModel] exposes 3 different instances of // [LocationBasedWifiViewModel]. In practice, these 3 different instances will get the exact // same data for icon, activity, etc. flows. So, most of these tests will test just one of the @@ -460,11 +478,64 @@ class WifiViewModelTest : SysuiTestCase() { job.cancel() } + @Test + fun airplaneSpacer_notAirplaneMode_outputsFalse() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(false) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun airplaneSpacer_airplaneForceHidden_outputsFalse() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun airplaneSpacer_airplaneIconVisible_outputsTrue() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + private fun createAndSetViewModel() { // [WifiViewModel] creates its flows as soon as it's instantiated, and some of those flow // creations rely on certain config values that we mock out in individual tests. This method // allows tests to create the view model only after those configs are correctly set up. underTest = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 6ace4044b3f7..915e999c2646 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -23,8 +23,12 @@ import static junit.framework.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.PendingIntent; @@ -43,10 +47,14 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.ContentInfo; import android.view.View; +import android.view.ViewRootImpl; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.widget.EditText; import android.widget.ImageButton; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; +import android.window.WindowOnBackInvokedDispatcher; import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; @@ -67,6 +75,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -229,6 +238,67 @@ public class RemoteInputViewTest extends SysuiTestCase { } @Test + public void testPredictiveBack_registerAndUnregister() throws Exception { + NotificationTestHelper helper = new NotificationTestHelper( + mContext, + mDependency, + TestableLooper.get(this)); + ExpandableNotificationRow row = helper.createRow(); + RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); + + ViewRootImpl viewRoot = mock(ViewRootImpl.class); + WindowOnBackInvokedDispatcher backInvokedDispatcher = mock( + WindowOnBackInvokedDispatcher.class); + ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass( + OnBackInvokedCallback.class); + when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher); + view.setViewRootImpl(viewRoot); + + /* verify that predictive back callback registered when RemoteInputView becomes visible */ + view.onVisibilityAggregated(true); + verify(backInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), + onBackInvokedCallbackCaptor.capture()); + + /* verify that same callback unregistered when RemoteInputView becomes invisible */ + view.onVisibilityAggregated(false); + verify(backInvokedDispatcher).unregisterOnBackInvokedCallback( + eq(onBackInvokedCallbackCaptor.getValue())); + } + + @Test + public void testUiPredictiveBack_openAndDispatchCallback() throws Exception { + NotificationTestHelper helper = new NotificationTestHelper( + mContext, + mDependency, + TestableLooper.get(this)); + ExpandableNotificationRow row = helper.createRow(); + RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); + ViewRootImpl viewRoot = mock(ViewRootImpl.class); + WindowOnBackInvokedDispatcher backInvokedDispatcher = mock( + WindowOnBackInvokedDispatcher.class); + ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass( + OnBackInvokedCallback.class); + when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher); + view.setViewRootImpl(viewRoot); + view.onVisibilityAggregated(true); + view.setEditTextReferenceToSelf(); + + /* capture the callback during registration */ + verify(backInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), + onBackInvokedCallbackCaptor.capture()); + + view.focus(); + + /* invoke the captured callback */ + onBackInvokedCallbackCaptor.getValue().onBackInvoked(); + + /* verify that the RemoteInputView goes away */ + assertEquals(view.getVisibility(), View.GONE); + } + + @Test public void testUiEventLogging_openAndSend() throws Exception { NotificationTestHelper helper = new NotificationTestHelper( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt index 76ecc1c7f36d..169f4fb2715b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt @@ -57,14 +57,18 @@ import com.android.systemui.settings.UserTracker import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.telephony.TelephonyListenerManager import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper +import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.nullable import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -123,7 +127,7 @@ class UserSwitcherControllerOldImplTest : SysuiTestCase() { private val ownerId = UserHandle.USER_SYSTEM private val ownerInfo = UserInfo(ownerId, "Owner", null, UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL or UserInfo.FLAG_INITIALIZED or - UserInfo.FLAG_PRIMARY or UserInfo.FLAG_SYSTEM, + UserInfo.FLAG_PRIMARY or UserInfo.FLAG_SYSTEM or UserInfo.FLAG_ADMIN, UserManager.USER_TYPE_FULL_SYSTEM) private val guestId = 1234 private val guestInfo = UserInfo(guestId, "Guest", null, @@ -597,6 +601,76 @@ class UserSwitcherControllerOldImplTest : SysuiTestCase() { } @Test + fun testCanManageUser_userSwitcherEnabled_addUserWhenLocked() { + `when`( + globalSettings.getIntForUser( + eq(Settings.Global.USER_SWITCHER_ENABLED), + anyInt(), + eq(UserHandle.USER_SYSTEM) + ) + ).thenReturn(1) + + `when`( + globalSettings.getIntForUser( + eq(Settings.Global.ADD_USERS_WHEN_LOCKED), + anyInt(), + eq(UserHandle.USER_SYSTEM) + ) + ).thenReturn(1) + setupController() + assertTrue(userSwitcherController.canManageUsers()) + } + + @Test + fun testCanManageUser_userSwitcherDisabled_addUserWhenLocked() { + `when`( + globalSettings.getIntForUser( + eq(Settings.Global.USER_SWITCHER_ENABLED), + anyInt(), + eq(UserHandle.USER_SYSTEM) + ) + ).thenReturn(0) + + `when`( + globalSettings.getIntForUser( + eq(Settings.Global.ADD_USERS_WHEN_LOCKED), + anyInt(), + eq(UserHandle.USER_SYSTEM) + ) + ).thenReturn(1) + setupController() + assertFalse(userSwitcherController.canManageUsers()) + } + + @Test + fun testCanManageUser_userSwitcherEnabled_isAdmin() { + `when`( + globalSettings.getIntForUser( + eq(Settings.Global.USER_SWITCHER_ENABLED), + anyInt(), + eq(UserHandle.USER_SYSTEM) + ) + ).thenReturn(1) + + setupController() + assertTrue(userSwitcherController.canManageUsers()) + } + + @Test + fun testCanManageUser_userSwitcherDisabled_isAdmin() { + `when`( + globalSettings.getIntForUser( + eq(Settings.Global.USER_SWITCHER_ENABLED), + anyInt(), + eq(UserHandle.USER_SYSTEM) + ) + ).thenReturn(0) + + setupController() + assertFalse(userSwitcherController.canManageUsers()) + } + + @Test fun addUserSwitchCallback() { val broadcastReceiverCaptor = argumentCaptor<BroadcastReceiver>() verify(broadcastDispatcher).registerReceiver( @@ -632,4 +706,22 @@ class UserSwitcherControllerOldImplTest : SysuiTestCase() { bgExecutor.runAllReady() verify(userManager).createGuest(context) } + + @Test + fun onUserItemClicked_manageUsers() { + val manageUserRecord = LegacyUserDataHelper.createRecord( + mContext, + ownerId, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + isRestricted = false, + isSwitchToEnabled = true + ) + + userSwitcherController.onUserListItemClicked(manageUserRecord, null) + val intentCaptor = kotlinArgumentCaptor<Intent>() + verify(activityStarter).startActivity(intentCaptor.capture(), + eq(true) + ) + Truth.assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt new file mode 100644 index 000000000000..773a0d8ceb64 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.telephony.data.repository + +import android.telephony.TelephonyCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.telephony.TelephonyListenerManager +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class TelephonyRepositoryImplTest : SysuiTestCase() { + + @Mock private lateinit var manager: TelephonyListenerManager + + private lateinit var underTest: TelephonyRepositoryImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + TelephonyRepositoryImpl( + manager = manager, + ) + } + + @Test + fun callState() = + runBlocking(IMMEDIATE) { + var callState: Int? = null + val job = underTest.callState.onEach { callState = it }.launchIn(this) + val listenerCaptor = kotlinArgumentCaptor<TelephonyCallback.CallStateListener>() + verify(manager).addCallStateListener(listenerCaptor.capture()) + val listener = listenerCaptor.value + + listener.onCallStateChanged(0) + assertThat(callState).isEqualTo(0) + + listener.onCallStateChanged(1) + assertThat(callState).isEqualTo(1) + + listener.onCallStateChanged(2) + assertThat(callState).isEqualTo(2) + + job.cancel() + + verify(manager).removeCallStateListener(listener) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt index 921b7efc38eb..b68eb88d46db 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt @@ -17,7 +17,9 @@ package com.android.systemui.temporarydisplay import android.content.Context +import android.graphics.Rect import android.os.PowerManager +import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager @@ -229,17 +231,23 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { accessibilityManager, configurationController, powerManager, - R.layout.media_ttt_chip, + R.layout.chipbar, "Window Title", "WAKE_REASON", ) { var mostRecentViewInfo: ViewInfo? = null override val windowLayoutParams = commonWindowLayoutParams + + override fun start() {} + override fun updateView(newInfo: ViewInfo, currentView: ViewGroup) { - super.updateView(newInfo, currentView) mostRecentViewInfo = newInfo } + + override fun getTouchableRegion(view: View, outRect: Rect) { + outRect.setEmpty() + } } inner class ViewInfo(val name: String) : TemporaryViewInfo { diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt index c9f2b4db81ef..13e9f608158e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt @@ -19,9 +19,9 @@ package com.android.systemui.temporarydisplay import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory -import com.android.systemui.log.LogcatEchoTracker +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogcatEchoTracker import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt new file mode 100644 index 000000000000..7586fe48b308 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay + +import android.graphics.Rect +import android.view.View +import android.view.ViewTreeObserver +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +class TouchableRegionViewControllerTest : SysuiTestCase() { + + @Mock private lateinit var view: View + @Mock private lateinit var viewTreeObserver: ViewTreeObserver + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(view.viewTreeObserver).thenReturn(viewTreeObserver) + } + + @Test + fun viewAttached_listenerAdded() { + val controller = TouchableRegionViewController(view) { _, _ -> } + + controller.onViewAttached() + + verify(viewTreeObserver).addOnComputeInternalInsetsListener(any()) + } + + @Test + fun viewDetached_listenerRemoved() { + val controller = TouchableRegionViewController(view) { _, _ -> } + + controller.onViewDetached() + + verify(viewTreeObserver).removeOnComputeInternalInsetsListener(any()) + } + + @Test + fun listener_usesPassedInFunction() { + val controller = + TouchableRegionViewController(view) { _, outRect -> outRect.set(1, 2, 3, 4) } + + controller.onViewAttached() + + val captor = + ArgumentCaptor.forClass(ViewTreeObserver.OnComputeInternalInsetsListener::class.java) + verify(viewTreeObserver).addOnComputeInternalInsetsListener(captor.capture()) + val listener = captor.value!! + + val inoutInfo = ViewTreeObserver.InternalInsetsInfo() + listener.onComputeInternalInsets(inoutInfo) + + assertThat(inoutInfo.touchableRegion.bounds).isEqualTo(Rect(1, 2, 3, 4)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt new file mode 100644 index 000000000000..9fbf159ec348 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay.chipbar + +import android.os.PowerManager +import android.os.VibrationEffect +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.view.accessibility.AccessibilityManager +import android.widget.ImageView +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.view.ViewUtil +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class ChipbarCoordinatorTest : SysuiTestCase() { + private lateinit var underTest: FakeChipbarCoordinator + + @Mock private lateinit var logger: MediaTttLogger + @Mock private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var powerManager: PowerManager + @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var falsingManager: FalsingManager + @Mock private lateinit var falsingCollector: FalsingCollector + @Mock private lateinit var viewUtil: ViewUtil + @Mock private lateinit var vibratorHelper: VibratorHelper + private lateinit var fakeClock: FakeSystemClock + private lateinit var fakeExecutor: FakeExecutor + private lateinit var uiEventLoggerFake: UiEventLoggerFake + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) + + fakeClock = FakeSystemClock() + fakeExecutor = FakeExecutor(fakeClock) + + uiEventLoggerFake = UiEventLoggerFake() + + underTest = + FakeChipbarCoordinator( + context, + logger, + windowManager, + fakeExecutor, + accessibilityManager, + configurationController, + powerManager, + falsingManager, + falsingCollector, + viewUtil, + vibratorHelper, + ) + underTest.start() + } + + @Test + fun displayView_loadedIcon_correctlyRendered() { + val drawable = context.getDrawable(R.drawable.ic_celebration)!! + + underTest.displayView( + ChipbarInfo( + Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), + Text.Loaded("text"), + endItem = null, + ) + ) + + val iconView = getChipbarView().getStartIconView() + assertThat(iconView.drawable).isEqualTo(drawable) + assertThat(iconView.contentDescription).isEqualTo("loadedCD") + } + + @Test + fun displayView_resourceIcon_correctlyRendered() { + val contentDescription = ContentDescription.Resource(R.string.controls_error_timeout) + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.drawable.ic_cake, contentDescription), + Text.Loaded("text"), + endItem = null, + ) + ) + + val iconView = getChipbarView().getStartIconView() + assertThat(iconView.contentDescription) + .isEqualTo(contentDescription.loadContentDescription(context)) + } + + @Test + fun displayView_loadedText_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("display view text here"), + endItem = null, + ) + ) + + assertThat(getChipbarView().getChipText()).isEqualTo("display view text here") + } + + @Test + fun displayView_resourceText_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Resource(R.string.screenrecord_start_error), + endItem = null, + ) + ) + + assertThat(getChipbarView().getChipText()) + .isEqualTo(context.getString(R.string.screenrecord_start_error)) + } + + @Test + fun displayView_endItemNull_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = null, + ) + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun displayView_endItemLoading_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Loading, + ) + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun displayView_endItemError_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Error, + ) + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + } + + @Test + fun displayView_endItemButton_correctlyRendered() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + onClickListener = {}, + ), + ) + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().text).isEqualTo("button text") + assertThat(chipbarView.getEndButton().hasOnClickListeners()).isTrue() + } + + @Test + fun displayView_endItemButtonClicked_falseTap_listenerNotRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) + var isClicked = false + val buttonClickListener = View.OnClickListener { isClicked = true } + + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + buttonClickListener, + ), + ) + ) + + getChipbarView().getEndButton().performClick() + + assertThat(isClicked).isFalse() + } + + @Test + fun displayView_endItemButtonClicked_notFalseTap_listenerRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false) + var isClicked = false + val buttonClickListener = View.OnClickListener { isClicked = true } + + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = + ChipbarEndItem.Button( + Text.Loaded("button text"), + buttonClickListener, + ), + ) + ) + + getChipbarView().getEndButton().performClick() + + assertThat(isClicked).isTrue() + } + + @Test + fun displayView_vibrationEffect_doubleClickEffect() { + underTest.displayView( + ChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = null, + vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK), + ) + ) + + verify(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)) + } + + @Test + fun updateView_viewUpdated() { + // First, display a view + val drawable = context.getDrawable(R.drawable.ic_celebration)!! + + underTest.displayView( + ChipbarInfo( + Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), + Text.Loaded("title text"), + endItem = ChipbarEndItem.Loading, + ) + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getStartIconView().drawable).isEqualTo(drawable) + assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("loadedCD") + assertThat(chipbarView.getChipText()).isEqualTo("title text") + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + + // WHEN the view is updated + val newDrawable = context.getDrawable(R.drawable.ic_cake)!! + underTest.updateView( + ChipbarInfo( + Icon.Loaded(newDrawable, ContentDescription.Loaded("new CD")), + Text.Loaded("new title text"), + endItem = ChipbarEndItem.Error, + ), + chipbarView + ) + + // THEN we display the new view + assertThat(chipbarView.getStartIconView().drawable).isEqualTo(newDrawable) + assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("new CD") + assertThat(chipbarView.getChipText()).isEqualTo("new title text") + assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE) + assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE) + assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) + } + + private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon) + + private fun ViewGroup.getChipText(): String = + (this.requireViewById<TextView>(R.id.text)).text as String + + private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading) + + private fun ViewGroup.getEndButton(): TextView = this.requireViewById(R.id.end_button) + + private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error) + + private fun getChipbarView(): ViewGroup { + val viewCaptor = ArgumentCaptor.forClass(View::class.java) + verify(windowManager).addView(viewCaptor.capture(), any()) + return viewCaptor.value as ViewGroup + } +} + +private const val TIMEOUT = 10000 diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt new file mode 100644 index 000000000000..17d402319246 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay.chipbar + +import android.content.Context +import android.os.PowerManager +import android.view.ViewGroup +import android.view.WindowManager +import android.view.accessibility.AccessibilityManager +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.view.ViewUtil + +/** A fake implementation of [ChipbarCoordinator] for testing. */ +class FakeChipbarCoordinator( + context: Context, + @MediaTttReceiverLogger logger: MediaTttLogger, + windowManager: WindowManager, + mainExecutor: DelayableExecutor, + accessibilityManager: AccessibilityManager, + configurationController: ConfigurationController, + powerManager: PowerManager, + falsingManager: FalsingManager, + falsingCollector: FalsingCollector, + viewUtil: ViewUtil, + vibratorHelper: VibratorHelper, +) : + ChipbarCoordinator( + context, + logger, + windowManager, + mainExecutor, + accessibilityManager, + configurationController, + powerManager, + falsingManager, + falsingCollector, + viewUtil, + vibratorHelper, + ) { + override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { + // Just bypass the animation in tests + onAnimationEnd.run() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java index 50259b5246f5..2a93ffff87a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -885,4 +885,31 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { assertThat(themeOverlays.getValue().get(OVERLAY_CATEGORY_SYSTEM_PALETTE)) .isEqualTo(new OverlayIdentifier("ff00ff00")); } + + // Regression test for b/234603929, where a reboot would generate a wallpaper colors changed + // event for the already-set colors that would then set the theme incorrectly on screen sleep. + @Test + public void onWallpaperColorsSetToSame_keepsTheme() { + // Set initial colors and verify. + WallpaperColors startingColors = new WallpaperColors(Color.valueOf(Color.RED), + Color.valueOf(Color.BLUE), null); + WallpaperColors sameColors = new WallpaperColors(Color.valueOf(Color.RED), + Color.valueOf(Color.BLUE), null); + mColorsListener.getValue().onColorsChanged(startingColors, WallpaperManager.FLAG_SYSTEM, + USER_SYSTEM); + verify(mThemeOverlayApplier) + .applyCurrentUserOverlays(any(), any(), anyInt(), any()); + clearInvocations(mThemeOverlayApplier); + + // Set to the same colors. + mColorsListener.getValue().onColorsChanged(sameColors, WallpaperManager.FLAG_SYSTEM, + USER_SYSTEM); + verify(mThemeOverlayApplier, never()) + .applyCurrentUserOverlays(any(), any(), anyInt(), any()); + + // Verify that no change resulted. + mWakefulnessLifecycleObserver.getValue().onFinishedGoingToSleep(); + verify(mThemeOverlayApplier, never()).applyCurrentUserOverlays(any(), any(), anyInt(), + any()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt index 7e0704007700..e18dd3a3c846 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt @@ -25,16 +25,21 @@ import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.system.ActivityManagerActivityTypeProvider import com.android.systemui.unfold.updates.FoldProvider.FoldCallback +import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener import com.android.systemui.unfold.updates.hinge.HingeAngleProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture import com.google.common.truth.Truth.assertThat import java.util.concurrent.Executor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -48,6 +53,12 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { @Mock private lateinit var handler: Handler + @Mock + private lateinit var rotationChangeProvider: RotationChangeProvider + + @Captor + private lateinit var rotationListener: ArgumentCaptor<RotationListener> + private val foldProvider = TestFoldProvider() private val screenOnStatusProvider = TestScreenOnStatusProvider() private val testHingeAngleProvider = TestHingeAngleProvider() @@ -76,6 +87,7 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { screenOnStatusProvider, foldProvider, activityTypeProvider, + rotationChangeProvider, context.mainExecutor, handler ) @@ -92,6 +104,8 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { }) foldStateProvider.start() + verify(rotationChangeProvider).addCallback(capture(rotationListener)) + whenever(handler.postDelayed(any<Runnable>(), any())).then { invocationOnMock -> scheduledRunnable = invocationOnMock.getArgument<Runnable>(0) scheduledRunnableDelay = invocationOnMock.getArgument<Long>(1) @@ -372,6 +386,27 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { assertThat(testHingeAngleProvider.isStarted).isFalse() } + @Test + fun onRotationChanged_whileInProgress_cancelled() { + setFoldState(folded = false) + assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING) + + rotationListener.value.onRotationChanged(1) + + assertThat(foldUpdates).containsExactly( + FOLD_UPDATE_START_OPENING, FOLD_UPDATE_FINISH_HALF_OPEN) + } + + @Test + fun onRotationChanged_whileNotInProgress_noUpdates() { + setFoldState(folded = true) + assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED) + + rotationListener.value.onRotationChanged(1) + + assertThat(foldUpdates).containsExactly(FOLD_UPDATE_FINISH_CLOSED) + } + private fun setupForegroundActivityType(isHomeActivity: Boolean?) { whenever(activityTypeProvider.isHomeActivity).thenReturn(isHomeActivity) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt new file mode 100644 index 000000000000..85cfef727954 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/RotationChangeProviderTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.unfold.updates + +import android.testing.AndroidTestingRunner +import android.view.IRotationWatcher +import android.view.IWindowManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class RotationChangeProviderTest : SysuiTestCase() { + + private lateinit var rotationChangeProvider: RotationChangeProvider + + @Mock lateinit var windowManagerInterface: IWindowManager + @Mock lateinit var listener: RotationListener + @Captor lateinit var rotationWatcher: ArgumentCaptor<IRotationWatcher> + private val fakeExecutor = FakeExecutor(FakeSystemClock()) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + rotationChangeProvider = + RotationChangeProvider(windowManagerInterface, context, fakeExecutor) + rotationChangeProvider.addCallback(listener) + fakeExecutor.runAllReady() + verify(windowManagerInterface).watchRotation(rotationWatcher.capture(), anyInt()) + } + + @Test + fun onRotationChanged_rotationUpdated_listenerReceivesIt() { + sendRotationUpdate(42) + + verify(listener).onRotationChanged(42) + } + + @Test + fun onRotationChanged_subscribersRemoved_noRotationChangeReceived() { + sendRotationUpdate(42) + verify(listener).onRotationChanged(42) + + rotationChangeProvider.removeCallback(listener) + fakeExecutor.runAllReady() + sendRotationUpdate(43) + + verify(windowManagerInterface).removeRotationWatcher(any()) + verifyNoMoreInteractions(listener) + } + + private fun sendRotationUpdate(newRotation: Int) { + rotationWatcher.value.onRotationChanged(newRotation) + fakeExecutor.runAllReady() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt index b2cedbf8d606..a25469bfc09b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/util/NaturalRotationUnfoldProgressProviderTest.kt @@ -16,18 +16,19 @@ package com.android.systemui.unfold.util import android.testing.AndroidTestingRunner -import android.view.IRotationWatcher -import android.view.IWindowManager import android.view.Surface import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.TestUnfoldTransitionProvider import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener -import com.android.systemui.util.mockito.any +import com.android.systemui.unfold.updates.RotationChangeProvider +import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener +import com.android.systemui.util.mockito.capture import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never @@ -38,32 +39,26 @@ import org.mockito.MockitoAnnotations @SmallTest class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() { - @Mock - lateinit var windowManager: IWindowManager + @Mock lateinit var rotationChangeProvider: RotationChangeProvider private val sourceProvider = TestUnfoldTransitionProvider() - @Mock - lateinit var transitionListener: TransitionProgressListener + @Mock lateinit var transitionListener: TransitionProgressListener - lateinit var progressProvider: NaturalRotationUnfoldProgressProvider + @Captor private lateinit var rotationListenerCaptor: ArgumentCaptor<RotationListener> - private val rotationWatcherCaptor = - ArgumentCaptor.forClass(IRotationWatcher.Stub::class.java) + lateinit var progressProvider: NaturalRotationUnfoldProgressProvider @Before fun setUp() { MockitoAnnotations.initMocks(this) - progressProvider = NaturalRotationUnfoldProgressProvider( - context, - windowManager, - sourceProvider - ) + progressProvider = + NaturalRotationUnfoldProgressProvider(context, rotationChangeProvider, sourceProvider) progressProvider.init() - verify(windowManager).watchRotation(rotationWatcherCaptor.capture(), any()) + verify(rotationChangeProvider).addCallback(capture(rotationListenerCaptor)) progressProvider.addCallback(transitionListener) } @@ -127,6 +122,6 @@ class NaturalRotationUnfoldProgressProviderTest : SysuiTestCase() { } private fun onRotationChanged(rotation: Int) { - rotationWatcherCaptor.value.onRotationChanged(rotation) + rotationListenerCaptor.value.onRotationChanged(rotation) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/UserSwitcherActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/UserSwitcherActivityTest.kt deleted file mode 100644 index 3968bb798bb7..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/user/UserSwitcherActivityTest.kt +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.user - -import android.app.Application -import android.os.UserManager -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper.RunWithLooper -import android.view.LayoutInflater -import android.view.View -import android.view.Window -import android.window.OnBackInvokedCallback -import android.window.OnBackInvokedDispatcher -import androidx.test.filters.SmallTest -import com.android.systemui.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.settings.UserTracker -import com.android.systemui.statusbar.policy.UserSwitcherController -import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.`when` -import org.mockito.Mockito.any -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.doNothing -import org.mockito.Mockito.eq -import org.mockito.Mockito.mock -import org.mockito.Mockito.spy -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations -import java.util.concurrent.Executor - -@SmallTest -@RunWith(AndroidTestingRunner::class) -@RunWithLooper(setAsMainLooper = true) -class UserSwitcherActivityTest : SysuiTestCase() { - @Mock - private lateinit var activity: UserSwitcherActivity - @Mock - private lateinit var userSwitcherController: UserSwitcherController - @Mock - private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock - private lateinit var layoutInflater: LayoutInflater - @Mock - private lateinit var falsingCollector: FalsingCollector - @Mock - private lateinit var falsingManager: FalsingManager - @Mock - private lateinit var userManager: UserManager - @Mock - private lateinit var userTracker: UserTracker - @Mock - private lateinit var flags: FeatureFlags - @Mock - private lateinit var viewModelFactoryLazy: dagger.Lazy<UserSwitcherViewModel.Factory> - @Mock - private lateinit var onBackDispatcher: OnBackInvokedDispatcher - @Mock - private lateinit var decorView: View - @Mock - private lateinit var window: Window - @Mock - private lateinit var userSwitcherRootView: UserSwitcherRootView - @Captor - private lateinit var onBackInvokedCallback: ArgumentCaptor<OnBackInvokedCallback> - var isFinished = false - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - activity = spy(object : UserSwitcherActivity( - userSwitcherController, - broadcastDispatcher, - falsingCollector, - falsingManager, - userManager, - userTracker, - flags, - viewModelFactoryLazy, - ) { - override fun getOnBackInvokedDispatcher() = onBackDispatcher - override fun getMainExecutor(): Executor = FakeExecutor(FakeSystemClock()) - override fun finish() { - isFinished = true - } - }) - `when`(activity.window).thenReturn(window) - `when`(window.decorView).thenReturn(decorView) - `when`(activity.findViewById<UserSwitcherRootView>(R.id.user_switcher_root)) - .thenReturn(userSwitcherRootView) - `when`(activity.findViewById<View>(R.id.cancel)).thenReturn(mock(View::class.java)) - `when`(activity.findViewById<View>(R.id.add)).thenReturn(mock(View::class.java)) - `when`(activity.application).thenReturn(mock(Application::class.java)) - doNothing().`when`(activity).setContentView(anyInt()) - } - - @Test - fun testMaxColumns() { - assertThat(activity.getMaxColumns(3)).isEqualTo(4) - assertThat(activity.getMaxColumns(4)).isEqualTo(4) - assertThat(activity.getMaxColumns(5)).isEqualTo(3) - assertThat(activity.getMaxColumns(6)).isEqualTo(3) - assertThat(activity.getMaxColumns(7)).isEqualTo(4) - assertThat(activity.getMaxColumns(9)).isEqualTo(5) - } - - @Test - fun onCreate_callbackRegistration() { - activity.createActivity() - verify(onBackDispatcher).registerOnBackInvokedCallback( - eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), any()) - - activity.destroyActivity() - verify(onBackDispatcher).unregisterOnBackInvokedCallback(any()) - } - - @Test - fun onBackInvokedCallback_finishesActivity() { - activity.createActivity() - verify(onBackDispatcher).registerOnBackInvokedCallback( - eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), onBackInvokedCallback.capture()) - - onBackInvokedCallback.value.onBackInvoked() - assertThat(isFinished).isTrue() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt new file mode 100644 index 000000000000..525d8371c9ff --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.data.repository + +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(JUnit4::class) +class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { + + @Before + fun setUp() { + super.setUp(isRefactored = true) + } + + @Test + fun userSwitcherSettings() = runSelfCancelingTest { + setUpGlobalSettings( + isSimpleUserSwitcher = true, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + underTest = create(this) + + var value: UserSwitcherSettingsModel? = null + underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) + + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = true, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + + setUpGlobalSettings( + isSimpleUserSwitcher = false, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = false, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + } + + @Test + fun refreshUsers() = runSelfCancelingTest { + underTest = create(this) + val initialExpectedValue = + setUpUsers( + count = 3, + selectedIndex = 0, + ) + var userInfos: List<UserInfo>? = null + var selectedUserInfo: UserInfo? = null + underTest.userInfos.onEach { userInfos = it }.launchIn(this) + underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) + + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(initialExpectedValue) + assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val secondExpectedValue = + setUpUsers( + count = 4, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(secondExpectedValue) + assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val selectedNonGuestUserId = selectedUserInfo?.id + val thirdExpectedValue = + setUpUsers( + count = 2, + isLastGuestUser = true, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(thirdExpectedValue) + assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1]) + assertThat(selectedUserInfo?.isGuest).isTrue() + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId) + } + + @Test + fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest { + underTest = create(this) + val unsortedUsers = + setUpUsers( + count = 3, + selectedIndex = 0, + isLastGuestUser = true, + ) + unsortedUsers[0].creationTime = 999 + unsortedUsers[1].creationTime = 900 + unsortedUsers[2].creationTime = 950 + val expectedUsers = + listOf( + unsortedUsers[1], + unsortedUsers[0], + unsortedUsers[2], // last because this is the guest + ) + var userInfos: List<UserInfo>? = null + underTest.userInfos.onEach { userInfos = it }.launchIn(this) + + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(expectedUsers) + } + + private fun setUpUsers( + count: Int, + isLastGuestUser: Boolean = false, + selectedIndex: Int = 0, + ): List<UserInfo> { + val userInfos = + (0 until count).map { index -> + createUserInfo( + index, + isGuest = isLastGuestUser && index == count - 1, + ) + } + whenever(manager.aliveUsers).thenReturn(userInfos) + tracker.set(userInfos, selectedIndex) + return userInfos + } + + private fun createUserInfo( + id: Int, + isGuest: Boolean, + ): UserInfo { + val flags = 0 + return UserInfo( + id, + "user_$id", + /* iconPath= */ "", + flags, + if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags), + ) + } + + private fun setUpGlobalSettings( + isSimpleUserSwitcher: Boolean = false, + isAddUsersFromLockscreen: Boolean = false, + isUserSwitcherEnabled: Boolean = true, + ) { + context.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_expandLockScreenUserSwitcher, + true, + ) + globalSettings.putIntForUser( + UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER, + if (isSimpleUserSwitcher) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.ADD_USERS_WHEN_LOCKED, + if (isAddUsersFromLockscreen) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.USER_SWITCHER_ENABLED, + if (isUserSwitcherEnabled) 1 else 0, + UserHandle.USER_SYSTEM, + ) + } + + private fun assertUserSwitcherSettings( + model: UserSwitcherSettingsModel?, + expectedSimpleUserSwitcher: Boolean, + expectedAddUsersFromLockscreen: Boolean, + expectedUserSwitcherEnabled: Boolean, + ) { + checkNotNull(model) + assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher) + assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen) + assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled) + } + + /** + * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which + * is then automatically canceled and cleaned-up. + */ + private fun runSelfCancelingTest( + block: suspend CoroutineScope.() -> Unit, + ) = + runBlocking(Dispatchers.Main.immediate) { + val scope = CoroutineScope(coroutineContext + Job()) + block(scope) + scope.cancel() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index 6fec343d036c..dcea83a55a74 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -17,201 +17,54 @@ package com.android.systemui.user.data.repository -import android.content.pm.UserInfo import android.os.UserManager -import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.FakeUserTracker import com.android.systemui.statusbar.policy.UserSwitcherController -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.capture -import com.google.common.truth.Truth.assertThat +import com.android.systemui.util.settings.FakeSettings +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.ArgumentCaptor -import org.mockito.Captor +import kotlinx.coroutines.test.TestCoroutineScope import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations -@SmallTest -@RunWith(JUnit4::class) -class UserRepositoryImplTest : SysuiTestCase() { +abstract class UserRepositoryImplTest : SysuiTestCase() { - @Mock private lateinit var manager: UserManager - @Mock private lateinit var controller: UserSwitcherController - @Captor - private lateinit var userSwitchCallbackCaptor: - ArgumentCaptor<UserSwitcherController.UserSwitchCallback> + @Mock protected lateinit var manager: UserManager + @Mock protected lateinit var controller: UserSwitcherController - private lateinit var underTest: UserRepositoryImpl + protected lateinit var underTest: UserRepositoryImpl - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) - whenever(controller.isGuestUserAutoCreated).thenReturn(false) - whenever(controller.isGuestUserResetting).thenReturn(false) - - underTest = - UserRepositoryImpl( - appContext = context, - manager = manager, - controller = controller, - ) - } - - @Test - fun `users - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `users - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `users - does not include actions`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - ) - ) - var models: List<UserModel>? = null - val job = underTest.users.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)?.id).isEqualTo(0) - assertThat(models?.get(0)?.isSelected).isTrue() - assertThat(models?.get(1)?.id).isEqualTo(1) - assertThat(models?.get(1)?.isSelected).isFalse() - assertThat(models?.get(2)?.id).isEqualTo(2) - assertThat(models?.get(2)?.isSelected).isFalse() - job.cancel() - } - - @Test - fun selectedUser() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createUserRecord(1), - createUserRecord(2), - ) - ) - var id: Int? = null - val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) + protected lateinit var globalSettings: FakeSettings + protected lateinit var tracker: FakeUserTracker + protected lateinit var featureFlags: FakeFeatureFlags - assertThat(id).isEqualTo(0) - - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0), - createUserRecord(1), - createUserRecord(2, isSelected = true), - ) - ) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - userSwitchCallbackCaptor.value.onUserSwitched() - assertThat(id).isEqualTo(2) - - job.cancel() - } - - @Test - fun `actions - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `actions - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `actopms - does not include users`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - ) - ) - var models: List<UserActionModel>? = null - val job = underTest.actions.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) - assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) - assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) - job.cancel() - } + protected fun setUp(isRefactored: Boolean) { + MockitoAnnotations.initMocks(this) - private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { - return UserRecord( - info = UserInfo(id, "name$id", 0), - isCurrent = isSelected, - ) + globalSettings = FakeSettings() + tracker = FakeUserTracker() + featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored) } - private fun createActionRecord(action: UserActionModel): UserRecord { - return UserRecord( - isAddUser = action == UserActionModel.ADD_USER, - isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, - isGuest = action == UserActionModel.ENTER_GUEST_MODE, + protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl { + return UserRepositoryImpl( + appContext = context, + manager = manager, + controller = controller, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + globalSettings = globalSettings, + tracker = tracker, + featureFlags = featureFlags, ) } companion object { - private val IMMEDIATE = Dispatchers.Main.immediate + @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt new file mode 100644 index 000000000000..a363a037c499 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.data.repository + +import android.content.pm.UserInfo +import androidx.test.filters.SmallTest +import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(JUnit4::class) +class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } + + @Captor + private lateinit var userSwitchCallbackCaptor: + ArgumentCaptor<UserSwitcherController.UserSwitchCallback> + + @Before + fun setUp() { + super.setUp(isRefactored = false) + + whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) + whenever(controller.isGuestUserAutoCreated).thenReturn(false) + whenever(controller.isGuestUserResetting).thenReturn(false) + + underTest = create() + } + + @Test + fun `users - registers for updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.users.onEach {}.launchIn(this) + + verify(controller).addUserSwitchCallback(any()) + + job.cancel() + } + + @Test + fun `users - unregisters from updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.users.onEach {}.launchIn(this) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + + job.cancel() + + verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) + } + + @Test + fun `users - does not include actions`() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createActionRecord(UserActionModel.ADD_USER), + createUserRecord(1), + createUserRecord(2), + createActionRecord(UserActionModel.ADD_SUPERVISED_USER), + createActionRecord(UserActionModel.ENTER_GUEST_MODE), + createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT), + ) + ) + var models: List<UserModel>? = null + val job = underTest.users.onEach { models = it }.launchIn(this) + + assertThat(models).hasSize(3) + assertThat(models?.get(0)?.id).isEqualTo(0) + assertThat(models?.get(0)?.isSelected).isTrue() + assertThat(models?.get(1)?.id).isEqualTo(1) + assertThat(models?.get(1)?.isSelected).isFalse() + assertThat(models?.get(2)?.id).isEqualTo(2) + assertThat(models?.get(2)?.isSelected).isFalse() + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createUserRecord(1), + createUserRecord(2), + ) + ) + var id: Int? = null + val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) + + assertThat(id).isEqualTo(0) + + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0), + createUserRecord(1), + createUserRecord(2, isSelected = true), + ) + ) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + userSwitchCallbackCaptor.value.onUserSwitched() + assertThat(id).isEqualTo(2) + + job.cancel() + } + + @Test + fun `actions - unregisters from updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.actions.onEach {}.launchIn(this) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + + job.cancel() + + verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) + } + + @Test + fun `actions - registers for updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.actions.onEach {}.launchIn(this) + + verify(controller).addUserSwitchCallback(any()) + + job.cancel() + } + + @Test + fun `actions - does not include users`() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createActionRecord(UserActionModel.ADD_USER), + createUserRecord(1), + createUserRecord(2), + createActionRecord(UserActionModel.ADD_SUPERVISED_USER), + createActionRecord(UserActionModel.ENTER_GUEST_MODE), + createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT), + ) + ) + var models: List<UserActionModel>? = null + val job = underTest.actions.onEach { models = it }.launchIn(this) + + assertThat(models).hasSize(4) + assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) + assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) + assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) + assertThat(models?.get(3)).isEqualTo(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + job.cancel() + } + + private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { + return UserRecord( + info = UserInfo(id, "name$id", 0), + isCurrent = isSelected, + ) + } + + private fun createActionRecord(action: UserActionModel): UserRecord { + return UserRecord( + isAddUser = action == UserActionModel.ADD_USER, + isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, + isGuest = action == UserActionModel.ENTER_GUEST_MODE, + isManageUsers = action == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt new file mode 100644 index 000000000000..120bf791c462 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.app.admin.DevicePolicyManager +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class GuestUserInteractorTest : SysuiTestCase() { + + @Mock private lateinit var manager: UserManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var showDialog: (ShowDialogRequestModel) -> Unit + @Mock private lateinit var dismissDialog: () -> Unit + @Mock private lateinit var selectUser: (Int) -> Unit + @Mock private lateinit var switchUser: (Int) -> Unit + + private lateinit var underTest: GuestUserInteractor + + private lateinit var scope: TestCoroutineScope + private lateinit var repository: FakeUserRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(manager.createGuest(any())).thenReturn(GUEST_USER_INFO) + + scope = TestCoroutineScope() + repository = FakeUserRepository() + repository.setUserInfos(ALL_USERS) + + underTest = + GuestUserInteractor( + applicationContext = context, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = repository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = scope, + mainDispatcher = IMMEDIATE, + repository = repository, + ), + uiEventLogger = uiEventLogger, + ) + } + + @Test + fun `onDeviceBootCompleted - allowed to add - create guest`() = + runBlocking(IMMEDIATE) { + setAllowedToAdd() + + underTest.onDeviceBootCompleted() + + verify(manager).createGuest(any()) + verify(deviceProvisionedController, never()).addCallback(any()) + } + + @Test + fun `onDeviceBootCompleted - await provisioning - and create guest`() = + runBlocking(IMMEDIATE) { + setAllowedToAdd(isAllowed = false) + underTest.onDeviceBootCompleted() + val captor = + kotlinArgumentCaptor<DeviceProvisionedController.DeviceProvisionedListener>() + verify(deviceProvisionedController).addCallback(captor.capture()) + + setAllowedToAdd(isAllowed = true) + captor.value.onDeviceProvisionedChanged() + + verify(manager).createGuest(any()) + verify(deviceProvisionedController).removeCallback(captor.value) + } + + @Test + fun createAndSwitchTo() = + runBlocking(IMMEDIATE) { + underTest.createAndSwitchTo( + showDialog = showDialog, + dismissDialog = dismissDialog, + selectUser = selectUser, + ) + + verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + verify(manager).createGuest(any()) + verify(dismissDialog).invoke() + verify(selectUser).invoke(GUEST_USER_INFO.id) + } + + @Test + fun `createAndSwitchTo - fails to create - does not switch to`() = + runBlocking(IMMEDIATE) { + whenever(manager.createGuest(any())).thenReturn(null) + + underTest.createAndSwitchTo( + showDialog = showDialog, + dismissDialog = dismissDialog, + selectUser = selectUser, + ) + + verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + verify(manager).createGuest(any()) + verify(dismissDialog).invoke() + verify(selectUser, never()).invoke(anyInt()) + } + + @Test + fun `exit - returns to target user`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(GUEST_USER_INFO) + + val targetUserId = NON_GUEST_USER_INFO.id + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - returns to last non-guest`() = + runBlocking(IMMEDIATE) { + val expectedUserId = NON_GUEST_USER_INFO.id + whenever(manager.getUserInfo(expectedUserId)).thenReturn(NON_GUEST_USER_INFO) + repository.lastSelectedNonGuestUserId = expectedUserId + repository.setSelectedUserInfo(GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = UserHandle.USER_NULL, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(expectedUserId) + } + + @Test + fun `exit - last non-guest was removed - returns to system`() = + runBlocking(IMMEDIATE) { + val removedUserId = 310 + repository.lastSelectedNonGuestUserId = removedUserId + repository.setSelectedUserInfo(GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = UserHandle.USER_NULL, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(UserHandle.USER_SYSTEM) + } + + @Test + fun `exit - guest was ephemeral - it is removed`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setUserInfos(listOf(NON_GUEST_USER_INFO, EPHEMERAL_GUEST_USER_INFO)) + repository.setSelectedUserInfo(EPHEMERAL_GUEST_USER_INFO) + val targetUserId = NON_GUEST_USER_INFO.id + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager).markGuestForDeletion(EPHEMERAL_GUEST_USER_INFO.id) + verify(manager).removeUser(EPHEMERAL_GUEST_USER_INFO.id) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - force remove guest - it is removed`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setSelectedUserInfo(GUEST_USER_INFO) + val targetUserId = NON_GUEST_USER_INFO.id + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = true, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager).markGuestForDeletion(GUEST_USER_INFO.id) + verify(manager).removeUser(GUEST_USER_INFO.id) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - selected different from guest user - do nothing`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = 123, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotExit() + } + + @Test + fun `exit - selected is actually not a guest user - do nothing`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.exit( + guestUserId = NON_GUEST_USER_INFO.id, + targetUserId = 123, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotExit() + } + + @Test + fun `remove - returns to target user`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setSelectedUserInfo(GUEST_USER_INFO) + + val targetUserId = NON_GUEST_USER_INFO.id + underTest.remove( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager).markGuestForDeletion(GUEST_USER_INFO.id) + verify(manager).removeUser(GUEST_USER_INFO.id) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `remove - selected different from guest user - do nothing`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.remove( + guestUserId = GUEST_USER_INFO.id, + targetUserId = 123, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotRemove() + } + + @Test + fun `remove - selected is actually not a guest user - do nothing`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.remove( + guestUserId = NON_GUEST_USER_INFO.id, + targetUserId = 123, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotRemove() + } + + private fun setAllowedToAdd(isAllowed: Boolean = true) { + whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(isAllowed) + whenever(devicePolicyManager.isDeviceManaged).thenReturn(!isAllowed) + } + + private fun verifyDidNotExit() { + verifyDidNotRemove() + verify(manager, never()).getUserInfo(anyInt()) + verify(uiEventLogger, never()).log(any()) + } + + private fun verifyDidNotRemove() { + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(showDialog, never()).invoke(any()) + verify(dismissDialog, never()).invoke() + verify(switchUser, never()).invoke(anyInt()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private val NON_GUEST_USER_INFO = + UserInfo( + /* id= */ 818, + /* name= */ "non_guest", + /* flags= */ 0, + ) + private val GUEST_USER_INFO = + UserInfo( + /* id= */ 669, + /* name= */ "guest", + /* iconPath= */ "", + /* flags= */ 0, + UserManager.USER_TYPE_FULL_GUEST, + ) + private val EPHEMERAL_GUEST_USER_INFO = + UserInfo( + /* id= */ 669, + /* name= */ "guest", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_EPHEMERAL, + UserManager.USER_TYPE_FULL_GUEST, + ) + private val ALL_USERS = + listOf( + NON_GUEST_USER_INFO, + GUEST_USER_INFO, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt new file mode 100644 index 000000000000..593ce1f0a2f5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.user.data.repository.FakeUserRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class RefreshUsersSchedulerTest : SysuiTestCase() { + + private lateinit var underTest: RefreshUsersScheduler + + private lateinit var repository: FakeUserRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + repository = FakeUserRepository() + } + + @Test + fun `pause - prevents the next refresh from happening`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.pause() + + underTest.refreshIfNotPaused() + assertThat(repository.refreshUsersCallCount).isEqualTo(0) + } + + @Test + fun `unpauseAndRefresh - forces the refresh even when paused`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.pause() + + underTest.unpauseAndRefresh() + + assertThat(repository.refreshUsersCallCount).isEqualTo(1) + } + + @Test + fun `refreshIfNotPaused - refreshes when not paused`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.refreshIfNotPaused() + + assertThat(repository.refreshUsersCallCount).isEqualTo(1) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt new file mode 100644 index 000000000000..97571b23be56 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt @@ -0,0 +1,739 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.content.Intent +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.internal.R.drawable.ic_account_circle +import com.android.systemui.R +import com.android.systemui.common.shared.model.Text +import com.android.systemui.qs.user.UserSwitchDialogController +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(JUnit4::class) +class UserInteractorRefactoredTest : UserInteractorTest() { + + override fun isRefactored(): Boolean { + return true + } + + @Before + override fun setUp() { + super.setUp() + + overrideResource(R.drawable.ic_account_circle, GUEST_ICON) + overrideResource(R.dimen.max_avatar_size, 10) + overrideResource( + com.android.internal.R.string.config_supervisedUserCreationPackage, + SUPERVISED_USER_CREATION_APP_PACKAGE, + ) + whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) + whenever(manager.canAddMoreUsers(any())).thenReturn(true) + } + + @Test + fun `onRecordSelected - user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower) + + verify(dialogShower).dismiss() + verify(activityManager).switchUser(userInfos[1].id) + Unit + } + + @Test + fun `onRecordSelected - switch to guest user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(info = userInfos.last())) + + verify(activityManager).switchUser(userInfos.last().id) + Unit + } + + @Test + fun `onRecordSelected - enter guest mode`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) + whenever(manager.createGuest(any())).thenReturn(guestUserInfo) + + underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower) + + verify(dialogShower).dismiss() + verify(manager).createGuest(any()) + Unit + } + + @Test + fun `onRecordSelected - action`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower) + + verify(dialogShower, never()).dismiss() + verify(activityStarter).startActivity(any(), anyBoolean()) + } + + @Test + fun `users - switcher enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 3, includeGuest = true) + + job.cancel() + } + + @Test + fun `users - switches to second user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + userRepository.setSelectedUserInfo(userInfos[1]) + + assertUsers(models = value, count = 2, selectedIndex = 1) + job.cancel() + } + + @Test + fun `users - switcher not enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 1) + + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: UserModel? = null + val job = underTest.selectedUser.onEach { value = it }.launchIn(this) + assertUser(value, id = 0, isSelected = true) + + userRepository.setSelectedUserInfo(userInfos[1]) + assertUser(value, id = 1, isSelected = true) + + job.cancel() + } + + @Test + fun `actions - device unlocked`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device unlocked user not primary - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList<UserActionModel>()) + + job.cancel() + } + + @Test + fun `actions - device unlocked user is guest - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + assertThat(userInfos[1].isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList<UserActionModel>()) + + job.cancel() + } + + @Test + fun `actions - device locked add from lockscreen set - full list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + isAddUsersFromLockscreen = true, + ) + ) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device locked - only guest action and manage user is shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(true) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT + ) + ) + + job.cancel() + } + + @Test + fun `executeAction - add user - dialog shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + val dialogShower: UserSwitchDialogController.DialogShower = mock() + + underTest.executeAction(UserActionModel.ADD_USER, dialogShower) + assertThat(dialogRequest) + .isEqualTo( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = userInfos[0].userHandle, + isKeyguardShowing = false, + showEphemeralMessage = false, + dialogShower = dialogShower, + ) + ) + + underTest.onDialogShown() + assertThat(dialogRequest).isNull() + + job.cancel() + } + + @Test + fun `executeAction - add supervised user - starts activity`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + val intentCaptor = kotlinArgumentCaptor<Intent>() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) + assertThat(intentCaptor.value.action) + .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER) + assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE) + } + + @Test + fun `executeAction - navigate to manage users`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + val intentCaptor = kotlinArgumentCaptor<Intent>() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) + assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) + } + + @Test + fun `executeAction - guest mode`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) + whenever(manager.createGuest(any())).thenReturn(guestUserInfo) + val dialogRequests = mutableListOf<ShowDialogRequestModel?>() + val showDialogsJob = + underTest.dialogShowRequests + .onEach { + dialogRequests.add(it) + if (it != null) { + underTest.onDialogShown() + } + } + .launchIn(this) + val dismissDialogsJob = + underTest.dialogDismissRequests + .onEach { + if (it != null) { + underTest.onDialogDismissed() + } + } + .launchIn(this) + + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + assertThat(dialogRequests) + .contains( + ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), + ) + verify(activityManager).switchUser(guestUserInfo.id) + + showDialogsJob.cancel() + dismissDialogsJob.cancel() + } + + @Test + fun `selectUser - already selected guest re-selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser( + newlySelectedUserId = guestUserInfo.id, + dialogShower = dialogShower, + ) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + verify(dialogShower, never()).dismiss() + job.cancel() + } + + @Test + fun `selectUser - currently guest non-guest selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + verify(dialogShower, never()).dismiss() + job.cancel() + } + + @Test + fun `selectUser - not currently guest - switches users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower) + + assertThat(dialogRequest).isNull() + verify(activityManager).switchUser(userInfos[1].id) + verify(dialogShower).dismiss() + job.cancel() + } + + @Test + fun `Telephony call state changes - refreshes users`() = + runBlocking(IMMEDIATE) { + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + telephonyRepository.setCallState(1) + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User switched broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val callback1: UserInteractor.UserCallback = mock() + val callback2: UserInteractor.UserCallback = mock() + underTest.addCallback(callback1) + underTest.addCallback(callback2) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + userRepository.setSelectedUserInfo(userInfos[1]) + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_SWITCHED) + .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), + ) + } + + verify(callback1).onUserStateChanged() + verify(callback2).onUserStateChanged() + assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User info changed broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_INFO_CHANGED), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `System user unlocked broadcast - refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED) + .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `Non-system user unlocked broadcast - do not refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) + } + + @Test + fun userRecords() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + testCoroutineScope.advanceUntilIdle() + + assertRecords( + records = underTest.userRecords.value, + userIds = listOf(0, 1, 2), + selectedUserIndex = 0, + includeGuest = false, + expectedActions = + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ), + ) + } + + @Test + fun selectedUserRecord() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + assertRecordForUser( + record = underTest.selectedUserRecord.value, + id = 0, + hasPicture = true, + isCurrent = true, + isSwitchToEnabled = true, + ) + } + + private fun assertUsers( + models: List<UserModel>?, + count: Int, + selectedIndex: Int = 0, + includeGuest: Boolean = false, + ) { + checkNotNull(models) + assertThat(models.size).isEqualTo(count) + models.forEachIndexed { index, model -> + assertUser( + model = model, + id = index, + isSelected = index == selectedIndex, + isGuest = includeGuest && index == count - 1 + ) + } + } + + private fun assertUser( + model: UserModel?, + id: Int, + isSelected: Boolean = false, + isGuest: Boolean = false, + ) { + checkNotNull(model) + assertThat(model.id).isEqualTo(id) + assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id")) + assertThat(model.isSelected).isEqualTo(isSelected) + assertThat(model.isSelectable).isTrue() + assertThat(model.isGuest).isEqualTo(isGuest) + } + + private fun assertRecords( + records: List<UserRecord>, + userIds: List<Int>, + selectedUserIndex: Int = 0, + includeGuest: Boolean = false, + expectedActions: List<UserActionModel> = emptyList(), + ) { + assertThat(records.size >= userIds.size).isTrue() + userIds.indices.forEach { userIndex -> + val record = records[userIndex] + assertThat(record.info).isNotNull() + val isGuest = includeGuest && userIndex == userIds.size - 1 + assertRecordForUser( + record = record, + id = userIds[userIndex], + hasPicture = !isGuest, + isCurrent = userIndex == selectedUserIndex, + isGuest = isGuest, + isSwitchToEnabled = true, + ) + } + + assertThat(records.size - userIds.size).isEqualTo(expectedActions.size) + (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex -> + val record = records[actionIndex] + assertThat(record.info).isNull() + assertRecordForAction( + record = record, + type = expectedActions[actionIndex - userIds.size], + ) + } + } + + private fun assertRecordForUser( + record: UserRecord?, + id: Int? = null, + hasPicture: Boolean = false, + isCurrent: Boolean = false, + isGuest: Boolean = false, + isSwitchToEnabled: Boolean = false, + ) { + checkNotNull(record) + assertThat(record.info?.id).isEqualTo(id) + assertThat(record.picture != null).isEqualTo(hasPicture) + assertThat(record.isCurrent).isEqualTo(isCurrent) + assertThat(record.isGuest).isEqualTo(isGuest) + assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled) + } + + private fun assertRecordForAction( + record: UserRecord, + type: UserActionModel, + ) { + assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE) + assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER) + assertThat(record.isAddSupervisedUser) + .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER) + } + + private fun createUserInfos( + count: Int, + includeGuest: Boolean, + ): List<UserInfo> { + return (0 until count).map { index -> + val isGuest = includeGuest && index == count - 1 + createUserInfo( + id = index, + name = + if (isGuest) { + "guest" + } else { + "user_$index" + }, + isPrimary = !isGuest && index == 0, + isGuest = isGuest, + ) + } + } + + private fun createUserInfo( + id: Int, + name: String, + isPrimary: Boolean = false, + isGuest: Boolean = false, + ): UserInfo { + return UserInfo( + id, + name, + /* iconPath= */ "", + /* flags= */ if (isPrimary) { + UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN + } else { + 0 + }, + if (isGuest) { + UserManager.USER_TYPE_FULL_GUEST + } else { + UserManager.USER_TYPE_FULL_SYSTEM + }, + ) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val GUEST_ICON: Drawable = mock() + private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index e914e2e0a1da..1680c36cef87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -17,51 +17,63 @@ package com.android.systemui.user.domain.interactor -import androidx.test.filters.SmallTest +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager +import android.os.UserManager +import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.qs.user.UserSwitchDialogController +import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.nullable -import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 +import kotlinx.coroutines.test.TestCoroutineScope import org.mockito.Mock -import org.mockito.Mockito.anyBoolean -import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -@SmallTest -@RunWith(JUnit4::class) -class UserInteractorTest : SysuiTestCase() { +abstract class UserInteractorTest : SysuiTestCase() { - @Mock private lateinit var controller: UserSwitcherController - @Mock private lateinit var activityStarter: ActivityStarter + @Mock protected lateinit var controller: UserSwitcherController + @Mock protected lateinit var activityStarter: ActivityStarter + @Mock protected lateinit var manager: UserManager + @Mock protected lateinit var activityManager: ActivityManager + @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock protected lateinit var devicePolicyManager: DevicePolicyManager + @Mock protected lateinit var uiEventLogger: UiEventLogger + @Mock protected lateinit var dialogShower: UserSwitchDialogController.DialogShower - private lateinit var underTest: UserInteractor + protected lateinit var underTest: UserInteractor - private lateinit var userRepository: FakeUserRepository - private lateinit var keyguardRepository: FakeKeyguardRepository + protected lateinit var testCoroutineScope: TestCoroutineScope + protected lateinit var userRepository: FakeUserRepository + protected lateinit var keyguardRepository: FakeKeyguardRepository + protected lateinit var telephonyRepository: FakeTelephonyRepository - @Before - fun setUp() { + abstract fun isRefactored(): Boolean + + open fun setUp() { MockitoAnnotations.initMocks(this) userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() + telephonyRepository = FakeTelephonyRepository() + testCoroutineScope = TestCoroutineScope() + val refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = testCoroutineScope, + mainDispatcher = IMMEDIATE, + repository = userRepository, + ) underTest = UserInteractor( + applicationContext = context, repository = userRepository, controller = controller, activityStarter = activityStarter, @@ -69,142 +81,34 @@ class UserInteractorTest : SysuiTestCase() { KeyguardInteractor( repository = keyguardRepository, ), - ) - } - - @Test - fun `actions - not actionable when locked and locked - no actions`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(true) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions).isEmpty() - job.cancel() - } - - @Test - fun `actions - not actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(false) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(false) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + featureFlags = + FakeFeatureFlags().apply { + set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored()) + }, + manager = manager, + applicationScope = testCoroutineScope, + telephonyInteractor = + TelephonyInteractor( + repository = telephonyRepository, + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = IMMEDIATE, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = testCoroutineScope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) ) - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(true) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun selectUser() { - val userId = 3 - - underTest.selectUser(userId) - - verify(controller).onUserSelected(eq(userId), nullable()) - } - - @Test - fun `executeAction - guest`() { - underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) - - verify(controller).createAndSwitchToGuestUser(nullable()) - } - - @Test - fun `executeAction - add user`() { - underTest.executeAction(UserActionModel.ADD_USER) - - verify(controller).showAddUserDialog(nullable()) - } - - @Test - fun `executeAction - add supervised user`() { - underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) - - verify(controller).startSupervisedUserActivity() - } - - @Test - fun `executeAction - manage users`() { - underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - - verify(activityStarter).startActivity(any(), anyBoolean()) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt new file mode 100644 index 000000000000..6a17c8ddc63d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.nullable +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(JUnit4::class) +open class UserInteractorUnrefactoredTest : UserInteractorTest() { + + override fun isRefactored(): Boolean { + return false + } + + @Before + override fun setUp() { + super.setUp() + } + + @Test + fun `actions - not actionable when locked and locked - no actions`() = + runBlocking(IMMEDIATE) { + userRepository.setActions(UserActionModel.values().toList()) + userRepository.setActionableWhenLocked(false) + keyguardRepository.setKeyguardShowing(true) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions).isEmpty() + job.cancel() + } + + @Test + fun `actions - not actionable when locked and not locked`() = + runBlocking(IMMEDIATE) { + setActions() + userRepository.setActionableWhenLocked(false) + keyguardRepository.setKeyguardShowing(false) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun `actions - actionable when locked and not locked`() = + runBlocking(IMMEDIATE) { + setActions() + userRepository.setActionableWhenLocked(true) + keyguardRepository.setKeyguardShowing(false) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun `actions - actionable when locked and locked`() = + runBlocking(IMMEDIATE) { + setActions() + userRepository.setActionableWhenLocked(true) + keyguardRepository.setKeyguardShowing(true) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun selectUser() { + val userId = 3 + + underTest.selectUser(userId) + + verify(controller).onUserSelected(eq(userId), nullable()) + } + + @Test + fun `executeAction - guest`() { + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + verify(controller).createAndSwitchToGuestUser(nullable()) + } + + @Test + fun `executeAction - add user`() { + underTest.executeAction(UserActionModel.ADD_USER) + + verify(controller).showAddUserDialog(nullable()) + } + + @Test + fun `executeAction - add supervised user`() { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + verify(controller).startSupervisedUserActivity() + } + + @Test + fun `executeAction - manage users`() { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + verify(activityStarter).startActivity(any(), anyBoolean()) + } + + private fun setActions() { + userRepository.setActions(UserActionModel.values().toList()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index ef4500df3600..c12a868dbaed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -17,17 +17,28 @@ package com.android.systemui.user.ui.viewmodel +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager import android.graphics.drawable.Drawable +import android.os.UserManager import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Text +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.domain.interactor.GuestUserInteractor +import com.android.systemui.user.domain.interactor.RefreshUsersScheduler import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel @@ -38,6 +49,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test @@ -52,6 +64,11 @@ class UserSwitcherViewModelTest : SysuiTestCase() { @Mock private lateinit var controller: UserSwitcherController @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var activityManager: ActivityManager + @Mock private lateinit var manager: UserManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger private lateinit var underTest: UserSwitcherViewModel @@ -66,22 +83,60 @@ class UserSwitcherViewModelTest : SysuiTestCase() { userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() powerRepository = FakePowerRepository() + val featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true) + val scope = TestCoroutineScope() + val refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = scope, + mainDispatcher = IMMEDIATE, + repository = userRepository, + ) + val guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, + ) + underTest = UserSwitcherViewModel.Factory( userInteractor = UserInteractor( + applicationContext = context, repository = userRepository, controller = controller, activityStarter = activityStarter, keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, - ) + ), + featureFlags = featureFlags, + manager = manager, + applicationScope = scope, + telephonyInteractor = + TelephonyInteractor( + repository = FakeTelephonyRepository(), + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = IMMEDIATE, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = guestUserInteractor, ), powerInteractor = PowerInteractor( repository = powerRepository, ), + featureFlags = featureFlags, + guestUserInteractor = guestUserInteractor, ) .create(UserSwitcherViewModel::class.java) } @@ -97,6 +152,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = true, isSelectable = true, + isGuest = false, ), UserModel( id = 1, @@ -104,6 +160,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = false, isSelectable = true, + isGuest = false, ), UserModel( id = 2, @@ -111,6 +168,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = false, isSelectable = false, + isGuest = false, ), ) ) @@ -210,6 +268,26 @@ class UserSwitcherViewModelTest : SysuiTestCase() { } @Test + fun `menu actions`() = + runBlocking(IMMEDIATE) { + userRepository.setActions(UserActionModel.values().toList()) + var actions: List<UserActionViewModel>? = null + val job = underTest.menu.onEach { actions = it }.launchIn(this) + + assertThat(actions?.map { it.viewKey }) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(), + UserActionModel.ADD_USER.ordinal.toLong(), + UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(), + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(), + ) + ) + + job.cancel() + } + + @Test fun `isFinishRequested - finishes when user is switched`() = runBlocking(IMMEDIATE) { setUsers(count = 2) @@ -260,7 +338,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { job.cancel() } - private fun setUsers(count: Int) { + private suspend fun setUsers(count: Int) { userRepository.setUsers( (0 until count).map { index -> UserModel( @@ -269,6 +347,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = index == 0, isSelectable = true, + isGuest = false, ) } ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt deleted file mode 100644 index 5e09b81da4e8..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.util.collection - -import android.testing.AndroidTestingRunner -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertSame -import org.junit.Assert.assertThrows -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.MockitoAnnotations - -@SmallTest -@RunWith(AndroidTestingRunner::class) -class RingBufferTest : SysuiTestCase() { - - private val buffer = RingBuffer(5) { TestElement() } - - private val history = mutableListOf<TestElement>() - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - } - - @Test - fun testBarelyFillBuffer() { - fillBuffer(5) - - assertEquals(0, buffer[0].id) - assertEquals(1, buffer[1].id) - assertEquals(2, buffer[2].id) - assertEquals(3, buffer[3].id) - assertEquals(4, buffer[4].id) - } - - @Test - fun testPartiallyFillBuffer() { - fillBuffer(3) - - assertEquals(3, buffer.size) - - assertEquals(0, buffer[0].id) - assertEquals(1, buffer[1].id) - assertEquals(2, buffer[2].id) - - assertThrows(IndexOutOfBoundsException::class.java) { buffer[3] } - assertThrows(IndexOutOfBoundsException::class.java) { buffer[4] } - } - - @Test - fun testSpinBuffer() { - fillBuffer(277) - - assertEquals(272, buffer[0].id) - assertEquals(273, buffer[1].id) - assertEquals(274, buffer[2].id) - assertEquals(275, buffer[3].id) - assertEquals(276, buffer[4].id) - assertThrows(IndexOutOfBoundsException::class.java) { buffer[5] } - - assertEquals(5, buffer.size) - } - - @Test - fun testElementsAreRecycled() { - fillBuffer(23) - - assertSame(history[4], buffer[1]) - assertSame(history[9], buffer[1]) - assertSame(history[14], buffer[1]) - assertSame(history[19], buffer[1]) - } - - @Test - fun testIterator() { - fillBuffer(13) - - val iterator = buffer.iterator() - - for (i in 0 until 5) { - assertEquals(history[8 + i], iterator.next()) - } - assertFalse(iterator.hasNext()) - assertThrows(NoSuchElementException::class.java) { iterator.next() } - } - - @Test - fun testForEach() { - fillBuffer(13) - var i = 8 - - buffer.forEach { - assertEquals(history[i], it) - i++ - } - assertEquals(13, i) - } - - private fun fillBuffer(count: Int) { - for (i in 0 until count) { - val elem = buffer.advance() - elem.id = history.size - history.add(elem) - } - } -} - -private class TestElement(var id: Int = 0) { - override fun toString(): String { - return "{TestElement $id}" - } -}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java index 125b3627b342..17d81c8338cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java @@ -73,10 +73,16 @@ public class ConditionMonitorTest extends SysuiTestCase { .addConditions(mConditions); } + private Condition createMockCondition() { + final Condition condition = Mockito.mock(Condition.class); + when(condition.isConditionSet()).thenReturn(true); + return condition; + } + @Test public void testOverridingCondition() { - final Condition overridingCondition = Mockito.mock(Condition.class); - final Condition regularCondition = Mockito.mock(Condition.class); + final Condition overridingCondition = createMockCondition(); + final Condition regularCondition = createMockCondition(); final Monitor.Callback callback = Mockito.mock(Monitor.Callback.class); final Monitor.Callback referenceCallback = Mockito.mock(Monitor.Callback.class); @@ -127,9 +133,9 @@ public class ConditionMonitorTest extends SysuiTestCase { */ @Test public void testMultipleOverridingConditions() { - final Condition overridingCondition = Mockito.mock(Condition.class); - final Condition overridingCondition2 = Mockito.mock(Condition.class); - final Condition regularCondition = Mockito.mock(Condition.class); + final Condition overridingCondition = createMockCondition(); + final Condition overridingCondition2 = createMockCondition(); + final Condition regularCondition = createMockCondition(); final Monitor.Callback callback = Mockito.mock(Monitor.Callback.class); final Monitor monitor = new Monitor(mExecutor); @@ -340,4 +346,114 @@ public class ConditionMonitorTest extends SysuiTestCase { mExecutor.runAllReady(); verify(callback).onConditionsChanged(true); } + + @Test + public void clearCondition_shouldUpdateValue() { + mCondition1.fakeUpdateCondition(false); + mCondition2.fakeUpdateCondition(true); + mCondition3.fakeUpdateCondition(true); + + final Monitor.Callback callback = + mock(Monitor.Callback.class); + mConditionMonitor.addSubscription(getDefaultBuilder(callback).build()); + mExecutor.runAllReady(); + verify(callback).onConditionsChanged(false); + + mCondition1.clearCondition(); + mExecutor.runAllReady(); + verify(callback).onConditionsChanged(true); + } + + @Test + public void unsetCondition_shouldNotAffectValue() { + final FakeCondition settableCondition = new FakeCondition(null, false); + mCondition1.fakeUpdateCondition(true); + mCondition2.fakeUpdateCondition(true); + mCondition3.fakeUpdateCondition(true); + + final Monitor.Callback callback = + mock(Monitor.Callback.class); + + mConditionMonitor.addSubscription(getDefaultBuilder(callback) + .addCondition(settableCondition) + .build()); + + mExecutor.runAllReady(); + verify(callback).onConditionsChanged(true); + } + + @Test + public void setUnsetCondition_shouldAffectValue() { + final FakeCondition settableCondition = new FakeCondition(null, false); + mCondition1.fakeUpdateCondition(true); + mCondition2.fakeUpdateCondition(true); + mCondition3.fakeUpdateCondition(true); + + final Monitor.Callback callback = + mock(Monitor.Callback.class); + + mConditionMonitor.addSubscription(getDefaultBuilder(callback) + .addCondition(settableCondition) + .build()); + + mExecutor.runAllReady(); + verify(callback).onConditionsChanged(true); + clearInvocations(callback); + + settableCondition.fakeUpdateCondition(false); + mExecutor.runAllReady(); + verify(callback).onConditionsChanged(false); + clearInvocations(callback); + + + settableCondition.clearCondition(); + mExecutor.runAllReady(); + verify(callback).onConditionsChanged(true); + } + + @Test + public void clearingOverridingCondition_shouldBeExcluded() { + final FakeCondition overridingCondition = new FakeCondition(true, true); + mCondition1.fakeUpdateCondition(false); + mCondition2.fakeUpdateCondition(false); + mCondition3.fakeUpdateCondition(false); + + final Monitor.Callback callback = + mock(Monitor.Callback.class); + + mConditionMonitor.addSubscription(getDefaultBuilder(callback) + .addCondition(overridingCondition) + .build()); + + mExecutor.runAllReady(); + verify(callback).onConditionsChanged(true); + clearInvocations(callback); + + overridingCondition.clearCondition(); + mExecutor.runAllReady(); + verify(callback).onConditionsChanged(false); + } + + @Test + public void settingUnsetOverridingCondition_shouldBeIncluded() { + final FakeCondition overridingCondition = new FakeCondition(null, true); + mCondition1.fakeUpdateCondition(false); + mCondition2.fakeUpdateCondition(false); + mCondition3.fakeUpdateCondition(false); + + final Monitor.Callback callback = + mock(Monitor.Callback.class); + + mConditionMonitor.addSubscription(getDefaultBuilder(callback) + .addCondition(overridingCondition) + .build()); + + mExecutor.runAllReady(); + verify(callback).onConditionsChanged(false); + clearInvocations(callback); + + overridingCondition.fakeUpdateCondition(true); + mExecutor.runAllReady(); + verify(callback).onConditionsChanged(true); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionTest.java index 9e0f863acc1a..0b53133e9353 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionTest.java @@ -133,4 +133,12 @@ public class ConditionTest extends SysuiTestCase { mCondition.fakeUpdateCondition(false); verify(callback, never()).onConditionChanged(eq(mCondition)); } + + @Test + public void clearCondition_reportsNotSet() { + mCondition.fakeUpdateCondition(false); + assertThat(mCondition.isConditionSet()).isTrue(); + mCondition.clearCondition(); + assertThat(mCondition.isConditionSet()).isFalse(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt index 15ba67205034..4ca1fd39682d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/IpcSerializerTest.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith @@ -35,6 +36,7 @@ class IpcSerializerTest : SysuiTestCase() { private val serializer = IpcSerializer() + @Ignore("b/253046405") @Test fun serializeManyIncomingIpcs(): Unit = runBlocking(Dispatchers.Main.immediate) { val processor = launch(start = CoroutineStart.LAZY) { serializer.process() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/view/ViewUtilTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/view/ViewUtilTest.kt index dead1592992d..e3cd9b2d6eaf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/view/ViewUtilTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/view/ViewUtilTest.kt @@ -1,12 +1,31 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.android.systemui.util.view +import android.graphics.Rect import android.view.View import android.widget.TextView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test +import org.mockito.Mockito.doAnswer import org.mockito.Mockito.spy import org.mockito.Mockito.`when` @@ -25,6 +44,12 @@ class ViewUtilTest : SysuiTestCase() { location[0] = VIEW_LEFT location[1] = VIEW_TOP `when`(view.locationOnScreen).thenReturn(location) + doAnswer { invocation -> + val pos = invocation.arguments[0] as IntArray + pos[0] = VIEW_LEFT + pos[1] = VIEW_TOP + null + }.`when`(view).getLocationInWindow(any()) } @Test @@ -64,6 +89,18 @@ class ViewUtilTest : SysuiTestCase() { fun touchIsWithinView_yTooLarge_returnsFalse() { assertThat(viewUtil.touchIsWithinView(view, VIEW_LEFT + 1f, VIEW_BOTTOM + 1f)).isFalse() } + + @Test + fun setRectToViewWindowLocation_rectHasLocation() { + val outRect = Rect() + + viewUtil.setRectToViewWindowLocation(view, outRect) + + assertThat(outRect.left).isEqualTo(VIEW_LEFT) + assertThat(outRect.right).isEqualTo(VIEW_RIGHT) + assertThat(outRect.top).isEqualTo(VIEW_TOP) + assertThat(outRect.bottom).isEqualTo(VIEW_BOTTOM) + } } private const val VIEW_LEFT = 30 diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java index aaf2188a2612..3769f52456fb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java @@ -46,6 +46,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.RingerModeLiveData; @@ -99,6 +100,8 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { private KeyguardManager mKeyguardManager; @Mock private ActivityManager mActivityManager; + @Mock + private DumpManager mDumpManager; @Before @@ -121,7 +124,7 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { mBroadcastDispatcher, mRingerModeTracker, mThreadFactory, mAudioManager, mNotificationManager, mVibrator, mIAudioService, mAccessibilityManager, mPackageManager, mWakefullnessLifcycle, mCaptioningManager, mKeyguardManager, - mActivityManager, mCallback); + mActivityManager, mDumpManager, mCallback); mVolumeController.setEnableDialogs(true, true); } @@ -202,11 +205,12 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { CaptioningManager captioningManager, KeyguardManager keyguardManager, ActivityManager activityManager, + DumpManager dumpManager, C callback) { super(context, broadcastDispatcher, ringerModeTracker, theadFactory, audioManager, notificationManager, optionalVibrator, iAudioService, accessibilityManager, packageManager, wakefulnessLifecycle, captioningManager, keyguardManager, - activityManager); + activityManager, dumpManager); mCallbacks = callback; ArgumentCaptor<WakefulnessLifecycle.Observer> observerCaptor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java index 343437634b29..c2543589bfb8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java @@ -16,14 +16,23 @@ package com.android.systemui.wallpapers; +import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.hamcrest.MockitoHamcrest.intThat; import android.app.WallpaperManager; import android.content.Context; @@ -31,17 +40,25 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.ColorSpace; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerGlobal; import android.os.Handler; -import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.Display; import android.view.DisplayInfo; +import android.view.Surface; import android.view.SurfaceHolder; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer; import org.junit.Before; @@ -56,7 +73,6 @@ import java.util.concurrent.CountDownLatch; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -@Ignore public class ImageWallpaperTest extends SysuiTestCase { private static final int LOW_BMP_WIDTH = 128; private static final int LOW_BMP_HEIGHT = 128; @@ -66,44 +82,86 @@ public class ImageWallpaperTest extends SysuiTestCase { private static final int DISPLAY_HEIGHT = 1080; @Mock + private WindowManager mWindowManager; + @Mock + private WindowMetrics mWindowMetrics; + @Mock + private DisplayManager mDisplayManager; + @Mock + private WallpaperManager mWallpaperManager; + @Mock private SurfaceHolder mSurfaceHolder; @Mock + private Surface mSurface; + @Mock private Context mMockContext; + @Mock private Bitmap mWallpaperBitmap; + private int mBitmapWidth = 1; + private int mBitmapHeight = 1; + @Mock private Handler mHandler; @Mock private FeatureFlags mFeatureFlags; + FakeSystemClock mFakeSystemClock = new FakeSystemClock(); + FakeExecutor mFakeMainExecutor = new FakeExecutor(mFakeSystemClock); + FakeExecutor mFakeBackgroundExecutor = new FakeExecutor(mFakeSystemClock); + private CountDownLatch mEventCountdown; @Before public void setUp() throws Exception { allowTestableLooperAsMainThread(); MockitoAnnotations.initMocks(this); - mEventCountdown = new CountDownLatch(1); + //mEventCountdown = new CountDownLatch(1); - WallpaperManager wallpaperManager = mock(WallpaperManager.class); - Resources resources = mock(Resources.class); + // set up window manager + when(mWindowMetrics.getBounds()).thenReturn( + new Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT)); + when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); + when(mMockContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager); - when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(wallpaperManager); - when(mMockContext.getResources()).thenReturn(resources); - when(resources.getConfiguration()).thenReturn(mock(Configuration.class)); + // set up display manager + doNothing().when(mDisplayManager).registerDisplayListener(any(), any()); + when(mMockContext.getSystemService(DisplayManager.class)).thenReturn(mDisplayManager); + // set up bitmap + when(mWallpaperBitmap.getColorSpace()).thenReturn(ColorSpace.get(ColorSpace.Named.SRGB)); + when(mWallpaperBitmap.getConfig()).thenReturn(Bitmap.Config.ARGB_8888); + when(mWallpaperBitmap.getWidth()).thenReturn(mBitmapWidth); + when(mWallpaperBitmap.getHeight()).thenReturn(mBitmapHeight); + + // set up wallpaper manager + when(mWallpaperManager.peekBitmapDimensions()).thenReturn( + new Rect(0, 0, mBitmapWidth, mBitmapHeight)); + when(mWallpaperManager.getBitmap(false)).thenReturn(mWallpaperBitmap); + when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(mWallpaperManager); + + // set up surface + when(mSurfaceHolder.getSurface()).thenReturn(mSurface); + doNothing().when(mSurface).hwuiDestroy(); + + // TODO remove code below. Outdated, used in only in old GL tests (that are ignored) + Resources resources = mock(Resources.class); + when(resources.getConfiguration()).thenReturn(mock(Configuration.class)); + when(mMockContext.getResources()).thenReturn(resources); DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = DISPLAY_WIDTH; displayInfo.logicalHeight = DISPLAY_HEIGHT; when(mMockContext.getDisplay()).thenReturn( new Display(mock(DisplayManagerGlobal.class), 0, displayInfo, (Resources) null)); + } - when(wallpaperManager.getBitmap(false)).thenReturn(mWallpaperBitmap); - when(mWallpaperBitmap.getColorSpace()).thenReturn(ColorSpace.get(ColorSpace.Named.SRGB)); - when(mWallpaperBitmap.getConfig()).thenReturn(Bitmap.Config.ARGB_8888); + private void setBitmapDimensions(int bitmapWidth, int bitmapHeight) { + mBitmapWidth = bitmapWidth; + mBitmapHeight = bitmapHeight; } private ImageWallpaper createImageWallpaper() { - return new ImageWallpaper(mFeatureFlags) { + return new ImageWallpaper(mFeatureFlags, mFakeBackgroundExecutor, mFakeMainExecutor) { @Override public Engine onCreateEngine() { return new GLEngine(mHandler) { @@ -130,6 +188,7 @@ public class ImageWallpaperTest extends SysuiTestCase { } @Test + @Ignore public void testBitmapWallpaper_normal() { // Will use a image wallpaper with dimensions DISPLAY_WIDTH x DISPLAY_WIDTH. // Then we expect the surface size will be also DISPLAY_WIDTH x DISPLAY_WIDTH. @@ -140,6 +199,7 @@ public class ImageWallpaperTest extends SysuiTestCase { } @Test + @Ignore public void testBitmapWallpaper_low_resolution() { // Will use a image wallpaper with dimensions BMP_WIDTH x BMP_HEIGHT. // Then we expect the surface size will be also BMP_WIDTH x BMP_HEIGHT. @@ -150,6 +210,7 @@ public class ImageWallpaperTest extends SysuiTestCase { } @Test + @Ignore public void testBitmapWallpaper_too_small() { // Will use a image wallpaper with dimensions INVALID_BMP_WIDTH x INVALID_BMP_HEIGHT. // Then we expect the surface size will be also MIN_SURFACE_WIDTH x MIN_SURFACE_HEIGHT. @@ -166,8 +227,7 @@ public class ImageWallpaperTest extends SysuiTestCase { ImageWallpaper.GLEngine engineSpy = spy(wallpaperEngine); - when(mWallpaperBitmap.getWidth()).thenReturn(bmpWidth); - when(mWallpaperBitmap.getHeight()).thenReturn(bmpHeight); + setBitmapDimensions(bmpWidth, bmpHeight); ImageWallpaperRenderer renderer = new ImageWallpaperRenderer(mMockContext); doReturn(renderer).when(engineSpy).getRendererInstance(); @@ -177,4 +237,116 @@ public class ImageWallpaperTest extends SysuiTestCase { assertWithMessage("setFixedSizeAllowed should have been called.").that( mEventCountdown.getCount()).isEqualTo(0); } + + + private ImageWallpaper createImageWallpaperCanvas() { + return new ImageWallpaper(mFeatureFlags, mFakeBackgroundExecutor, mFakeMainExecutor) { + @Override + public Engine onCreateEngine() { + return new CanvasEngine() { + @Override + public Context getDisplayContext() { + return mMockContext; + } + + @Override + public SurfaceHolder getSurfaceHolder() { + return mSurfaceHolder; + } + + @Override + public void setFixedSizeAllowed(boolean allowed) { + super.setFixedSizeAllowed(allowed); + assertWithMessage("mFixedSizeAllowed should be true").that( + allowed).isTrue(); + } + }; + } + }; + } + + private ImageWallpaper.CanvasEngine getSpyEngine() { + ImageWallpaper imageWallpaper = createImageWallpaperCanvas(); + ImageWallpaper.CanvasEngine engine = + (ImageWallpaper.CanvasEngine) imageWallpaper.onCreateEngine(); + ImageWallpaper.CanvasEngine spyEngine = spy(engine); + doNothing().when(spyEngine).drawFrameOnCanvas(any(Bitmap.class)); + doNothing().when(spyEngine).reportEngineShown(anyBoolean()); + doAnswer(invocation -> { + ((ImageWallpaper.CanvasEngine) invocation.getMock()).onMiniBitmapUpdated(); + return null; + }).when(spyEngine).recomputeColorExtractorMiniBitmap(); + return spyEngine; + } + + @Test + public void testMinSurface() { + + // test that the surface is always at least MIN_SURFACE_WIDTH x MIN_SURFACE_HEIGHT + testMinSurfaceHelper(8, 8); + testMinSurfaceHelper(100, 2000); + testMinSurfaceHelper(200, 1); + testMinSurfaceHelper(0, 1); + testMinSurfaceHelper(1, 0); + testMinSurfaceHelper(0, 0); + } + + private void testMinSurfaceHelper(int bitmapWidth, int bitmapHeight) { + + clearInvocations(mSurfaceHolder); + setBitmapDimensions(bitmapWidth, bitmapHeight); + + ImageWallpaper imageWallpaper = createImageWallpaperCanvas(); + ImageWallpaper.CanvasEngine engine = + (ImageWallpaper.CanvasEngine) imageWallpaper.onCreateEngine(); + engine.onCreate(mSurfaceHolder); + + verify(mSurfaceHolder, times(1)).setFixedSize( + intThat(greaterThanOrEqualTo(ImageWallpaper.CanvasEngine.MIN_SURFACE_WIDTH)), + intThat(greaterThanOrEqualTo(ImageWallpaper.CanvasEngine.MIN_SURFACE_HEIGHT))); + } + + @Test + public void testZeroBitmap() { + // test that a frame is never drawn with a 0 bitmap + testZeroBitmapHelper(0, 1); + testZeroBitmapHelper(1, 0); + testZeroBitmapHelper(0, 0); + } + + private void testZeroBitmapHelper(int bitmapWidth, int bitmapHeight) { + + clearInvocations(mSurfaceHolder); + setBitmapDimensions(bitmapWidth, bitmapHeight); + + ImageWallpaper imageWallpaper = createImageWallpaperCanvas(); + ImageWallpaper.CanvasEngine engine = + (ImageWallpaper.CanvasEngine) imageWallpaper.onCreateEngine(); + ImageWallpaper.CanvasEngine spyEngine = spy(engine); + spyEngine.onCreate(mSurfaceHolder); + spyEngine.onSurfaceRedrawNeeded(mSurfaceHolder); + verify(spyEngine, never()).drawFrameOnCanvas(any()); + } + + @Test + public void testLoadDrawAndUnloadBitmap() { + setBitmapDimensions(LOW_BMP_WIDTH, LOW_BMP_HEIGHT); + + ImageWallpaper.CanvasEngine spyEngine = getSpyEngine(); + spyEngine.onCreate(mSurfaceHolder); + spyEngine.onSurfaceRedrawNeeded(mSurfaceHolder); + assertThat(mFakeBackgroundExecutor.numPending()).isAtLeast(1); + + int n = 0; + while (mFakeBackgroundExecutor.numPending() + mFakeMainExecutor.numPending() >= 1) { + n++; + assertThat(n).isAtMost(10); + mFakeBackgroundExecutor.runNextReady(); + mFakeMainExecutor.runNextReady(); + mFakeSystemClock.advanceTime(1000); + } + + verify(spyEngine, times(1)).drawFrameOnCanvas(mWallpaperBitmap); + assertThat(spyEngine.isBitmapLoaded()).isFalse(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRendererTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRendererTest.java deleted file mode 100644 index 93f4f8223955..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/ImageCanvasWallpaperRendererTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.wallpapers.canvas; - -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.hamcrest.MockitoHamcrest.intThat; - -import android.graphics.Bitmap; -import android.test.suitebuilder.annotation.SmallTest; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.view.DisplayInfo; -import android.view.SurfaceHolder; - -import com.android.systemui.SysuiTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper -public class ImageCanvasWallpaperRendererTest extends SysuiTestCase { - - private static final int MOBILE_DISPLAY_WIDTH = 720; - private static final int MOBILE_DISPLAY_HEIGHT = 1600; - - @Mock - private SurfaceHolder mMockSurfaceHolder; - - @Mock - private DisplayInfo mMockDisplayInfo; - - @Mock - private Bitmap mMockBitmap; - - @Before - public void setUp() throws Exception { - allowTestableLooperAsMainThread(); - MockitoAnnotations.initMocks(this); - } - - private void setDimensions( - int bitmapWidth, int bitmapHeight, - int displayWidth, int displayHeight) { - when(mMockBitmap.getWidth()).thenReturn(bitmapWidth); - when(mMockBitmap.getHeight()).thenReturn(bitmapHeight); - mMockDisplayInfo.logicalWidth = displayWidth; - mMockDisplayInfo.logicalHeight = displayHeight; - } - - private void testMinDimensions( - int bitmapWidth, int bitmapHeight) { - - clearInvocations(mMockSurfaceHolder); - setDimensions(bitmapWidth, bitmapHeight, - ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_WIDTH, - ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_HEIGHT); - - ImageCanvasWallpaperRenderer renderer = - new ImageCanvasWallpaperRenderer(mMockSurfaceHolder); - renderer.drawFrame(mMockBitmap, true); - - verify(mMockSurfaceHolder, times(1)).setFixedSize( - intThat(greaterThanOrEqualTo(ImageCanvasWallpaperRenderer.MIN_SURFACE_WIDTH)), - intThat(greaterThanOrEqualTo(ImageCanvasWallpaperRenderer.MIN_SURFACE_HEIGHT))); - } - - @Test - public void testMinSurface() { - // test that the surface is always at least MIN_SURFACE_WIDTH x MIN_SURFACE_HEIGHT - testMinDimensions(8, 8); - - testMinDimensions(100, 2000); - - testMinDimensions(200, 1); - } - - private void testZeroDimensions(int bitmapWidth, int bitmapHeight) { - - clearInvocations(mMockSurfaceHolder); - setDimensions(bitmapWidth, bitmapHeight, - ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_WIDTH, - ImageCanvasWallpaperRendererTest.MOBILE_DISPLAY_HEIGHT); - - ImageCanvasWallpaperRenderer renderer = - new ImageCanvasWallpaperRenderer(mMockSurfaceHolder); - ImageCanvasWallpaperRenderer spyRenderer = spy(renderer); - spyRenderer.drawFrame(mMockBitmap, true); - - verify(mMockSurfaceHolder, never()).setFixedSize(anyInt(), anyInt()); - verify(spyRenderer, never()).drawWallpaperWithCanvas(any()); - } - - @Test - public void testZeroBitmap() { - // test that updateSurfaceSize is not called with a bitmap of width 0 or height 0 - testZeroDimensions( - 0, 1 - ); - - testZeroDimensions(1, 0 - ); - - testZeroDimensions(0, 0 - ); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java new file mode 100644 index 000000000000..76bff1d72141 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.wallpapers.canvas; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.app.WallpaperColors; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.RectF; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class WallpaperColorExtractorTest extends SysuiTestCase { + private static final int LOW_BMP_WIDTH = 128; + private static final int LOW_BMP_HEIGHT = 128; + private static final int HIGH_BMP_WIDTH = 3000; + private static final int HIGH_BMP_HEIGHT = 4000; + private static final int VERY_LOW_BMP_WIDTH = 1; + private static final int VERY_LOW_BMP_HEIGHT = 1; + private static final int DISPLAY_WIDTH = 1920; + private static final int DISPLAY_HEIGHT = 1080; + + private static final int PAGES_LOW = 4; + private static final int PAGES_HIGH = 7; + + private static final int MIN_AREAS = 4; + private static final int MAX_AREAS = 10; + + private int mMiniBitmapWidth; + private int mMiniBitmapHeight; + + @Mock + private Executor mBackgroundExecutor; + + private int mColorsProcessed; + private int mMiniBitmapUpdatedCount; + private int mActivatedCount; + private int mDeactivatedCount; + + @Before + public void setUp() throws Exception { + allowTestableLooperAsMainThread(); + MockitoAnnotations.initMocks(this); + doAnswer(invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }).when(mBackgroundExecutor).execute(any(Runnable.class)); + } + + private void resetCounters() { + mColorsProcessed = 0; + mMiniBitmapUpdatedCount = 0; + mActivatedCount = 0; + mDeactivatedCount = 0; + } + + private Bitmap getMockBitmap(int width, int height) { + Bitmap bitmap = mock(Bitmap.class); + when(bitmap.getWidth()).thenReturn(width); + when(bitmap.getHeight()).thenReturn(height); + return bitmap; + } + + private WallpaperColorExtractor getSpyWallpaperColorExtractor() { + + WallpaperColorExtractor wallpaperColorExtractor = new WallpaperColorExtractor( + mBackgroundExecutor, + new WallpaperColorExtractor.WallpaperColorExtractorCallback() { + @Override + public void onColorsProcessed(List<RectF> regions, + List<WallpaperColors> colors) { + assertThat(regions.size()).isEqualTo(colors.size()); + mColorsProcessed += regions.size(); + } + + @Override + public void onMiniBitmapUpdated() { + mMiniBitmapUpdatedCount++; + } + + @Override + public void onActivated() { + mActivatedCount++; + } + + @Override + public void onDeactivated() { + mDeactivatedCount++; + } + }); + WallpaperColorExtractor spyWallpaperColorExtractor = spy(wallpaperColorExtractor); + + doAnswer(invocation -> { + mMiniBitmapWidth = invocation.getArgument(1); + mMiniBitmapHeight = invocation.getArgument(2); + return getMockBitmap(mMiniBitmapWidth, mMiniBitmapHeight); + }).when(spyWallpaperColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt()); + + + doAnswer(invocation -> getMockBitmap( + invocation.getArgument(1), + invocation.getArgument(2))) + .when(spyWallpaperColorExtractor) + .createMiniBitmap(any(Bitmap.class), anyInt(), anyInt()); + + doReturn(new WallpaperColors(Color.valueOf(0), Color.valueOf(0), Color.valueOf(0))) + .when(spyWallpaperColorExtractor).getLocalWallpaperColors(any(Rect.class)); + + return spyWallpaperColorExtractor; + } + + private RectF randomArea() { + float width = (float) Math.random(); + float startX = (float) (Math.random() * (1 - width)); + float height = (float) Math.random(); + float startY = (float) (Math.random() * (1 - height)); + return new RectF(startX, startY, startX + width, startY + height); + } + + private List<RectF> listOfRandomAreas(int min, int max) { + int nAreas = randomBetween(min, max); + List<RectF> result = new ArrayList<>(); + for (int i = 0; i < nAreas; i++) { + result.add(randomArea()); + } + return result; + } + + private int randomBetween(int minIncluded, int maxIncluded) { + return (int) (Math.random() * ((maxIncluded - minIncluded) + 1)) + minIncluded; + } + + /** + * Test that for bitmaps of random dimensions, the mini bitmap is always created + * with either a width <= SMALL_SIDE or a height <= SMALL_SIDE + */ + @Test + public void testMiniBitmapCreation() { + WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + int nSimulations = 10; + for (int i = 0; i < nSimulations; i++) { + resetCounters(); + int width = randomBetween(LOW_BMP_WIDTH, HIGH_BMP_WIDTH); + int height = randomBetween(LOW_BMP_HEIGHT, HIGH_BMP_HEIGHT); + Bitmap bitmap = getMockBitmap(width, height); + spyWallpaperColorExtractor.onBitmapChanged(bitmap); + + assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); + assertThat(Math.min(mMiniBitmapWidth, mMiniBitmapHeight)) + .isAtMost(WallpaperColorExtractor.SMALL_SIDE); + } + } + + /** + * Test that for bitmaps with both width and height <= SMALL_SIDE, + * the mini bitmap is always created with both width and height <= SMALL_SIDE + */ + @Test + public void testSmallMiniBitmapCreation() { + WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + int nSimulations = 10; + for (int i = 0; i < nSimulations; i++) { + resetCounters(); + int width = randomBetween(VERY_LOW_BMP_WIDTH, LOW_BMP_WIDTH); + int height = randomBetween(VERY_LOW_BMP_HEIGHT, LOW_BMP_HEIGHT); + Bitmap bitmap = getMockBitmap(width, height); + spyWallpaperColorExtractor.onBitmapChanged(bitmap); + + assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); + assertThat(Math.max(mMiniBitmapWidth, mMiniBitmapHeight)) + .isAtMost(WallpaperColorExtractor.SMALL_SIDE); + } + } + + /** + * Test that for a new color extractor with information + * (number of pages, display dimensions, wallpaper bitmap) given in random order, + * the colors are processed and all the callbacks are properly executed. + */ + @Test + public void testNewColorExtraction() { + Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); + + int nSimulations = 10; + for (int i = 0; i < nSimulations; i++) { + resetCounters(); + WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + List<RectF> regions = listOfRandomAreas(MIN_AREAS, MAX_AREAS); + int nPages = randomBetween(PAGES_LOW, PAGES_HIGH); + List<Runnable> tasks = Arrays.asList( + () -> spyWallpaperColorExtractor.onPageChanged(nPages), + () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap), + () -> spyWallpaperColorExtractor.setDisplayDimensions( + DISPLAY_WIDTH, DISPLAY_HEIGHT), + () -> spyWallpaperColorExtractor.addLocalColorsAreas( + regions)); + Collections.shuffle(tasks); + tasks.forEach(Runnable::run); + + assertThat(mActivatedCount).isEqualTo(1); + assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); + assertThat(mColorsProcessed).isEqualTo(regions.size()); + + spyWallpaperColorExtractor.removeLocalColorAreas(regions); + assertThat(mDeactivatedCount).isEqualTo(1); + } + } + + /** + * Test that the method removeLocalColorAreas behaves properly and does not call + * the onDeactivated callback unless all color areas are removed. + */ + @Test + public void testRemoveColors() { + Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); + int nSimulations = 10; + for (int i = 0; i < nSimulations; i++) { + resetCounters(); + WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); + List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); + List<RectF> regions = new ArrayList<>(); + regions.addAll(regions1); + regions.addAll(regions2); + int nPages = randomBetween(PAGES_LOW, PAGES_HIGH); + List<Runnable> tasks = Arrays.asList( + () -> spyWallpaperColorExtractor.onPageChanged(nPages), + () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap), + () -> spyWallpaperColorExtractor.setDisplayDimensions( + DISPLAY_WIDTH, DISPLAY_HEIGHT), + () -> spyWallpaperColorExtractor.removeLocalColorAreas(regions1)); + + spyWallpaperColorExtractor.addLocalColorsAreas(regions); + assertThat(mActivatedCount).isEqualTo(1); + Collections.shuffle(tasks); + tasks.forEach(Runnable::run); + + assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); + assertThat(mDeactivatedCount).isEqualTo(0); + spyWallpaperColorExtractor.removeLocalColorAreas(regions2); + assertThat(mDeactivatedCount).isEqualTo(1); + } + } + + /** + * Test that if we change some information (wallpaper bitmap, number of pages), + * the colors are correctly recomputed. + * Test that if we remove some color areas in the middle of the process, + * only the remaining areas are recomputed. + */ + @Test + public void testRecomputeColorExtraction() { + Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); + WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); + List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); + List<RectF> regions = new ArrayList<>(); + regions.addAll(regions1); + regions.addAll(regions2); + spyWallpaperColorExtractor.addLocalColorsAreas(regions); + assertThat(mActivatedCount).isEqualTo(1); + int nPages = PAGES_LOW; + spyWallpaperColorExtractor.onBitmapChanged(bitmap); + spyWallpaperColorExtractor.onPageChanged(nPages); + spyWallpaperColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT); + + int nSimulations = 20; + for (int i = 0; i < nSimulations; i++) { + resetCounters(); + + // verify that if we remove some regions, they are not recomputed after other changes + if (i == nSimulations / 2) { + regions.removeAll(regions2); + spyWallpaperColorExtractor.removeLocalColorAreas(regions2); + } + + if (Math.random() >= 0.5) { + int nPagesNew = randomBetween(PAGES_LOW, PAGES_HIGH); + if (nPagesNew == nPages) continue; + nPages = nPagesNew; + spyWallpaperColorExtractor.onPageChanged(nPagesNew); + } else { + Bitmap newBitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); + spyWallpaperColorExtractor.onBitmapChanged(newBitmap); + assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); + } + assertThat(mColorsProcessed).isEqualTo(regions.size()); + } + spyWallpaperColorExtractor.removeLocalColorAreas(regions); + assertThat(mDeactivatedCount).isEqualTo(1); + } + + @Test + public void testCleanUp() { + resetCounters(); + Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); + doNothing().when(bitmap).recycle(); + WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + spyWallpaperColorExtractor.onPageChanged(PAGES_LOW); + spyWallpaperColorExtractor.onBitmapChanged(bitmap); + assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); + spyWallpaperColorExtractor.cleanUp(); + spyWallpaperColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS)); + assertThat(mColorsProcessed).isEqualTo(0); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 09da52e7685c..fa7ebf6a2449 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -80,6 +80,7 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; +import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; @@ -91,6 +92,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationShadeWindowControllerImpl; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.RankingBuilder; @@ -190,6 +192,8 @@ public class BubblesTest extends SysuiTestCase { private NotificationShadeWindowView mNotificationShadeWindowView; @Mock private AuthController mAuthController; + @Mock + private ShadeExpansionStateManager mShadeExpansionStateManager; private SysUiState mSysUiState; private boolean mSysUiStateBubblesExpanded; @@ -290,7 +294,7 @@ public class BubblesTest extends SysuiTestCase { mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController, mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController, mColorExtractor, mDumpManager, mKeyguardStateController, - mScreenOffAnimationController, mAuthController); + mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager); mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView); mNotificationShadeWindowController.attach(); @@ -343,7 +347,8 @@ public class BubblesTest extends SysuiTestCase { mock(NotificationInterruptLogger.class), mock(Handler.class), mock(NotifPipelineFlags.class), - mock(KeyguardNotificationVisibilityProvider.class) + mock(KeyguardNotificationVisibilityProvider.class), + mock(UiEventLogger.class) ); when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor); mBubbleController = new TestableBubbleController( diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java index 9635faf6e858..e5316bc83a12 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java @@ -22,6 +22,7 @@ import android.os.Handler; import android.os.PowerManager; import android.service.dreams.IDreamManager; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider; @@ -46,7 +47,8 @@ public class TestableNotificationInterruptStateProviderImpl NotificationInterruptLogger logger, Handler mainHandler, NotifPipelineFlags flags, - KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) { + KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider, + UiEventLogger uiEventLogger) { super(contentResolver, powerManager, dreamManager, @@ -58,7 +60,8 @@ public class TestableNotificationInterruptStateProviderImpl logger, mainHandler, flags, - keyguardNotificationVisibilityProvider); + keyguardNotificationVisibilityProvider, + uiEventLogger); mUseHeadsUp = true; } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java index cebe946a459c..7af66f641837 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java @@ -34,6 +34,8 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.tracing.ProtoTracer; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.desktopmode.DesktopMode; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.floating.FloatingTasks; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEventCallback; @@ -49,6 +51,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Optional; +import java.util.concurrent.Executor; /** * Tests for {@link WMShell}. @@ -76,12 +79,14 @@ public class WMShellTest extends SysuiTestCase { @Mock UserTracker mUserTracker; @Mock ShellExecutor mSysUiMainExecutor; @Mock FloatingTasks mFloatingTasks; + @Mock DesktopMode mDesktopMode; @Before public void setUp() { MockitoAnnotations.initMocks(this); mWMShell = new WMShell(mContext, mShellInterface, Optional.of(mPip), Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mFloatingTasks), + Optional.of(mDesktopMode), mCommandQueue, mConfigurationController, mKeyguardStateController, mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer, mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor); @@ -103,4 +108,12 @@ public class WMShellTest extends SysuiTestCase { verify(mOneHanded).registerTransitionCallback(any(OneHandedTransitionCallback.class)); verify(mOneHanded).registerEventCallback(any(OneHandedEventCallback.class)); } + + @Test + public void initDesktopMode_registersListener() { + mWMShell.initDesktopMode(mDesktopMode); + verify(mDesktopMode).addListener( + any(DesktopModeTaskRepository.VisibleTasksListener.class), + any(Executor.class)); + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java index c83189dbc616..fa3cc9905c3f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java @@ -84,9 +84,14 @@ public abstract class SysuiTestCase { initializer.init(true); mDependency = new TestableDependency(initializer.getSysUIComponent().createDependency()); Dependency.setInstance(mDependency); - mFakeBroadcastDispatcher = new FakeBroadcastDispatcher(mContext, mock(Looper.class), - mock(Executor.class), mock(DumpManager.class), - mock(BroadcastDispatcherLogger.class), mock(UserTracker.class)); + mFakeBroadcastDispatcher = new FakeBroadcastDispatcher( + mContext, + mContext.getMainExecutor(), + mock(Looper.class), + mock(Executor.class), + mock(DumpManager.class), + mock(BroadcastDispatcherLogger.class), + mock(UserTracker.class)); mRealInstrumentation = InstrumentationRegistry.getInstrumentation(); Instrumentation inst = spy(mRealInstrumentation); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt index 53dcc8d269c9..52e0c982cae0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt @@ -32,15 +32,25 @@ import java.util.concurrent.Executor class FakeBroadcastDispatcher( context: SysuiTestableContext, - looper: Looper, - executor: Executor, + mainExecutor: Executor, + broadcastRunningLooper: Looper, + broadcastRunningExecutor: Executor, dumpManager: DumpManager, logger: BroadcastDispatcherLogger, userTracker: UserTracker -) : BroadcastDispatcher( - context, looper, executor, dumpManager, logger, userTracker, PendingRemovalStore(logger)) { +) : + BroadcastDispatcher( + context, + mainExecutor, + broadcastRunningLooper, + broadcastRunningExecutor, + dumpManager, + logger, + userTracker, + PendingRemovalStore(logger) + ) { - private val registeredReceivers = ArraySet<BroadcastReceiver>() + val registeredReceivers = ArraySet<BroadcastReceiver>() override fun registerReceiverWithHandler( receiver: BroadcastReceiver, @@ -78,4 +88,4 @@ class FakeBroadcastDispatcher( } registeredReceivers.clear() } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt index c56fdb17b5f1..a60b7735fbd4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt @@ -16,6 +16,8 @@ package com.android.systemui.flags +import java.io.PrintWriter + class FakeFeatureFlags : FeatureFlags { private val booleanFlags = mutableMapOf<Int, Boolean>() private val stringFlags = mutableMapOf<Int, String>() @@ -24,7 +26,7 @@ class FakeFeatureFlags : FeatureFlags { private val listenerFlagIds = mutableMapOf<FlagListenable.Listener, MutableSet<Int>>() init { - Flags.getFlagFields().forEach { field -> + Flags.flagFields.forEach { field -> val flag: Flag<*> = field.get(null) as Flag<*> knownFlagNames[flag.id] = field.name } @@ -106,6 +108,10 @@ class FakeFeatureFlags : FeatureFlags { } } + override fun dump(writer: PrintWriter, args: Array<out String>?) { + // no-op + } + private fun flagName(flagId: Int): String { return knownFlagNames[flagId] ?: "UNKNOWN(id=$flagId)" } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 42b434a9deaf..0c126805fb78 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.common.shared.model.Position +import com.android.systemui.keyguard.shared.model.StatusBarState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -44,6 +45,13 @@ class FakeKeyguardRepository : KeyguardRepository { private val _dozeAmount = MutableStateFlow(0f) override val dozeAmount: Flow<Float> = _dozeAmount + private val _statusBarState = MutableStateFlow(StatusBarState.SHADE) + override val statusBarState: Flow<StatusBarState> = _statusBarState + + override fun isKeyguardShowing(): Boolean { + return _isKeyguardShowing.value + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.tryEmit(animate) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt index 527258579372..c33ce5d9484d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt @@ -16,7 +16,7 @@ package com.android.systemui.qs -import android.view.View +import com.android.systemui.animation.Expandable import com.android.systemui.qs.FgsManagerController.OnDialogDismissedListener import com.android.systemui.qs.FgsManagerController.OnNumberOfPackagesChangedListener import kotlinx.coroutines.flow.MutableStateFlow @@ -54,7 +54,7 @@ class FakeFgsManagerController( override fun init() {} - override fun showDialog(viewLaunchedFrom: View?) {} + override fun showDialog(expandable: Expandable?) {} override fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) { numRunningPackagesListeners.add(listener) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt index 2a9aeddc9aa8..325da4ead666 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -57,7 +57,6 @@ import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings -import com.android.systemui.util.time.FakeSystemClock import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.test.TestCoroutineDispatcher @@ -68,7 +67,6 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher class FooterActionsTestUtils( private val context: Context, private val testableLooper: TestableLooper, - private val fakeClock: FakeSystemClock = FakeSystemClock(), ) { /** Enable or disable the user switcher in the settings. */ fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt index b2b176420e40..9726bf83b263 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt @@ -26,20 +26,24 @@ import java.util.concurrent.Executor /** A fake [UserTracker] to be used in tests. */ class FakeUserTracker( - userId: Int = 0, - userHandle: UserHandle = UserHandle.of(userId), - userInfo: UserInfo = mock(), - userProfiles: List<UserInfo> = emptyList(), + private var _userId: Int = 0, + private var _userHandle: UserHandle = UserHandle.of(_userId), + private var _userInfo: UserInfo = mock(), + private var _userProfiles: List<UserInfo> = emptyList(), userContentResolver: ContentResolver = MockContentResolver(), userContext: Context = mock(), private val onCreateCurrentUserContext: (Context) -> Context = { mock() }, ) : UserTracker { val callbacks = mutableListOf<UserTracker.Callback>() - override val userId: Int = userId - override val userHandle: UserHandle = userHandle - override val userInfo: UserInfo = userInfo - override val userProfiles: List<UserInfo> = userProfiles + override val userId: Int + get() = _userId + override val userHandle: UserHandle + get() = _userHandle + override val userInfo: UserInfo + get() = _userInfo + override val userProfiles: List<UserInfo> + get() = _userProfiles override val userContentResolver: ContentResolver = userContentResolver override val userContext: Context = userContext @@ -55,4 +59,13 @@ class FakeUserTracker( override fun createCurrentUserContext(context: Context): Context { return onCreateCurrentUserContext(context) } + + fun set(userInfos: List<UserInfo>, selectedUserIndex: Int) { + _userProfiles = userInfos + _userInfo = userInfos[selectedUserIndex] + _userId = _userInfo.id + _userHandle = UserHandle.of(_userId) + + callbacks.forEach { it.onUserChanged(_userId, userContext) } + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt new file mode 100644 index 000000000000..59f24ef2a706 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.telephony.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeTelephonyRepository : TelephonyRepository { + + private val _callState = MutableStateFlow(0) + override val callState: Flow<Int> = _callState.asStateFlow() + + fun setCallState(value: Int) { + _callState.value = value + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 20f1e367944f..4df8aa42ea2f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -17,12 +17,18 @@ package com.android.systemui.user.data.repository +import android.content.pm.UserInfo +import android.os.UserHandle +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.yield class FakeUserRepository : UserRepository { @@ -34,21 +40,71 @@ class FakeUserRepository : UserRepository { private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList()) override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow() + private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel()) + override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = + _userSwitcherSettings.asStateFlow() + + private val _userInfos = MutableStateFlow<List<UserInfo>>(emptyList()) + override val userInfos: Flow<List<UserInfo>> = _userInfos.asStateFlow() + + private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null) + override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull() + + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM + private val _isActionableWhenLocked = MutableStateFlow(false) override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow() private var _isGuestUserAutoCreated: Boolean = false override val isGuestUserAutoCreated: Boolean get() = _isGuestUserAutoCreated - private var _isGuestUserResetting: Boolean = false - override val isGuestUserResetting: Boolean - get() = _isGuestUserResetting + + override var isGuestUserResetting: Boolean = false + + override val isGuestUserCreationScheduled = AtomicBoolean() + + override var secondaryUserId: Int = UserHandle.USER_NULL + + override var isRefreshUsersPaused: Boolean = false + + var refreshUsersCallCount: Int = 0 + private set + + override fun refreshUsers() { + refreshUsersCallCount++ + } + + override fun getSelectedUserInfo(): UserInfo { + return checkNotNull(_selectedUserInfo.value) + } + + override fun isSimpleUserSwitcher(): Boolean { + return _userSwitcherSettings.value.isSimpleUserSwitcher + } + + fun setUserInfos(infos: List<UserInfo>) { + _userInfos.value = infos + } + + suspend fun setSelectedUserInfo(userInfo: UserInfo) { + check(_userInfos.value.contains(userInfo)) { + "Cannot select the following user, it is not in the list of user infos: $userInfo!" + } + + _selectedUserInfo.value = userInfo + yield() + } + + suspend fun setSettings(settings: UserSwitcherSettingsModel) { + _userSwitcherSettings.value = settings + yield() + } fun setUsers(models: List<UserModel>) { _users.value = models } - fun setSelectedUser(userId: Int) { + suspend fun setSelectedUser(userId: Int) { check(_users.value.find { it.id == userId } != null) { "Cannot select a user with ID $userId - no user with that ID found!" } @@ -62,6 +118,7 @@ class FakeUserRepository : UserRepository { } } ) + yield() } fun setActions(models: List<UserActionModel>) { @@ -75,8 +132,4 @@ class FakeUserRepository : UserRepository { fun setGuestUserAutoCreated(value: Boolean) { _isGuestUserAutoCreated = value } - - fun setGuestUserResetting(value: Boolean) { - _isGuestUserResetting = value - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java index 9d5ccbec87ea..1353ad25d057 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java @@ -21,6 +21,14 @@ package com.android.systemui.util.condition; * condition fulfillment. */ public class FakeCondition extends Condition { + FakeCondition() { + super(); + } + + FakeCondition(Boolean initialValue, Boolean overriding) { + super(initialValue, overriding); + } + @Override public void start() {} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt index 8d171be87cb6..69575a987e5d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt @@ -26,7 +26,9 @@ package com.android.systemui.util.mockito import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatcher import org.mockito.Mockito +import org.mockito.Mockito.`when` import org.mockito.stubbing.OngoingStubbing +import org.mockito.stubbing.Stubber /** * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when @@ -89,7 +91,8 @@ inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T: * * @see Mockito.when */ -fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall) +fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) +fun <T> Stubber.whenever(mock: T): T = `when`(mock) /** * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java index 2be67edfc946..23c7a6139de8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java @@ -70,6 +70,10 @@ public class FakeStatusBarIconController extends BaseLeakChecker<IconManager> } @Override + public void setNewMobileIconSubIds(List<Integer> subIds) { + } + + @Override public void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states) { } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt index a5ec0a454412..5a868a4df354 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedComponent.kt @@ -20,10 +20,12 @@ import android.content.ContentResolver import android.content.Context import android.hardware.SensorManager import android.os.Handler +import android.view.IWindowManager import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.dagger.UnfoldBackground import com.android.systemui.unfold.dagger.UnfoldMain import com.android.systemui.unfold.updates.FoldProvider +import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider import com.android.systemui.unfold.util.CurrentActivityTypeProvider import com.android.systemui.unfold.util.UnfoldTransitionATracePrefix @@ -39,11 +41,11 @@ import javax.inject.Singleton * * This component is meant to be used for places that don't use dagger. By providing those * parameters to the factory, all dagger objects are correctly instantiated. See - * [createUnfoldTransitionProgressProvider] for an example. + * [createUnfoldSharedComponent] for an example. */ @Singleton @Component(modules = [UnfoldSharedModule::class]) -internal interface UnfoldSharedComponent { +interface UnfoldSharedComponent { @Component.Factory interface Factory { @@ -58,9 +60,11 @@ internal interface UnfoldSharedComponent { @BindsInstance @UnfoldMain executor: Executor, @BindsInstance @UnfoldBackground backgroundExecutor: Executor, @BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String, + @BindsInstance windowManager: IWindowManager, @BindsInstance contentResolver: ContentResolver = context.contentResolver ): UnfoldSharedComponent } val unfoldTransitionProvider: Optional<UnfoldTransitionProgressProvider> + val rotationChangeProvider: RotationChangeProvider } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt index 402dd8474bc4..a1ed17844e8e 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt @@ -20,6 +20,7 @@ package com.android.systemui.unfold import android.content.Context import android.hardware.SensorManager import android.os.Handler +import android.view.IWindowManager import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.updates.FoldProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider @@ -27,14 +28,15 @@ import com.android.systemui.unfold.util.CurrentActivityTypeProvider import java.util.concurrent.Executor /** - * Factory for [UnfoldTransitionProgressProvider]. + * Factory for [UnfoldSharedComponent]. * - * This is needed as Launcher has to create the object manually. If dagger is available, this object - * is provided in [UnfoldSharedModule]. + * This wraps the autogenerated factory (for discoverability), and is needed as Launcher has to + * create the object manually. If dagger is available, this object is provided in + * [UnfoldSharedModule]. * * This should **never** be called from sysui, as the object is already provided in that process. */ -fun createUnfoldTransitionProgressProvider( +fun createUnfoldSharedComponent( context: Context, config: UnfoldTransitionConfig, screenStatusProvider: ScreenStatusProvider, @@ -44,8 +46,9 @@ fun createUnfoldTransitionProgressProvider( mainHandler: Handler, mainExecutor: Executor, backgroundExecutor: Executor, - tracingTagPrefix: String -): UnfoldTransitionProgressProvider = + tracingTagPrefix: String, + windowManager: IWindowManager, +): UnfoldSharedComponent = DaggerUnfoldSharedComponent.factory() .create( context, @@ -57,9 +60,6 @@ fun createUnfoldTransitionProgressProvider( mainHandler, mainExecutor, backgroundExecutor, - tracingTagPrefix) - .unfoldTransitionProvider - .orElse(null) - ?: throw IllegalStateException( - "Trying to create " + - "UnfoldTransitionProgressProvider when the transition is disabled") + tracingTagPrefix, + windowManager, + ) diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt index d54481c72bfd..7117aafba54a 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldTransitionProgressProvider.kt @@ -26,7 +26,8 @@ import com.android.systemui.unfold.util.CallbackController * * onTransitionProgress callback could be called on each frame. * - * Use [createUnfoldTransitionProgressProvider] to create instances of this interface + * Use [createUnfoldSharedComponent] to create instances of this interface when dagger is not + * available. */ interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgressListener> { diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt index 2ab28c65f32f..043aff659d6c 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt @@ -203,6 +203,6 @@ class PhysicsBasedUnfoldTransitionProgressProvider( private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider" private const val DEBUG = true -private const val SPRING_STIFFNESS = 200.0f +private const val SPRING_STIFFNESS = 600.0f private const val MINIMAL_VISIBLE_CHANGE = 0.001f private const val FINAL_HINGE_ANGLE_POSITION = 165f diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt index 19cfc805d17b..07473b30dd58 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt @@ -24,6 +24,7 @@ import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.dagger.UnfoldMain import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener +import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener import com.android.systemui.unfold.updates.hinge.FULLY_CLOSED_DEGREES import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES import com.android.systemui.unfold.updates.hinge.HingeAngleProvider @@ -40,22 +41,24 @@ constructor( private val screenStatusProvider: ScreenStatusProvider, private val foldProvider: FoldProvider, private val activityTypeProvider: CurrentActivityTypeProvider, + private val rotationChangeProvider: RotationChangeProvider, @UnfoldMain private val mainExecutor: Executor, @UnfoldMain private val handler: Handler ) : FoldStateProvider { private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf() - @FoldUpdate - private var lastFoldUpdate: Int? = null + @FoldUpdate private var lastFoldUpdate: Int? = null - @FloatRange(from = 0.0, to = 180.0) - private var lastHingeAngle: Float = 0f + @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngle: Float = 0f private val hingeAngleListener = HingeAngleListener() private val screenListener = ScreenStatusListener() private val foldStateListener = FoldStateListener() - private val timeoutRunnable = TimeoutRunnable() + private val timeoutRunnable = Runnable { cancelAnimation() } + private val rotationListener = RotationListener { + if (isTransitionInProgress) cancelAnimation() + } /** * Time after which [FOLD_UPDATE_FINISH_HALF_OPEN] is emitted following a @@ -72,6 +75,7 @@ constructor( foldProvider.registerCallback(foldStateListener, mainExecutor) screenStatusProvider.addCallback(screenListener) hingeAngleProvider.addCallback(hingeAngleListener) + rotationChangeProvider.addCallback(rotationListener) } override fun stop() { @@ -79,6 +83,7 @@ constructor( foldProvider.unregisterCallback(foldStateListener) hingeAngleProvider.removeCallback(hingeAngleListener) hingeAngleProvider.stop() + rotationChangeProvider.removeCallback(rotationListener) } override fun addCallback(listener: FoldUpdatesListener) { @@ -90,14 +95,15 @@ constructor( } override val isFinishedOpening: Boolean - get() = !isFolded && + get() = + !isFolded && (lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN || - lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN) + lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN) private val isTransitionInProgress: Boolean get() = lastFoldUpdate == FOLD_UPDATE_START_OPENING || - lastFoldUpdate == FOLD_UPDATE_START_CLOSING + lastFoldUpdate == FOLD_UPDATE_START_CLOSING private fun onHingeAngle(angle: Float) { if (DEBUG) { @@ -168,7 +174,7 @@ constructor( private fun notifyFoldUpdate(@FoldUpdate update: Int) { if (DEBUG) { - Log.d(TAG, stateToString(update)) + Log.d(TAG, update.name()) } outputListeners.forEach { it.onFoldUpdate(update) } lastFoldUpdate = update @@ -185,6 +191,8 @@ constructor( handler.removeCallbacks(timeoutRunnable) } + private fun cancelAnimation(): Unit = notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN) + private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener { override fun onScreenTurnedOn() { @@ -225,16 +233,10 @@ constructor( onHingeAngle(angle) } } - - private inner class TimeoutRunnable : Runnable { - override fun run() { - notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN) - } - } } -private fun stateToString(@FoldUpdate update: Int): String { - return when (update) { +fun @receiver:FoldUpdate Int.name() = + when (this) { FOLD_UPDATE_START_OPENING -> "START_OPENING" FOLD_UPDATE_START_CLOSING -> "START_CLOSING" FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> "UNFOLDED_SCREEN_AVAILABLE" @@ -243,15 +245,12 @@ private fun stateToString(@FoldUpdate update: Int): String { FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED" else -> "UNKNOWN" } -} private const val TAG = "DeviceFoldProvider" private const val DEBUG = false /** Threshold after which we consider the device fully unfolded. */ -@VisibleForTesting -const val FULLY_OPEN_THRESHOLD_DEGREES = 15f +@VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f /** Fold animation on top of apps only when the angle exceeds this threshold. */ -@VisibleForTesting -const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60 +@VisibleForTesting const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60 diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt new file mode 100644 index 000000000000..0cf8224d3a3f --- /dev/null +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.unfold.updates + +import android.content.Context +import android.os.RemoteException +import android.view.IRotationWatcher +import android.view.IWindowManager +import android.view.Surface.Rotation +import com.android.systemui.unfold.dagger.UnfoldMain +import com.android.systemui.unfold.util.CallbackController +import java.util.concurrent.Executor +import javax.inject.Inject + +/** + * Allows to subscribe to rotation changes. + * + * This is needed as rotation updates from [IWindowManager] are received in a binder thread, while + * most of the times we want them in the main one. Updates are provided for the display associated + * to [context]. + */ +class RotationChangeProvider +@Inject +constructor( + private val windowManagerInterface: IWindowManager, + private val context: Context, + @UnfoldMain private val mainExecutor: Executor, +) : CallbackController<RotationChangeProvider.RotationListener> { + + private val listeners = mutableListOf<RotationListener>() + + private val rotationWatcher = RotationWatcher() + + override fun addCallback(listener: RotationListener) { + mainExecutor.execute { + if (listeners.isEmpty()) { + subscribeToRotation() + } + listeners += listener + } + } + + override fun removeCallback(listener: RotationListener) { + mainExecutor.execute { + listeners -= listener + if (listeners.isEmpty()) { + unsubscribeToRotation() + } + } + } + + private fun subscribeToRotation() { + try { + windowManagerInterface.watchRotation(rotationWatcher, context.displayId) + } catch (e: RemoteException) { + throw e.rethrowFromSystemServer() + } + } + + private fun unsubscribeToRotation() { + try { + windowManagerInterface.removeRotationWatcher(rotationWatcher) + } catch (e: RemoteException) { + throw e.rethrowFromSystemServer() + } + } + + /** Gets notified of rotation changes. */ + fun interface RotationListener { + /** Called once rotation changes. */ + fun onRotationChanged(@Rotation newRotation: Int) + } + + private inner class RotationWatcher : IRotationWatcher.Stub() { + override fun onRotationChanged(rotation: Int) { + mainExecutor.execute { listeners.forEach { it.onRotationChanged(rotation) } } + } + } +} diff --git a/proto/src/task_snapshot.proto b/proto/src/task_snapshot.proto deleted file mode 100644 index 1cbc17ed9f41..000000000000 --- a/proto/src/task_snapshot.proto +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - syntax = "proto3"; - - package com.android.server.wm; - - option java_package = "com.android.server.wm"; - option java_outer_classname = "WindowManagerProtos"; - - message TaskSnapshotProto { - int32 orientation = 1; - int32 inset_left = 2; - int32 inset_top = 3; - int32 inset_right = 4; - int32 inset_bottom = 5; - bool is_real_snapshot = 6; - int32 windowing_mode = 7; - int32 system_ui_visibility = 8 [deprecated=true]; - bool is_translucent = 9; - string top_activity_component = 10; - // deprecated because original width and height are stored now instead of the scale. - float legacy_scale = 11 [deprecated=true]; - int64 id = 12; - int32 rotation = 13; - // The task width when the snapshot was taken - int32 task_width = 14; - // The task height when the snapshot was taken - int32 task_height = 15; - int32 appearance = 16; - int32 letterbox_inset_left = 17; - int32 letterbox_inset_top = 18; - int32 letterbox_inset_right = 19; - int32 letterbox_inset_bottom = 20; - } diff --git a/proto/src/windowmanager.proto b/proto/src/windowmanager.proto new file mode 100644 index 000000000000..f26404c66623 --- /dev/null +++ b/proto/src/windowmanager.proto @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package com.android.server.wm; + +option java_package = "com.android.server.wm"; +option java_outer_classname = "WindowManagerProtos"; + +message TaskSnapshotProto { + int32 orientation = 1; + int32 inset_left = 2; + int32 inset_top = 3; + int32 inset_right = 4; + int32 inset_bottom = 5; + bool is_real_snapshot = 6; + int32 windowing_mode = 7; + int32 system_ui_visibility = 8 [deprecated=true]; + bool is_translucent = 9; + string top_activity_component = 10; + // deprecated because original width and height are stored now instead of the scale. + float legacy_scale = 11 [deprecated=true]; + int64 id = 12; + int32 rotation = 13; + // The task width when the snapshot was taken + int32 task_width = 14; + // The task height when the snapshot was taken + int32 task_height = 15; + int32 appearance = 16; + int32 letterbox_inset_left = 17; + int32 letterbox_inset_top = 18; + int32 letterbox_inset_right = 19; + int32 letterbox_inset_bottom = 20; +} + +// Persistent letterboxing configurations +message LetterboxProto { + + // Possible values for the letterbox horizontal reachability + enum LetterboxHorizontalReachability { + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT = 0; + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER = 1; + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT = 2; + } + + // Possible values for the letterbox vertical reachability + enum LetterboxVerticalReachability { + LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP = 0; + LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER = 1; + LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM = 2; + } + + // Represents the current horizontal position for the letterboxed activity + LetterboxHorizontalReachability letterbox_position_for_horizontal_reachability = 1; + // Represents the current vertical position for the letterboxed activity + LetterboxVerticalReachability letterbox_position_for_vertical_reachability = 2; +}
\ No newline at end of file diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 2a4bcb08b54b..1e9c3b72f57c 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -657,25 +657,27 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub userState.mBindingServices.removeIf(filter); userState.mCrashedServices.removeIf(filter); final Iterator<ComponentName> it = userState.mEnabledServices.iterator(); + boolean anyServiceRemoved = false; while (it.hasNext()) { final ComponentName comp = it.next(); final String compPkg = comp.getPackageName(); if (compPkg.equals(packageName)) { it.remove(); - // Update the enabled services setting. - persistComponentNamesToSettingLocked( - Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, - userState.mEnabledServices, userId); - // Update the touch exploration granted services setting. userState.mTouchExplorationGrantedServices.remove(comp); - persistComponentNamesToSettingLocked( - Settings.Secure. - TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, - userState.mTouchExplorationGrantedServices, userId); - onUserStateChangedLocked(userState); - return; + anyServiceRemoved = true; } } + if (anyServiceRemoved) { + // Update the enabled services setting. + persistComponentNamesToSettingLocked( + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + userState.mEnabledServices, userId); + // Update the touch exploration granted services setting. + persistComponentNamesToSettingLocked( + Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + userState.mTouchExplorationGrantedServices, userId); + onUserStateChangedLocked(userState); + } } } diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index 38237fa8eabd..23cacf3c54cf 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -866,6 +866,33 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } @Override + public void setAppWidgetHidden(String callingPackage, int hostId) { + final int userId = UserHandle.getCallingUserId(); + + if (DEBUG) { + Slog.i(TAG, "setAppWidgetHidden() " + userId); + } + + mSecurityPolicy.enforceCallFromPackage(callingPackage); + + synchronized (mLock) { + ensureGroupStateLoadedLocked(userId, /* enforceUserUnlockingOrUnlocked */false); + + HostId id = new HostId(Binder.getCallingUid(), hostId, callingPackage); + Host host = lookupHostLocked(id); + + if (host != null) { + try { + mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false); + } catch (NullPointerException e) { + Slog.e(TAG, "setAppWidgetHidden(): Getting host uids: " + host.toString(), e); + throw e; + } + } + } + } + + @Override public void deleteAppWidgetId(String callingPackage, int appWidgetId) { final int userId = UserHandle.getCallingUserId(); diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index b9cbf1298e6f..e0643b750104 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -3721,21 +3721,34 @@ public class UserBackupManagerService { Slog.w(TAG, "agentDisconnected: the backup agent for " + packageName + " died: cancel current operations"); - // handleCancel() causes the PerformFullTransportBackupTask to go on to - // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so - // that the package being backed up doesn't get stuck in restricted mode until the - // backup time-out elapses. - for (int token : mOperationStorage.operationTokensForPackage(packageName)) { - if (MORE_DEBUG) { - Slog.d(TAG, "agentDisconnected: will handleCancel(all) for token:" - + Integer.toHexString(token)); + // Offload operation cancellation off the main thread as the cancellation callbacks + // might call out to BackupTransport. Other operations started on the same package + // before the cancellation callback has executed will also be cancelled by the callback. + Runnable cancellationRunnable = () -> { + // handleCancel() causes the PerformFullTransportBackupTask to go on to + // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so + // that the package being backed up doesn't get stuck in restricted mode until the + // backup time-out elapses. + for (int token : mOperationStorage.operationTokensForPackage(packageName)) { + if (MORE_DEBUG) { + Slog.d(TAG, "agentDisconnected: will handleCancel(all) for token:" + + Integer.toHexString(token)); + } + handleCancel(token, true /* cancelAll */); } - handleCancel(token, true /* cancelAll */); - } + }; + getThreadForAsyncOperation(/* operationName */ "agent-disconnected", + cancellationRunnable).start(); + mAgentConnectLock.notifyAll(); } } + @VisibleForTesting + Thread getThreadForAsyncOperation(String operationName, Runnable operation) { + return new Thread(operation, operationName); + } + /** * An application being installed will need a restore pass, then the {@link PackageManager} will * need to be told when the restore is finished. diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index abc49372053e..a94e4b9b492d 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -1152,6 +1152,9 @@ public class CompanionDeviceManagerService extends SystemService { } private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) { + if (packageInfo == null) { + return; + } if (containsEither(packageInfo.requestedPermissions, android.Manifest.permission.RUN_IN_BACKGROUND, android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) { diff --git a/services/companion/java/com/android/server/companion/PackageUtils.java b/services/companion/java/com/android/server/companion/PackageUtils.java index f523773033d1..451a7005054f 100644 --- a/services/companion/java/com/android/server/companion/PackageUtils.java +++ b/services/companion/java/com/android/server/companion/PackageUtils.java @@ -54,12 +54,19 @@ final class PackageUtils { private static final String PROPERTY_PRIMARY_TAG = "android.companion.PROPERTY_PRIMARY_COMPANION_DEVICE_SERVICE"; - static @Nullable PackageInfo getPackageInfo(@NonNull Context context, + @Nullable + static PackageInfo getPackageInfo(@NonNull Context context, @UserIdInt int userId, @NonNull String packageName) { final PackageManager pm = context.getPackageManager(); final PackageInfoFlags flags = PackageInfoFlags.of(GET_PERMISSIONS | GET_CONFIGURATIONS); - return Binder.withCleanCallingIdentity(() -> - pm.getPackageInfoAsUser(packageName, flags , userId)); + return Binder.withCleanCallingIdentity(() -> { + try { + return pm.getPackageInfoAsUser(packageName, flags, userId); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Package [" + packageName + "] is not found."); + return null; + } + }); } static void enforceUsesCompanionDeviceFeature(@NonNull Context context, diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index e81bab1dcaa9..5d46de335781 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -152,6 +152,8 @@ final class UiModeManagerService extends SystemService { // flag set by resource, whether to start dream immediately upon docking even if unlocked. private boolean mStartDreamImmediatelyOnDock = true; + // flag set by resource, whether to disable dreams when ambient mode suppression is enabled. + private boolean mDreamsDisabledByAmbientModeSuppression = false; // flag set by resource, whether to enable Car dock launch when starting car mode. private boolean mEnableCarDockLaunch = true; // flag set by resource, whether to lock UI mode to the default one or not. @@ -364,6 +366,11 @@ final class UiModeManagerService extends SystemService { mStartDreamImmediatelyOnDock = startDreamImmediatelyOnDock; } + @VisibleForTesting + void setDreamsDisabledByAmbientModeSuppression(boolean disabledByAmbientModeSuppression) { + mDreamsDisabledByAmbientModeSuppression = disabledByAmbientModeSuppression; + } + @Override public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) { mCurrentUser = to.getUserIdentifier(); @@ -424,6 +431,8 @@ final class UiModeManagerService extends SystemService { final Resources res = context.getResources(); mStartDreamImmediatelyOnDock = res.getBoolean( com.android.internal.R.bool.config_startDreamImmediatelyOnDock); + mDreamsDisabledByAmbientModeSuppression = res.getBoolean( + com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig); mNightMode = res.getInteger( com.android.internal.R.integer.config_defaultNightMode); mDefaultUiModeType = res.getInteger( @@ -1827,11 +1836,15 @@ final class UiModeManagerService extends SystemService { // Send the new configuration. applyConfigurationExternallyLocked(); + final boolean dreamsSuppressed = mDreamsDisabledByAmbientModeSuppression + && mLocalPowerManager.isAmbientDisplaySuppressed(); + // If we did not start a dock app, then start dreaming if appropriate. - if (category != null && !dockAppStarted && (mStartDreamImmediatelyOnDock - || mWindowManager.isKeyguardShowingAndNotOccluded() - || !mPowerManager.isInteractive())) { - Sandman.startDreamWhenDockedIfAppropriate(getContext()); + if (category != null && !dockAppStarted && !dreamsSuppressed && ( + mStartDreamImmediatelyOnDock + || mWindowManager.isKeyguardShowingAndNotOccluded() + || !mPowerManager.isInteractive())) { + mInjector.startDreamWhenDockedIfAppropriate(getContext()); } } @@ -2148,5 +2161,9 @@ final class UiModeManagerService extends SystemService { public int getCallingUid() { return Binder.getCallingUid(); } + + public void startDreamWhenDockedIfAppropriate(Context context) { + Sandman.startDreamWhenDockedIfAppropriate(context); + } } } diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 6b731c319c4b..c128b5ead406 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -89,6 +89,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.stats.devicepolicy.DevicePolicyEnums; import android.text.TextUtils; +import android.util.EventLog; import android.util.Log; import android.util.Pair; import android.util.Slog; @@ -3100,7 +3101,7 @@ public class AccountManagerService */ if (!checkKeyIntent( Binder.getCallingUid(), - intent)) { + result)) { onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "invalid intent in bundle returned"); return; @@ -3519,7 +3520,7 @@ public class AccountManagerService && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) { if (!checkKeyIntent( Binder.getCallingUid(), - intent)) { + result)) { onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "invalid intent in bundle returned"); return; @@ -4870,7 +4871,13 @@ public class AccountManagerService * into launching arbitrary intents on the device via by tricking to click authenticator * supplied entries in the system Settings app. */ - protected boolean checkKeyIntent(int authUid, Intent intent) { + protected boolean checkKeyIntent(int authUid, Bundle bundle) { + if (!checkKeyIntentParceledCorrectly(bundle)) { + EventLog.writeEvent(0x534e4554, "250588548", authUid, ""); + return false; + } + + Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class); // Explicitly set an empty ClipData to ensure that we don't offer to // promote any Uris contained inside for granting purposes if (intent.getClipData() == null) { @@ -4905,6 +4912,25 @@ public class AccountManagerService } } + /** + * Simulate the client side's deserialization of KEY_INTENT value, to make sure they don't + * violate our security policy. + * + * In particular we want to make sure the Authenticator doesn't trick users + * into launching arbitrary intents on the device via exploiting any other Parcel read/write + * mismatch problems. + */ + private boolean checkKeyIntentParceledCorrectly(Bundle bundle) { + Parcel p = Parcel.obtain(); + p.writeBundle(bundle); + p.setDataPosition(0); + Bundle simulateBundle = p.readBundle(); + p.recycle(); + Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class); + return (intent.filterEquals(simulateBundle.getParcelable(AccountManager.KEY_INTENT, + Intent.class))); + } + private boolean isExportedSystemActivity(ActivityInfo activityInfo) { String className = activityInfo.name; return "android".equals(activityInfo.packageName) && @@ -5051,7 +5077,7 @@ public class AccountManagerService && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) { if (!checkKeyIntent( Binder.getCallingUid(), - intent)) { + result)) { onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "invalid intent in bundle returned"); return; diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 9840e0ff90ce..9669c060b716 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -4219,7 +4219,8 @@ public final class ActiveServices { final String procName = r.processName; HostingRecord hostingRecord = new HostingRecord( HostingRecord.HOSTING_TYPE_SERVICE, r.instanceName, - r.definingPackageName, r.definingUid, r.serviceInfo.processName); + r.definingPackageName, r.definingUid, r.serviceInfo.processName, + getHostingRecordTriggerType(r)); ProcessRecord app; if (!isolated) { @@ -4323,6 +4324,14 @@ public final class ActiveServices { return null; } + private String getHostingRecordTriggerType(ServiceRecord r) { + if (Manifest.permission.BIND_JOB_SERVICE.equals(r.permission) + && r.mRecentCallingUid == SYSTEM_UID) { + return HostingRecord.TRIGGER_TYPE_JOB; + } + return HostingRecord.TRIGGER_TYPE_UNKNOWN; + } + private final void requestServiceBindingsLocked(ServiceRecord r, boolean execInFg) throws TransactionTooLargeException { for (int i=r.bindings.size()-1; i>=0; i--) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 0d558b6522e9..75e30e57018c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -5009,7 +5009,8 @@ public class ActivityManagerService extends IActivityManager.Stub hostingRecord.getType(), hostingRecord.getName(), shortAction, - HostingRecord.getHostingTypeIdStatsd(hostingRecord.getType())); + HostingRecord.getHostingTypeIdStatsd(hostingRecord.getType()), + HostingRecord.getTriggerTypeForStatsd(hostingRecord.getTriggerType())); return true; } diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 606a09cb1cac..207c10c44c9b 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -36,6 +36,7 @@ import android.net.ConnectivityManager; import android.net.INetworkManagementEventObserver; import android.net.Network; import android.net.NetworkCapabilities; +import android.os.BatteryConsumer; import android.os.BatteryManagerInternal; import android.os.BatteryStats; import android.os.BatteryStatsInternal; @@ -2282,6 +2283,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub pw.println(" --settings: dump the settings key/values related to batterystats"); pw.println(" --cpu: dump cpu stats for debugging purpose"); pw.println(" --power-profile: dump the power profile constants"); + pw.println(" --usage: write battery usage stats. Optional arguments:"); + pw.println(" --proto: output as a binary protobuffer"); + pw.println(" --model power-profile: use the power profile model" + + " even if measured energy is available"); pw.println(" <package.name>: optional name of package to filter output by."); pw.println(" -h: print this help text."); pw.println("Battery stats (batterystats) commands:"); @@ -2325,6 +2330,31 @@ public final class BatteryStatsService extends IBatteryStats.Stub } } + private void dumpUsageStatsToProto(FileDescriptor fd, PrintWriter pw, int model, + boolean proto) { + awaitCompletion(); + syncStats("dump", BatteryExternalStatsWorker.UPDATE_ALL); + + BatteryUsageStatsQuery.Builder builder = new BatteryUsageStatsQuery.Builder() + .setMaxStatsAgeMs(0) + .includeProcessStateData() + .includePowerModels(); + if (model == BatteryConsumer.POWER_MODEL_POWER_PROFILE) { + builder.powerProfileModeledOnly(); + } + BatteryUsageStatsQuery query = builder.build(); + synchronized (mStats) { + mStats.prepareForDumpLocked(); + BatteryUsageStats batteryUsageStats = + mBatteryUsageStatsProvider.getBatteryUsageStats(query); + if (proto) { + batteryUsageStats.dumpToProto(fd); + } else { + batteryUsageStats.dump(pw, ""); + } + } + } + private int doEnableOrDisable(PrintWriter pw, int i, String[] args, boolean enable) { i++; if (i >= args.length) { @@ -2478,6 +2508,35 @@ public final class BatteryStatsService extends IBatteryStats.Stub } else if ("--power-profile".equals(arg)) { dumpPowerProfile(pw); return; + } else if ("--usage".equals(arg)) { + int model = BatteryConsumer.POWER_MODEL_UNDEFINED; + boolean proto = false; + for (int j = i + 1; j < args.length; j++) { + switch (args[j]) { + case "--proto": + proto = true; + break; + case "--model": { + if (j + 1 < args.length) { + j++; + if ("power-profile".equals(args[j])) { + model = BatteryConsumer.POWER_MODEL_POWER_PROFILE; + } else { + pw.println("Unknown power model: " + args[j]); + dumpHelp(pw); + return; + } + } else { + pw.println("--model without a value"); + dumpHelp(pw); + return; + } + break; + } + } + } + dumpUsageStatsToProto(fd, pw, model, proto); + return; } else if ("-a".equals(arg)) { flags |= BatteryStats.DUMP_VERBOSE; } else if (arg.length() > 0 && arg.charAt(0) == '-'){ diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java index 58569495f70d..c235e0597384 100644 --- a/services/core/java/com/android/server/am/BroadcastQueue.java +++ b/services/core/java/com/android/server/am/BroadcastQueue.java @@ -519,7 +519,7 @@ public final class BroadcastQueue { final Object curReceiver = r.receivers.get(curIndex); FrameworkStatsLog.write(BROADCAST_DELIVERY_EVENT_REPORTED, r.curApp.uid, r.callingUid == -1 ? Process.SYSTEM_UID : r.callingUid, - ActivityManagerService.getShortAction(r.intent.getAction()), + r.intent.getAction(), curReceiver instanceof BroadcastFilter ? BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__RUNTIME : BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__MANIFEST, @@ -692,7 +692,7 @@ public final class BroadcastQueue { FrameworkStatsLog.write(BROADCAST_DELIVERY_EVENT_REPORTED, receiverUid == -1 ? Process.SYSTEM_UID : receiverUid, callingUid == -1 ? Process.SYSTEM_UID : callingUid, - ActivityManagerService.getShortAction(intent.getAction()), + intent.getAction(), BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__RUNTIME, BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM, dispatchDelay, receiveDelay, 0 /* finish_delay */); @@ -1921,7 +1921,7 @@ public final class BroadcastQueue { info.activityInfo.applicationInfo, true, r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND, new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST, r.curComponent, - r.intent.getAction()), + r.intent.getAction(), getHostingRecordTriggerType(r)), isActivityCapable ? ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE : ZYGOTE_POLICY_FLAG_EMPTY, (r.intent.getFlags() & Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false); if (r.curApp == null) { @@ -1944,6 +1944,16 @@ public final class BroadcastQueue { mPendingBroadcastRecvIndex = recIdx; } + private String getHostingRecordTriggerType(BroadcastRecord r) { + if (r.alarm) { + return HostingRecord.TRIGGER_TYPE_ALARM; + } else if (r.pushMessage) { + return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE; + } else if (r.pushMessageOverQuota) { + return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA; + } + return HostingRecord.TRIGGER_TYPE_UNKNOWN; + } @Nullable private String getTargetPackage(BroadcastRecord r) { diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index ce4528bca887..baaae1d7a555 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -71,6 +71,8 @@ final class BroadcastRecord extends Binder { final boolean ordered; // serialize the send to receivers? final boolean sticky; // originated from existing sticky data? final boolean alarm; // originated from an alarm triggering? + final boolean pushMessage; // originated from a push message? + final boolean pushMessageOverQuota; // originated from a push message which was over quota? final boolean initialSticky; // initial broadcast from register to sticky? final int userId; // user id this broadcast was for final String resolvedType; // the resolved data type @@ -309,6 +311,8 @@ final class BroadcastRecord extends Binder { mBackgroundActivityStartsToken = backgroundActivityStartsToken; this.timeoutExempt = timeoutExempt; alarm = options != null && options.isAlarmBroadcast(); + pushMessage = options != null && options.isPushMessagingBroadcast(); + pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast(); } /** @@ -362,6 +366,8 @@ final class BroadcastRecord extends Binder { mBackgroundActivityStartsToken = from.mBackgroundActivityStartsToken; timeoutExempt = from.timeoutExempt; alarm = from.alarm; + pushMessage = from.pushMessage; + pushMessageOverQuota = from.pushMessageOverQuota; } /** diff --git a/services/core/java/com/android/server/am/HostingRecord.java b/services/core/java/com/android/server/am/HostingRecord.java index f88a8ce83d02..30811a175bac 100644 --- a/services/core/java/com/android/server/am/HostingRecord.java +++ b/services/core/java/com/android/server/am/HostingRecord.java @@ -16,10 +16,30 @@ package com.android.server.am; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ACTIVITY; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ADDED_APPLICATION; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_BACKUP; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_BROADCAST; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_CONTENT_PROVIDER; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_EMPTY; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_LINK_FAIL; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_NEXT_ACTIVITY; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_NEXT_TOP_ACTIVITY; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ON_HOLD; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_RESTART; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SERVICE; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SYSTEM; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_TOP_ACTIVITY; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_ALARM; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_JOB; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_PUSH_MESSAGE; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TYPE__UNKNOWN; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; -import android.os.ProcessStartTime; /** * This class describes various information required to start a process. @@ -32,6 +52,9 @@ import android.os.ProcessStartTime; * * The {@code mHostingZygote} field describes from which Zygote the new process should be spawned. * + * The {@code mTriggerType} field describes the trigger that started this processs. This could be + * an alarm or a push-message for a broadcast, for example. This is purely for logging and stats. + * * {@code mDefiningPackageName} contains the packageName of the package that defines the * component we want to start; this can be different from the packageName and uid in the * ApplicationInfo that we're creating the process with, in case the service is a @@ -71,7 +94,13 @@ public final class HostingRecord { public static final String HOSTING_TYPE_TOP_ACTIVITY = "top-activity"; public static final String HOSTING_TYPE_EMPTY = ""; - private @NonNull final String mHostingType; + public static final String TRIGGER_TYPE_UNKNOWN = "unknown"; + public static final String TRIGGER_TYPE_ALARM = "alarm"; + public static final String TRIGGER_TYPE_PUSH_MESSAGE = "push_message"; + public static final String TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA = "push_message_over_quota"; + public static final String TRIGGER_TYPE_JOB = "job"; + + @NonNull private final String mHostingType; private final String mHostingName; private final int mHostingZygote; private final String mDefiningPackageName; @@ -79,11 +108,12 @@ public final class HostingRecord { private final boolean mIsTopApp; private final String mDefiningProcessName; @Nullable private final String mAction; + @NonNull private final String mTriggerType; public HostingRecord(@NonNull String hostingType) { this(hostingType, null /* hostingName */, REGULAR_ZYGOTE, null /* definingPackageName */, -1 /* mDefiningUid */, false /* isTopApp */, null /* definingProcessName */, - null /* action */); + null /* action */, TRIGGER_TYPE_UNKNOWN); } public HostingRecord(@NonNull String hostingType, ComponentName hostingName) { @@ -91,22 +121,24 @@ public final class HostingRecord { } public HostingRecord(@NonNull String hostingType, ComponentName hostingName, - @Nullable String action) { + @Nullable String action, @Nullable String triggerType) { this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE, null /* definingPackageName */, -1 /* mDefiningUid */, false /* isTopApp */, - null /* definingProcessName */, action); + null /* definingProcessName */, action, triggerType); } public HostingRecord(@NonNull String hostingType, ComponentName hostingName, - String definingPackageName, int definingUid, String definingProcessName) { + String definingPackageName, int definingUid, String definingProcessName, + String triggerType) { this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE, definingPackageName, - definingUid, false /* isTopApp */, definingProcessName, null /* action */); + definingUid, false /* isTopApp */, definingProcessName, null /* action */, + triggerType); } public HostingRecord(@NonNull String hostingType, ComponentName hostingName, boolean isTopApp) { this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE, null /* definingPackageName */, -1 /* mDefiningUid */, isTopApp /* isTopApp */, - null /* definingProcessName */, null /* action */); + null /* definingProcessName */, null /* action */, TRIGGER_TYPE_UNKNOWN); } public HostingRecord(@NonNull String hostingType, String hostingName) { @@ -121,12 +153,12 @@ public final class HostingRecord { private HostingRecord(@NonNull String hostingType, String hostingName, int hostingZygote) { this(hostingType, hostingName, hostingZygote, null /* definingPackageName */, -1 /* mDefiningUid */, false /* isTopApp */, null /* definingProcessName */, - null /* action */); + null /* action */, TRIGGER_TYPE_UNKNOWN); } private HostingRecord(@NonNull String hostingType, String hostingName, int hostingZygote, String definingPackageName, int definingUid, boolean isTopApp, - String definingProcessName, @Nullable String action) { + String definingProcessName, @Nullable String action, String triggerType) { mHostingType = hostingType; mHostingName = hostingName; mHostingZygote = hostingZygote; @@ -135,6 +167,7 @@ public final class HostingRecord { mIsTopApp = isTopApp; mDefiningProcessName = definingProcessName; mAction = action; + mTriggerType = triggerType; } public @NonNull String getType() { @@ -188,6 +221,11 @@ public final class HostingRecord { return mAction; } + /** Returns the type of trigger that led to this process start. */ + public @NonNull String getTriggerType() { + return mTriggerType; + } + /** * Creates a HostingRecord for a process that must spawn from the webview zygote * @param hostingName name of the component to be hosted in this process @@ -197,7 +235,7 @@ public final class HostingRecord { String definingPackageName, int definingUid, String definingProcessName) { return new HostingRecord(HostingRecord.HOSTING_TYPE_EMPTY, hostingName.toShortString(), WEBVIEW_ZYGOTE, definingPackageName, definingUid, false /* isTopApp */, - definingProcessName, null /* action */); + definingProcessName, null /* action */, TRIGGER_TYPE_UNKNOWN); } /** @@ -211,7 +249,7 @@ public final class HostingRecord { int definingUid, String definingProcessName) { return new HostingRecord(HostingRecord.HOSTING_TYPE_EMPTY, hostingName.toShortString(), APP_ZYGOTE, definingPackageName, definingUid, false /* isTopApp */, - definingProcessName, null /* action */); + definingProcessName, null /* action */, TRIGGER_TYPE_UNKNOWN); } /** @@ -236,35 +274,55 @@ public final class HostingRecord { public static int getHostingTypeIdStatsd(@NonNull String hostingType) { switch(hostingType) { case HOSTING_TYPE_ACTIVITY: - return ProcessStartTime.HOSTING_TYPE_ACTIVITY; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ACTIVITY; case HOSTING_TYPE_ADDED_APPLICATION: - return ProcessStartTime.HOSTING_TYPE_ADDED_APPLICATION; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ADDED_APPLICATION; case HOSTING_TYPE_BACKUP: - return ProcessStartTime.HOSTING_TYPE_BACKUP; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_BACKUP; case HOSTING_TYPE_BROADCAST: - return ProcessStartTime.HOSTING_TYPE_BROADCAST; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_BROADCAST; case HOSTING_TYPE_CONTENT_PROVIDER: - return ProcessStartTime.HOSTING_TYPE_CONTENT_PROVIDER; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_CONTENT_PROVIDER; case HOSTING_TYPE_LINK_FAIL: - return ProcessStartTime.HOSTING_TYPE_LINK_FAIL; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_LINK_FAIL; case HOSTING_TYPE_ON_HOLD: - return ProcessStartTime.HOSTING_TYPE_ON_HOLD; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ON_HOLD; case HOSTING_TYPE_NEXT_ACTIVITY: - return ProcessStartTime.HOSTING_TYPE_NEXT_ACTIVITY; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_NEXT_ACTIVITY; case HOSTING_TYPE_NEXT_TOP_ACTIVITY: - return ProcessStartTime.HOSTING_TYPE_NEXT_TOP_ACTIVITY; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_NEXT_TOP_ACTIVITY; case HOSTING_TYPE_RESTART: - return ProcessStartTime.HOSTING_TYPE_RESTART; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_RESTART; case HOSTING_TYPE_SERVICE: - return ProcessStartTime.HOSTING_TYPE_SERVICE; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SERVICE; case HOSTING_TYPE_SYSTEM: - return ProcessStartTime.HOSTING_TYPE_SYSTEM; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SYSTEM; case HOSTING_TYPE_TOP_ACTIVITY: - return ProcessStartTime.HOSTING_TYPE_TOP_ACTIVITY; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_TOP_ACTIVITY; case HOSTING_TYPE_EMPTY: - return ProcessStartTime.HOSTING_TYPE_EMPTY; + return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_EMPTY; + default: + return PROCESS_START_TIME__TYPE__UNKNOWN; + } + } + + /** + * Map the string triggerType to enum TriggerType defined in ProcessStartTime proto. + * @param triggerType + * @return enum TriggerType defined in ProcessStartTime proto + */ + public static int getTriggerTypeForStatsd(@NonNull String triggerType) { + switch(triggerType) { + case TRIGGER_TYPE_ALARM: + return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_ALARM; + case TRIGGER_TYPE_PUSH_MESSAGE: + return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_PUSH_MESSAGE; + case TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA: + return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA; + case TRIGGER_TYPE_JOB: + return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_JOB; default: - return ProcessStartTime.HOSTING_TYPE_UNKNOWN; + return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_UNKNOWN; } } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 38cd8c0bbf48..4d2bcc7390b5 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -41,10 +41,12 @@ import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.app.AlarmManager; import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.IUidObserver; import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.role.OnRoleHoldersChangedListener; import android.app.role.RoleManager; import android.bluetooth.BluetoothAdapter; @@ -1185,6 +1187,8 @@ public class AudioService extends IAudioService.Stub mSafeMediaVolumeIndex = mContext.getResources().getInteger( com.android.internal.R.integer.config_safe_media_volume_index) * 10; + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + mUseFixedVolume = mContext.getResources().getBoolean( com.android.internal.R.bool.config_useFixedVolume); @@ -1202,7 +1206,7 @@ public class AudioService extends IAudioService.Stub mPlaybackMonitor = new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM], device -> onMuteAwaitConnectionTimeout(device)); - mPlaybackMonitor.registerPlaybackCallback(mVoicePlaybackActivityMonitor, true); + mPlaybackMonitor.registerPlaybackCallback(mPlaybackActivityMonitor, true); mMediaFocusControl = new MediaFocusControl(mContext, mPlaybackMonitor); @@ -1308,6 +1312,7 @@ public class AudioService extends IAudioService.Stub intentFilter.addAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); intentFilter.addAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); + intentFilter.addAction(ACTION_CHECK_MUSIC_ACTIVE); mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, intentFilter, null, null, Context.RECEIVER_EXPORTED); @@ -1927,13 +1932,7 @@ public class AudioService extends IAudioService.Stub if (state == AudioService.CONNECTION_STATE_CONNECTED) { // DEVICE_OUT_HDMI is now connected if (mSafeMediaVolumeDevices.contains(AudioSystem.DEVICE_OUT_HDMI)) { - sendMsg(mAudioHandler, - MSG_CHECK_MUSIC_ACTIVE, - SENDMSG_REPLACE, - 0, - 0, - caller, - MUSIC_ACTIVE_POLL_PERIOD_MS); + scheduleMusicActiveCheck(); } if (isPlatformTelevision()) { @@ -3822,8 +3821,9 @@ public class AudioService extends IAudioService.Stub } private AtomicBoolean mVoicePlaybackActive = new AtomicBoolean(false); + private AtomicBoolean mMediaPlaybackActive = new AtomicBoolean(false); - private final IPlaybackConfigDispatcher mVoicePlaybackActivityMonitor = + private final IPlaybackConfigDispatcher mPlaybackActivityMonitor = new IPlaybackConfigDispatcher.Stub() { @Override public void dispatchPlaybackConfigChange(List<AudioPlaybackConfiguration> configs, @@ -3836,19 +3836,26 @@ public class AudioService extends IAudioService.Stub private void onPlaybackConfigChange(List<AudioPlaybackConfiguration> configs) { boolean voiceActive = false; + boolean mediaActive = false; for (AudioPlaybackConfiguration config : configs) { final int usage = config.getAudioAttributes().getUsage(); - if ((usage == AudioAttributes.USAGE_VOICE_COMMUNICATION - || usage == AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) - && config.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) { + if (!config.isActive()) { + continue; + } + if (usage == AudioAttributes.USAGE_VOICE_COMMUNICATION + || usage == AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) { voiceActive = true; - break; + } + if (usage == AudioAttributes.USAGE_MEDIA || usage == AudioAttributes.USAGE_GAME) { + mediaActive = true; } } if (mVoicePlaybackActive.getAndSet(voiceActive) != voiceActive) { updateHearingAidVolumeOnVoiceActivityUpdate(); } - + if (mMediaPlaybackActive.getAndSet(mediaActive) != mediaActive && mediaActive) { + scheduleMusicActiveCheck(); + } // Update playback active state for all apps in audio mode stack. // When the audio mode owner becomes active, replace any delayed MSG_UPDATE_AUDIO_MODE // and request an audio mode update immediately. Upon any other change, queue the message @@ -6035,30 +6042,52 @@ public class AudioService extends IAudioService.Stub return mContentResolver; } + private void scheduleMusicActiveCheck() { + synchronized (mSafeMediaVolumeStateLock) { + cancelMusicActiveCheck(); + mMusicActiveIntent = PendingIntent.getBroadcast(mContext, + REQUEST_CODE_CHECK_MUSIC_ACTIVE, + new Intent(ACTION_CHECK_MUSIC_ACTIVE), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + + MUSIC_ACTIVE_POLL_PERIOD_MS, mMusicActiveIntent); + } + } + + private void cancelMusicActiveCheck() { + synchronized (mSafeMediaVolumeStateLock) { + if (mMusicActiveIntent != null) { + mAlarmManager.cancel(mMusicActiveIntent); + mMusicActiveIntent = null; + } + } + } private void onCheckMusicActive(String caller) { synchronized (mSafeMediaVolumeStateLock) { if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE) { int device = getDeviceForStream(AudioSystem.STREAM_MUSIC); - - if (mSafeMediaVolumeDevices.contains(device)) { - sendMsg(mAudioHandler, - MSG_CHECK_MUSIC_ACTIVE, - SENDMSG_REPLACE, - 0, - 0, - caller, - MUSIC_ACTIVE_POLL_PERIOD_MS); + if (mSafeMediaVolumeDevices.contains(device) + && mAudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)) { + scheduleMusicActiveCheck(); int index = mStreamStates[AudioSystem.STREAM_MUSIC].getIndex(device); - if (mAudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0) - && (index > safeMediaVolumeIndex(device))) { + if (index > safeMediaVolumeIndex(device)) { // Approximate cumulative active music time - mMusicActiveMs += MUSIC_ACTIVE_POLL_PERIOD_MS; + long curTimeMs = SystemClock.elapsedRealtime(); + if (mLastMusicActiveTimeMs != 0) { + mMusicActiveMs += (int) (curTimeMs - mLastMusicActiveTimeMs); + } + mLastMusicActiveTimeMs = curTimeMs; + Log.i(TAG, "onCheckMusicActive() mMusicActiveMs: " + mMusicActiveMs); if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) { setSafeMediaVolumeEnabled(true, caller); mMusicActiveMs = 0; } saveMusicActiveMs(); } + } else { + cancelMusicActiveCheck(); + mLastMusicActiveTimeMs = 0; } } } @@ -6127,6 +6156,7 @@ public class AudioService extends IAudioService.Stub } else { // We have existing playback time recorded, already confirmed. mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE; + mLastMusicActiveTimeMs = 0; } } } else { @@ -8636,13 +8666,7 @@ public class AudioService extends IAudioService.Stub @VisibleForTesting public void checkMusicActive(int deviceType, String caller) { if (mSafeMediaVolumeDevices.contains(deviceType)) { - sendMsg(mAudioHandler, - MSG_CHECK_MUSIC_ACTIVE, - SENDMSG_REPLACE, - 0, - 0, - caller, - MUSIC_ACTIVE_POLL_PERIOD_MS); + scheduleMusicActiveCheck(); } } @@ -8767,6 +8791,8 @@ public class AudioService extends IAudioService.Stub suspendedPackages[i], suspendedUids[i]); } } + } else if (action.equals(ACTION_CHECK_MUSIC_ACTIVE)) { + onCheckMusicActive(ACTION_CHECK_MUSIC_ACTIVE); } } } // end class AudioServiceBroadcastReceiver @@ -9712,12 +9738,20 @@ public class AudioService extends IAudioService.Stub // When this time reaches UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX, the safe media volume is re-enabled // automatically. mMusicActiveMs is rounded to a multiple of MUSIC_ACTIVE_POLL_PERIOD_MS. private int mMusicActiveMs; + private long mLastMusicActiveTimeMs = 0; + private PendingIntent mMusicActiveIntent = null; + private AlarmManager mAlarmManager; + private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours private static final int MUSIC_ACTIVE_POLL_PERIOD_MS = 60000; // 1 minute polling interval private static final int SAFE_VOLUME_CONFIGURE_TIMEOUT_MS = 30000; // 30s after boot completed // check playback or record activity every 6 seconds for UIDs owning mode IN_COMMUNICATION private static final int CHECK_MODE_FOR_UID_PERIOD_MS = 6000; + private static final String ACTION_CHECK_MUSIC_ACTIVE = + AudioService.class.getSimpleName() + ".CHECK_MUSIC_ACTIVE"; + private static final int REQUEST_CODE_CHECK_MUSIC_ACTIVE = 1; + private int safeMediaVolumeIndex(int device) { if (!mSafeMediaVolumeDevices.contains(device)) { return MAX_STREAM_VOLUME[AudioSystem.STREAM_MUSIC]; @@ -9739,14 +9773,9 @@ public class AudioService extends IAudioService.Stub } else if (!on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)) { mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE; mMusicActiveMs = 1; // nonzero = confirmed + mLastMusicActiveTimeMs = 0; saveMusicActiveMs(); - sendMsg(mAudioHandler, - MSG_CHECK_MUSIC_ACTIVE, - SENDMSG_REPLACE, - 0, - 0, - caller, - MUSIC_ACTIVE_POLL_PERIOD_MS); + scheduleMusicActiveCheck(); } } } @@ -9788,7 +9817,9 @@ public class AudioService extends IAudioService.Stub public void disableSafeMediaVolume(String callingPackage) { enforceVolumeController("disable the safe media volume"); synchronized (mSafeMediaVolumeStateLock) { + final long identity = Binder.clearCallingIdentity(); setSafeMediaVolumeEnabled(false, callingPackage); + Binder.restoreCallingIdentity(identity); if (mPendingVolumeCommand != null) { onSetStreamVolume(mPendingVolumeCommand.mStreamType, mPendingVolumeCommand.mIndex, diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java index 62f94ed05e0a..da4361843681 100644 --- a/services/core/java/com/android/server/biometrics/log/ALSProbe.java +++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java @@ -30,7 +30,10 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.sensors.BaseClientMonitor; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; /** Probe for ambient light. */ final class ALSProbe implements Probe { @@ -47,12 +50,15 @@ final class ALSProbe implements Probe { private boolean mEnabled = false; private boolean mDestroyed = false; + private boolean mDestroyRequested = false; + private boolean mDisableRequested = false; + private NextConsumer mNextConsumer = null; private volatile float mLastAmbientLux = -1; private final SensorEventListener mLightSensorListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { - mLastAmbientLux = event.values[0]; + onNext(event.values[0]); } @Override @@ -102,29 +108,85 @@ final class ALSProbe implements Probe { @Override public synchronized void enable() { - if (!mDestroyed) { + if (!mDestroyed && !mDestroyRequested) { + mDisableRequested = false; enableLightSensorLoggingLocked(); } } @Override public synchronized void disable() { - if (!mDestroyed) { + mDisableRequested = true; + + // if a final consumer is set it will call destroy/disable on the next value if requested + if (!mDestroyed && mNextConsumer == null) { disableLightSensorLoggingLocked(); } } @Override public synchronized void destroy() { - disable(); - mDestroyed = true; + mDestroyRequested = true; + + // if a final consumer is set it will call destroy/disable on the next value if requested + if (!mDestroyed && mNextConsumer == null) { + disableLightSensorLoggingLocked(); + mDestroyed = true; + } + } + + private synchronized void onNext(float value) { + mLastAmbientLux = value; + + final NextConsumer consumer = mNextConsumer; + mNextConsumer = null; + if (consumer != null) { + Slog.v(TAG, "Finishing next consumer"); + + if (mDestroyRequested) { + destroy(); + } else if (mDisableRequested) { + disable(); + } + + consumer.consume(value); + } } /** The most recent lux reading. */ - public float getCurrentLux() { + public float getMostRecentLux() { return mLastAmbientLux; } + /** + * Register a listener for the next available ALS reading, which will be reported to the given + * consumer even if this probe is {@link #disable()}'ed or {@link #destroy()}'ed before a value + * is available. + * + * This method is intended to be used for event logs that occur when the screen may be + * off and sampling may have been {@link #disable()}'ed. In these cases, this method will turn + * on the sensor (if needed), fetch & report the first value, and then destroy or disable this + * probe (if needed). + * + * @param consumer consumer to notify when the data is available + * @param handler handler for notifying the consumer, or null + */ + public synchronized void awaitNextLux(@NonNull Consumer<Float> consumer, + @Nullable Handler handler) { + final NextConsumer nextConsumer = new NextConsumer(consumer, handler); + final float current = mLastAmbientLux; + if (current > -1f) { + nextConsumer.consume(current); + } else if (mDestroyed) { + nextConsumer.consume(-1f); + } else if (mNextConsumer != null) { + mNextConsumer.add(nextConsumer); + } else { + mNextConsumer = nextConsumer; + enableLightSensorLoggingLocked(); + } + } + private void enableLightSensorLoggingLocked() { if (!mEnabled) { mEnabled = true; @@ -155,9 +217,39 @@ final class ALSProbe implements Probe { } } - private void onTimeout() { + private synchronized void onTimeout() { Slog.e(TAG, "Max time exceeded for ALS logger - disabling: " + mLightSensorListener.hashCode()); + + // if consumers are waiting but there was no sensor change, complete them with the latest + // value before disabling + onNext(mLastAmbientLux); disable(); } + + private static class NextConsumer { + @NonNull private final Consumer<Float> mConsumer; + @Nullable private final Handler mHandler; + @NonNull private final List<NextConsumer> mOthers = new ArrayList<>(); + + private NextConsumer(@NonNull Consumer<Float> consumer, @Nullable Handler handler) { + mConsumer = consumer; + mHandler = handler; + } + + public void consume(float value) { + if (mHandler != null) { + mHandler.post(() -> mConsumer.accept(value)); + } else { + mConsumer.accept(value); + } + for (NextConsumer c : mOthers) { + c.consume(value); + } + } + + public void add(NextConsumer consumer) { + mOthers.add(consumer); + } + } } diff --git a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java index d6ca8a68145e..27a70c51f667 100644 --- a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java +++ b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java @@ -62,8 +62,7 @@ public class BiometricFrameworkStatsLogger { /** {@see FrameworkStatsLog.BIOMETRIC_AUTHENTICATED}. */ public void authenticate(OperationContext operationContext, int statsModality, int statsAction, int statsClient, boolean isDebug, long latency, - int authState, boolean requireConfirmation, - int targetUserId, float ambientLightLux) { + int authState, boolean requireConfirmation, int targetUserId, float ambientLightLux) { FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_AUTHENTICATED, statsModality, targetUserId, @@ -80,6 +79,16 @@ public class BiometricFrameworkStatsLogger { operationContext.isAod); } + /** {@see FrameworkStatsLog.BIOMETRIC_AUTHENTICATED}. */ + public void authenticate(OperationContext operationContext, + int statsModality, int statsAction, int statsClient, boolean isDebug, long latency, + int authState, boolean requireConfirmation, int targetUserId, ALSProbe alsProbe) { + alsProbe.awaitNextLux((ambientLightLux) -> { + authenticate(operationContext, statsModality, statsAction, statsClient, isDebug, + latency, authState, requireConfirmation, targetUserId, ambientLightLux); + }, null /* handler */); + } + /** {@see FrameworkStatsLog.BIOMETRIC_ENROLLED}. */ public void enroll(int statsModality, int statsAction, int statsClient, int targetUserId, long latency, boolean enrollSuccessful, float ambientLightLux) { diff --git a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java index 02b350e97ef8..55fe854e1404 100644 --- a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java +++ b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java @@ -220,7 +220,7 @@ public class BiometricLogger { + ", RequireConfirmation: " + requireConfirmation + ", State: " + authState + ", Latency: " + latency - + ", Lux: " + mALSProbe.getCurrentLux()); + + ", Lux: " + mALSProbe.getMostRecentLux()); } else { Slog.v(TAG, "Authentication latency: " + latency); } @@ -231,7 +231,7 @@ public class BiometricLogger { mSink.authenticate(operationContext, mStatsModality, mStatsAction, mStatsClient, Utils.isDebugEnabled(context, targetUserId), - latency, authState, requireConfirmation, targetUserId, mALSProbe.getCurrentLux()); + latency, authState, requireConfirmation, targetUserId, mALSProbe); } /** Log enrollment outcome. */ @@ -245,7 +245,7 @@ public class BiometricLogger { + ", User: " + targetUserId + ", Client: " + mStatsClient + ", Latency: " + latency - + ", Lux: " + mALSProbe.getCurrentLux() + + ", Lux: " + mALSProbe.getMostRecentLux() + ", Success: " + enrollSuccessful); } else { Slog.v(TAG, "Enroll latency: " + latency); @@ -256,7 +256,7 @@ public class BiometricLogger { } mSink.enroll(mStatsModality, mStatsAction, mStatsClient, - targetUserId, latency, enrollSuccessful, mALSProbe.getCurrentLux()); + targetUserId, latency, enrollSuccessful, mALSProbe.getMostRecentLux()); } /** Report unexpected enrollment reported by the HAL. */ diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index b3f42be41cd7..a778b573f225 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -16,6 +16,7 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; +import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR; import android.annotation.NonNull; @@ -57,6 +58,7 @@ import com.android.server.biometrics.sensors.SensorOverlays; import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler; import com.android.server.biometrics.sensors.fingerprint.Udfps; +import java.time.Clock; import java.util.ArrayList; import java.util.function.Supplier; @@ -88,7 +90,9 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> private long mWaitForAuthKeyguard; private long mWaitForAuthBp; private long mIgnoreAuthFor; + private long mSideFpsLastAcquireStartTime; private Runnable mAuthSuccessRunnable; + private final Clock mClock; FingerprintAuthenticationClient( @NonNull Context context, @@ -112,7 +116,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> @Nullable ISidefpsController sidefpsController, boolean allowBackgroundAuthentication, @NonNull FingerprintSensorPropertiesInternal sensorProps, - @NonNull Handler handler) { + @NonNull Handler handler, + @NonNull Clock clock) { super( context, lazyDaemon, @@ -154,6 +159,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> mSkipWaitForPowerVendorAcquireMessage = context.getResources().getInteger( R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage); + mSideFpsLastAcquireStartTime = -1; + mClock = clock; if (mSensorProps.isAnySidefpsType()) { if (Build.isDebuggable()) { @@ -235,8 +242,14 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> return; } delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp; - Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps waiting for power for: " - + delay + "ms"); + + if (mSideFpsLastAcquireStartTime != -1) { + delay = Math.max(0, + delay - (mClock.millis() - mSideFpsLastAcquireStartTime)); + } + + Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps " + + "waiting for power until: " + delay + "ms"); } if (mHandler.hasMessages(MESSAGE_FINGER_UP)) { @@ -260,13 +273,15 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo)); super.onAcquired(acquiredInfo, vendorCode); if (mSensorProps.isAnySidefpsType()) { + if (acquiredInfo == FINGERPRINT_ACQUIRED_START) { + mSideFpsLastAcquireStartTime = mClock.millis(); + } final boolean shouldLookForVendor = mSkipWaitForPowerAcquireMessage == FINGERPRINT_ACQUIRED_VENDOR; final boolean acquireMessageMatch = acquiredInfo == mSkipWaitForPowerAcquireMessage; final boolean vendorMessageMatch = vendorCode == mSkipWaitForPowerVendorAcquireMessage; final boolean ignorePowerPress = - (acquireMessageMatch && !shouldLookForVendor) || (shouldLookForVendor - && acquireMessageMatch && vendorMessageMatch); + acquireMessageMatch && (!shouldLookForVendor || vendorMessageMatch); if (ignorePowerPress) { Slog.d(TAG, "(sideFPS) onFingerUp"); @@ -333,6 +348,9 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> mALSProbeCallback.getProbe().disable(); } }); + if (getBiometricContext().isAwake()) { + mALSProbeCallback.getProbe().enable(); + } if (session.hasContextMethods()) { return session.getSession().authenticateWithContext(mOperationId, opContext); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 6f6c09b91a66..dabb2cf5819a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -47,6 +47,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; import android.os.UserManager; import android.util.Slog; import android.util.SparseArray; @@ -447,7 +448,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mBiometricContext, isStrongBiometric, mTaskStackListener, mSensors.get(sensorId).getLockoutCache(), mUdfpsOverlayController, mSidefpsController, allowBackgroundAuthentication, - mSensors.get(sensorId).getSensorProperties(), mHandler); + mSensors.get(sensorId).getSensorProperties(), mHandler, + SystemClock.elapsedRealtimeClock()); scheduleForSensor(sensorId, client, mBiometricStateCallback); }); } diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index a7d372977ebb..40e28dadb4da 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -592,10 +592,14 @@ class AutomaticBrightnessController { } pw.println(); + pw.println(" mAmbientBrightnessThresholds="); mAmbientBrightnessThresholds.dump(pw); + pw.println(" mScreenBrightnessThresholds="); mScreenBrightnessThresholds.dump(pw); + pw.println(" mScreenBrightnessThresholdsIdle="); mScreenBrightnessThresholdsIdle.dump(pw); - mScreenBrightnessThresholdsIdle.dump(pw); + pw.println(" mAmbientBrightnessThresholdsIdle="); + mAmbientBrightnessThresholdsIdle.dump(pw); } private String configStateToString(int state) { @@ -860,6 +864,7 @@ class AutomaticBrightnessController { Slog.d(TAG, "updateAmbientLux: " + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": " + "mBrighteningLuxThreshold=" + mAmbientBrighteningThreshold + ", " + + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", " + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " + "mAmbientLux=" + mAmbientLux); } diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java index 6de08aed9687..1686cb2b246d 100644 --- a/services/core/java/com/android/server/display/BrightnessTracker.java +++ b/services/core/java/com/android/server/display/BrightnessTracker.java @@ -220,6 +220,11 @@ public class BrightnessTracker { } private void backgroundStart(float initialBrightness) { + synchronized (mDataCollectionLock) { + if (mStarted) { + return; + } + } if (DEBUG) { Slog.d(TAG, "Background start"); } @@ -250,6 +255,11 @@ public class BrightnessTracker { /** Stop listening for events */ void stop() { + synchronized (mDataCollectionLock) { + if (!mStarted) { + return; + } + } if (DEBUG) { Slog.d(TAG, "Stop"); } diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 416518613568..687d03d4f774 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -27,6 +27,7 @@ import android.os.Environment; import android.os.PowerManager; import android.text.TextUtils; import android.util.MathUtils; +import android.util.Pair; import android.util.Slog; import android.util.Spline; import android.view.DisplayAddress; @@ -51,7 +52,7 @@ import com.android.server.display.config.SdrHdrRatioPoint; import com.android.server.display.config.SensorDetails; import com.android.server.display.config.ThermalStatus; import com.android.server.display.config.ThermalThrottling; -import com.android.server.display.config.Thresholds; +import com.android.server.display.config.ThresholdPoint; import com.android.server.display.config.XmlParser; import org.xmlpull.v1.XmlPullParserException; @@ -149,7 +150,7 @@ import javax.xml.datatype.DatatypeConfigurationException; * <quirk>canSetBrightnessViaHwc</quirk> * </quirks> * - * <autoBrightness> + * <autoBrightness enable="true"> * <brighteningLightDebounceMillis> * 2000 * </brighteningLightDebounceMillis> @@ -188,42 +189,153 @@ import javax.xml.datatype.DatatypeConfigurationException; * <ambientLightHorizonLong>10001</ambientLightHorizonLong> * <ambientLightHorizonShort>2001</ambientLightHorizonShort> * - * <displayBrightnessChangeThresholds> // Thresholds for screen changes - * <brighteningThresholds> // Thresholds for active mode brightness changes. - * <minimum>0.001</minimum> // Minimum change needed in screen brightness to brighten. - * </brighteningThresholds> - * <darkeningThresholds> - * <minimum>0.002</minimum> // Minimum change needed in screen brightness to darken. - * </darkeningThresholds> - * </displayBrightnessChangeThresholds> - * - * <ambientBrightnessChangeThresholds> // Thresholds for lux changes - * <brighteningThresholds> // Thresholds for active mode brightness changes. - * <minimum>0.003</minimum> // Minimum change needed in ambient brightness to brighten. - * </brighteningThresholds> - * <darkeningThresholds> - * <minimum>0.004</minimum> // Minimum change needed in ambient brightness to darken. - * </darkeningThresholds> - * </ambientBrightnessChangeThresholds> - * - * <displayBrightnessChangeThresholdsIdle> // Thresholds for screen changes in idle mode - * <brighteningThresholds> // Thresholds for idle mode brightness changes. - * <minimum>0.001</minimum> // Minimum change needed in screen brightness to brighten. - * </brighteningThresholds> - * <darkeningThresholds> - * <minimum>0.002</minimum> // Minimum change needed in screen brightness to darken. - * </darkeningThresholds> - * </displayBrightnessChangeThresholdsIdle> - * - * <ambientBrightnessChangeThresholdsIdle> // Thresholds for lux changes in idle mode - * <brighteningThresholds> // Thresholds for idle mode brightness changes. - * <minimum>0.003</minimum> // Minimum change needed in ambient brightness to brighten. - * </brighteningThresholds> - * <darkeningThresholds> - * <minimum>0.004</minimum> // Minimum change needed in ambient brightness to darken. - * </darkeningThresholds> - * </ambientBrightnessChangeThresholdsIdle> - * + * <ambientBrightnessChangeThresholds> // Thresholds for lux changes + * <brighteningThresholds> + * // Minimum change needed in ambient brightness to brighten screen. + * <minimum>10</minimum> + * // Percentage increase of lux needed to increase the screen brightness at a lux range + * // above the specified threshold. + * <brightnessThresholdPoints> + * <brightnessThresholdPoint> + * <threshold>0</threshold><percentage>13</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>100</threshold><percentage>14</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>200</threshold><percentage>15</percentage> + * </brightnessThresholdPoint> + * </brightnessThresholdPoints> + * </brighteningThresholds> + * <darkeningThresholds> +* // Minimum change needed in ambient brightness to darken screen. + * <minimum>30</minimum> + * // Percentage increase of lux needed to decrease the screen brightness at a lux range + * // above the specified threshold. + * <brightnessThresholdPoints> + * <brightnessThresholdPoint> + * <threshold>0</threshold><percentage>15</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>300</threshold><percentage>16</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>400</threshold><percentage>17</percentage> + * </brightnessThresholdPoint> + * </brightnessThresholdPoints> + * </darkeningThresholds> + * </ambientBrightnessChangeThresholds> + * <displayBrightnessChangeThresholds> // Thresholds for screen brightness changes + * <brighteningThresholds> + * // Minimum change needed in screen brightness to brighten screen. + * <minimum>0.1</minimum> + * // Percentage increase of screen brightness needed to increase the screen brightness + * // at a lux range above the specified threshold. + * <brightnessThresholdPoints> + * <brightnessThresholdPoint> + * <threshold>0</threshold> + * <percentage>9</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>0.10</threshold> + * <percentage>10</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>0.20</threshold> + * <percentage>11</percentage> + * </brightnessThresholdPoint> + * </brightnessThresholdPoints> + * </brighteningThresholds> + * <darkeningThresholds> + * // Minimum change needed in screen brightness to darken screen. + * <minimum>0.3</minimum> + * // Percentage increase of screen brightness needed to decrease the screen brightness + * // at a lux range above the specified threshold. + * <brightnessThresholdPoints> + * <brightnessThresholdPoint> + * <threshold>0</threshold><percentage>11</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>0.11</threshold><percentage>12</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>0.21</threshold><percentage>13</percentage> + * </brightnessThresholdPoint> + * </brightnessThresholdPoints> + * </darkeningThresholds> + * </displayBrightnessChangeThresholds> + * <ambientBrightnessChangeThresholdsIdle> // Thresholds for lux changes in idle mode + * <brighteningThresholds> + * // Minimum change needed in ambient brightness to brighten screen in idle mode + * <minimum>20</minimum> + * // Percentage increase of lux needed to increase the screen brightness at a lux range + * // above the specified threshold whilst in idle mode. + * <brightnessThresholdPoints> + * <brightnessThresholdPoint> + * <threshold>0</threshold><percentage>21</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>500</threshold><percentage>22</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>600</threshold><percentage>23</percentage> + * </brightnessThresholdPoint> + * </brightnessThresholdPoints> + * </brighteningThresholds> + * <darkeningThresholds> + * // Minimum change needed in ambient brightness to darken screen in idle mode + * <minimum>40</minimum> + * // Percentage increase of lux needed to decrease the screen brightness at a lux range + * // above the specified threshold whilst in idle mode. + * <brightnessThresholdPoints> + * <brightnessThresholdPoint> + * <threshold>0</threshold><percentage>23</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>700</threshold><percentage>24</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>800</threshold><percentage>25</percentage> + * </brightnessThresholdPoint> + * </brightnessThresholdPoints> + * </darkeningThresholds> + * </ambientBrightnessChangeThresholdsIdle> + * <displayBrightnessChangeThresholdsIdle> // Thresholds for idle screen brightness changes + * <brighteningThresholds> + * // Minimum change needed in screen brightness to brighten screen in idle mode + * <minimum>0.2</minimum> + * // Percentage increase of screen brightness needed to increase the screen brightness + * // at a lux range above the specified threshold whilst in idle mode + * <brightnessThresholdPoints> + * <brightnessThresholdPoint> + * <threshold>0</threshold><percentage>17</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>0.12</threshold><percentage>18</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>0.22</threshold><percentage>19</percentage> + * </brightnessThresholdPoint> + * </brightnessThresholdPoints> + * </brighteningThresholds> + * <darkeningThresholds> + * // Minimum change needed in screen brightness to darken screen in idle mode + * <minimum>0.4</minimum> + * // Percentage increase of screen brightness needed to decrease the screen brightness + * // at a lux range above the specified threshold whilst in idle mode + * <brightnessThresholdPoints> + * <brightnessThresholdPoint> + * <threshold>0</threshold><percentage>19</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>0.13</threshold><percentage>20</percentage> + * </brightnessThresholdPoint> + * <brightnessThresholdPoint> + * <threshold>0.23</threshold><percentage>21</percentage> + * </brightnessThresholdPoint> + * </brightnessThresholdPoints> + * </darkeningThresholds> + * </displayBrightnessChangeThresholdsIdle> * </displayConfiguration> * } * </pre> @@ -247,6 +359,13 @@ public class DisplayDeviceConfig { private static final String NO_SUFFIX_FORMAT = "%d"; private static final long STABLE_FLAG = 1L << 62; + private static final float[] DEFAULT_AMBIENT_THRESHOLD_LEVELS = new float[]{0f}; + private static final float[] DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS = new float[]{100f}; + private static final float[] DEFAULT_AMBIENT_DARKENING_THRESHOLDS = new float[]{200f}; + private static final float[] DEFAULT_SCREEN_THRESHOLD_LEVELS = new float[]{0f}; + private static final float[] DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS = new float[]{100f}; + private static final float[] DEFAULT_SCREEN_DARKENING_THRESHOLDS = new float[]{200f}; + private static final int INTERPOLATION_DEFAULT = 0; private static final int INTERPOLATION_LINEAR = 1; @@ -344,6 +463,31 @@ public class DisplayDeviceConfig { private float mAmbientLuxBrighteningMinThresholdIdle = 0.0f; private float mAmbientLuxDarkeningMinThreshold = 0.0f; private float mAmbientLuxDarkeningMinThresholdIdle = 0.0f; + + // Screen brightness thresholds levels & percentages + private float[] mScreenBrighteningLevels = DEFAULT_SCREEN_THRESHOLD_LEVELS; + private float[] mScreenBrighteningPercentages = DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS; + private float[] mScreenDarkeningLevels = DEFAULT_SCREEN_THRESHOLD_LEVELS; + private float[] mScreenDarkeningPercentages = DEFAULT_SCREEN_DARKENING_THRESHOLDS; + + // Screen brightness thresholds levels & percentages for idle mode + private float[] mScreenBrighteningLevelsIdle = DEFAULT_SCREEN_THRESHOLD_LEVELS; + private float[] mScreenBrighteningPercentagesIdle = DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS; + private float[] mScreenDarkeningLevelsIdle = DEFAULT_SCREEN_THRESHOLD_LEVELS; + private float[] mScreenDarkeningPercentagesIdle = DEFAULT_SCREEN_DARKENING_THRESHOLDS; + + // Ambient brightness thresholds levels & percentages + private float[] mAmbientBrighteningLevels = DEFAULT_AMBIENT_THRESHOLD_LEVELS; + private float[] mAmbientBrighteningPercentages = DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS; + private float[] mAmbientDarkeningLevels = DEFAULT_AMBIENT_THRESHOLD_LEVELS; + private float[] mAmbientDarkeningPercentages = DEFAULT_AMBIENT_DARKENING_THRESHOLDS; + + // Ambient brightness thresholds levels & percentages for idle mode + private float[] mAmbientBrighteningLevelsIdle = DEFAULT_AMBIENT_THRESHOLD_LEVELS; + private float[] mAmbientBrighteningPercentagesIdle = DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS; + private float[] mAmbientDarkeningLevelsIdle = DEFAULT_AMBIENT_THRESHOLD_LEVELS; + private float[] mAmbientDarkeningPercentagesIdle = DEFAULT_AMBIENT_DARKENING_THRESHOLDS; + private Spline mBrightnessToBacklightSpline; private Spline mBacklightToBrightnessSpline; private Spline mBacklightToNitsSpline; @@ -363,6 +507,11 @@ public class DisplayDeviceConfig { private long mAutoBrightnessDarkeningLightDebounce = INVALID_AUTO_BRIGHTNESS_LIGHT_DEBOUNCE; + // This setting allows non-default displays to have autobrightness enabled. + private boolean mAutoBrightnessAvailable = false; + // This stores the raw value loaded from the config file - true if not written. + private boolean mDdcAutoBrightnessAvailable = true; + // Brightness Throttling data may be updated via the DeviceConfig. Here we store the original // data, which comes from the ddc, and the current one, which may be the DeviceConfig // overwritten value. @@ -684,7 +833,7 @@ public class DisplayDeviceConfig { /** * The minimum value for the ambient lux increase for a screen brightness change to actually * occur. - * @return float value in brightness scale of 0 - 1. + * @return float value in lux. */ public float getAmbientLuxBrighteningMinThreshold() { return mAmbientLuxBrighteningMinThreshold; @@ -693,7 +842,7 @@ public class DisplayDeviceConfig { /** * The minimum value for the ambient lux decrease for a screen brightness change to actually * occur. - * @return float value in brightness scale of 0 - 1. + * @return float value in lux. */ public float getAmbientLuxDarkeningMinThreshold() { return mAmbientLuxDarkeningMinThreshold; @@ -702,7 +851,7 @@ public class DisplayDeviceConfig { /** * The minimum value for the ambient lux increase for a screen brightness change to actually * occur while in idle screen brightness mode. - * @return float value in brightness scale of 0 - 1. + * @return float value in lux. */ public float getAmbientLuxBrighteningMinThresholdIdle() { return mAmbientLuxBrighteningMinThresholdIdle; @@ -711,12 +860,262 @@ public class DisplayDeviceConfig { /** * The minimum value for the ambient lux decrease for a screen brightness change to actually * occur while in idle screen brightness mode. - * @return float value in brightness scale of 0 - 1. + * @return float value in lux. */ public float getAmbientLuxDarkeningMinThresholdIdle() { return mAmbientLuxDarkeningMinThresholdIdle; } + /** + * The array that describes the range of screen brightness that each threshold percentage + * applies within. + * + * The (zero-based) index is calculated as follows + * value = current screen brightness value + * level = mScreenBrighteningLevels + * + * condition return + * value < level[0] = 0.0f + * level[n] <= value < level[n+1] = mScreenBrighteningPercentages[n] + * level[MAX] <= value = mScreenBrighteningPercentages[MAX] + * + * @return the screen brightness levels between 0.0 and 1.0 for which each + * mScreenBrighteningPercentages applies + */ + public float[] getScreenBrighteningLevels() { + return mScreenBrighteningLevels; + } + + /** + * The array that describes the screen brightening threshold percentage change at each screen + * brightness level described in mScreenBrighteningLevels. + * + * @return the percentages between 0 and 100 of brightness increase required in order for the + * screen brightness to change + */ + public float[] getScreenBrighteningPercentages() { + return mScreenBrighteningPercentages; + } + + /** + * The array that describes the range of screen brightness that each threshold percentage + * applies within. + * + * The (zero-based) index is calculated as follows + * value = current screen brightness value + * level = mScreenDarkeningLevels + * + * condition return + * value < level[0] = 0.0f + * level[n] <= value < level[n+1] = mScreenDarkeningPercentages[n] + * level[MAX] <= value = mScreenDarkeningPercentages[MAX] + * + * @return the screen brightness levels between 0.0 and 1.0 for which each + * mScreenDarkeningPercentages applies + */ + public float[] getScreenDarkeningLevels() { + return mScreenDarkeningLevels; + } + + /** + * The array that describes the screen darkening threshold percentage change at each screen + * brightness level described in mScreenDarkeningLevels. + * + * @return the percentages between 0 and 100 of brightness decrease required in order for the + * screen brightness to change + */ + public float[] getScreenDarkeningPercentages() { + return mScreenDarkeningPercentages; + } + + /** + * The array that describes the range of ambient brightness that each threshold + * percentage applies within. + * + * The (zero-based) index is calculated as follows + * value = current ambient brightness value + * level = mAmbientBrighteningLevels + * + * condition return + * value < level[0] = 0.0f + * level[n] <= value < level[n+1] = mAmbientBrighteningPercentages[n] + * level[MAX] <= value = mAmbientBrighteningPercentages[MAX] + * + * @return the ambient brightness levels from 0 lux upwards for which each + * mAmbientBrighteningPercentages applies + */ + public float[] getAmbientBrighteningLevels() { + return mAmbientBrighteningLevels; + } + + /** + * The array that describes the ambient brightening threshold percentage change at each ambient + * brightness level described in mAmbientBrighteningLevels. + * + * @return the percentages between 0 and 100 of brightness increase required in order for the + * screen brightness to change + */ + public float[] getAmbientBrighteningPercentages() { + return mAmbientBrighteningPercentages; + } + + /** + * The array that describes the range of ambient brightness that each threshold percentage + * applies within. + * + * The (zero-based) index is calculated as follows + * value = current ambient brightness value + * level = mAmbientDarkeningLevels + * + * condition return + * value < level[0] = 0.0f + * level[n] <= value < level[n+1] = mAmbientDarkeningPercentages[n] + * level[MAX] <= value = mAmbientDarkeningPercentages[MAX] + * + * @return the ambient brightness levels from 0 lux upwards for which each + * mAmbientDarkeningPercentages applies + */ + public float[] getAmbientDarkeningLevels() { + return mAmbientDarkeningLevels; + } + + /** + * The array that describes the ambient darkening threshold percentage change at each ambient + * brightness level described in mAmbientDarkeningLevels. + * + * @return the percentages between 0 and 100 of brightness decrease required in order for the + * screen brightness to change + */ + public float[] getAmbientDarkeningPercentages() { + return mAmbientDarkeningPercentages; + } + + /** + * The array that describes the range of screen brightness that each threshold percentage + * applies within whilst in idle screen brightness mode. + * + * The (zero-based) index is calculated as follows + * value = current screen brightness value + * level = mScreenBrighteningLevelsIdle + * + * condition return + * value < level[0] = 0.0f + * level[n] <= value < level[n+1] = mScreenBrighteningPercentagesIdle[n] + * level[MAX] <= value = mScreenBrighteningPercentagesIdle[MAX] + * + * @return the screen brightness levels between 0.0 and 1.0 for which each + * mScreenBrighteningPercentagesIdle applies + */ + public float[] getScreenBrighteningLevelsIdle() { + return mScreenBrighteningLevelsIdle; + } + + /** + * The array that describes the screen brightening threshold percentage change at each screen + * brightness level described in mScreenBrighteningLevelsIdle. + * + * @return the percentages between 0 and 100 of brightness increase required in order for the + * screen brightness to change while in idle mode. + */ + public float[] getScreenBrighteningPercentagesIdle() { + return mScreenBrighteningPercentagesIdle; + } + + /** + * The array that describes the range of screen brightness that each threshold percentage + * applies within whilst in idle screen brightness mode. + * + * The (zero-based) index is calculated as follows + * value = current screen brightness value + * level = mScreenDarkeningLevelsIdle + * + * condition return + * value < level[0] = 0.0f + * level[n] <= value < level[n+1] = mScreenDarkeningPercentagesIdle[n] + * level[MAX] <= value = mScreenDarkeningPercentagesIdle[MAX] + * + * @return the screen brightness levels between 0.0 and 1.0 for which each + * mScreenDarkeningPercentagesIdle applies + */ + public float[] getScreenDarkeningLevelsIdle() { + return mScreenDarkeningLevelsIdle; + } + + /** + * The array that describes the screen darkening threshold percentage change at each screen + * brightness level described in mScreenDarkeningLevelsIdle. + * + * @return the percentages between 0 and 100 of brightness decrease required in order for the + * screen brightness to change while in idle mode. + */ + public float[] getScreenDarkeningPercentagesIdle() { + return mScreenDarkeningPercentagesIdle; + } + + /** + * The array that describes the range of ambient brightness that each threshold percentage + * applies within whilst in idle screen brightness mode. + * + * The (zero-based) index is calculated as follows + * value = current ambient brightness value + * level = mAmbientBrighteningLevelsIdle + * + * condition return + * value < level[0] = 0.0f + * level[n] <= value < level[n+1] = mAmbientBrighteningPercentagesIdle[n] + * level[MAX] <= value = mAmbientBrighteningPercentagesIdle[MAX] + * + * @return the ambient brightness levels from 0 lux upwards for which each + * mAmbientBrighteningPercentagesIdle applies + */ + public float[] getAmbientBrighteningLevelsIdle() { + return mAmbientBrighteningLevelsIdle; + } + + /** + * The array that describes the ambient brightness threshold percentage change whilst in + * idle screen brightness mode at each ambient brightness level described in + * mAmbientBrighteningLevelsIdle. + * + * @return the percentages between 0 and 100 of ambient brightness increase required in order + * for the screen brightness to change + */ + public float[] getAmbientBrighteningPercentagesIdle() { + return mAmbientBrighteningPercentagesIdle; + } + + /** + * The array that describes the range of ambient brightness that each threshold percentage + * applies within whilst in idle screen brightness mode. + * + * The (zero-based) index is calculated as follows + * value = current ambient brightness value + * level = mAmbientDarkeningLevelsIdle + * + * condition return + * value < level[0] = 0.0f + * level[n] <= value < level[n+1] = mAmbientDarkeningPercentagesIdle[n] + * level[MAX] <= value = mAmbientDarkeningPercentagesIdle[MAX] + * + * @return the ambient brightness levels from 0 lux upwards for which each + * mAmbientDarkeningPercentagesIdle applies + */ + public float[] getAmbientDarkeningLevelsIdle() { + return mAmbientDarkeningLevelsIdle; + } + + /** + * The array that describes the ambient brightness threshold percentage change whilst in + * idle screen brightness mode at each ambient brightness level described in + * mAmbientDarkeningLevelsIdle. + * + * @return the percentages between 0 and 100 of ambient brightness decrease required in order + * for the screen brightness to change + */ + public float[] getAmbientDarkeningPercentagesIdle() { + return mAmbientDarkeningPercentagesIdle; + } + SensorData getAmbientLightSensor() { return mAmbientLightSensor; } @@ -725,6 +1124,10 @@ public class DisplayDeviceConfig { return mProximitySensor; } + boolean isAutoBrightnessAvailable() { + return mAutoBrightnessAvailable; + } + /** * @param quirkValue The quirk to test. * @return {@code true} if the specified quirk is present in this configuration, {@code false} @@ -790,6 +1193,60 @@ public class DisplayDeviceConfig { return mBrightnessLevelsNits; } + /** + * @return Default peak refresh rate of the associated display + */ + public int getDefaultPeakRefreshRate() { + return mContext.getResources().getInteger(R.integer.config_defaultPeakRefreshRate); + } + + /** + * @return Default refresh rate of the associated display + */ + public int getDefaultRefreshRate() { + return mContext.getResources().getInteger(R.integer.config_defaultRefreshRate); + } + + /** + * @return An array of lower display brightness thresholds. This, in combination with lower + * ambient brightness thresholds help define buckets in which the refresh rate switching is not + * allowed + */ + public int[] getLowDisplayBrightnessThresholds() { + return mContext.getResources().getIntArray( + R.array.config_brightnessThresholdsOfPeakRefreshRate); + } + + /** + * @return An array of lower ambient brightness thresholds. This, in combination with lower + * display brightness thresholds help define buckets in which the refresh rate switching is not + * allowed + */ + public int[] getLowAmbientBrightnessThresholds() { + return mContext.getResources().getIntArray( + R.array.config_ambientThresholdsOfPeakRefreshRate); + } + + /** + * @return An array of high display brightness thresholds. This, in combination with high + * ambient brightness thresholds help define buckets in which the refresh rate switching is not + * allowed + */ + public int[] getHighDisplayBrightnessThresholds() { + return mContext.getResources().getIntArray( + R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate); + } + + /** + * @return An array of high ambient brightness thresholds. This, in combination with high + * display brightness thresholds help define buckets in which the refresh rate switching is not + * allowed + */ + public int[] getHighAmbientBrightnessThresholds() { + return mContext.getResources().getIntArray( + R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate); + } + @Override public String toString() { return "DisplayDeviceConfig{" @@ -812,14 +1269,17 @@ public class DisplayDeviceConfig { + ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline + ", mBrightnessThrottlingData=" + mBrightnessThrottlingData + ", mOriginalBrightnessThrottlingData=" + mOriginalBrightnessThrottlingData + + "\n" + ", mBrightnessRampFastDecrease=" + mBrightnessRampFastDecrease + ", mBrightnessRampFastIncrease=" + mBrightnessRampFastIncrease + ", mBrightnessRampSlowDecrease=" + mBrightnessRampSlowDecrease + ", mBrightnessRampSlowIncrease=" + mBrightnessRampSlowIncrease + ", mBrightnessRampDecreaseMaxMillis=" + mBrightnessRampDecreaseMaxMillis + ", mBrightnessRampIncreaseMaxMillis=" + mBrightnessRampIncreaseMaxMillis + + "\n" + ", mAmbientHorizonLong=" + mAmbientHorizonLong + ", mAmbientHorizonShort=" + mAmbientHorizonShort + + "\n" + ", mScreenDarkeningMinThreshold=" + mScreenDarkeningMinThreshold + ", mScreenDarkeningMinThresholdIdle=" + mScreenDarkeningMinThresholdIdle + ", mScreenBrighteningMinThreshold=" + mScreenBrighteningMinThreshold @@ -829,6 +1289,41 @@ public class DisplayDeviceConfig { + ", mAmbientLuxBrighteningMinThreshold=" + mAmbientLuxBrighteningMinThreshold + ", mAmbientLuxBrighteningMinThresholdIdle=" + mAmbientLuxBrighteningMinThresholdIdle + + "\n" + + ", mScreenBrighteningLevels=" + Arrays.toString( + mScreenBrighteningLevels) + + ", mScreenBrighteningPercentages=" + Arrays.toString( + mScreenBrighteningPercentages) + + ", mScreenDarkeningLevels=" + Arrays.toString( + mScreenDarkeningLevels) + + ", mScreenDarkeningPercentages=" + Arrays.toString( + mScreenDarkeningPercentages) + + ", mAmbientBrighteningLevels=" + Arrays.toString( + mAmbientBrighteningLevels) + + ", mAmbientBrighteningPercentages=" + Arrays.toString( + mAmbientBrighteningPercentages) + + ", mAmbientDarkeningLevels=" + Arrays.toString( + mAmbientDarkeningLevels) + + ", mAmbientDarkeningPercentages=" + Arrays.toString( + mAmbientDarkeningPercentages) + + "\n" + + ", mAmbientBrighteningLevelsIdle=" + Arrays.toString( + mAmbientBrighteningLevelsIdle) + + ", mAmbientBrighteningPercentagesIdle=" + Arrays.toString( + mAmbientBrighteningPercentagesIdle) + + ", mAmbientDarkeningLevelsIdle=" + Arrays.toString( + mAmbientDarkeningLevelsIdle) + + ", mAmbientDarkeningPercentagesIdle=" + Arrays.toString( + mAmbientDarkeningPercentagesIdle) + + ", mScreenBrighteningLevelsIdle=" + Arrays.toString( + mScreenBrighteningLevelsIdle) + + ", mScreenBrighteningPercentagesIdle=" + Arrays.toString( + mScreenBrighteningPercentagesIdle) + + ", mScreenDarkeningLevelsIdle=" + Arrays.toString( + mScreenDarkeningLevelsIdle) + + ", mScreenDarkeningPercentagesIdle=" + Arrays.toString( + mScreenDarkeningPercentagesIdle) + + "\n" + ", mAmbientLightSensor=" + mAmbientLightSensor + ", mProximitySensor=" + mProximitySensor + ", mRefreshRateLimitations= " + Arrays.toString(mRefreshRateLimitations.toArray()) @@ -839,6 +1334,8 @@ public class DisplayDeviceConfig { + mAutoBrightnessDarkeningLightDebounce + ", mBrightnessLevelsLux= " + Arrays.toString(mBrightnessLevelsLux) + ", mBrightnessLevelsNits= " + Arrays.toString(mBrightnessLevelsNits) + + ", mDdcAutoBrightnessAvailable= " + mDdcAutoBrightnessAvailable + + ", mAutoBrightnessAvailable= " + mAutoBrightnessAvailable + "}"; } @@ -914,8 +1411,10 @@ public class DisplayDeviceConfig { loadBrightnessMapFromConfigXml(); loadBrightnessRampsFromConfigXml(); loadAmbientLightSensorFromConfigXml(); + loadBrightnessChangeThresholdsFromXml(); setProxSensorUnspecified(); loadAutoBrightnessConfigsFromConfigXml(); + loadAutoBrightnessAvailableFromConfigXml(); mLoadedFrom = "<config.xml>"; } @@ -934,6 +1433,7 @@ public class DisplayDeviceConfig { setSimpleMappingStrategyValues(); loadAmbientLightSensorFromConfigXml(); setProxSensorUnspecified(); + loadAutoBrightnessAvailableFromConfigXml(); } private void copyUninitializedValuesFromSecondaryConfig(DisplayConfiguration defaultConfig) { @@ -1126,9 +1626,11 @@ public class DisplayDeviceConfig { } private void loadAutoBrightnessConfigValues(DisplayConfiguration config) { - loadAutoBrightnessBrighteningLightDebounce(config.getAutoBrightness()); - loadAutoBrightnessDarkeningLightDebounce(config.getAutoBrightness()); - loadAutoBrightnessDisplayBrightnessMapping(config.getAutoBrightness()); + final AutoBrightness autoBrightness = config.getAutoBrightness(); + loadAutoBrightnessBrighteningLightDebounce(autoBrightness); + loadAutoBrightnessDarkeningLightDebounce(autoBrightness); + loadAutoBrightnessDisplayBrightnessMapping(autoBrightness); + loadEnableAutoBrightness(autoBrightness); } /** @@ -1190,6 +1692,11 @@ public class DisplayDeviceConfig { } } + private void loadAutoBrightnessAvailableFromConfigXml() { + mAutoBrightnessAvailable = mContext.getResources().getBoolean( + R.bool.config_automatic_brightness_available); + } + private void loadBrightnessMapFromConfigXml() { // Use the config.xml mapping final Resources res = mContext.getResources(); @@ -1454,91 +1961,287 @@ public class DisplayDeviceConfig { } } + private void loadBrightnessChangeThresholdsFromXml() { + loadBrightnessChangeThresholds(/* config= */ null); + } + private void loadBrightnessChangeThresholds(DisplayConfiguration config) { - Thresholds displayBrightnessThresholds = config.getDisplayBrightnessChangeThresholds(); - Thresholds ambientBrightnessThresholds = config.getAmbientBrightnessChangeThresholds(); - Thresholds displayBrightnessThresholdsIdle = - config.getDisplayBrightnessChangeThresholdsIdle(); - Thresholds ambientBrightnessThresholdsIdle = - config.getAmbientBrightnessChangeThresholdsIdle(); - - loadDisplayBrightnessThresholds(displayBrightnessThresholds); - loadAmbientBrightnessThresholds(ambientBrightnessThresholds); - loadIdleDisplayBrightnessThresholds(displayBrightnessThresholdsIdle); - loadIdleAmbientBrightnessThresholds(ambientBrightnessThresholdsIdle); - } - - private void loadDisplayBrightnessThresholds(Thresholds displayBrightnessThresholds) { - if (displayBrightnessThresholds != null) { - BrightnessThresholds brighteningScreen = - displayBrightnessThresholds.getBrighteningThresholds(); - BrightnessThresholds darkeningScreen = - displayBrightnessThresholds.getDarkeningThresholds(); - - if (brighteningScreen != null && brighteningScreen.getMinimum() != null) { - mScreenBrighteningMinThreshold = brighteningScreen.getMinimum().floatValue(); + loadDisplayBrightnessThresholds(config); + loadAmbientBrightnessThresholds(config); + loadDisplayBrightnessThresholdsIdle(config); + loadAmbientBrightnessThresholdsIdle(config); + } + + private void loadDisplayBrightnessThresholds(DisplayConfiguration config) { + BrightnessThresholds brighteningScreen = null; + BrightnessThresholds darkeningScreen = null; + if (config != null && config.getDisplayBrightnessChangeThresholds() != null) { + brighteningScreen = + config.getDisplayBrightnessChangeThresholds().getBrighteningThresholds(); + darkeningScreen = + config.getDisplayBrightnessChangeThresholds().getDarkeningThresholds(); + + } + + // Screen bright/darkening threshold levels for active mode + Pair<float[], float[]> screenBrighteningPair = getBrightnessLevelAndPercentage( + brighteningScreen, + com.android.internal.R.array.config_screenThresholdLevels, + com.android.internal.R.array.config_screenBrighteningThresholds, + DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS, + /* potentialOldBrightnessScale= */ true); + + mScreenBrighteningLevels = screenBrighteningPair.first; + mScreenBrighteningPercentages = screenBrighteningPair.second; + + Pair<float[], float[]> screenDarkeningPair = getBrightnessLevelAndPercentage( + darkeningScreen, + com.android.internal.R.array.config_screenThresholdLevels, + com.android.internal.R.array.config_screenDarkeningThresholds, + DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_DARKENING_THRESHOLDS, + /* potentialOldBrightnessScale= */ true); + mScreenDarkeningLevels = screenDarkeningPair.first; + mScreenDarkeningPercentages = screenDarkeningPair.second; + + // Screen bright/darkening threshold minimums for active mode + if (brighteningScreen != null && brighteningScreen.getMinimum() != null) { + mScreenBrighteningMinThreshold = brighteningScreen.getMinimum().floatValue(); + } + if (darkeningScreen != null && darkeningScreen.getMinimum() != null) { + mScreenDarkeningMinThreshold = darkeningScreen.getMinimum().floatValue(); + } + } + + private void loadAmbientBrightnessThresholds(DisplayConfiguration config) { + // Ambient Brightness Threshold Levels + BrightnessThresholds brighteningAmbientLux = null; + BrightnessThresholds darkeningAmbientLux = null; + if (config != null && config.getAmbientBrightnessChangeThresholds() != null) { + brighteningAmbientLux = + config.getAmbientBrightnessChangeThresholds().getBrighteningThresholds(); + darkeningAmbientLux = + config.getAmbientBrightnessChangeThresholds().getDarkeningThresholds(); + } + + // Ambient bright/darkening threshold levels for active mode + Pair<float[], float[]> ambientBrighteningPair = getBrightnessLevelAndPercentage( + brighteningAmbientLux, + com.android.internal.R.array.config_ambientThresholdLevels, + com.android.internal.R.array.config_ambientBrighteningThresholds, + DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS); + mAmbientBrighteningLevels = ambientBrighteningPair.first; + mAmbientBrighteningPercentages = ambientBrighteningPair.second; + + Pair<float[], float[]> ambientDarkeningPair = getBrightnessLevelAndPercentage( + darkeningAmbientLux, + com.android.internal.R.array.config_ambientThresholdLevels, + com.android.internal.R.array.config_ambientDarkeningThresholds, + DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_DARKENING_THRESHOLDS); + mAmbientDarkeningLevels = ambientDarkeningPair.first; + mAmbientDarkeningPercentages = ambientDarkeningPair.second; + + // Ambient bright/darkening threshold minimums for active/idle mode + if (brighteningAmbientLux != null && brighteningAmbientLux.getMinimum() != null) { + mAmbientLuxBrighteningMinThreshold = + brighteningAmbientLux.getMinimum().floatValue(); + } + + if (darkeningAmbientLux != null && darkeningAmbientLux.getMinimum() != null) { + mAmbientLuxDarkeningMinThreshold = darkeningAmbientLux.getMinimum().floatValue(); + } + } + + private void loadDisplayBrightnessThresholdsIdle(DisplayConfiguration config) { + BrightnessThresholds brighteningScreenIdle = null; + BrightnessThresholds darkeningScreenIdle = null; + if (config != null && config.getDisplayBrightnessChangeThresholdsIdle() != null) { + brighteningScreenIdle = + config.getDisplayBrightnessChangeThresholdsIdle().getBrighteningThresholds(); + darkeningScreenIdle = + config.getDisplayBrightnessChangeThresholdsIdle().getDarkeningThresholds(); + } + + Pair<float[], float[]> screenBrighteningPair = getBrightnessLevelAndPercentage( + brighteningScreenIdle, + com.android.internal.R.array.config_screenThresholdLevels, + com.android.internal.R.array.config_screenBrighteningThresholds, + DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS, + /* potentialOldBrightnessScale= */ true); + mScreenBrighteningLevelsIdle = screenBrighteningPair.first; + mScreenBrighteningPercentagesIdle = screenBrighteningPair.second; + + Pair<float[], float[]> screenDarkeningPair = getBrightnessLevelAndPercentage( + darkeningScreenIdle, + com.android.internal.R.array.config_screenThresholdLevels, + com.android.internal.R.array.config_screenDarkeningThresholds, + DEFAULT_SCREEN_THRESHOLD_LEVELS, DEFAULT_SCREEN_DARKENING_THRESHOLDS, + /* potentialOldBrightnessScale= */ true); + mScreenDarkeningLevelsIdle = screenDarkeningPair.first; + mScreenDarkeningPercentagesIdle = screenDarkeningPair.second; + + if (brighteningScreenIdle != null + && brighteningScreenIdle.getMinimum() != null) { + mScreenBrighteningMinThresholdIdle = + brighteningScreenIdle.getMinimum().floatValue(); + } + if (darkeningScreenIdle != null && darkeningScreenIdle.getMinimum() != null) { + mScreenDarkeningMinThresholdIdle = + darkeningScreenIdle.getMinimum().floatValue(); + } + } + + private void loadAmbientBrightnessThresholdsIdle(DisplayConfiguration config) { + BrightnessThresholds brighteningAmbientLuxIdle = null; + BrightnessThresholds darkeningAmbientLuxIdle = null; + if (config != null && config.getAmbientBrightnessChangeThresholdsIdle() != null) { + brighteningAmbientLuxIdle = + config.getAmbientBrightnessChangeThresholdsIdle().getBrighteningThresholds(); + darkeningAmbientLuxIdle = + config.getAmbientBrightnessChangeThresholdsIdle().getDarkeningThresholds(); + } + + Pair<float[], float[]> ambientBrighteningPair = getBrightnessLevelAndPercentage( + brighteningAmbientLuxIdle, + com.android.internal.R.array.config_ambientThresholdLevels, + com.android.internal.R.array.config_ambientBrighteningThresholds, + DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS); + mAmbientBrighteningLevelsIdle = ambientBrighteningPair.first; + mAmbientBrighteningPercentagesIdle = ambientBrighteningPair.second; + + Pair<float[], float[]> ambientDarkeningPair = getBrightnessLevelAndPercentage( + darkeningAmbientLuxIdle, + com.android.internal.R.array.config_ambientThresholdLevels, + com.android.internal.R.array.config_ambientDarkeningThresholds, + DEFAULT_AMBIENT_THRESHOLD_LEVELS, DEFAULT_AMBIENT_DARKENING_THRESHOLDS); + mAmbientDarkeningLevelsIdle = ambientDarkeningPair.first; + mAmbientDarkeningPercentagesIdle = ambientDarkeningPair.second; + + if (brighteningAmbientLuxIdle != null + && brighteningAmbientLuxIdle.getMinimum() != null) { + mAmbientLuxBrighteningMinThresholdIdle = + brighteningAmbientLuxIdle.getMinimum().floatValue(); + } + + if (darkeningAmbientLuxIdle != null && darkeningAmbientLuxIdle.getMinimum() != null) { + mAmbientLuxDarkeningMinThresholdIdle = + darkeningAmbientLuxIdle.getMinimum().floatValue(); + } + } + + private Pair<float[], float[]> getBrightnessLevelAndPercentage(BrightnessThresholds thresholds, + int configFallbackThreshold, int configFallbackPercentage, float[] defaultLevels, + float[] defaultPercentage) { + return getBrightnessLevelAndPercentage(thresholds, configFallbackThreshold, + configFallbackPercentage, defaultLevels, defaultPercentage, false); + } + + // Returns two float arrays, one of the brightness levels and one of the corresponding threshold + // percentages for brightness levels at or above the lux value. + // Historically, config.xml would have an array for brightness levels that was 1 shorter than + // the levels array. Now we prepend a 0 to this array so they can be treated the same in the + // rest of the framework. Values were also defined in different units (permille vs percent). + private Pair<float[], float[]> getBrightnessLevelAndPercentage(BrightnessThresholds thresholds, + int configFallbackThreshold, int configFallbackPermille, + float[] defaultLevels, float[] defaultPercentage, + boolean potentialOldBrightnessScale) { + if (thresholds != null + && thresholds.getBrightnessThresholdPoints() != null + && thresholds.getBrightnessThresholdPoints() + .getBrightnessThresholdPoint().size() != 0) { + + // The level and percentages arrays are equal length in the ddc (new system) + List<ThresholdPoint> points = + thresholds.getBrightnessThresholdPoints().getBrightnessThresholdPoint(); + final int size = points.size(); + + float[] thresholdLevels = new float[size]; + float[] thresholdPercentages = new float[size]; + + int i = 0; + for (ThresholdPoint point : points) { + thresholdLevels[i] = point.getThreshold().floatValue(); + thresholdPercentages[i] = point.getPercentage().floatValue(); + i++; } - if (darkeningScreen != null && darkeningScreen.getMinimum() != null) { - mScreenDarkeningMinThreshold = darkeningScreen.getMinimum().floatValue(); + return new Pair<>(thresholdLevels, thresholdPercentages); + } else { + // The level and percentages arrays are unequal length in config.xml (old system) + // We prefix the array with a 0 value to ensure they can be handled consistently + // with the new system. + + // Load levels array + int[] configThresholdArray = mContext.getResources().getIntArray( + configFallbackThreshold); + int configThresholdsSize; + if (configThresholdArray == null || configThresholdArray.length == 0) { + configThresholdsSize = 1; + } else { + configThresholdsSize = configThresholdArray.length + 1; } - } - } - private void loadAmbientBrightnessThresholds(Thresholds ambientBrightnessThresholds) { - if (ambientBrightnessThresholds != null) { - BrightnessThresholds brighteningAmbientLux = - ambientBrightnessThresholds.getBrighteningThresholds(); - BrightnessThresholds darkeningAmbientLux = - ambientBrightnessThresholds.getDarkeningThresholds(); - final BigDecimal ambientBrighteningThreshold = brighteningAmbientLux.getMinimum(); - final BigDecimal ambientDarkeningThreshold = darkeningAmbientLux.getMinimum(); + // Load percentage array + int[] configPermille = mContext.getResources().getIntArray( + configFallbackPermille); - if (ambientBrighteningThreshold != null) { - mAmbientLuxBrighteningMinThreshold = ambientBrighteningThreshold.floatValue(); + // Ensure lengths match up + boolean emptyArray = configPermille == null || configPermille.length == 0; + if (emptyArray && configThresholdsSize == 1) { + return new Pair<>(defaultLevels, defaultPercentage); } - if (ambientDarkeningThreshold != null) { - mAmbientLuxDarkeningMinThreshold = ambientDarkeningThreshold.floatValue(); + if (emptyArray || configPermille.length != configThresholdsSize) { + throw new IllegalArgumentException( + "Brightness threshold arrays do not align in length"); } - } - } - - private void loadIdleDisplayBrightnessThresholds(Thresholds idleDisplayBrightnessThresholds) { - if (idleDisplayBrightnessThresholds != null) { - BrightnessThresholds brighteningScreenIdle = - idleDisplayBrightnessThresholds.getBrighteningThresholds(); - BrightnessThresholds darkeningScreenIdle = - idleDisplayBrightnessThresholds.getDarkeningThresholds(); - if (brighteningScreenIdle != null - && brighteningScreenIdle.getMinimum() != null) { - mScreenBrighteningMinThresholdIdle = - brighteningScreenIdle.getMinimum().floatValue(); + // Calculate levels array + float[] configThresholdWithZeroPrefixed = new float[configThresholdsSize]; + // Start at 1, so that 0 index value is 0.0f (default) + for (int i = 1; i < configThresholdsSize; i++) { + configThresholdWithZeroPrefixed[i] = (float) configThresholdArray[i - 1]; } - if (darkeningScreenIdle != null && darkeningScreenIdle.getMinimum() != null) { - mScreenDarkeningMinThresholdIdle = - darkeningScreenIdle.getMinimum().floatValue(); + if (potentialOldBrightnessScale) { + configThresholdWithZeroPrefixed = + constraintInRangeIfNeeded(configThresholdWithZeroPrefixed); } + + // Calculate percentages array + float[] configPercentage = new float[configThresholdsSize]; + for (int i = 0; i < configPermille.length; i++) { + configPercentage[i] = configPermille[i] / 10.0f; + } + return new Pair<>(configThresholdWithZeroPrefixed, configPercentage); } } - private void loadIdleAmbientBrightnessThresholds(Thresholds idleAmbientBrightnessThresholds) { - if (idleAmbientBrightnessThresholds != null) { - BrightnessThresholds brighteningAmbientLuxIdle = - idleAmbientBrightnessThresholds.getBrighteningThresholds(); - BrightnessThresholds darkeningAmbientLuxIdle = - idleAmbientBrightnessThresholds.getDarkeningThresholds(); + /** + * This check is due to historical reasons, where screen thresholdLevels used to be + * integer values in the range of [0-255], but then was changed to be float values from [0,1]. + * To accommodate both the possibilities, we first check if all the thresholdLevels are in + * [0,1], and if not, we divide all the levels with 255 to bring them down to the same scale. + */ + private float[] constraintInRangeIfNeeded(float[] thresholdLevels) { + if (isAllInRange(thresholdLevels, /* minValueInclusive= */ 0.0f, + /* maxValueInclusive= */ 1.0f)) { + return thresholdLevels; + } + + Slog.w(TAG, "Detected screen thresholdLevels on a deprecated brightness scale"); + float[] thresholdLevelsScaled = new float[thresholdLevels.length]; + for (int index = 0; thresholdLevels.length > index; ++index) { + thresholdLevelsScaled[index] = thresholdLevels[index] / 255.0f; + } + return thresholdLevelsScaled; + } - if (brighteningAmbientLuxIdle != null - && brighteningAmbientLuxIdle.getMinimum() != null) { - mAmbientLuxBrighteningMinThresholdIdle = - brighteningAmbientLuxIdle.getMinimum().floatValue(); - } - if (darkeningAmbientLuxIdle != null && darkeningAmbientLuxIdle.getMinimum() != null) { - mAmbientLuxDarkeningMinThresholdIdle = - darkeningAmbientLuxIdle.getMinimum().floatValue(); + private boolean isAllInRange(float[] configArray, float minValueInclusive, + float maxValueInclusive) { + for (float v : configArray) { + if (v < minValueInclusive || v > maxValueInclusive) { + return false; } } + return true; } private boolean thermalStatusIsValid(ThermalStatus value) { @@ -1634,6 +2337,20 @@ public class DisplayDeviceConfig { return levels; } + private void loadEnableAutoBrightness(AutoBrightness autobrightness) { + // mDdcAutoBrightnessAvailable is initialised to true, so that we fallback to using the + // config.xml values if the autobrightness tag is not defined in the ddc file. + // Autobrightness can still be turned off globally via config_automatic_brightness_available + mDdcAutoBrightnessAvailable = true; + if (autobrightness != null) { + mDdcAutoBrightnessAvailable = autobrightness.getEnabled(); + } + + mAutoBrightnessAvailable = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_automatic_brightness_available) + && mDdcAutoBrightnessAvailable; + } + static class SensorData { public String type; public String name; diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index b153c1ba98e0..b8ff6ed93666 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1519,6 +1519,7 @@ public final class DisplayManagerService extends SystemService { display.setUserDisabledHdrTypes(mUserDisabledHdrTypes); } if (isDefault) { + notifyDefaultDisplayDeviceUpdated(display); recordStableDisplayStatsIfNeededLocked(display); recordTopInsetLocked(display); } @@ -1612,6 +1613,11 @@ public final class DisplayManagerService extends SystemService { mHandler.post(work); } final int displayId = display.getDisplayIdLocked(); + + if (displayId == Display.DEFAULT_DISPLAY) { + notifyDefaultDisplayDeviceUpdated(display); + } + DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); if (dpc != null) { dpc.onDisplayChanged(); @@ -1621,6 +1627,11 @@ public final class DisplayManagerService extends SystemService { handleLogicalDisplayChangedLocked(display); } + private void notifyDefaultDisplayDeviceUpdated(LogicalDisplay display) { + mDisplayModeDirector.defaultDisplayDeviceUpdated(display.getPrimaryDisplayDeviceLocked() + .mDisplayDeviceConfig); + } + private void handleLogicalDisplayDeviceStateTransitionLocked(@NonNull LogicalDisplay display) { final int displayId = display.getDisplayIdLocked(); final DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); @@ -1759,9 +1770,13 @@ public final class DisplayManagerService extends SystemService { if (displayDevice == null) { return; } - mPersistentDataStore.setUserPreferredResolution( - displayDevice, resolutionWidth, resolutionHeight); - mPersistentDataStore.setUserPreferredRefreshRate(displayDevice, refreshRate); + try { + mPersistentDataStore.setUserPreferredResolution( + displayDevice, resolutionWidth, resolutionHeight); + mPersistentDataStore.setUserPreferredRefreshRate(displayDevice, refreshRate); + } finally { + mPersistentDataStore.saveIfNeeded(); + } } private void setUserPreferredModeForDisplayLocked(int displayId, Display.Mode mode) { diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java index ac72b1725432..912b1b2e8da2 100644 --- a/services/core/java/com/android/server/display/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/DisplayModeDirector.java @@ -79,6 +79,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.concurrent.Callable; /** * The DisplayModeDirector is responsible for determining what modes are allowed to be automatically @@ -153,8 +154,10 @@ public class DisplayModeDirector { mSupportedModesByDisplay = new SparseArray<>(); mDefaultModeByDisplay = new SparseArray<>(); mAppRequestObserver = new AppRequestObserver(); - mSettingsObserver = new SettingsObserver(context, handler); mDisplayObserver = new DisplayObserver(context, handler); + mDeviceConfig = injector.getDeviceConfig(); + mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings(); + mSettingsObserver = new SettingsObserver(context, handler); mBrightnessObserver = new BrightnessObserver(context, handler, injector); mUdfpsObserver = new UdfpsObserver(); final BallotBox ballotBox = (displayId, priority, vote) -> { @@ -164,10 +167,8 @@ public class DisplayModeDirector { }; mSensorObserver = new SensorObserver(context, ballotBox, injector); mSkinThermalStatusObserver = new SkinThermalStatusObserver(injector, ballotBox); - mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings(); mHbmObserver = new HbmObserver(injector, ballotBox, BackgroundThread.getHandler(), mDeviceConfigDisplaySettings); - mDeviceConfig = injector.getDeviceConfig(); mAlwaysRespectAppRequest = false; } @@ -518,6 +519,15 @@ public class DisplayModeDirector { } /** + * A utility to make this class aware of the new display configs whenever the default display is + * changed + */ + public void defaultDisplayDeviceUpdated(DisplayDeviceConfig displayDeviceConfig) { + mSettingsObserver.setRefreshRates(displayDeviceConfig); + mBrightnessObserver.updateBlockingZoneThresholds(displayDeviceConfig); + } + + /** * When enabled the app requested display mode is always selected and all * other votes will be ignored. This is used for testing purposes. */ @@ -1132,10 +1142,19 @@ public class DisplayModeDirector { SettingsObserver(@NonNull Context context, @NonNull Handler handler) { super(handler); mContext = context; - mDefaultPeakRefreshRate = (float) context.getResources().getInteger( - R.integer.config_defaultPeakRefreshRate); + setRefreshRates(/* displayDeviceConfig= */ null); + } + + /** + * This is used to update the refresh rate configs from the DeviceConfig, which + * if missing from DisplayDeviceConfig, and finally fallback to config.xml. + */ + public void setRefreshRates(DisplayDeviceConfig displayDeviceConfig) { + setDefaultPeakRefreshRate(displayDeviceConfig); mDefaultRefreshRate = - (float) context.getResources().getInteger(R.integer.config_defaultRefreshRate); + (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger( + R.integer.config_defaultRefreshRate) + : (float) displayDeviceConfig.getDefaultRefreshRate(); } public void observe() { @@ -1196,6 +1215,23 @@ public class DisplayModeDirector { } } + private void setDefaultPeakRefreshRate(DisplayDeviceConfig displayDeviceConfig) { + Float defaultPeakRefreshRate = null; + try { + defaultPeakRefreshRate = + mDeviceConfigDisplaySettings.getDefaultPeakRefreshRate(); + } catch (Exception exception) { + // Do nothing + } + if (defaultPeakRefreshRate == null) { + defaultPeakRefreshRate = + (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger( + R.integer.config_defaultPeakRefreshRate) + : (float) displayDeviceConfig.getDefaultPeakRefreshRate(); + } + mDefaultPeakRefreshRate = defaultPeakRefreshRate; + } + private void updateLowPowerModeSettingLocked() { boolean inLowPowerMode = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.LOW_POWER_MODE, 0 /*default*/) != 0; @@ -1508,12 +1544,31 @@ public class DisplayModeDirector { mContext = context; mHandler = handler; mInjector = injector; + updateBlockingZoneThresholds(/* displayDeviceConfig= */ null); + mRefreshRateInHighZone = context.getResources().getInteger( + R.integer.config_fixedRefreshRateInHighZone); + } - mLowDisplayBrightnessThresholds = context.getResources().getIntArray( - R.array.config_brightnessThresholdsOfPeakRefreshRate); - mLowAmbientBrightnessThresholds = context.getResources().getIntArray( - R.array.config_ambientThresholdsOfPeakRefreshRate); - + /** + * This is used to update the blocking zone thresholds from the DeviceConfig, which + * if missing from DisplayDeviceConfig, and finally fallback to config.xml. + */ + public void updateBlockingZoneThresholds(DisplayDeviceConfig displayDeviceConfig) { + loadLowBrightnessThresholds(displayDeviceConfig); + loadHighBrightnessThresholds(displayDeviceConfig); + } + + private void loadLowBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig) { + mLowDisplayBrightnessThresholds = loadBrightnessThresholds( + () -> mDeviceConfigDisplaySettings.getLowDisplayBrightnessThresholds(), + () -> displayDeviceConfig.getLowDisplayBrightnessThresholds(), + R.array.config_brightnessThresholdsOfPeakRefreshRate, + displayDeviceConfig); + mLowAmbientBrightnessThresholds = loadBrightnessThresholds( + () -> mDeviceConfigDisplaySettings.getLowAmbientBrightnessThresholds(), + () -> displayDeviceConfig.getLowAmbientBrightnessThresholds(), + R.array.config_ambientThresholdsOfPeakRefreshRate, + displayDeviceConfig); if (mLowDisplayBrightnessThresholds.length != mLowAmbientBrightnessThresholds.length) { throw new RuntimeException("display low brightness threshold array and ambient " + "brightness threshold array have different length: " @@ -1522,11 +1577,19 @@ public class DisplayModeDirector { + ", ambientBrightnessThresholds=" + Arrays.toString(mLowAmbientBrightnessThresholds)); } + } - mHighDisplayBrightnessThresholds = context.getResources().getIntArray( - R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate); - mHighAmbientBrightnessThresholds = context.getResources().getIntArray( - R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate); + private void loadHighBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig) { + mHighDisplayBrightnessThresholds = loadBrightnessThresholds( + () -> mDeviceConfigDisplaySettings.getHighDisplayBrightnessThresholds(), + () -> displayDeviceConfig.getHighDisplayBrightnessThresholds(), + R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate, + displayDeviceConfig); + mHighAmbientBrightnessThresholds = loadBrightnessThresholds( + () -> mDeviceConfigDisplaySettings.getHighAmbientBrightnessThresholds(), + () -> displayDeviceConfig.getHighAmbientBrightnessThresholds(), + R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate, + displayDeviceConfig); if (mHighDisplayBrightnessThresholds.length != mHighAmbientBrightnessThresholds.length) { throw new RuntimeException("display high brightness threshold array and ambient " @@ -1536,8 +1599,32 @@ public class DisplayModeDirector { + ", ambientBrightnessThresholds=" + Arrays.toString(mHighAmbientBrightnessThresholds)); } - mRefreshRateInHighZone = context.getResources().getInteger( - R.integer.config_fixedRefreshRateInHighZone); + } + + private int[] loadBrightnessThresholds( + Callable<int[]> loadFromDeviceConfigDisplaySettingsCallable, + Callable<int[]> loadFromDisplayDeviceConfigCallable, + int brightnessThresholdOfFixedRefreshRateKey, + DisplayDeviceConfig displayDeviceConfig) { + int[] brightnessThresholds = null; + try { + brightnessThresholds = + loadFromDeviceConfigDisplaySettingsCallable.call(); + } catch (Exception exception) { + // Do nothing + } + if (brightnessThresholds == null) { + try { + brightnessThresholds = + (displayDeviceConfig == null) ? mContext.getResources().getIntArray( + brightnessThresholdOfFixedRefreshRateKey) + : loadFromDisplayDeviceConfigCallable.call(); + } catch (Exception e) { + Slog.e(TAG, "Unexpectedly failed to load display brightness threshold"); + e.printStackTrace(); + } + } + return brightnessThresholds; } /** @@ -1590,7 +1677,6 @@ public class DisplayModeDirector { mLowAmbientBrightnessThresholds = lowAmbientBrightnessThresholds; } - int[] highDisplayBrightnessThresholds = mDeviceConfigDisplaySettings.getHighDisplayBrightnessThresholds(); int[] highAmbientBrightnessThresholds = diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 9734601d65ae..9485bcce627b 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -558,13 +558,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mScreenBrightnessForVrRangeMinimum = clampAbsoluteBrightness( pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM_VR)); - // Check the setting, but also verify that it is the default display. Only the default - // display has an automatic brightness controller running. - // TODO: b/179021925 - Fix to work with multiple displays - mUseSoftwareAutoBrightnessConfig = resources.getBoolean( - com.android.internal.R.bool.config_automatic_brightness_available) - && mDisplayId == Display.DEFAULT_DISPLAY; - mAllowAutoBrightnessWhileDozingConfig = resources.getBoolean( com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing); @@ -938,6 +931,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } private void setUpAutoBrightness(Resources resources, Handler handler) { + mUseSoftwareAutoBrightnessConfig = mDisplayDeviceConfig.isAutoBrightnessAvailable(); + if (!mUseSoftwareAutoBrightnessConfig) { return; } @@ -956,54 +951,78 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call com.android.internal.R.fraction.config_screenAutoBrightnessDozeScaleFactor, 1, 1); - int[] ambientBrighteningThresholds = resources.getIntArray( - com.android.internal.R.array.config_ambientBrighteningThresholds); - int[] ambientDarkeningThresholds = resources.getIntArray( - com.android.internal.R.array.config_ambientDarkeningThresholds); - int[] ambientThresholdLevels = resources.getIntArray( - com.android.internal.R.array.config_ambientThresholdLevels); + // Ambient Lux - Active Mode Brightness Thresholds + float[] ambientBrighteningThresholds = + mDisplayDeviceConfig.getAmbientBrighteningPercentages(); + float[] ambientDarkeningThresholds = + mDisplayDeviceConfig.getAmbientDarkeningPercentages(); + float[] ambientBrighteningLevels = + mDisplayDeviceConfig.getAmbientBrighteningLevels(); + float[] ambientDarkeningLevels = + mDisplayDeviceConfig.getAmbientDarkeningLevels(); float ambientDarkeningMinThreshold = mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(); float ambientBrighteningMinThreshold = mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(); HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels( ambientBrighteningThresholds, ambientDarkeningThresholds, - ambientThresholdLevels, ambientDarkeningMinThreshold, + ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold, ambientBrighteningMinThreshold); - int[] screenBrighteningThresholds = resources.getIntArray( - com.android.internal.R.array.config_screenBrighteningThresholds); - int[] screenDarkeningThresholds = resources.getIntArray( - com.android.internal.R.array.config_screenDarkeningThresholds); - float[] screenThresholdLevels = BrightnessMappingStrategy.getFloatArray(resources - .obtainTypedArray(com.android.internal.R.array.config_screenThresholdLevels)); + // Display - Active Mode Brightness Thresholds + float[] screenBrighteningThresholds = + mDisplayDeviceConfig.getScreenBrighteningPercentages(); + float[] screenDarkeningThresholds = + mDisplayDeviceConfig.getScreenDarkeningPercentages(); + float[] screenBrighteningLevels = + mDisplayDeviceConfig.getScreenBrighteningLevels(); + float[] screenDarkeningLevels = + mDisplayDeviceConfig.getScreenDarkeningLevels(); float screenDarkeningMinThreshold = mDisplayDeviceConfig.getScreenDarkeningMinThreshold(); float screenBrighteningMinThreshold = mDisplayDeviceConfig.getScreenBrighteningMinThreshold(); HysteresisLevels screenBrightnessThresholds = new HysteresisLevels( - screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels, - screenDarkeningMinThreshold, screenBrighteningMinThreshold); + screenBrighteningThresholds, screenDarkeningThresholds, + screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold, + screenBrighteningMinThreshold); + + // Ambient Lux - Idle Screen Brightness Thresholds + float ambientDarkeningMinThresholdIdle = + mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(); + float ambientBrighteningMinThresholdIdle = + mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(); + float[] ambientBrighteningThresholdsIdle = + mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(); + float[] ambientDarkeningThresholdsIdle = + mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(); + float[] ambientBrighteningLevelsIdle = + mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(); + float[] ambientDarkeningLevelsIdle = + mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(); + HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels( + ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle, + ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle, + ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle); - // Idle screen thresholds + // Display - Idle Screen Brightness Thresholds float screenDarkeningMinThresholdIdle = mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(); float screenBrighteningMinThresholdIdle = mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(); + float[] screenBrighteningThresholdsIdle = + mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(); + float[] screenDarkeningThresholdsIdle = + mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(); + float[] screenBrighteningLevelsIdle = + mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(); + float[] screenDarkeningLevelsIdle = + mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(); HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels( - screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels, + screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle, + screenBrighteningLevelsIdle, screenDarkeningLevelsIdle, screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle); - // Idle ambient thresholds - float ambientDarkeningMinThresholdIdle = - mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(); - float ambientBrighteningMinThresholdIdle = - mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(); - HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels( - ambientBrighteningThresholds, ambientDarkeningThresholds, - ambientThresholdLevels, ambientDarkeningMinThresholdIdle, - ambientBrighteningMinThresholdIdle); - long brighteningLightDebounce = mDisplayDeviceConfig .getAutoBrightnessBrighteningLightDebounce(); long darkeningLightDebounce = mDisplayDeviceConfig diff --git a/services/core/java/com/android/server/display/HysteresisLevels.java b/services/core/java/com/android/server/display/HysteresisLevels.java index abf8fe3d3f17..faa4c3d2abc5 100644 --- a/services/core/java/com/android/server/display/HysteresisLevels.java +++ b/services/core/java/com/android/server/display/HysteresisLevels.java @@ -29,52 +29,38 @@ public class HysteresisLevels { private static final boolean DEBUG = false; - private final float[] mBrighteningThresholds; - private final float[] mDarkeningThresholds; - private final float[] mThresholdLevels; + private final float[] mBrighteningThresholdsPercentages; + private final float[] mDarkeningThresholdsPercentages; + private final float[] mBrighteningThresholdLevels; + private final float[] mDarkeningThresholdLevels; private final float mMinDarkening; private final float mMinBrightening; /** - * Creates a {@code HysteresisLevels} object for ambient brightness. - * @param brighteningThresholds an array of brightening hysteresis constraint constants. - * @param darkeningThresholds an array of darkening hysteresis constraint constants. - * @param thresholdLevels a monotonically increasing array of threshold levels. - * @param minBrighteningThreshold the minimum value for which the brightening value needs to - * return. - * @param minDarkeningThreshold the minimum value for which the darkening value needs to return. - */ - HysteresisLevels(int[] brighteningThresholds, int[] darkeningThresholds, - int[] thresholdLevels, float minDarkeningThreshold, float minBrighteningThreshold) { - if (brighteningThresholds.length != darkeningThresholds.length - || darkeningThresholds.length != thresholdLevels.length + 1) { - throw new IllegalArgumentException("Mismatch between hysteresis array lengths."); - } - mBrighteningThresholds = setArrayFormat(brighteningThresholds, 1000.0f); - mDarkeningThresholds = setArrayFormat(darkeningThresholds, 1000.0f); - mThresholdLevels = setArrayFormat(thresholdLevels, 1.0f); - mMinDarkening = minDarkeningThreshold; - mMinBrightening = minBrighteningThreshold; - } - - /** - * Creates a {@code HysteresisLevels} object for screen brightness. - * @param brighteningThresholds an array of brightening hysteresis constraint constants. - * @param darkeningThresholds an array of darkening hysteresis constraint constants. - * @param thresholdLevels a monotonically increasing array of threshold levels. + * Creates a {@code HysteresisLevels} object with the given equal-length + * float arrays. + * @param brighteningThresholdsPercentages 0-100 of thresholds + * @param darkeningThresholdsPercentages 0-100 of thresholds + * @param brighteningThresholdLevels float array of brightness values in the relevant units + * @param darkeningThresholdLevels float array of brightness values in the relevant units * @param minBrighteningThreshold the minimum value for which the brightening value needs to * return. * @param minDarkeningThreshold the minimum value for which the darkening value needs to return. */ - HysteresisLevels(int[] brighteningThresholds, int[] darkeningThresholds, - float[] thresholdLevels, float minDarkeningThreshold, float minBrighteningThreshold) { - if (brighteningThresholds.length != darkeningThresholds.length - || darkeningThresholds.length != thresholdLevels.length + 1) { + HysteresisLevels(float[] brighteningThresholdsPercentages, + float[] darkeningThresholdsPercentages, + float[] brighteningThresholdLevels, float[] darkeningThresholdLevels, + float minDarkeningThreshold, float minBrighteningThreshold) { + if (brighteningThresholdsPercentages.length != brighteningThresholdLevels.length + || darkeningThresholdsPercentages.length != darkeningThresholdLevels.length) { throw new IllegalArgumentException("Mismatch between hysteresis array lengths."); } - mBrighteningThresholds = setArrayFormat(brighteningThresholds, 1000.0f); - mDarkeningThresholds = setArrayFormat(darkeningThresholds, 1000.0f); - mThresholdLevels = constraintInRangeIfNeeded(thresholdLevels); + mBrighteningThresholdsPercentages = + setArrayFormat(brighteningThresholdsPercentages, 100.0f); + mDarkeningThresholdsPercentages = + setArrayFormat(darkeningThresholdsPercentages, 100.0f); + mBrighteningThresholdLevels = setArrayFormat(brighteningThresholdLevels, 1.0f); + mDarkeningThresholdLevels = setArrayFormat(darkeningThresholdLevels, 1.0f); mMinDarkening = minDarkeningThreshold; mMinBrightening = minBrighteningThreshold; } @@ -83,7 +69,9 @@ public class HysteresisLevels { * Return the brightening hysteresis threshold for the given value level. */ public float getBrighteningThreshold(float value) { - final float brightConstant = getReferenceLevel(value, mBrighteningThresholds); + final float brightConstant = getReferenceLevel(value, + mBrighteningThresholdLevels, mBrighteningThresholdsPercentages); + float brightThreshold = value * (1.0f + brightConstant); if (DEBUG) { Slog.d(TAG, "bright hysteresis constant=" + brightConstant + ", threshold=" @@ -98,7 +86,8 @@ public class HysteresisLevels { * Return the darkening hysteresis threshold for the given value level. */ public float getDarkeningThreshold(float value) { - final float darkConstant = getReferenceLevel(value, mDarkeningThresholds); + final float darkConstant = getReferenceLevel(value, + mDarkeningThresholdLevels, mDarkeningThresholdsPercentages); float darkThreshold = value * (1.0f - darkConstant); if (DEBUG) { Slog.d(TAG, "dark hysteresis constant=: " + darkConstant + ", threshold=" @@ -111,60 +100,39 @@ public class HysteresisLevels { /** * Return the hysteresis constant for the closest threshold value from the given array. */ - private float getReferenceLevel(float value, float[] referenceLevels) { + private float getReferenceLevel(float value, float[] thresholdLevels, + float[] thresholdPercentages) { + if (thresholdLevels == null || thresholdLevels.length == 0 || value < thresholdLevels[0]) { + return 0.0f; + } int index = 0; - while (mThresholdLevels.length > index && value >= mThresholdLevels[index]) { - ++index; + while (index < thresholdLevels.length - 1 && value >= thresholdLevels[index + 1]) { + index++; } - return referenceLevels[index]; + return thresholdPercentages[index]; } /** * Return a float array where each i-th element equals {@code configArray[i]/divideFactor}. */ - private float[] setArrayFormat(int[] configArray, float divideFactor) { + private float[] setArrayFormat(float[] configArray, float divideFactor) { float[] levelArray = new float[configArray.length]; for (int index = 0; levelArray.length > index; ++index) { - levelArray[index] = (float) configArray[index] / divideFactor; + levelArray[index] = configArray[index] / divideFactor; } return levelArray; } - /** - * This check is due to historical reasons, where screen thresholdLevels used to be - * integer values in the range of [0-255], but then was changed to be float values from [0,1]. - * To accommodate both the possibilities, we first check if all the thresholdLevels are in [0, - * 1], and if not, we divide all the levels with 255 to bring them down to the same scale. - */ - private float[] constraintInRangeIfNeeded(float[] thresholdLevels) { - if (isAllInRange(thresholdLevels, /* minValueInclusive = */ 0.0f, /* maxValueInclusive = */ - 1.0f)) { - return thresholdLevels; - } - - Slog.w(TAG, "Detected screen thresholdLevels on a deprecated brightness scale"); - float[] thresholdLevelsScaled = new float[thresholdLevels.length]; - for (int index = 0; thresholdLevels.length > index; ++index) { - thresholdLevelsScaled[index] = thresholdLevels[index] / 255.0f; - } - return thresholdLevelsScaled; - } - - private boolean isAllInRange(float[] configArray, float minValueInclusive, - float maxValueInclusive) { - int configArraySize = configArray.length; - for (int index = 0; configArraySize > index; ++index) { - if (configArray[index] < minValueInclusive || configArray[index] > maxValueInclusive) { - return false; - } - } - return true; - } void dump(PrintWriter pw) { pw.println("HysteresisLevels"); - pw.println(" mBrighteningThresholds=" + Arrays.toString(mBrighteningThresholds)); - pw.println(" mDarkeningThresholds=" + Arrays.toString(mDarkeningThresholds)); - pw.println(" mThresholdLevels=" + Arrays.toString(mThresholdLevels)); + pw.println(" mBrighteningThresholdLevels=" + Arrays.toString(mBrighteningThresholdLevels)); + pw.println(" mBrighteningThresholdsPercentages=" + + Arrays.toString(mBrighteningThresholdsPercentages)); + pw.println(" mMinBrightening=" + mMinBrightening); + pw.println(" mDarkeningThresholdLevels=" + Arrays.toString(mDarkeningThresholdLevels)); + pw.println(" mDarkeningThresholdsPercentages=" + + Arrays.toString(mDarkeningThresholdsPercentages)); + pw.println(" mMinDarkening=" + mMinDarkening); } } diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java index b9a0738d15c4..a11f1721a4b9 100644 --- a/services/core/java/com/android/server/display/PersistentDataStore.java +++ b/services/core/java/com/android/server/display/PersistentDataStore.java @@ -75,6 +75,11 @@ import java.util.Objects; * </brightness-curve> * </brightness-configuration> * </brightness-configurations> + * <display-mode>0< + * <resolution-width>1080</resolution-width> + * <resolution-height>1920</resolution-height> + * <refresh-rate>60</refresh-rate> + * </display-mode> * </display> * </display-states> * <stable-device-values> @@ -121,6 +126,10 @@ final class PersistentDataStore { private static final String ATTR_PACKAGE_NAME = "package-name"; private static final String ATTR_TIME_STAMP = "timestamp"; + private static final String TAG_RESOLUTION_WIDTH = "resolution-width"; + private static final String TAG_RESOLUTION_HEIGHT = "resolution-height"; + private static final String TAG_REFRESH_RATE = "refresh-rate"; + // Remembered Wifi display devices. private ArrayList<WifiDisplay> mRememberedWifiDisplays = new ArrayList<WifiDisplay>(); @@ -696,6 +705,18 @@ final class PersistentDataStore { case TAG_BRIGHTNESS_CONFIGURATIONS: mDisplayBrightnessConfigurations.loadFromXml(parser); break; + case TAG_RESOLUTION_WIDTH: + String width = parser.nextText(); + mWidth = Integer.parseInt(width); + break; + case TAG_RESOLUTION_HEIGHT: + String height = parser.nextText(); + mHeight = Integer.parseInt(height); + break; + case TAG_REFRESH_RATE: + String refreshRate = parser.nextText(); + mRefreshRate = Float.parseFloat(refreshRate); + break; } } } @@ -712,6 +733,18 @@ final class PersistentDataStore { serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS); mDisplayBrightnessConfigurations.saveToXml(serializer); serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATIONS); + + serializer.startTag(null, TAG_RESOLUTION_WIDTH); + serializer.text(Integer.toString(mWidth)); + serializer.endTag(null, TAG_RESOLUTION_WIDTH); + + serializer.startTag(null, TAG_RESOLUTION_HEIGHT); + serializer.text(Integer.toString(mHeight)); + serializer.endTag(null, TAG_RESOLUTION_HEIGHT); + + serializer.startTag(null, TAG_REFRESH_RATE); + serializer.text(Float.toString(mRefreshRate)); + serializer.endTag(null, TAG_REFRESH_RATE); } public void dump(final PrintWriter pw, final String prefix) { @@ -719,6 +752,8 @@ final class PersistentDataStore { pw.println(prefix + "BrightnessValue=" + mBrightness); pw.println(prefix + "DisplayBrightnessConfigurations: "); mDisplayBrightnessConfigurations.dump(pw, prefix); + pw.println(prefix + "Resolution=" + mWidth + " " + mHeight); + pw.println(prefix + "RefreshRate=" + mRefreshRate); } } diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java index 4e4f4544e068..b11a06eda025 100644 --- a/services/core/java/com/android/server/dreams/DreamController.java +++ b/services/core/java/com/android/server/dreams/DreamController.java @@ -34,13 +34,13 @@ import android.os.UserHandle; import android.service.dreams.DreamService; import android.service.dreams.IDreamService; import android.util.Slog; -import android.view.IWindowManager; -import android.view.WindowManagerGlobal; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Iterator; import java.util.NoSuchElementException; /** @@ -60,9 +60,6 @@ final class DreamController { private final Context mContext; private final Handler mHandler; private final Listener mListener; - private final IWindowManager mIWindowManager; - private long mDreamStartTime; - private String mSavedStopReason; private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED) .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); @@ -73,27 +70,20 @@ final class DreamController { private DreamRecord mCurrentDream; - private final Runnable mStopUnconnectedDreamRunnable = new Runnable() { - @Override - public void run() { - if (mCurrentDream != null && mCurrentDream.mBound && !mCurrentDream.mConnected) { - Slog.w(TAG, "Bound dream did not connect in the time allotted"); - stopDream(true /*immediate*/, "slow to connect"); - } - } - }; + // Whether a dreaming started intent has been broadcast. + private boolean mSentStartBroadcast = false; - private final Runnable mStopStubbornDreamRunnable = () -> { - Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted"); - stopDream(true /*immediate*/, "slow to finish"); - mSavedStopReason = null; - }; + // When a new dream is started and there is an existing dream, the existing dream is allowed to + // live a little longer until the new dream is started, for a smoother transition. This dream is + // stopped as soon as the new dream is started, and this list is cleared. Usually there should + // only be one previous dream while waiting for a new dream to start, but we store a list to + // proof the edge case of multiple previous dreams. + private final ArrayList<DreamRecord> mPreviousDreams = new ArrayList<>(); public DreamController(Context context, Handler handler, Listener listener) { mContext = context; mHandler = handler; mListener = listener; - mIWindowManager = WindowManagerGlobal.getWindowManagerService(); mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); mCloseNotificationShadeIntent.putExtra("reason", "dream"); } @@ -109,18 +99,17 @@ final class DreamController { pw.println(" mUserId=" + mCurrentDream.mUserId); pw.println(" mBound=" + mCurrentDream.mBound); pw.println(" mService=" + mCurrentDream.mService); - pw.println(" mSentStartBroadcast=" + mCurrentDream.mSentStartBroadcast); pw.println(" mWakingGently=" + mCurrentDream.mWakingGently); } else { pw.println(" mCurrentDream: null"); } + + pw.println(" mSentStartBroadcast=" + mSentStartBroadcast); } public void startDream(Binder token, ComponentName name, boolean isPreviewMode, boolean canDoze, int userId, PowerManager.WakeLock wakeLock, - ComponentName overlayComponentName) { - stopDream(true /*immediate*/, "starting new dream"); - + ComponentName overlayComponentName, String reason) { Trace.traceBegin(Trace.TRACE_TAG_POWER, "startDream"); try { // Close the notification shade. No need to send to all, but better to be explicit. @@ -128,11 +117,14 @@ final class DreamController { Slog.i(TAG, "Starting dream: name=" + name + ", isPreviewMode=" + isPreviewMode + ", canDoze=" + canDoze - + ", userId=" + userId); + + ", userId=" + userId + ", reason='" + reason + "'"); + if (mCurrentDream != null) { + mPreviousDreams.add(mCurrentDream); + } mCurrentDream = new DreamRecord(token, name, isPreviewMode, canDoze, userId, wakeLock); - mDreamStartTime = SystemClock.elapsedRealtime(); + mCurrentDream.mDreamStartTime = SystemClock.elapsedRealtime(); MetricsLogger.visible(mContext, mCurrentDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING); @@ -155,31 +147,49 @@ final class DreamController { } mCurrentDream.mBound = true; - mHandler.postDelayed(mStopUnconnectedDreamRunnable, DREAM_CONNECTION_TIMEOUT); + mHandler.postDelayed(mCurrentDream.mStopUnconnectedDreamRunnable, + DREAM_CONNECTION_TIMEOUT); } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } } + /** + * Stops dreaming. + * + * The current dream, if any, and any unstopped previous dreams are stopped. The device stops + * dreaming. + */ public void stopDream(boolean immediate, String reason) { - if (mCurrentDream == null) { + stopPreviousDreams(); + stopDreamInstance(immediate, reason, mCurrentDream); + } + + /** + * Stops the given dream instance. + * + * The device may still be dreaming afterwards if there are other dreams running. + */ + private void stopDreamInstance(boolean immediate, String reason, DreamRecord dream) { + if (dream == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_POWER, "stopDream"); try { if (!immediate) { - if (mCurrentDream.mWakingGently) { + if (dream.mWakingGently) { return; // already waking gently } - if (mCurrentDream.mService != null) { + if (dream.mService != null) { // Give the dream a moment to wake up and finish itself gently. - mCurrentDream.mWakingGently = true; + dream.mWakingGently = true; try { - mSavedStopReason = reason; - mCurrentDream.mService.wakeUp(); - mHandler.postDelayed(mStopStubbornDreamRunnable, DREAM_FINISH_TIMEOUT); + dream.mStopReason = reason; + dream.mService.wakeUp(); + mHandler.postDelayed(dream.mStopStubbornDreamRunnable, + DREAM_FINISH_TIMEOUT); return; } catch (RemoteException ex) { // oh well, we tried, finish immediately instead @@ -187,54 +197,73 @@ final class DreamController { } } - final DreamRecord oldDream = mCurrentDream; - mCurrentDream = null; - Slog.i(TAG, "Stopping dream: name=" + oldDream.mName - + ", isPreviewMode=" + oldDream.mIsPreviewMode - + ", canDoze=" + oldDream.mCanDoze - + ", userId=" + oldDream.mUserId + Slog.i(TAG, "Stopping dream: name=" + dream.mName + + ", isPreviewMode=" + dream.mIsPreviewMode + + ", canDoze=" + dream.mCanDoze + + ", userId=" + dream.mUserId + ", reason='" + reason + "'" - + (mSavedStopReason == null ? "" : "(from '" + mSavedStopReason + "')")); + + (dream.mStopReason == null ? "" : "(from '" + + dream.mStopReason + "')")); MetricsLogger.hidden(mContext, - oldDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING); + dream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING); MetricsLogger.histogram(mContext, - oldDream.mCanDoze ? "dozing_minutes" : "dreaming_minutes" , - (int) ((SystemClock.elapsedRealtime() - mDreamStartTime) / (1000L * 60L))); + dream.mCanDoze ? "dozing_minutes" : "dreaming_minutes", + (int) ((SystemClock.elapsedRealtime() - dream.mDreamStartTime) / (1000L + * 60L))); - mHandler.removeCallbacks(mStopUnconnectedDreamRunnable); - mHandler.removeCallbacks(mStopStubbornDreamRunnable); - mSavedStopReason = null; + mHandler.removeCallbacks(dream.mStopUnconnectedDreamRunnable); + mHandler.removeCallbacks(dream.mStopStubbornDreamRunnable); - if (oldDream.mSentStartBroadcast) { - mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL); - } - - if (oldDream.mService != null) { + if (dream.mService != null) { try { - oldDream.mService.detach(); + dream.mService.detach(); } catch (RemoteException ex) { // we don't care; this thing is on the way out } try { - oldDream.mService.asBinder().unlinkToDeath(oldDream, 0); + dream.mService.asBinder().unlinkToDeath(dream, 0); } catch (NoSuchElementException ex) { // don't care } - oldDream.mService = null; + dream.mService = null; } - if (oldDream.mBound) { - mContext.unbindService(oldDream); + if (dream.mBound) { + mContext.unbindService(dream); } - oldDream.releaseWakeLockIfNeeded(); + dream.releaseWakeLockIfNeeded(); + + // Current dream stopped, device no longer dreaming. + if (dream == mCurrentDream) { + mCurrentDream = null; + + if (mSentStartBroadcast) { + mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL); + } - mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken)); + mListener.onDreamStopped(dream.mToken); + } } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } } + /** + * Stops all previous dreams, if any. + */ + private void stopPreviousDreams() { + if (mPreviousDreams.isEmpty()) { + return; + } + + // Using an iterator because mPreviousDreams is modified while the iteration is in process. + for (final Iterator<DreamRecord> it = mPreviousDreams.iterator(); it.hasNext(); ) { + stopDreamInstance(true /*immediate*/, "stop previous dream", it.next()); + it.remove(); + } + } + private void attach(IDreamService service) { try { service.asBinder().linkToDeath(mCurrentDream, 0); @@ -248,9 +277,9 @@ final class DreamController { mCurrentDream.mService = service; - if (!mCurrentDream.mIsPreviewMode) { + if (!mCurrentDream.mIsPreviewMode && !mSentStartBroadcast) { mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL); - mCurrentDream.mSentStartBroadcast = true; + mSentStartBroadcast = true; } } @@ -272,10 +301,35 @@ final class DreamController { public boolean mBound; public boolean mConnected; public IDreamService mService; - public boolean mSentStartBroadcast; - + private String mStopReason; + private long mDreamStartTime; public boolean mWakingGently; + private final Runnable mStopPreviousDreamsIfNeeded = this::stopPreviousDreamsIfNeeded; + private final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded; + + private final Runnable mStopUnconnectedDreamRunnable = () -> { + if (mBound && !mConnected) { + Slog.w(TAG, "Bound dream did not connect in the time allotted"); + stopDream(true /*immediate*/, "slow to connect" /*reason*/); + } + }; + + private final Runnable mStopStubbornDreamRunnable = () -> { + Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted"); + stopDream(true /*immediate*/, "slow to finish" /*reason*/); + mStopReason = null; + }; + + private final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() { + // May be called on any thread. + @Override + public void sendResult(Bundle data) { + mHandler.post(mStopPreviousDreamsIfNeeded); + mHandler.post(mReleaseWakeLockIfNeeded); + } + }; + DreamRecord(Binder token, ComponentName name, boolean isPreviewMode, boolean canDoze, int userId, PowerManager.WakeLock wakeLock) { mToken = token; @@ -286,7 +340,9 @@ final class DreamController { mWakeLock = wakeLock; // Hold the lock while we're waiting for the service to connect and start dreaming. // Released after the service has started dreaming, we stop dreaming, or it timed out. - mWakeLock.acquire(); + if (mWakeLock != null) { + mWakeLock.acquire(); + } mHandler.postDelayed(mReleaseWakeLockIfNeeded, 10000); } @@ -326,6 +382,12 @@ final class DreamController { }); } + void stopPreviousDreamsIfNeeded() { + if (mCurrentDream == DreamRecord.this) { + stopPreviousDreams(); + } + } + void releaseWakeLockIfNeeded() { if (mWakeLock != null) { mWakeLock.release(); @@ -333,15 +395,5 @@ final class DreamController { mHandler.removeCallbacks(mReleaseWakeLockIfNeeded); } } - - final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded; - - final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() { - // May be called on any thread. - @Override - public void sendResult(Bundle data) throws RemoteException { - mHandler.post(mReleaseWakeLockIfNeeded); - } - }; } } diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index fee1f5c2a559..5589673973c3 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -60,6 +60,7 @@ import android.util.Slog; import android.view.Display; import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.util.DumpUtils; @@ -84,6 +85,9 @@ public final class DreamManagerService extends SystemService { private static final boolean DEBUG = false; private static final String TAG = "DreamManagerService"; + private static final String DOZE_WAKE_LOCK_TAG = "dream:doze"; + private static final String DREAM_WAKE_LOCK_TAG = "dream:dream"; + private final Object mLock = new Object(); private final Context mContext; @@ -98,17 +102,11 @@ public final class DreamManagerService extends SystemService { private final ComponentName mAmbientDisplayComponent; private final boolean mDismissDreamOnActivityStart; - private Binder mCurrentDreamToken; - private ComponentName mCurrentDreamName; - private int mCurrentDreamUserId; - private boolean mCurrentDreamIsPreview; - private boolean mCurrentDreamCanDoze; - private boolean mCurrentDreamIsDozing; - private boolean mCurrentDreamIsWaking; + @GuardedBy("mLock") + private DreamRecord mCurrentDream; + private boolean mForceAmbientDisplayEnabled; - private boolean mDreamsOnlyEnabledForSystemUser; - private int mCurrentDreamDozeScreenState = Display.STATE_UNKNOWN; - private int mCurrentDreamDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT; + private final boolean mDreamsOnlyEnabledForSystemUser; // A temporary dream component that, when present, takes precedence over user configured dream // component. @@ -116,7 +114,7 @@ public final class DreamManagerService extends SystemService { private ComponentName mDreamOverlayServiceName; - private AmbientDisplayConfiguration mDozeConfig; + private final AmbientDisplayConfiguration mDozeConfig; private final ActivityInterceptorCallback mActivityInterceptorCallback = new ActivityInterceptorCallback() { @Nullable @@ -132,8 +130,14 @@ public final class DreamManagerService extends SystemService { final boolean activityAllowed = activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_DREAM || activityType == ACTIVITY_TYPE_ASSISTANT; - if (mCurrentDreamToken != null && !mCurrentDreamIsWaking - && !mCurrentDreamIsDozing && !activityAllowed) { + + boolean shouldRequestAwaken; + synchronized (mLock) { + shouldRequestAwaken = mCurrentDream != null && !mCurrentDream.isWaking + && !mCurrentDream.isDozing && !activityAllowed; + } + + if (shouldRequestAwaken) { requestAwakenInternal( "stopping dream due to activity start: " + activityInfo.name); } @@ -149,7 +153,7 @@ public final class DreamManagerService extends SystemService { mPowerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE); mPowerManagerInternal = getLocalService(PowerManagerInternal.class); mAtmInternal = getLocalService(ActivityTaskManagerInternal.class); - mDozeWakeLock = mPowerManager.newWakeLock(PowerManager.DOZE_WAKE_LOCK, TAG); + mDozeWakeLock = mPowerManager.newWakeLock(PowerManager.DOZE_WAKE_LOCK, DOZE_WAKE_LOCK_TAG); mDozeConfig = new AmbientDisplayConfiguration(mContext); mUiEventLogger = new UiEventLoggerImpl(); mDreamUiEventLogger = new DreamUiEventLoggerImpl( @@ -197,43 +201,38 @@ public final class DreamManagerService extends SystemService { } private void dumpInternal(PrintWriter pw) { - pw.println("DREAM MANAGER (dumpsys dreams)"); - pw.println(); - pw.println("mCurrentDreamToken=" + mCurrentDreamToken); - pw.println("mCurrentDreamName=" + mCurrentDreamName); - pw.println("mCurrentDreamUserId=" + mCurrentDreamUserId); - pw.println("mCurrentDreamIsPreview=" + mCurrentDreamIsPreview); - pw.println("mCurrentDreamCanDoze=" + mCurrentDreamCanDoze); - pw.println("mCurrentDreamIsDozing=" + mCurrentDreamIsDozing); - pw.println("mCurrentDreamIsWaking=" + mCurrentDreamIsWaking); - pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled); - pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser); - pw.println("mCurrentDreamDozeScreenState=" - + Display.stateToString(mCurrentDreamDozeScreenState)); - pw.println("mCurrentDreamDozeScreenBrightness=" + mCurrentDreamDozeScreenBrightness); - pw.println("getDozeComponent()=" + getDozeComponent()); - pw.println(); - - DumpUtils.dumpAsync(mHandler, new DumpUtils.Dump() { - @Override - public void dump(PrintWriter pw, String prefix) { - mController.dump(pw); - } - }, pw, "", 200); + synchronized (mLock) { + pw.println("DREAM MANAGER (dumpsys dreams)"); + pw.println(); + pw.println("mCurrentDream=" + mCurrentDream); + pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled); + pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser); + pw.println("getDozeComponent()=" + getDozeComponent()); + pw.println(); + + DumpUtils.dumpAsync(mHandler, (pw1, prefix) -> mController.dump(pw1), pw, "", 200); + } } /** Whether a real dream is occurring. */ private boolean isDreamingInternal() { synchronized (mLock) { - return mCurrentDreamToken != null && !mCurrentDreamIsPreview - && !mCurrentDreamIsWaking; + return mCurrentDream != null && !mCurrentDream.isPreview + && !mCurrentDream.isWaking; + } + } + + /** Whether a doze is occurring. */ + private boolean isDozingInternal() { + synchronized (mLock) { + return mCurrentDream != null && mCurrentDream.isDozing; } } /** Whether a real dream, or a dream preview is occurring. */ private boolean isDreamingOrInPreviewInternal() { synchronized (mLock) { - return mCurrentDreamToken != null && !mCurrentDreamIsWaking; + return mCurrentDream != null && !mCurrentDream.isWaking; } } @@ -247,8 +246,8 @@ public final class DreamManagerService extends SystemService { // Because napping could cause the screen to turn off immediately if the dream // cannot be started, we keep one eye open and gently poke user activity. long time = SystemClock.uptimeMillis(); - mPowerManager.userActivity(time, true /*noChangeLights*/); - mPowerManager.nap(time); + mPowerManager.userActivity(time, /* noChangeLights= */ true); + mPowerManagerInternal.nap(time, /* allowWake= */ true); } private void requestAwakenInternal(String reason) { @@ -273,7 +272,7 @@ public final class DreamManagerService extends SystemService { // locks are held and the user activity timeout has expired then the // device may simply go to sleep. synchronized (mLock) { - if (mCurrentDreamToken == token) { + if (mCurrentDream != null && mCurrentDream.token == token) { stopDreamLocked(immediate, "finished self"); } } @@ -281,16 +280,17 @@ public final class DreamManagerService extends SystemService { private void testDreamInternal(ComponentName dream, int userId) { synchronized (mLock) { - startDreamLocked(dream, true /*isPreviewMode*/, false /*canDoze*/, userId); + startDreamLocked(dream, true /*isPreviewMode*/, false /*canDoze*/, userId, + "test dream" /*reason*/); } } - private void startDreamInternal(boolean doze) { + private void startDreamInternal(boolean doze, String reason) { final int userId = ActivityManager.getCurrentUser(); final ComponentName dream = chooseDreamForUser(doze, userId); if (dream != null) { synchronized (mLock) { - startDreamLocked(dream, false /*isPreviewMode*/, doze, userId); + startDreamLocked(dream, false /*isPreviewMode*/, doze, userId, reason); } } } @@ -314,13 +314,13 @@ public final class DreamManagerService extends SystemService { } synchronized (mLock) { - if (mCurrentDreamToken == token && mCurrentDreamCanDoze) { - mCurrentDreamDozeScreenState = screenState; - mCurrentDreamDozeScreenBrightness = screenBrightness; + if (mCurrentDream != null && mCurrentDream.token == token && mCurrentDream.canDoze) { + mCurrentDream.dozeScreenState = screenState; + mCurrentDream.dozeScreenBrightness = screenBrightness; mPowerManagerInternal.setDozeOverrideFromDreamManager( screenState, screenBrightness); - if (!mCurrentDreamIsDozing) { - mCurrentDreamIsDozing = true; + if (!mCurrentDream.isDozing) { + mCurrentDream.isDozing = true; mDozeWakeLock.acquire(); } } @@ -333,8 +333,8 @@ public final class DreamManagerService extends SystemService { } synchronized (mLock) { - if (mCurrentDreamToken == token && mCurrentDreamIsDozing) { - mCurrentDreamIsDozing = false; + if (mCurrentDream != null && mCurrentDream.token == token && mCurrentDream.isDozing) { + mCurrentDream.isDozing = false; mDozeWakeLock.release(); mPowerManagerInternal.setDozeOverrideFromDreamManager( Display.STATE_UNKNOWN, PowerManager.BRIGHTNESS_DEFAULT); @@ -403,7 +403,7 @@ public final class DreamManagerService extends SystemService { ComponentName[] components = componentsFromString(names); // first, ensure components point to valid services - List<ComponentName> validComponents = new ArrayList<ComponentName>(); + List<ComponentName> validComponents = new ArrayList<>(); if (components != null) { for (ComponentName component : components) { if (validateDream(component)) { @@ -439,8 +439,9 @@ public final class DreamManagerService extends SystemService { mSystemDreamComponent = componentName; // Switch dream if currently dreaming and not dozing. - if (isDreamingInternal() && !mCurrentDreamIsDozing) { - startDreamInternal(false); + if (isDreamingInternal() && !isDozingInternal()) { + startDreamInternal(false /*doze*/, (mSystemDreamComponent == null ? "clear" : "set") + + " system dream component" /*reason*/); } } } @@ -478,88 +479,76 @@ public final class DreamManagerService extends SystemService { } } + @GuardedBy("mLock") private void startDreamLocked(final ComponentName name, - final boolean isPreviewMode, final boolean canDoze, final int userId) { - if (!mCurrentDreamIsWaking - && Objects.equals(mCurrentDreamName, name) - && mCurrentDreamIsPreview == isPreviewMode - && mCurrentDreamCanDoze == canDoze - && mCurrentDreamUserId == userId) { + final boolean isPreviewMode, final boolean canDoze, final int userId, + final String reason) { + if (mCurrentDream != null + && !mCurrentDream.isWaking + && Objects.equals(mCurrentDream.name, name) + && mCurrentDream.isPreview == isPreviewMode + && mCurrentDream.canDoze == canDoze + && mCurrentDream.userId == userId) { Slog.i(TAG, "Already in target dream."); return; } - stopDreamLocked(true /*immediate*/, "starting new dream"); - Slog.i(TAG, "Entering dreamland."); - final Binder newToken = new Binder(); - mCurrentDreamToken = newToken; - mCurrentDreamName = name; - mCurrentDreamIsPreview = isPreviewMode; - mCurrentDreamCanDoze = canDoze; - mCurrentDreamUserId = userId; + mCurrentDream = new DreamRecord(name, userId, isPreviewMode, canDoze); - if (!mCurrentDreamName.equals(mAmbientDisplayComponent)) { + if (!mCurrentDream.name.equals(mAmbientDisplayComponent)) { // TODO(b/213906448): Remove when metrics based on new atom are fully rolled out. mUiEventLogger.log(DreamUiEventLogger.DreamUiEventEnum.DREAM_START); mDreamUiEventLogger.log(DreamUiEventLogger.DreamUiEventEnum.DREAM_START, - mCurrentDreamName.flattenToString()); + mCurrentDream.name.flattenToString()); } PowerManager.WakeLock wakeLock = mPowerManager - .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "startDream"); + .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, DREAM_WAKE_LOCK_TAG); + final Binder dreamToken = mCurrentDream.token; mHandler.post(wakeLock.wrap(() -> { mAtmInternal.notifyDreamStateChanged(true); - mController.startDream(newToken, name, isPreviewMode, canDoze, userId, wakeLock, - mDreamOverlayServiceName); + mController.startDream(dreamToken, name, isPreviewMode, canDoze, userId, wakeLock, + mDreamOverlayServiceName, reason); })); } + @GuardedBy("mLock") private void stopDreamLocked(final boolean immediate, String reason) { - if (mCurrentDreamToken != null) { + if (mCurrentDream != null) { if (immediate) { Slog.i(TAG, "Leaving dreamland."); cleanupDreamLocked(); - } else if (mCurrentDreamIsWaking) { + } else if (mCurrentDream.isWaking) { return; // already waking } else { Slog.i(TAG, "Gently waking up from dream."); - mCurrentDreamIsWaking = true; + mCurrentDream.isWaking = true; } - mHandler.post(new Runnable() { - @Override - public void run() { - Slog.i(TAG, "Performing gentle wake from dream."); - mController.stopDream(immediate, reason); - } - }); + mHandler.post(() -> mController.stopDream(immediate, reason)); } } + @GuardedBy("mLock") private void cleanupDreamLocked() { - if (!mCurrentDreamName.equals(mAmbientDisplayComponent)) { + mHandler.post(() -> mAtmInternal.notifyDreamStateChanged(false /*dreaming*/)); + + if (mCurrentDream == null) { + return; + } + + if (!mCurrentDream.name.equals(mAmbientDisplayComponent)) { // TODO(b/213906448): Remove when metrics based on new atom are fully rolled out. mUiEventLogger.log(DreamUiEventLogger.DreamUiEventEnum.DREAM_STOP); mDreamUiEventLogger.log(DreamUiEventLogger.DreamUiEventEnum.DREAM_STOP, - mCurrentDreamName.flattenToString()); - } - mCurrentDreamToken = null; - mCurrentDreamName = null; - mCurrentDreamIsPreview = false; - mCurrentDreamCanDoze = false; - mCurrentDreamUserId = 0; - mCurrentDreamIsWaking = false; - if (mCurrentDreamIsDozing) { - mCurrentDreamIsDozing = false; + mCurrentDream.name.flattenToString()); + } + if (mCurrentDream.isDozing) { mDozeWakeLock.release(); } - mCurrentDreamDozeScreenState = Display.STATE_UNKNOWN; - mCurrentDreamDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT; - mHandler.post(() -> { - mAtmInternal.notifyDreamStateChanged(false); - }); + mCurrentDream = null; } private void checkPermission(String permission) { @@ -606,7 +595,7 @@ public final class DreamManagerService extends SystemService { @Override public void onDreamStopped(Binder token) { synchronized (mLock) { - if (mCurrentDreamToken == token) { + if (mCurrentDream != null && mCurrentDream.token == token) { cleanupDreamLocked(); } } @@ -624,7 +613,7 @@ public final class DreamManagerService extends SystemService { * Handler for asynchronous operations performed by the dream manager. * Ensures operations to {@link DreamController} are single-threaded. */ - private final class DreamHandler extends Handler { + private static final class DreamHandler extends Handler { public DreamHandler(Looper looper) { super(looper, null, true /*async*/); } @@ -646,7 +635,7 @@ public final class DreamManagerService extends SystemService { @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback, @NonNull ResultReceiver resultReceiver) throws RemoteException { - new DreamShellCommand(DreamManagerService.this, mPowerManager) + new DreamShellCommand(DreamManagerService.this) .exec(this, in, out, err, args, callback, resultReceiver); } @@ -865,13 +854,13 @@ public final class DreamManagerService extends SystemService { private final class LocalService extends DreamManagerInternal { @Override - public void startDream(boolean doze) { - startDreamInternal(doze); + public void startDream(boolean doze, String reason) { + startDreamInternal(doze, reason); } @Override - public void stopDream(boolean immediate) { - stopDreamInternal(immediate, "requested stopDream"); + public void stopDream(boolean immediate, String reason) { + stopDreamInternal(immediate, reason); } @Override @@ -890,13 +879,47 @@ public final class DreamManagerService extends SystemService { } } + private static final class DreamRecord { + public final Binder token = new Binder(); + public final ComponentName name; + public final int userId; + public final boolean isPreview; + public final boolean canDoze; + public boolean isDozing = false; + public boolean isWaking = false; + public int dozeScreenState = Display.STATE_UNKNOWN; + public int dozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT; + + DreamRecord(ComponentName name, int userId, boolean isPreview, boolean canDoze) { + this.name = name; + this.userId = userId; + this.isPreview = isPreview; + this.canDoze = canDoze; + } + + @Override + public String toString() { + return "DreamRecord{" + + "token=" + token + + ", name=" + name + + ", userId=" + userId + + ", isPreview=" + isPreview + + ", canDoze=" + canDoze + + ", isDozing=" + isDozing + + ", isWaking=" + isWaking + + ", dozeScreenState=" + dozeScreenState + + ", dozeScreenBrightness=" + dozeScreenBrightness + + '}'; + } + } + private final Runnable mSystemPropertiesChanged = new Runnable() { @Override public void run() { if (DEBUG) Slog.d(TAG, "System properties changed"); synchronized (mLock) { - if (mCurrentDreamName != null && mCurrentDreamCanDoze - && !mCurrentDreamName.equals(getDozeComponent())) { + if (mCurrentDream != null && mCurrentDream.name != null && mCurrentDream.canDoze + && !mCurrentDream.name.equals(getDozeComponent())) { // May have updated the doze component, wake up mPowerManager.wakeUp(SystemClock.uptimeMillis(), "android.server.dreams:SYSPROP"); diff --git a/services/core/java/com/android/server/dreams/DreamShellCommand.java b/services/core/java/com/android/server/dreams/DreamShellCommand.java index eae7e80a89f1..ab84ae4c08a2 100644 --- a/services/core/java/com/android/server/dreams/DreamShellCommand.java +++ b/services/core/java/com/android/server/dreams/DreamShellCommand.java @@ -18,10 +18,8 @@ package com.android.server.dreams; import android.annotation.NonNull; import android.os.Binder; -import android.os.PowerManager; import android.os.Process; import android.os.ShellCommand; -import android.os.SystemClock; import android.text.TextUtils; import android.util.Slog; @@ -34,11 +32,9 @@ public class DreamShellCommand extends ShellCommand { private static final boolean DEBUG = true; private static final String TAG = "DreamShellCommand"; private final @NonNull DreamManagerService mService; - private final @NonNull PowerManager mPowerManager; - DreamShellCommand(@NonNull DreamManagerService service, @NonNull PowerManager powerManager) { + DreamShellCommand(@NonNull DreamManagerService service) { mService = service; - mPowerManager = powerManager; } @Override @@ -67,8 +63,6 @@ public class DreamShellCommand extends ShellCommand { } private int startDreaming() { - mPowerManager.wakeUp(SystemClock.uptimeMillis(), - PowerManager.WAKE_REASON_PLUGGED_IN, "shell:cmd:android.service.dreams:DREAM"); mService.requestStartDreamFromShell(); return 0; } diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java new file mode 100644 index 000000000000..c57d7e79c12a --- /dev/null +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input; + +import android.annotation.BinderThread; +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.input.IInputDeviceBatteryListener; +import android.hardware.input.InputManager; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; +import android.view.InputDevice; + +import com.android.internal.annotations.GuardedBy; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; + +/** + * A thread-safe component of {@link InputManagerService} responsible for managing the battery state + * of input devices. + */ +final class BatteryController { + private static final String TAG = BatteryController.class.getSimpleName(); + + // To enable these logs, run: + // 'adb shell setprop log.tag.BatteryController DEBUG' (requires restart) + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final Object mLock = new Object(); + private final Context mContext; + private final NativeInputManagerService mNative; + + // Maps a pid to the registered listener record for that process. There can only be one battery + // listener per process. + @GuardedBy("mLock") + private final ArrayMap<Integer, ListenerRecord> mListenerRecords = new ArrayMap<>(); + + BatteryController(Context context, NativeInputManagerService nativeService) { + mContext = context; + mNative = nativeService; + } + + /** + * Register the battery listener for the given input device and start monitoring its battery + * state. + */ + @BinderThread + void registerBatteryListener(int deviceId, @NonNull IInputDeviceBatteryListener listener, + int pid) { + synchronized (mLock) { + ListenerRecord listenerRecord = mListenerRecords.get(pid); + + if (listenerRecord == null) { + listenerRecord = new ListenerRecord(pid, listener); + try { + listener.asBinder().linkToDeath(listenerRecord.mDeathRecipient, 0); + } catch (RemoteException e) { + Slog.i(TAG, "Client died before battery listener could be registered."); + return; + } + mListenerRecords.put(pid, listenerRecord); + if (DEBUG) Slog.d(TAG, "Battery listener added for pid " + pid); + } + + if (listenerRecord.mListener.asBinder() != listener.asBinder()) { + throw new SecurityException( + "Cannot register a new battery listener when there is already another " + + "registered listener for pid " + + pid); + } + if (!listenerRecord.mMonitoredDevices.add(deviceId)) { + throw new IllegalArgumentException( + "The battery listener for pid " + pid + + " is already monitoring deviceId " + deviceId); + } + + if (DEBUG) { + Slog.d(TAG, "Battery listener for pid " + pid + + " is monitoring deviceId " + deviceId); + } + + notifyBatteryListener(deviceId, listenerRecord); + } + } + + private void notifyBatteryListener(int deviceId, ListenerRecord record) { + final long eventTime = SystemClock.uptimeMillis(); + try { + record.mListener.onBatteryStateChanged( + deviceId, + hasBattery(deviceId), + mNative.getBatteryStatus(deviceId), + mNative.getBatteryCapacity(deviceId) / 100.f, + eventTime); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to notify listener", e); + } + } + + private boolean hasBattery(int deviceId) { + final InputDevice device = + Objects.requireNonNull(mContext.getSystemService(InputManager.class)) + .getInputDevice(deviceId); + return device != null && device.hasBattery(); + } + + /** + * Unregister the battery listener for the given input device and stop monitoring its battery + * state. If there are no other input devices that this listener is monitoring, the listener is + * removed. + */ + @BinderThread + void unregisterBatteryListener(int deviceId, @NonNull IInputDeviceBatteryListener listener, + int pid) { + synchronized (mLock) { + final ListenerRecord listenerRecord = mListenerRecords.get(pid); + if (listenerRecord == null) { + throw new IllegalArgumentException( + "Cannot unregister battery callback: No listener registered for pid " + + pid); + } + + if (listenerRecord.mListener.asBinder() != listener.asBinder()) { + throw new IllegalArgumentException( + "Cannot unregister battery callback: The listener is not the one that " + + "is registered for pid " + + pid); + } + + if (!listenerRecord.mMonitoredDevices.contains(deviceId)) { + throw new IllegalArgumentException( + "Cannot unregister battery callback: The device is not being " + + "monitored for deviceId " + deviceId); + } + + unregisterRecordLocked(listenerRecord, deviceId); + } + } + + @GuardedBy("mLock") + private void unregisterRecordLocked(ListenerRecord listenerRecord, int deviceId) { + final int pid = listenerRecord.mPid; + + if (!listenerRecord.mMonitoredDevices.remove(deviceId)) { + throw new IllegalStateException("Cannot unregister battery callback: The deviceId " + + deviceId + + " is not being monitored by pid " + + pid); + } + + if (listenerRecord.mMonitoredDevices.isEmpty()) { + // There are no more devices being monitored by this listener. + listenerRecord.mListener.asBinder().unlinkToDeath(listenerRecord.mDeathRecipient, 0); + mListenerRecords.remove(pid); + if (DEBUG) Slog.d(TAG, "Battery listener removed for pid " + pid); + } + } + + private void handleListeningProcessDied(int pid) { + synchronized (mLock) { + final ListenerRecord listenerRecord = mListenerRecords.get(pid); + if (listenerRecord == null) { + return; + } + if (DEBUG) { + Slog.d(TAG, + "Removing battery listener for pid " + pid + " because the process died"); + } + for (final int deviceId : listenerRecord.mMonitoredDevices) { + unregisterRecordLocked(listenerRecord, deviceId); + } + } + } + + void dump(PrintWriter pw, String prefix) { + synchronized (mLock) { + pw.println(prefix + TAG + ": " + mListenerRecords.size() + + " battery listeners"); + for (int i = 0; i < mListenerRecords.size(); i++) { + pw.println(prefix + " " + i + ": " + mListenerRecords.valueAt(i)); + } + } + } + + @SuppressWarnings("all") + void monitor() { + synchronized (mLock) { + return; + } + } + + // A record of a registered battery listener from one process. + private class ListenerRecord { + final int mPid; + final IInputDeviceBatteryListener mListener; + final IBinder.DeathRecipient mDeathRecipient; + // The set of deviceIds that are currently being monitored by this listener. + final Set<Integer> mMonitoredDevices; + + ListenerRecord(int pid, IInputDeviceBatteryListener listener) { + mPid = pid; + mListener = listener; + mMonitoredDevices = new ArraySet<>(); + mDeathRecipient = () -> handleListeningProcessDied(pid); + } + + @Override + public String toString() { + return "pid=" + mPid + + ", monitored devices=" + Arrays.toString(mMonitoredDevices.toArray()); + } + } +} diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 72612a0468cd..91d5698123e8 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -47,6 +47,7 @@ import android.hardware.SensorPrivacyManager.Sensors; import android.hardware.SensorPrivacyManagerInternal; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayViewport; +import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.IInputDevicesChangedListener; import android.hardware.input.IInputManager; import android.hardware.input.IInputSensorEventListener; @@ -318,6 +319,9 @@ public class InputManagerService extends IInputManager.Stub @GuardedBy("mInputMonitors") final Map<IBinder, GestureMonitorSpyWindow> mInputMonitors = new HashMap<>(); + // Manages battery state for input devices. + private final BatteryController mBatteryController; + // Maximum number of milliseconds to wait for input event injection. private static final int INJECTION_TIMEOUT_MILLIS = 30 * 1000; @@ -425,6 +429,7 @@ public class InputManagerService extends IInputManager.Stub mContext = injector.getContext(); mHandler = new InputManagerHandler(injector.getLooper()); mNative = injector.getNativeService(this); + mBatteryController = new BatteryController(mContext, mNative); mUseDevInputEventForAudioJack = mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack); @@ -2674,6 +2679,18 @@ public class InputManagerService extends IInputManager.Stub } @Override + public void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener) { + Objects.requireNonNull(listener); + mBatteryController.registerBatteryListener(deviceId, listener, Binder.getCallingPid()); + } + + @Override + public void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener) { + Objects.requireNonNull(listener); + mBatteryController.unregisterBatteryListener(deviceId, listener, Binder.getCallingPid()); + } + + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; @@ -2686,7 +2703,8 @@ public class InputManagerService extends IInputManager.Stub pw.println("Input Manager Service (Java) State:"); dumpAssociations(pw, " " /*prefix*/); dumpSpyWindowGestureMonitors(pw, " " /*prefix*/); - dumpDisplayInputPropertiesValues(pw, " " /* prefix */); + dumpDisplayInputPropertiesValues(pw, " " /*prefix*/); + mBatteryController.dump(pw, " " /*prefix*/); } private void dumpAssociations(PrintWriter pw, String prefix) { @@ -2797,6 +2815,7 @@ public class InputManagerService extends IInputManager.Stub synchronized (mLidSwitchLock) { /* Test if blocked by lid switch lock. */ } synchronized (mInputMonitors) { /* Test if blocked by input monitor lock. */ } synchronized (mAdditionalDisplayInputPropertiesLock) { /* Test if blocked by props lock */ } + mBatteryController.monitor(); mNative.monitor(); } diff --git a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java index 6759d79eedca..9a190316f4eb 100644 --- a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java +++ b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java @@ -32,7 +32,6 @@ import android.os.Handler; import android.os.PowerWhitelistManager; import android.os.UserHandle; import android.text.TextUtils; -import android.util.EventLog; import android.util.Log; import android.view.KeyEvent; @@ -118,12 +117,6 @@ final class MediaButtonReceiverHolder { int componentType = getComponentType(pendingIntent); ComponentName componentName = getComponentName(pendingIntent, componentType); if (componentName != null) { - if (!TextUtils.equals(componentName.getPackageName(), sessionPackageName)) { - EventLog.writeEvent(0x534e4554, "238177121", -1, ""); // SafetyNet logging - throw new IllegalArgumentException("ComponentName does not belong to " - + "sessionPackageName. sessionPackageName = " + sessionPackageName - + ", ComponentName pkg = " + componentName.getPackageName()); - } return new MediaButtonReceiverHolder(userId, pendingIntent, componentName, componentType); } diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index b8131a8ee5b5..604e8f3949f4 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -52,8 +52,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.SystemClock; -import android.text.TextUtils; -import android.util.EventLog; import android.util.Log; import android.view.KeyEvent; @@ -940,14 +938,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR @Override public void setMediaButtonReceiver(PendingIntent pi, String sessionPackageName) throws RemoteException { - //mPackageName has been verified in MediaSessionService.enforcePackageName(). - if (!TextUtils.equals(sessionPackageName, mPackageName)) { - EventLog.writeEvent(0x534e4554, "238177121", -1, ""); // SafetyNet logging - throw new IllegalArgumentException("sessionPackageName name does not match " - + "package name provided to MediaSessionRecord. sessionPackageName = " - + sessionPackageName + ", pkg = " - + mPackageName); - } final long token = Binder.clearCallingIdentity(); try { if ((mPolicies & MediaSessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_RECEIVER) @@ -966,15 +956,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR public void setMediaButtonBroadcastReceiver(ComponentName receiver) throws RemoteException { final long token = Binder.clearCallingIdentity(); try { - //mPackageName has been verified in MediaSessionService.enforcePackageName(). - if (receiver != null && !TextUtils.equals( - mPackageName, receiver.getPackageName())) { - EventLog.writeEvent(0x534e4554, "238177121", -1, ""); // SafetyNet logging - throw new IllegalArgumentException("receiver does not belong to " - + "package name provided to MediaSessionRecord. Pkg = " + mPackageName - + ", Receiver Pkg = " + receiver.getPackageName()); - } - if ((mPolicies & MediaSessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_RECEIVER) != 0) { return; diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 7468d32e4a01..ae7df90bd4fb 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -61,6 +61,7 @@ import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_TELECOM; import static android.content.pm.PackageManager.FEATURE_TELEVISION; import static android.content.pm.PackageManager.MATCH_ALL; +import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -1977,34 +1978,39 @@ public class NotificationManagerService extends SystemService { return (haystack & needle) != 0; } - public boolean isInLockDownMode() { - return mIsInLockDownMode; + // Return whether the user is in lockdown mode. + // If the flag is not set, we assume the user is not in lockdown. + public boolean isInLockDownMode(int userId) { + return mUserInLockDownMode.get(userId, false); } @Override public synchronized void onStrongAuthRequiredChanged(int userId) { boolean userInLockDownModeNext = containsFlag(getStrongAuthForUser(userId), STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); - mUserInLockDownMode.put(userId, userInLockDownModeNext); - boolean isInLockDownModeNext = mUserInLockDownMode.indexOfValue(true) != -1; - if (mIsInLockDownMode == isInLockDownModeNext) { + // Nothing happens if the lockdown mode of userId keeps the same. + if (userInLockDownModeNext == isInLockDownMode(userId)) { return; } - if (isInLockDownModeNext) { - cancelNotificationsWhenEnterLockDownMode(); + // When the lockdown mode is changed, we perform the following steps. + // If the userInLockDownModeNext is true, all the function calls to + // notifyPostedLocked and notifyRemovedLocked will not be executed. + // The cancelNotificationsWhenEnterLockDownMode calls notifyRemovedLocked + // and postNotificationsWhenExitLockDownMode calls notifyPostedLocked. + // So we shall call cancelNotificationsWhenEnterLockDownMode before + // we set mUserInLockDownMode as true. + // On the other hand, if the userInLockDownModeNext is false, we shall call + // postNotificationsWhenExitLockDownMode after we put false into mUserInLockDownMode + if (userInLockDownModeNext) { + cancelNotificationsWhenEnterLockDownMode(userId); } - // When the mIsInLockDownMode is true, both notifyPostedLocked and - // notifyRemovedLocked will be dismissed. So we shall call - // cancelNotificationsWhenEnterLockDownMode before we set mIsInLockDownMode - // as true and call postNotificationsWhenExitLockDownMode after we set - // mIsInLockDownMode as false. - mIsInLockDownMode = isInLockDownModeNext; + mUserInLockDownMode.put(userId, userInLockDownModeNext); - if (!isInLockDownModeNext) { - postNotificationsWhenExitLockDownMode(); + if (!userInLockDownModeNext) { + postNotificationsWhenExitLockDownMode(userId); } } } @@ -4868,6 +4874,13 @@ public class NotificationManagerService extends SystemService { } @Override + public int getHintsFromListenerNoToken() { + synchronized (mNotificationLock) { + return mListenerHints; + } + } + + @Override public void requestInterruptionFilterFromListener(INotificationListener token, int interruptionFilter) throws RemoteException { final long identity = Binder.clearCallingIdentity(); @@ -9713,11 +9726,14 @@ public class NotificationManagerService extends SystemService { } } - private void cancelNotificationsWhenEnterLockDownMode() { + private void cancelNotificationsWhenEnterLockDownMode(int userId) { synchronized (mNotificationLock) { int numNotifications = mNotificationList.size(); for (int i = 0; i < numNotifications; i++) { NotificationRecord rec = mNotificationList.get(i); + if (rec.getUser().getIdentifier() != userId) { + continue; + } mListeners.notifyRemovedLocked(rec, REASON_CANCEL_ALL, rec.getStats()); } @@ -9725,14 +9741,23 @@ public class NotificationManagerService extends SystemService { } } - private void postNotificationsWhenExitLockDownMode() { + private void postNotificationsWhenExitLockDownMode(int userId) { synchronized (mNotificationLock) { int numNotifications = mNotificationList.size(); + // Set the delay to spread out the burst of notifications. + long delay = 0; for (int i = 0; i < numNotifications; i++) { NotificationRecord rec = mNotificationList.get(i); - mListeners.notifyPostedLocked(rec, rec); + if (rec.getUser().getIdentifier() != userId) { + continue; + } + mHandler.postDelayed(() -> { + synchronized (mNotificationLock) { + mListeners.notifyPostedLocked(rec, rec); + } + }, delay); + delay += 20; } - } } @@ -9911,6 +9936,9 @@ public class NotificationManagerService extends SystemService { for (int i = 0; i < N; i++) { NotificationRecord record = mNotificationList.get(i); + if (isInLockDownMode(record.getUser().getIdentifier())) { + continue; + } if (!isVisibleToListener(record.getSbn(), record.getNotificationType(), info)) { continue; } @@ -9952,8 +9980,8 @@ public class NotificationManagerService extends SystemService { rankings.toArray(new NotificationListenerService.Ranking[0])); } - boolean isInLockDownMode() { - return mStrongAuthTracker.isInLockDownMode(); + boolean isInLockDownMode(int userId) { + return mStrongAuthTracker.isInLockDownMode(userId); } boolean hasCompanionDevice(ManagedServiceInfo info) { @@ -10674,10 +10702,18 @@ public class NotificationManagerService extends SystemService { private final ArraySet<ManagedServiceInfo> mLightTrimListeners = new ArraySet<>(); ArrayMap<Pair<ComponentName, Integer>, NotificationListenerFilter> mRequestedNotificationListeners = new ArrayMap<>(); + private final boolean mIsHeadlessSystemUserMode; public NotificationListeners(Context context, Object lock, UserProfiles userProfiles, IPackageManager pm) { + this(context, lock, userProfiles, pm, UserManager.isHeadlessSystemUserMode()); + } + + @VisibleForTesting + public NotificationListeners(Context context, Object lock, UserProfiles userProfiles, + IPackageManager pm, boolean isHeadlessSystemUserMode) { super(context, lock, userProfiles, pm); + this.mIsHeadlessSystemUserMode = isHeadlessSystemUserMode; } @Override @@ -10702,10 +10738,16 @@ public class NotificationManagerService extends SystemService { if (TextUtils.isEmpty(listeners[i])) { continue; } + int packageQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE; + // In the headless system user mode, packages might not be installed for the + // system user. Match packages for any user since apps can be installed only for + // non-system users and would be considering uninstalled for the system user. + if (mIsHeadlessSystemUserMode) { + packageQueryFlags += MATCH_ANY_USER; + } ArraySet<ComponentName> approvedListeners = - this.queryPackageForServices(listeners[i], - MATCH_DIRECT_BOOT_AWARE - | MATCH_DIRECT_BOOT_UNAWARE, USER_SYSTEM); + this.queryPackageForServices(listeners[i], packageQueryFlags, + USER_SYSTEM); for (int k = 0; k < approvedListeners.size(); k++) { ComponentName cn = approvedListeners.valueAt(k); addDefaultComponentOrPackage(cn.flattenToString()); @@ -11015,7 +11057,7 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") void notifyPostedLocked(NotificationRecord r, NotificationRecord old, boolean notifyAllListeners) { - if (isInLockDownMode()) { + if (isInLockDownMode(r.getUser().getIdentifier())) { return; } @@ -11121,7 +11163,7 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") public void notifyRemovedLocked(NotificationRecord r, int reason, NotificationStats notificationStats) { - if (isInLockDownMode()) { + if (isInLockDownMode(r.getUser().getIdentifier())) { return; } @@ -11170,10 +11212,6 @@ public class NotificationManagerService extends SystemService { */ @GuardedBy("mNotificationLock") public void notifyRankingUpdateLocked(List<NotificationRecord> changedHiddenNotifications) { - if (isInLockDownMode()) { - return; - } - boolean isHiddenRankingUpdate = changedHiddenNotifications != null && changedHiddenNotifications.size() > 0; // TODO (b/73052211): if the ranking update changed the notification type, diff --git a/services/core/java/com/android/server/notification/NotificationRecordLogger.java b/services/core/java/com/android/server/notification/NotificationRecordLogger.java index 9a89efabc689..232a69b2d109 100644 --- a/services/core/java/com/android/server/notification/NotificationRecordLogger.java +++ b/services/core/java/com/android/server/notification/NotificationRecordLogger.java @@ -177,11 +177,13 @@ public interface NotificationRecordLogger { NOTIFICATION_CANCEL_USER_PEEK(190), @UiEvent(doc = "Notification was canceled due to user dismissal from the always-on display") NOTIFICATION_CANCEL_USER_AOD(191), + @UiEvent(doc = "Notification was canceled due to user dismissal from a bubble") + NOTIFICATION_CANCEL_USER_BUBBLE(1228), + @UiEvent(doc = "Notification was canceled due to user dismissal from the lockscreen") + NOTIFICATION_CANCEL_USER_LOCKSCREEN(193), @UiEvent(doc = "Notification was canceled due to user dismissal from the notification" + " shade.") NOTIFICATION_CANCEL_USER_SHADE(192), - @UiEvent(doc = "Notification was canceled due to user dismissal from the lockscreen") - NOTIFICATION_CANCEL_USER_LOCKSCREEN(193), @UiEvent(doc = "Notification was canceled due to an assistant adjustment update.") NOTIFICATION_CANCEL_ASSISTANT(906); @@ -232,6 +234,10 @@ public interface NotificationRecordLogger { return NOTIFICATION_CANCEL_USER_AOD; case NotificationStats.DISMISSAL_SHADE: return NOTIFICATION_CANCEL_USER_SHADE; + case NotificationStats.DISMISSAL_BUBBLE: + return NOTIFICATION_CANCEL_USER_BUBBLE; + case NotificationStats.DISMISSAL_LOCKSCREEN: + return NOTIFICATION_CANCEL_USER_LOCKSCREEN; default: if (NotificationManagerService.DBG) { throw new IllegalArgumentException("Unexpected surface for user-dismiss " diff --git a/services/core/java/com/android/server/om/IdmapDaemon.java b/services/core/java/com/android/server/om/IdmapDaemon.java index 8e944b7a965d..39d1188017bf 100644 --- a/services/core/java/com/android/server/om/IdmapDaemon.java +++ b/services/core/java/com/android/server/om/IdmapDaemon.java @@ -217,6 +217,7 @@ class IdmapDaemon { synchronized List<FabricatedOverlayInfo> getFabricatedOverlayInfos() { final ArrayList<FabricatedOverlayInfo> allInfos = new ArrayList<>(); Connection c = null; + int iteratorId = -1; try { c = connect(); final IIdmap2 service = c.getIdmap2(); @@ -225,9 +226,9 @@ class IdmapDaemon { return Collections.emptyList(); } - service.acquireFabricatedOverlayIterator(); + iteratorId = service.acquireFabricatedOverlayIterator(); List<FabricatedOverlayInfo> infos; - while (!(infos = service.nextFabricatedOverlayInfos()).isEmpty()) { + while (!(infos = service.nextFabricatedOverlayInfos(iteratorId)).isEmpty()) { allInfos.addAll(infos); } return allInfos; @@ -235,8 +236,8 @@ class IdmapDaemon { Slog.wtf(TAG, "failed to get all fabricated overlays", e); } finally { try { - if (c.getIdmap2() != null) { - c.getIdmap2().releaseFabricatedOverlayIterator(); + if (c.getIdmap2() != null && iteratorId != -1) { + c.getIdmap2().releaseFabricatedOverlayIterator(iteratorId); } } catch (RemoteException e) { // ignore diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index 0c601bfde05a..890c89152a7c 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -1962,10 +1962,15 @@ class ShortcutPackage extends ShortcutPackageItem { continue; case TAG_SHORTCUT: - final ShortcutInfo si = parseShortcut(parser, packageName, - shortcutUser.getUserId(), fromBackup); - // Don't use addShortcut(), we don't need to save the icon. - ret.mShortcuts.put(si.getId(), si); + try { + final ShortcutInfo si = parseShortcut(parser, packageName, + shortcutUser.getUserId(), fromBackup); + // Don't use addShortcut(), we don't need to save the icon. + ret.mShortcuts.put(si.getId(), si); + } catch (Exception e) { + // b/246540168 malformed shortcuts should be ignored + Slog.e(TAG, "Failed parsing shortcut.", e); + } continue; case TAG_SHARE_TARGET: ret.mShareTargets.add(ShareTargetInfo.loadFromXml(parser)); diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 014d580e520f..18c29fa9f3a2 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -644,8 +644,8 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt Permission bp = mRegistry.getPermission(info.name); added = bp == null; int fixedLevel = PermissionInfo.fixProtectionLevel(info.protectionLevel); + enforcePermissionCapLocked(info, tree); if (added) { - enforcePermissionCapLocked(info, tree); bp = new Permission(info.name, tree.getPackageName(), Permission.TYPE_DYNAMIC); } else if (!bp.isDynamic()) { throw new SecurityException("Not allowed to modify non-dynamic permission " diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 89ab09e36c5e..c13e18223b8c 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -63,7 +63,6 @@ import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType; import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD; import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER; import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; -import static android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION; import static android.view.WindowManagerGlobal.ADD_OKAY; import static android.view.WindowManagerGlobal.ADD_PERMISSION_DENIED; @@ -1599,7 +1598,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { // If there's a dream running then use home to escape the dream // but don't actually go home. if (mDreamManagerInternal != null && mDreamManagerInternal.isDreaming()) { - mDreamManagerInternal.stopDream(false /*immediate*/); + mDreamManagerInternal.stopDream(false /*immediate*/, "short press on home" /*reason*/); return; } @@ -2819,9 +2818,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; case KeyEvent.KEYCODE_S: if (down && event.isMetaPressed() && event.isCtrlPressed() && repeatCount == 0) { - int type = event.isShiftPressed() ? TAKE_SCREENSHOT_SELECTED_REGION - : TAKE_SCREENSHOT_FULLSCREEN; - interceptScreenshotChord(type, SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); + interceptScreenshotChord( + TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); return key_consumed; } break; @@ -4145,6 +4143,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { mCameraGestureTriggered = true; if (mRequestedOrSleepingDefaultDisplay) { mCameraGestureTriggeredDuringGoingToSleep = true; + // Wake device up early to prevent display doing redundant turning off/on stuff. + wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey, + PowerManager.WAKE_REASON_CAMERA_LAUNCH, + "android.policy:CAMERA_GESTURE_PREVENT_LOCK"); } return true; } @@ -4676,11 +4678,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } mDefaultDisplayRotation.updateOrientationListener(); reportScreenStateToVrManager(false); - if (mCameraGestureTriggeredDuringGoingToSleep) { - wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey, - PowerManager.WAKE_REASON_CAMERA_LAUNCH, - "com.android.systemui:CAMERA_GESTURE_PREVENT_LOCK"); - } } } diff --git a/services/core/java/com/android/server/power/AmbientDisplaySuppressionController.java b/services/core/java/com/android/server/power/AmbientDisplaySuppressionController.java index aad7b1457b3c..7440fc75bf51 100644 --- a/services/core/java/com/android/server/power/AmbientDisplaySuppressionController.java +++ b/services/core/java/com/android/server/power/AmbientDisplaySuppressionController.java @@ -40,13 +40,24 @@ import java.util.Set; public class AmbientDisplaySuppressionController { private static final String TAG = "AmbientDisplaySuppressionController"; - private final Context mContext; private final Set<Pair<String, Integer>> mSuppressionTokens; + private final AmbientDisplaySuppressionChangedCallback mCallback; private IStatusBarService mStatusBarService; - AmbientDisplaySuppressionController(Context context) { - mContext = requireNonNull(context); + /** Interface to get a list of available logical devices. */ + interface AmbientDisplaySuppressionChangedCallback { + /** + * Called when the suppression state changes. + * + * @param isSuppressed Whether ambient is suppressed. + */ + void onSuppressionChanged(boolean isSuppressed); + } + + AmbientDisplaySuppressionController( + @NonNull AmbientDisplaySuppressionChangedCallback callback) { mSuppressionTokens = Collections.synchronizedSet(new ArraySet<>()); + mCallback = requireNonNull(callback); } /** @@ -58,6 +69,7 @@ public class AmbientDisplaySuppressionController { */ public void suppress(@NonNull String token, int callingUid, boolean suppress) { Pair<String, Integer> suppressionToken = Pair.create(requireNonNull(token), callingUid); + final boolean wasSuppressed = isSuppressed(); if (suppress) { mSuppressionTokens.add(suppressionToken); @@ -65,9 +77,14 @@ public class AmbientDisplaySuppressionController { mSuppressionTokens.remove(suppressionToken); } + final boolean isSuppressed = isSuppressed(); + if (isSuppressed != wasSuppressed) { + mCallback.onSuppressionChanged(isSuppressed); + } + try { synchronized (mSuppressionTokens) { - getStatusBar().suppressAmbientDisplay(isSuppressed()); + getStatusBar().suppressAmbientDisplay(isSuppressed); } } catch (RemoteException e) { Slog.e(TAG, "Failed to suppress ambient display", e); diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index 5a2fb18673ac..dad9584c6722 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -571,7 +571,8 @@ public class Notifier { /** * Called when there has been user activity. */ - public void onUserActivity(int displayGroupId, int event, int uid) { + public void onUserActivity(int displayGroupId, @PowerManager.UserActivityEvent int event, + int uid) { if (DEBUG) { Slog.d(TAG, "onUserActivity: event=" + event + ", uid=" + uid); } diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java index fec61ac8f2cf..431cf3861804 100644 --- a/services/core/java/com/android/server/power/PowerGroup.java +++ b/services/core/java/com/android/server/power/PowerGroup.java @@ -74,6 +74,8 @@ public class PowerGroup { private long mLastPowerOnTime; private long mLastUserActivityTime; private long mLastUserActivityTimeNoChangeLights; + @PowerManager.UserActivityEvent + private int mLastUserActivityEvent; /** Timestamp (milliseconds since boot) of the last time the power group was awoken.*/ private long mLastWakeTime; /** Timestamp (milliseconds since boot) of the last time the power group was put to sleep. */ @@ -227,8 +229,8 @@ public class PowerGroup { } } - boolean dreamLocked(long eventTime, int uid) { - if (eventTime < mLastWakeTime || mWakefulness != WAKEFULNESS_AWAKE) { + boolean dreamLocked(long eventTime, int uid, boolean allowWake) { + if (eventTime < mLastWakeTime || (!allowWake && mWakefulness != WAKEFULNESS_AWAKE)) { return false; } @@ -244,7 +246,7 @@ public class PowerGroup { return true; } - boolean dozeLocked(long eventTime, int uid, int reason) { + boolean dozeLocked(long eventTime, int uid, @PowerManager.GoToSleepReason int reason) { if (eventTime < getLastWakeTimeLocked() || !isInteractive(mWakefulness)) { return false; } @@ -253,9 +255,14 @@ public class PowerGroup { try { reason = Math.min(PowerManager.GO_TO_SLEEP_REASON_MAX, Math.max(reason, PowerManager.GO_TO_SLEEP_REASON_MIN)); + long millisSinceLastUserActivity = eventTime - Math.max( + mLastUserActivityTimeNoChangeLights, mLastUserActivityTime); Slog.i(TAG, "Powering off display group due to " - + PowerManager.sleepReasonToString(reason) + " (groupId= " + getGroupId() - + ", uid= " + uid + ")..."); + + PowerManager.sleepReasonToString(reason) + + " (groupId= " + getGroupId() + ", uid= " + uid + + ", millisSinceLastUserActivity=" + millisSinceLastUserActivity + + ", lastUserActivityEvent=" + PowerManager.userActivityEventToString( + mLastUserActivityEvent) + ")..."); setSandmanSummonedLocked(/* isSandmanSummoned= */ true); setWakefulnessLocked(WAKEFULNESS_DOZING, eventTime, uid, reason, /* opUid= */ 0, @@ -266,14 +273,16 @@ public class PowerGroup { return true; } - boolean sleepLocked(long eventTime, int uid, int reason) { + boolean sleepLocked(long eventTime, int uid, @PowerManager.GoToSleepReason int reason) { if (eventTime < mLastWakeTime || getWakefulnessLocked() == WAKEFULNESS_ASLEEP) { return false; } Trace.traceBegin(Trace.TRACE_TAG_POWER, "sleepPowerGroup"); try { - Slog.i(TAG, "Sleeping power group (groupId=" + getGroupId() + ", uid=" + uid + ")..."); + Slog.i(TAG, + "Sleeping power group (groupId=" + getGroupId() + ", uid=" + uid + ", reason=" + + PowerManager.sleepReasonToString(reason) + ")..."); setSandmanSummonedLocked(/* isSandmanSummoned= */ true); setWakefulnessLocked(WAKEFULNESS_ASLEEP, eventTime, uid, reason, /* opUid= */0, /* opPackageName= */ null, /* details= */ null); @@ -287,16 +296,20 @@ public class PowerGroup { return mLastUserActivityTime; } - void setLastUserActivityTimeLocked(long lastUserActivityTime) { + void setLastUserActivityTimeLocked(long lastUserActivityTime, + @PowerManager.UserActivityEvent int event) { mLastUserActivityTime = lastUserActivityTime; + mLastUserActivityEvent = event; } public long getLastUserActivityTimeNoChangeLightsLocked() { return mLastUserActivityTimeNoChangeLights; } - public void setLastUserActivityTimeNoChangeLightsLocked(long time) { + public void setLastUserActivityTimeNoChangeLightsLocked(long time, + @PowerManager.UserActivityEvent int event) { mLastUserActivityTimeNoChangeLights = time; + mLastUserActivityEvent = event; } public int getUserActivitySummaryLocked() { diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index dbf05f1cd7c7..725fb3fec616 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -127,6 +127,7 @@ import com.android.server.am.BatteryStatsService; import com.android.server.lights.LightsManager; import com.android.server.lights.LogicalLight; import com.android.server.policy.WindowManagerPolicy; +import com.android.server.power.AmbientDisplaySuppressionController.AmbientDisplaySuppressionChangedCallback; import com.android.server.power.batterysaver.BatterySaverController; import com.android.server.power.batterysaver.BatterySaverPolicy; import com.android.server.power.batterysaver.BatterySaverStateMachine; @@ -467,6 +468,9 @@ public final class PowerManagerService extends SystemService // True if the device should wake up when plugged or unplugged. private boolean mWakeUpWhenPluggedOrUnpluggedConfig; + // True if the device should keep dreaming when undocked. + private boolean mKeepDreamingWhenUndockingConfig; + // True if the device should wake up when plugged or unplugged in theater mode. private boolean mWakeUpWhenPluggedOrUnpluggedInTheaterModeConfig; @@ -504,6 +508,9 @@ public final class PowerManagerService extends SystemService // effectively and terminate the dream. Use -1 to disable this safety feature. private int mDreamsBatteryLevelDrainCutoffConfig; + // Whether dreams should be disabled when ambient mode is suppressed. + private boolean mDreamsDisabledByAmbientModeSuppressionConfig; + // True if dreams are enabled by the user. private boolean mDreamsEnabledSetting; @@ -961,8 +968,8 @@ public final class PowerManagerService extends SystemService } AmbientDisplaySuppressionController createAmbientDisplaySuppressionController( - Context context) { - return new AmbientDisplaySuppressionController(context); + @NonNull AmbientDisplaySuppressionChangedCallback callback) { + return new AmbientDisplaySuppressionController(callback); } InattentiveSleepWarningController createInattentiveSleepWarningController() { @@ -1041,7 +1048,8 @@ public final class PowerManagerService extends SystemService mConstants = new Constants(mHandler); mAmbientDisplayConfiguration = mInjector.createAmbientDisplayConfiguration(context); mAmbientDisplaySuppressionController = - mInjector.createAmbientDisplaySuppressionController(context); + mInjector.createAmbientDisplaySuppressionController( + mAmbientSuppressionChangedCallback); mAttentionDetector = new AttentionDetector(this::onUserAttention, mLock); mFaceDownDetector = new FaceDownDetector(this::onFlip); mScreenUndimDetector = new ScreenUndimDetector(); @@ -1169,6 +1177,7 @@ public final class PowerManagerService extends SystemService return; } + Slog.i(TAG, "onFlip(): Face " + (isFaceDown ? "down." : "up.")); mIsFaceDown = isFaceDown; if (isFaceDown) { final long currentTime = mClock.uptimeMillis(); @@ -1373,6 +1382,8 @@ public final class PowerManagerService extends SystemService com.android.internal.R.bool.config_powerDecoupleInteractiveModeFromDisplay); mWakeUpWhenPluggedOrUnpluggedConfig = resources.getBoolean( com.android.internal.R.bool.config_unplugTurnsOnScreen); + mKeepDreamingWhenUndockingConfig = resources.getBoolean( + com.android.internal.R.bool.config_keepDreamingWhenUndocking); mWakeUpWhenPluggedOrUnpluggedInTheaterModeConfig = resources.getBoolean( com.android.internal.R.bool.config_allowTheaterModeWakeFromUnplug); mSuspendWhenScreenOffDueToProximityConfig = resources.getBoolean( @@ -1397,6 +1408,8 @@ public final class PowerManagerService extends SystemService com.android.internal.R.integer.config_dreamsBatteryLevelMinimumWhenNotPowered); mDreamsBatteryLevelDrainCutoffConfig = resources.getInteger( com.android.internal.R.integer.config_dreamsBatteryLevelDrainCutoff); + mDreamsDisabledByAmbientModeSuppressionConfig = resources.getBoolean( + com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig); mDozeAfterScreenOff = resources.getBoolean( com.android.internal.R.bool.config_dozeAfterScreenOffByDefault); mMinimumScreenOffTimeoutConfig = resources.getInteger( @@ -1888,12 +1901,13 @@ public final class PowerManagerService extends SystemService // Called from native code. @SuppressWarnings("unused") - private void userActivityFromNative(long eventTime, int event, int displayId, int flags) { + private void userActivityFromNative(long eventTime, @PowerManager.UserActivityEvent int event, + int displayId, int flags) { userActivityInternal(displayId, eventTime, event, flags, Process.SYSTEM_UID); } - private void userActivityInternal(int displayId, long eventTime, int event, int flags, - int uid) { + private void userActivityInternal(int displayId, long eventTime, + @PowerManager.UserActivityEvent int event, int flags, int uid) { synchronized (mLock) { if (displayId == Display.INVALID_DISPLAY) { if (userActivityNoUpdateLocked(eventTime, event, flags, uid)) { @@ -1917,6 +1931,13 @@ public final class PowerManagerService extends SystemService } } + private void napInternal(long eventTime, int uid, boolean allowWake) { + synchronized (mLock) { + dreamPowerGroupLocked(mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP), + eventTime, uid, allowWake); + } + } + private void onUserAttention() { synchronized (mLock) { if (userActivityNoUpdateLocked(mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP), @@ -1944,11 +1965,12 @@ public final class PowerManagerService extends SystemService @GuardedBy("mLock") private boolean userActivityNoUpdateLocked(final PowerGroup powerGroup, long eventTime, - int event, int flags, int uid) { + @PowerManager.UserActivityEvent int event, int flags, int uid) { final int groupId = powerGroup.getGroupId(); if (DEBUG_SPEW) { Slog.d(TAG, "userActivityNoUpdateLocked: groupId=" + groupId - + ", eventTime=" + eventTime + ", event=" + event + + ", eventTime=" + eventTime + + ", event=" + PowerManager.userActivityEventToString(event) + ", flags=0x" + Integer.toHexString(flags) + ", uid=" + uid); } @@ -1983,7 +2005,7 @@ public final class PowerManagerService extends SystemService if ((flags & PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS) != 0) { if (eventTime > powerGroup.getLastUserActivityTimeNoChangeLightsLocked() && eventTime > powerGroup.getLastUserActivityTimeLocked()) { - powerGroup.setLastUserActivityTimeNoChangeLightsLocked(eventTime); + powerGroup.setLastUserActivityTimeNoChangeLightsLocked(eventTime, event); mDirty |= DIRTY_USER_ACTIVITY; if (event == PowerManager.USER_ACTIVITY_EVENT_BUTTON) { mDirty |= DIRTY_QUIESCENT; @@ -1993,7 +2015,7 @@ public final class PowerManagerService extends SystemService } } else { if (eventTime > powerGroup.getLastUserActivityTimeLocked()) { - powerGroup.setLastUserActivityTimeLocked(eventTime); + powerGroup.setLastUserActivityTimeLocked(eventTime, event); mDirty |= DIRTY_USER_ACTIVITY; if (event == PowerManager.USER_ACTIVITY_EVENT_BUTTON) { mDirty |= DIRTY_QUIESCENT; @@ -2020,7 +2042,8 @@ public final class PowerManagerService extends SystemService @WakeReason int reason, String details, int uid, String opPackageName, int opUid) { if (DEBUG_SPEW) { Slog.d(TAG, "wakePowerGroupLocked: eventTime=" + eventTime - + ", groupId=" + powerGroup.getGroupId() + ", uid=" + uid); + + ", groupId=" + powerGroup.getGroupId() + + ", reason=" + PowerManager.wakeReasonToString(reason) + ", uid=" + uid); } if (mForceSuspendActive || !mSystemReady) { return; @@ -2030,7 +2053,8 @@ public final class PowerManagerService extends SystemService } @GuardedBy("mLock") - private boolean dreamPowerGroupLocked(PowerGroup powerGroup, long eventTime, int uid) { + private boolean dreamPowerGroupLocked(PowerGroup powerGroup, long eventTime, int uid, + boolean allowWake) { if (DEBUG_SPEW) { Slog.d(TAG, "dreamPowerGroup: groupId=" + powerGroup.getGroupId() + ", eventTime=" + eventTime + ", uid=" + uid); @@ -2038,16 +2062,16 @@ public final class PowerManagerService extends SystemService if (!mBootCompleted || !mSystemReady) { return false; } - return powerGroup.dreamLocked(eventTime, uid); + return powerGroup.dreamLocked(eventTime, uid, allowWake); } @GuardedBy("mLock") private boolean dozePowerGroupLocked(final PowerGroup powerGroup, long eventTime, - int reason, int uid) { + @GoToSleepReason int reason, int uid) { if (DEBUG_SPEW) { Slog.d(TAG, "dozePowerGroup: eventTime=" + eventTime - + ", groupId=" + powerGroup.getGroupId() + ", reason=" + reason - + ", uid=" + uid); + + ", groupId=" + powerGroup.getGroupId() + + ", reason=" + PowerManager.sleepReasonToString(reason) + ", uid=" + uid); } if (!mSystemReady || !mBootCompleted) { @@ -2058,10 +2082,12 @@ public final class PowerManagerService extends SystemService } @GuardedBy("mLock") - private boolean sleepPowerGroupLocked(final PowerGroup powerGroup, long eventTime, int reason, - int uid) { + private boolean sleepPowerGroupLocked(final PowerGroup powerGroup, long eventTime, + @GoToSleepReason int reason, int uid) { if (DEBUG_SPEW) { - Slog.d(TAG, "sleepPowerGroup: eventTime=" + eventTime + ", uid=" + uid); + Slog.d(TAG, "sleepPowerGroup: eventTime=" + eventTime + + ", groupId=" + powerGroup.getGroupId() + + ", reason=" + PowerManager.sleepReasonToString(reason) + ", uid=" + uid); } if (!mBootCompleted || !mSystemReady) { return false; @@ -2122,8 +2148,10 @@ public final class PowerManagerService extends SystemService case WAKEFULNESS_DOZING: traceMethodName = "goToSleep"; Slog.i(TAG, "Going to sleep due to " + PowerManager.sleepReasonToString(reason) - + " (uid " + uid + ")..."); - + + " (uid " + uid + ", screenOffTimeout=" + mScreenOffTimeoutSetting + + ", activityTimeoutWM=" + mUserActivityTimeoutOverrideFromWindowManager + + ", maxDimRatio=" + mMaximumScreenDimRatioConfig + + ", maxDimDur=" + mMaximumScreenDimDurationConfig + ")..."); mLastGlobalSleepTime = eventTime; mLastGlobalSleepReason = reason; mDozeStartInProgress = true; @@ -2479,6 +2507,14 @@ public final class PowerManagerService extends SystemService return false; } + // Don't wake when undocking while dreaming if configured not to. + if (mKeepDreamingWhenUndockingConfig + && getGlobalWakefulnessLocked() == WAKEFULNESS_DREAMING + && wasPowered && !mIsPowered + && oldPlugType == BatteryManager.BATTERY_PLUGGED_DOCK) { + return false; + } + // Don't wake when undocked from wireless charger. // See WirelessChargerDetector for justification. if (wasPowered && !mIsPowered @@ -3078,7 +3114,8 @@ public final class PowerManagerService extends SystemService changed = sleepPowerGroupLocked(powerGroup, time, PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE, Process.SYSTEM_UID); } else if (shouldNapAtBedTimeLocked()) { - changed = dreamPowerGroupLocked(powerGroup, time, Process.SYSTEM_UID); + changed = dreamPowerGroupLocked(powerGroup, time, + Process.SYSTEM_UID, /* allowWake= */ false); } else { changed = dozePowerGroupLocked(powerGroup, time, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT, Process.SYSTEM_UID); @@ -3218,8 +3255,10 @@ public final class PowerManagerService extends SystemService if (mDreamManager != null) { // Restart the dream whenever the sandman is summoned. if (startDreaming) { - mDreamManager.stopDream(/* immediate= */ false); - mDreamManager.startDream(wakefulness == WAKEFULNESS_DOZING); + mDreamManager.stopDream(/* immediate= */ false, + "power manager request before starting dream" /*reason*/); + mDreamManager.startDream(wakefulness == WAKEFULNESS_DOZING, + "power manager request" /*reason*/); } isDreaming = mDreamManager.isDreaming(); } else { @@ -3297,23 +3336,43 @@ public final class PowerManagerService extends SystemService } // Doze has ended or will be stopped. Update the power state. - sleepPowerGroupLocked(powerGroup, now, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT, + sleepPowerGroupLocked(powerGroup, now, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT, Process.SYSTEM_UID); } } // Stop dream. if (isDreaming) { - mDreamManager.stopDream(/* immediate= */ false); + mDreamManager.stopDream(/* immediate= */ false, "power manager request" /*reason*/); + } + } + + @GuardedBy("mLock") + private void onDreamSuppressionChangedLocked(final boolean isSuppressed) { + if (!mDreamsDisabledByAmbientModeSuppressionConfig) { + return; + } + if (!isSuppressed && mIsPowered && mDreamsSupportedConfig && mDreamsEnabledSetting + && shouldNapAtBedTimeLocked() && isItBedTimeYetLocked( + mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP))) { + napInternal(SystemClock.uptimeMillis(), Process.SYSTEM_UID, /* allowWake= */ true); + } else if (isSuppressed) { + mDirty |= DIRTY_SETTINGS; + updatePowerStateLocked(); } } + /** * Returns true if the {@code groupId} is allowed to dream in its current state. */ @GuardedBy("mLock") private boolean canDreamLocked(final PowerGroup powerGroup) { + final boolean dreamsSuppressed = mDreamsDisabledByAmbientModeSuppressionConfig + && mAmbientDisplaySuppressionController.isSuppressed(); + if (!mBootCompleted + || dreamsSuppressed || getGlobalWakefulnessLocked() != WAKEFULNESS_DREAMING || !mDreamsSupportedConfig || !mDreamsEnabledSetting @@ -4207,7 +4266,7 @@ public final class PowerManagerService extends SystemService void onUserActivity() { synchronized (mLock) { mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP).setLastUserActivityTimeLocked( - mClock.uptimeMillis()); + mClock.uptimeMillis(), PowerManager.USER_ACTIVITY_EVENT_OTHER); } } @@ -4398,6 +4457,8 @@ public final class PowerManagerService extends SystemService + mWakeUpWhenPluggedOrUnpluggedInTheaterModeConfig); pw.println(" mTheaterModeEnabled=" + mTheaterModeEnabled); + pw.println(" mKeepDreamingWhenUndockingConfig=" + + mKeepDreamingWhenUndockingConfig); pw.println(" mSuspendWhenScreenOffDueToProximityConfig=" + mSuspendWhenScreenOffDueToProximityConfig); pw.println(" mDreamsSupportedConfig=" + mDreamsSupportedConfig); @@ -5003,6 +5064,16 @@ public final class PowerManagerService extends SystemService } }; + private final AmbientDisplaySuppressionChangedCallback mAmbientSuppressionChangedCallback = + new AmbientDisplaySuppressionChangedCallback() { + @Override + public void onSuppressionChanged(boolean isSuppressed) { + synchronized (mLock) { + onDreamSuppressionChangedLocked(isSuppressed); + } + } + }; + /** * Callback for asynchronous operations performed by the power manager. */ @@ -5590,7 +5661,8 @@ public final class PowerManagerService extends SystemService } @Override // Binder call - public void userActivity(int displayId, long eventTime, int event, int flags) { + public void userActivity(int displayId, long eventTime, + @PowerManager.UserActivityEvent int event, int flags) { final long now = mClock.uptimeMillis(); if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER) != PackageManager.PERMISSION_GRANTED @@ -5690,10 +5762,7 @@ public final class PowerManagerService extends SystemService final int uid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); try { - synchronized (mLock) { - dreamPowerGroupLocked(mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP), - eventTime, uid); - } + napInternal(eventTime, uid, /* allowWake= */ false); } finally { Binder.restoreCallingIdentity(ident); } @@ -6621,6 +6690,16 @@ public final class PowerManagerService extends SystemService public boolean interceptPowerKeyDown(KeyEvent event) { return interceptPowerKeyDownInternal(event); } + + @Override + public void nap(long eventTime, boolean allowWake) { + napInternal(eventTime, Process.SYSTEM_UID, allowWake); + } + + @Override + public boolean isAmbientDisplaySuppressed() { + return mAmbientDisplaySuppressionController.isSuppressed(); + } } /** diff --git a/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java index 8b30995404f0..d8e6c262359d 100644 --- a/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java +++ b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java @@ -53,7 +53,7 @@ public class PowerStatsDataStorage { private static class DataElement { private static final int LENGTH_FIELD_WIDTH = 4; - private static final int MAX_DATA_ELEMENT_SIZE = 1000; + private static final int MAX_DATA_ELEMENT_SIZE = 32768; private byte[] mData; diff --git a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java index fd6ec065d421..f744d00b2066 100644 --- a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java +++ b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java @@ -46,6 +46,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener, SensorEventListener { @@ -275,7 +277,7 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis public void onSensorChanged(SensorEvent event) { // Using log space to represent human sensation (Fechner's Law) instead of lux // because lux values causes bright flashes to skew the average very high. - addElement(event.timestamp, Math.max(0, + addElement(TimeUnit.NANOSECONDS.toMillis(event.timestamp), Math.max(0, (int) (Math.log(event.values[0]) * LIGHT_VALUE_MULTIPLIER))); updateLightSession(); mHandler.removeCallbacksAndMessages(mDelayedUpdateToken); diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 653b51a95993..3df8f584fef1 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -2253,6 +2253,25 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; + boolean proto = false; + for (int i = 0; i < args.length; i++) { + if ("--proto".equals(args[i])) { + proto = true; + } + } + if (proto) { + if (mBar == null) return; + try (TransferPipe tp = new TransferPipe()) { + // Sending the command to the remote, which needs to execute async to avoid blocking + // See Binder#dumpAsync() for inspiration + mBar.dumpProto(args, tp.getWriteFd()); + // Times out after 5s + tp.go(fd); + } catch (Throwable t) { + Slog.e(TAG, "Error sending command to IStatusBar", t); + } + return; + } synchronized (mLock) { for (int i = 0; i < mDisplayUiState.size(); i++) { diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index f888ff60b12a..2888b9a2d3cc 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -602,9 +602,12 @@ public class TrustManagerService extends SystemService { synchronized (mUserTrustState) { wasTrusted = (mUserTrustState.get(userId) == TrustState.TRUSTED); wasTrustable = (mUserTrustState.get(userId) == TrustState.TRUSTABLE); + boolean isAutomotive = getContext().getPackageManager().hasSystemFeature( + PackageManager.FEATURE_AUTOMOTIVE); boolean renewingTrust = wasTrustable && ( (flags & TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) != 0); - boolean canMoveToTrusted = alreadyUnlocked || isFromUnlock || renewingTrust; + boolean canMoveToTrusted = + alreadyUnlocked || isFromUnlock || renewingTrust || isAutomotive; boolean upgradingTrustForCurrentUser = (userId == mCurrentUser); if (trustedByAtLeastOneAgent && wasTrusted) { @@ -687,7 +690,7 @@ public class TrustManagerService extends SystemService { */ public void lockUser(int userId) { mLockPatternUtils.requireStrongAuth( - StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, userId); + StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, userId); try { WindowManagerGlobal.getWindowManagerService().lockNow(null); } catch (RemoteException e) { @@ -2084,7 +2087,7 @@ public class TrustManagerService extends SystemService { if (mStrongAuthTracker.isTrustAllowedForUser(mUserId)) { if (DEBUG) Slog.d(TAG, "Revoking all trust because of trust timeout"); mLockPatternUtils.requireStrongAuth( - mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, mUserId); + mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, mUserId); } maybeLockScreen(mUserId); } diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java index 0799b955b6f1..9c455dbb5320 100644 --- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java +++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java @@ -65,6 +65,9 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { public final DeviceVibrationEffectAdapter deviceEffectAdapter; public final VibrationThread.VibratorManagerHooks vibratorManagerHooks; + // Not guarded by lock because they're not modified by this conductor, it's used here only to + // check immutable attributes. The status and other mutable states are changed by the service or + // by the vibrator steps. private final Vibration mVibration; private final SparseArray<VibratorController> mVibrators = new SparseArray<>(); @@ -405,6 +408,16 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { } } + /** Returns true if a cancellation signal was sent via {@link #notifyCancelled}. */ + public boolean wasNotifiedToCancel() { + if (Build.IS_DEBUGGABLE) { + expectIsVibrationThread(false); + } + synchronized (mLock) { + return mSignalCancel != null; + } + } + @GuardedBy("mLock") private boolean hasPendingNotifySignalLocked() { if (Build.IS_DEBUGGABLE) { diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 97275583ac36..14693599c3ec 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -852,8 +852,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } Vibration currentVibration = mCurrentVibration.getVibration(); - if (currentVibration.hasEnded()) { - // Current vibration is finishing up, it should not block incoming vibrations. + if (currentVibration.hasEnded() || mCurrentVibration.wasNotifiedToCancel()) { + // Current vibration has ended or is cancelling, should not block incoming vibrations. return null; } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index e197319707e3..5f420bfc7151 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -1166,7 +1166,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub try { connection.mService.attach(connection, mToken, TYPE_WALLPAPER, false, wpdData.mWidth, wpdData.mHeight, - wpdData.mPadding, mDisplayId); + wpdData.mPadding, mDisplayId, FLAG_SYSTEM | FLAG_LOCK); } catch (RemoteException e) { Slog.w(TAG, "Failed attaching wallpaper on display", e); if (wallpaper != null && !wallpaper.wallpaperUpdating diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index d2a00af245f6..eca2e7441f29 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -27,6 +27,8 @@ import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.service.voice.VoiceInteractionSession.SHOW_SOURCE_APPLICATION; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_IMMERSIVE; @@ -332,8 +334,8 @@ class ActivityClientController extends IActivityClientController.Stub { } @Override - public boolean navigateUpTo(IBinder token, Intent destIntent, int resultCode, - Intent resultData) { + public boolean navigateUpTo(IBinder token, Intent destIntent, String resolvedType, + int resultCode, Intent resultData) { final ActivityRecord r; synchronized (mGlobalLock) { r = ActivityRecord.isInRootTaskLocked(token); @@ -348,7 +350,7 @@ class ActivityClientController extends IActivityClientController.Stub { synchronized (mGlobalLock) { return r.getRootTask().navigateUpTo( - r, destIntent, destGrants, resultCode, resultData, resultGrants); + r, destIntent, resolvedType, destGrants, resultCode, resultData, resultGrants); } } @@ -707,7 +709,26 @@ class ActivityClientController extends IActivityClientController.Stub { try { synchronized (mGlobalLock) { final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token); - return r != null && r.setOccludesParent(true); + // Create a transition if the activity is playing in case the below activity didn't + // commit invisible. That's because if any activity below this one has changed its + // visibility while playing transition, there won't able to commit visibility until + // the running transition finish. + final Transition transition = r != null + && r.mTransitionController.inPlayingTransition(r) + ? r.mTransitionController.createTransition(TRANSIT_TO_BACK) : null; + if (transition != null) { + r.mTransitionController.requestStartTransition(transition, null /*startTask */, + null /* remoteTransition */, null /* displayChange */); + } + final boolean changed = r != null && r.setOccludesParent(true); + if (transition != null) { + if (changed) { + r.mTransitionController.setReady(r.getDisplayContent()); + } else { + transition.abort(); + } + } + return changed; } } finally { Binder.restoreCallingIdentity(origId); @@ -728,7 +749,25 @@ class ActivityClientController extends IActivityClientController.Stub { if (under != null) { under.returningOptions = safeOptions != null ? safeOptions.getOptions(r) : null; } - return r.setOccludesParent(false); + // Create a transition if the activity is playing in case the current activity + // didn't commit invisible. That's because if this activity has changed its + // visibility while playing transition, there won't able to commit visibility until + // the running transition finish. + final Transition transition = r.mTransitionController.inPlayingTransition(r) + ? r.mTransitionController.createTransition(TRANSIT_TO_FRONT) : null; + if (transition != null) { + r.mTransitionController.requestStartTransition(transition, null /*startTask */, + null /* remoteTransition */, null /* displayChange */); + } + final boolean changed = r.setOccludesParent(false); + if (transition != null) { + if (changed) { + r.mTransitionController.setReady(r.getDisplayContent()); + } else { + transition.abort(); + } + } + return changed; } } finally { Binder.restoreCallingIdentity(origId); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index b8486e7aa2b4..2eb2cf643c42 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -120,6 +120,8 @@ import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND; import static android.view.WindowManager.TRANSIT_OLD_UNSET; +import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; +import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ANIM; @@ -658,7 +660,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A private final WindowState.UpdateReportedVisibilityResults mReportedVisibilityResults = new WindowState.UpdateReportedVisibilityResults(); - boolean mUseTransferredAnimation; + int mTransitionChangeFlags; /** Whether we need to setup the animation to animate only within the letterbox. */ private boolean mNeedsLetterboxedAnimation; @@ -4074,7 +4076,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // to the restarted activity. nowVisible = mVisibleRequested; } - mTransitionController.requestCloseTransitionIfNeeded(this); + // upgrade transition trigger to task if this is the last activity since it means we are + // closing the task. + final WindowContainer trigger = remove && task != null && task.getChildCount() == 1 + ? task : this; + mTransitionController.requestCloseTransitionIfNeeded(trigger); cleanUp(true /* cleanServices */, true /* setState */); if (remove) { if (mStartingData != null && mVisible && task != null) { @@ -4395,10 +4401,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // When transferring an animation, we no longer need to apply an animation to // the token we transfer the animation over. Thus, set this flag to indicate // we've transferred the animation. - mUseTransferredAnimation = true; + mTransitionChangeFlags |= FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; } else if (mTransitionController.getTransitionPlayer() != null) { // In the new transit system, just set this every time we transfer the window - mUseTransferredAnimation = true; + mTransitionChangeFlags |= FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; } // Post cleanup after the visibility and animation are transferred. fromActivity.postWindowRemoveStartingWindowCleanup(tStartingWindow); @@ -5247,6 +5253,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // If in a transition, defer commits for activities that are going invisible if (!visible && inTransition()) { + if (mTransitionController.inPlayingTransition(this) + && mTransitionController.isCollecting(this)) { + mTransitionChangeFlags |= FLAG_IS_OCCLUDED; + } return; } // If we are preparing an app transition, then delay changing @@ -5297,7 +5307,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override boolean applyAnimation(LayoutParams lp, @TransitionOldType int transit, boolean enter, boolean isVoiceInteraction, @Nullable ArrayList<WindowContainer> sources) { - if (mUseTransferredAnimation) { + if ((mTransitionChangeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { return false; } // If it was set to true, reset the last request to force the transition. @@ -5370,7 +5380,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mWmService.mWindowPlacerLocked.performSurfacePlacement(); } displayContent.getInputMonitor().updateInputWindowsLw(false /*force*/); - mUseTransferredAnimation = false; + mTransitionChangeFlags = 0; postApplyAnimation(visible, fromTransition); } @@ -5812,8 +5822,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // in untrusted mode. Traverse bottom to top with boundary so that it will only check // activities above this activity. final ActivityRecord differentUidOverlayActivity = getTask().getActivity( - a -> a.getUid() != getUid(), this /* boundary */, false /* includeBoundary */, - false /* traverseTopToBottom */); + a -> !a.finishing && a.getUid() != getUid(), this /* boundary */, + false /* includeBoundary */, false /* traverseTopToBottom */); return differentUidOverlayActivity != null; } @@ -7628,6 +7638,31 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */); } + /** + * Returns the requested {@link Configuration.Orientation} for the current activity. + * + * <p>When The current orientation is set to {@link SCREEN_ORIENTATION_BEHIND} it returns the + * requested orientation for the activity below which is the first activity with an explicit + * (different from {@link SCREEN_ORIENTATION_UNSET}) orientation which is not {@link + * SCREEN_ORIENTATION_BEHIND}. + */ + @Configuration.Orientation + @Override + int getRequestedConfigurationOrientation(boolean forDisplay) { + if (mOrientation == SCREEN_ORIENTATION_BEHIND && task != null) { + // We use Task here because we want to be consistent with what happens in + // multi-window mode where other tasks orientations are ignored. + final ActivityRecord belowCandidate = task.getActivity( + a -> a.mOrientation != SCREEN_ORIENTATION_UNSET && !a.finishing + && a.mOrientation != ActivityInfo.SCREEN_ORIENTATION_BEHIND, this, + false /* includeBoundary */, true /* traverseTopToBottom */); + if (belowCandidate != null) { + return belowCandidate.getRequestedConfigurationOrientation(forDisplay); + } + } + return super.getRequestedConfigurationOrientation(forDisplay); + } + @Override void onCancelFixedRotationTransform(int originalDisplayRotation) { if (this != mDisplayContent.getLastOrientationSource()) { diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index d131457cb9a2..c49d6729effc 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -50,6 +50,7 @@ import android.util.Slog; import android.util.SparseArray; import android.view.RemoteAnimationAdapter; import android.view.WindowManager; +import android.window.RemoteTransition; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; @@ -391,9 +392,9 @@ public class ActivityStartController { SafeActivityOptions bottomOptions = null; if (options != null) { // To ensure the first N-1 activities (N == total # of activities) are also launched - // into the correct display, use a copy of the passed-in options (keeping only - // display-related info) for these activities. - bottomOptions = options.selectiveCloneDisplayOptions(); + // into the correct display and root task, use a copy of the passed-in options (keeping + // only display-related and launch-root-task information) for these activities. + bottomOptions = options.selectiveCloneLaunchOptions(); } try { intents = ArrayUtils.filterNotNull(intents, Intent[]::new); @@ -566,14 +567,39 @@ public class ActivityStartController { return false; } mService.mRootWindowContainer.startPowerModeLaunchIfNeeded(true /* forceSend */, r); + final RemoteTransition remote = options.getRemoteTransition(); + if (remote != null && rootTask.mTransitionController.isCollecting()) { + final Transition transition = new Transition(WindowManager.TRANSIT_TO_FRONT, + 0 /* flags */, rootTask.mTransitionController, + mService.mWindowManager.mSyncEngine); + // Special case: we are entering recents while an existing transition is running. In + // this case, we know it's safe to "defer" the activity launch, so lets do so now so + // that it can get its own transition and thus update launcher correctly. + mService.mWindowManager.mSyncEngine.queueSyncSet( + () -> rootTask.mTransitionController.moveToCollecting(transition), + () -> { + final Task task = r.getTask(); + task.mTransitionController.requestStartTransition(transition, + task, remote, null /* displayChange */); + task.mTransitionController.collect(task); + startExistingRecentsIfPossibleInner(intent, options, r, task, rootTask); + }); + } else { + final Task task = r.getTask(); + task.mTransitionController.requestTransitionIfNeeded(WindowManager.TRANSIT_TO_FRONT, + 0 /* flags */, task, task /* readyGroupRef */, + options.getRemoteTransition(), null /* displayChange */); + startExistingRecentsIfPossibleInner(intent, options, r, task, rootTask); + } + return true; + } + + void startExistingRecentsIfPossibleInner(Intent intent, ActivityOptions options, + ActivityRecord r, Task task, Task rootTask) { final ActivityMetricsLogger.LaunchingState launchingState = mSupervisor.getActivityMetricsLogger().notifyActivityLaunching(intent); - final Task task = r.getTask(); mService.deferWindowLayout(); try { - task.mTransitionController.requestTransitionIfNeeded(WindowManager.TRANSIT_TO_FRONT, - 0 /* flags */, task, task /* readyGroupRef */, - options.getRemoteTransition(), null /* displayChange */); r.mTransitionController.setTransientLaunch(r, TaskDisplayArea.getRootTaskAbove(rootTask)); task.moveToFront("startExistingRecents"); @@ -585,7 +611,6 @@ public class ActivityStartController { task.mInResumeTopActivity = false; mService.continueWindowLayout(); } - return true; } void registerRemoteAnimationForNextActivityStart(String packageName, diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index a870b8afe2f9..fe691c61a96b 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -1481,7 +1481,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { handleNonResizableTaskIfNeeded(task, WINDOWING_MODE_UNDEFINED, mRootWindowContainer.getDefaultTaskDisplayArea(), currentRootTask, forceNonResizeable); - if (r != null) { + if (r != null && (options == null || !options.getDisableStartingWindow())) { // Use a starting window to reduce the transition latency for reshowing the task. // Note that with shell transition, this should be executed before requesting // transition to avoid delaying the starting window. @@ -2593,6 +2593,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // activity lifecycle transaction to make sure the override pending app // transition will be applied immediately. targetActivity.applyOptionsAnimation(); + if (activityOptions != null && activityOptions.getLaunchCookie() != null) { + targetActivity.mLaunchCookie = activityOptions.getLaunchCookie(); + } } finally { mActivityMetricsLogger.notifyActivityLaunched(launchingState, START_TASK_TO_FRONT, false /* newActivityCreated */, diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java index fd6c9743cb6b..b160af6a3e11 100644 --- a/services/core/java/com/android/server/wm/AppTaskImpl.java +++ b/services/core/java/com/android/server/wm/AppTaskImpl.java @@ -98,7 +98,7 @@ class AppTaskImpl extends IAppTask.Stub { throw new IllegalArgumentException("Unable to find task ID " + mTaskId); } return mService.getRecentTasks().createRecentTaskInfo(task, - false /* stripExtras */); + false /* stripExtras */, true /* getTasksAllowed */); } finally { Binder.restoreCallingIdentity(origId); } diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java index 5c1a877cc865..27370bfe45dc 100644 --- a/services/core/java/com/android/server/wm/AppTransition.java +++ b/services/core/java/com/android/server/wm/AppTransition.java @@ -1460,6 +1460,12 @@ public class AppTransition implements Dump { || transit == TRANSIT_OLD_ACTIVITY_RELAUNCH; } + static boolean isTaskFragmentTransitOld(@TransitionOldType int transit) { + return transit == TRANSIT_OLD_TASK_FRAGMENT_OPEN + || transit == TRANSIT_OLD_TASK_FRAGMENT_CLOSE + || transit == TRANSIT_OLD_TASK_FRAGMENT_CHANGE; + } + static boolean isChangeTransitOld(@TransitionOldType int transit) { return transit == TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE || transit == TRANSIT_OLD_TASK_FRAGMENT_CHANGE; diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java index 8c5f05365837..7d9ae87517b0 100644 --- a/services/core/java/com/android/server/wm/AsyncRotationController.java +++ b/services/core/java/com/android/server/wm/AsyncRotationController.java @@ -202,8 +202,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume // target windows. But the windows still need to use sync transaction to keep the appearance // in previous rotation, so request a no-op sync to keep the state. for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { - if (TransitionController.SYNC_METHOD != BLASTSyncEngine.METHOD_BLAST - && mTargetWindowTokens.valueAt(i).mAction != Operation.ACTION_SEAMLESS) { + if (mTargetWindowTokens.valueAt(i).canDrawBeforeStartTransaction()) { // Expect a screenshot layer will cover the non seamless windows. continue; } @@ -489,7 +488,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume return false; } final Operation op = mTargetWindowTokens.get(w.mToken); - if (op == null) return false; + if (op == null || op.canDrawBeforeStartTransaction()) return false; if (DEBUG) Slog.d(TAG, "handleFinishDrawing " + w); if (op.mDrawTransaction == null) { if (w.isClientLocal()) { @@ -554,5 +553,14 @@ class AsyncRotationController extends FadeAnimationController implements Consume Operation(@Action int action) { mAction = action; } + + /** + * Returns {@code true} if the corresponding window can draw its latest content before the + * start transaction of rotation transition is applied. + */ + boolean canDrawBeforeStartTransaction() { + return TransitionController.SYNC_METHOD != BLASTSyncEngine.METHOD_BLAST + && mAction != ACTION_SEAMLESS; + } } } diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java new file mode 100644 index 000000000000..5e44d6c72bca --- /dev/null +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.util.DisplayMetrics.DENSITY_DEFAULT; + +import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; + +import android.annotation.Nullable; +import android.app.ActivityOptions; +import android.content.pm.ActivityInfo; +import android.graphics.Rect; +import android.os.SystemProperties; +import android.util.Slog; + +import com.android.server.wm.LaunchParamsController.LaunchParamsModifier; + +/** + * The class that defines default launch params for tasks in desktop mode + */ +public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { + + private static final String TAG = + TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM; + private static final boolean DEBUG = false; + + // Desktop mode feature flag. + static final boolean DESKTOP_MODE_SUPPORTED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_mode", false); + // Override default freeform task width when desktop mode is enabled. In dips. + private static final int DESKTOP_MODE_DEFAULT_WIDTH_DP = SystemProperties.getInt( + "persist.wm.debug.desktop_mode.default_width", 840); + // Override default freeform task height when desktop mode is enabled. In dips. + private static final int DESKTOP_MODE_DEFAULT_HEIGHT_DP = SystemProperties.getInt( + "persist.wm.debug.desktop_mode.default_height", 630); + + private StringBuilder mLogBuilder; + + @Override + public int onCalculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout, + @Nullable ActivityRecord activity, @Nullable ActivityRecord source, + @Nullable ActivityOptions options, @Nullable ActivityStarter.Request request, int phase, + LaunchParamsController.LaunchParams currentParams, + LaunchParamsController.LaunchParams outParams) { + + initLogBuilder(task, activity); + int result = calculate(task, layout, activity, source, options, request, phase, + currentParams, outParams); + outputLog(); + return result; + } + + private int calculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout, + @Nullable ActivityRecord activity, @Nullable ActivityRecord source, + @Nullable ActivityOptions options, @Nullable ActivityStarter.Request request, int phase, + LaunchParamsController.LaunchParams currentParams, + LaunchParamsController.LaunchParams outParams) { + + if (task == null) { + appendLog("task null, skipping"); + return RESULT_SKIP; + } + if (phase != PHASE_BOUNDS) { + appendLog("not in bounds phase, skipping"); + return RESULT_SKIP; + } + if (!task.inFreeformWindowingMode()) { + appendLog("not a freeform task, skipping"); + return RESULT_SKIP; + } + if (!currentParams.mBounds.isEmpty()) { + appendLog("currentParams has bounds set, not overriding"); + return RESULT_SKIP; + } + + // Copy over any values + outParams.set(currentParams); + + // Update width and height with default desktop mode values + float density = (float) task.getConfiguration().densityDpi / DENSITY_DEFAULT; + final int width = (int) (DESKTOP_MODE_DEFAULT_WIDTH_DP * density + 0.5f); + final int height = (int) (DESKTOP_MODE_DEFAULT_HEIGHT_DP * density + 0.5f); + outParams.mBounds.right = width; + outParams.mBounds.bottom = height; + + // Center the task in window bounds + Rect windowBounds = task.getWindowConfiguration().getBounds(); + outParams.mBounds.offset(windowBounds.centerX() - outParams.mBounds.centerX(), + windowBounds.centerY() - outParams.mBounds.centerY()); + + appendLog("setting desktop mode task bounds to %s", outParams.mBounds); + + return RESULT_DONE; + } + + private void initLogBuilder(Task task, ActivityRecord activity) { + if (DEBUG) { + mLogBuilder = new StringBuilder( + "DesktopModeLaunchParamsModifier: task=" + task + " activity=" + activity); + } + } + + private void appendLog(String format, Object... args) { + if (DEBUG) mLogBuilder.append(" ").append(String.format(format, args)); + } + + private void outputLog() { + if (DEBUG) Slog.d(TAG, mLogBuilder.toString()); + } +} diff --git a/services/core/java/com/android/server/wm/DeviceStateController.java b/services/core/java/com/android/server/wm/DeviceStateController.java new file mode 100644 index 000000000000..a6f855755192 --- /dev/null +++ b/services/core/java/com/android/server/wm/DeviceStateController.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.devicestate.DeviceStateManager; +import android.os.Handler; +import android.os.HandlerExecutor; + +import com.android.internal.util.ArrayUtils; + +import java.util.function.Consumer; + +/** + * Class that registers callbacks with the {@link DeviceStateManager} and + * responds to fold state changes by forwarding such events to a delegate. + */ +final class DeviceStateController { + private final DeviceStateManager mDeviceStateManager; + private final Context mContext; + + private FoldStateListener mDeviceStateListener; + + public enum FoldState { + UNKNOWN, OPEN, FOLDED, HALF_FOLDED + } + + DeviceStateController(Context context, Handler handler, Consumer<FoldState> delegate) { + mContext = context; + mDeviceStateManager = mContext.getSystemService(DeviceStateManager.class); + if (mDeviceStateManager != null) { + mDeviceStateListener = new FoldStateListener(mContext, delegate); + mDeviceStateManager + .registerCallback(new HandlerExecutor(handler), + mDeviceStateListener); + } + } + + void unregisterFromDeviceStateManager() { + if (mDeviceStateListener != null) { + mDeviceStateManager.unregisterCallback(mDeviceStateListener); + } + } + + /** + * A listener for half-fold device state events that dispatches state changes to a delegate. + */ + static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback { + + private final int[] mHalfFoldedDeviceStates; + private final int[] mFoldedDeviceStates; + + @Nullable + private FoldState mLastResult; + private final Consumer<FoldState> mDelegate; + + FoldStateListener(Context context, Consumer<FoldState> delegate) { + mFoldedDeviceStates = context.getResources().getIntArray( + com.android.internal.R.array.config_foldedDeviceStates); + mHalfFoldedDeviceStates = context.getResources().getIntArray( + com.android.internal.R.array.config_halfFoldedDeviceStates); + mDelegate = delegate; + } + + @Override + public void onStateChanged(int state) { + final boolean halfFolded = ArrayUtils.contains(mHalfFoldedDeviceStates, state); + FoldState result; + if (halfFolded) { + result = FoldState.HALF_FOLDED; + } else { + final boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state); + result = folded ? FoldState.FOLDED : FoldState.OPEN; + } + if (mLastResult == null || !mLastResult.equals(result)) { + mLastResult = result; + mDelegate.accept(result); + } + } + } +} diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 71c80fb9a97c..38f6a53982c4 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -564,6 +564,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final FixedRotationTransitionListener mFixedRotationTransitionListener = new FixedRotationTransitionListener(); + private final DeviceStateController mDeviceStateController; private final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher; final RemoteDisplayChangeController mRemoteDisplayChangeController; @@ -1119,6 +1120,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDisplayPolicy = new DisplayPolicy(mWmService, this); mDisplayRotation = new DisplayRotation(mWmService, this); + + mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH, + newFoldState -> { + mDisplaySwitchTransitionLauncher.foldStateChanged(newFoldState); + mDisplayRotation.foldStateChanged(newFoldState); + }); + mCloseToSquareMaxAspectRatio = mWmService.mContext.getResources().getFloat( R.dimen.config_closeToSquareDisplayMaxAspectRatio); if (isDefaultDisplay) { @@ -3218,7 +3226,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener); handleAnimatingStoppedAndTransition(); mWmService.stopFreezingDisplayLocked(); - mDisplaySwitchTransitionLauncher.destroy(); + mDeviceStateController.unregisterFromDeviceStateManager(); super.removeImmediately(); if (DEBUG_DISPLAY) Slog.v(TAG_WM, "Removing display=" + this); mPointerEventDispatcher.dispose(); diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 4c69f87106d1..42a3ec6abbc5 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -2710,7 +2710,7 @@ public class DisplayPolicy { * * @param screenshotType The type of screenshot, for example either * {@link WindowManager#TAKE_SCREENSHOT_FULLSCREEN} or - * {@link WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} + * {@link WindowManager#TAKE_SCREENSHOT_PROVIDED_IMAGE} * @param source Where the screenshot originated from (see WindowManager.ScreenshotSource) */ public void takeScreenshot(int screenshotType, int source) { diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 97609a7dd8ba..a8d13c57ffa2 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -40,6 +40,7 @@ import static com.android.server.wm.WindowManagerService.WINDOW_FREEZE_TIMEOUT_D import android.annotation.AnimRes; import android.annotation.IntDef; +import android.annotation.Nullable; import android.app.ActivityManager; import android.content.ContentResolver; import android.content.Context; @@ -108,6 +109,8 @@ public class DisplayRotation { private OrientationListener mOrientationListener; private StatusBarManagerInternal mStatusBarManagerInternal; private SettingsObserver mSettingsObserver; + @Nullable + private FoldController mFoldController; @ScreenOrientation private int mCurrentAppOrientation = SCREEN_ORIENTATION_UNSPECIFIED; @@ -238,6 +241,10 @@ public class DisplayRotation { mOrientationListener.setCurrentRotation(mRotation); mSettingsObserver = new SettingsObserver(uiHandler); mSettingsObserver.observe(); + if (mSupportAutoRotation && mContext.getResources().getBoolean( + R.bool.config_windowManagerHalfFoldAutoRotateOverride)) { + mFoldController = new FoldController(); + } } } @@ -436,7 +443,17 @@ public class DisplayRotation { final int oldRotation = mRotation; final int lastOrientation = mLastOrientation; - final int rotation = rotationForOrientation(lastOrientation, oldRotation); + int rotation = rotationForOrientation(lastOrientation, oldRotation); + // Use the saved rotation for tabletop mode, if set. + if (mFoldController != null && mFoldController.shouldRevertOverriddenRotation()) { + int prevRotation = rotation; + rotation = mFoldController.revertOverriddenRotation(); + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Reverting orientation. Rotating to %s from %s rather than %s.", + Surface.rotationToString(rotation), + Surface.rotationToString(oldRotation), + Surface.rotationToString(prevRotation)); + } ProtoLog.v(WM_DEBUG_ORIENTATION, "Computed rotation=%s (%d) for display id=%d based on lastOrientation=%s (%d) and " + "oldRotation=%s (%d)", @@ -1138,7 +1155,8 @@ public class DisplayRotation { // If we don't support auto-rotation then bail out here and ignore // the sensor and any rotation lock settings. preferredRotation = -1; - } else if ((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_FREE + } else if (((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_FREE + || isTabletopAutoRotateOverrideEnabled()) && (orientation == ActivityInfo.SCREEN_ORIENTATION_USER || orientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED || orientation == ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE @@ -1292,10 +1310,17 @@ public class DisplayRotation { return false; } + private boolean isTabletopAutoRotateOverrideEnabled() { + return mFoldController != null && mFoldController.overrideFrozenRotation(); + } + private boolean isRotationChoicePossible(int orientation) { // Rotation choice is only shown when the user is in locked mode. if (mUserRotationMode != WindowManagerPolicy.USER_ROTATION_LOCKED) return false; + // Don't show rotation choice if we are in tabletop or book modes. + if (isTabletopAutoRotateOverrideEnabled()) return false; + // We should only enable rotation choice if the rotation isn't forced by the lid, dock, // demo, hdmi, vr, etc mode. @@ -1496,6 +1521,74 @@ public class DisplayRotation { proto.end(token); } + /** + * Called by the DeviceStateManager callback when the device state changes. + */ + void foldStateChanged(DeviceStateController.FoldState foldState) { + if (mFoldController != null) { + synchronized (mLock) { + mFoldController.foldStateChanged(foldState); + } + } + } + + private class FoldController { + @Surface.Rotation + private int mHalfFoldSavedRotation = -1; // No saved rotation + private DeviceStateController.FoldState mFoldState = + DeviceStateController.FoldState.UNKNOWN; + + boolean overrideFrozenRotation() { + return mFoldState == DeviceStateController.FoldState.HALF_FOLDED; + } + + boolean shouldRevertOverriddenRotation() { + return mFoldState == DeviceStateController.FoldState.OPEN // When transitioning to open. + && mHalfFoldSavedRotation != -1 // Ignore if we've already reverted. + && mUserRotationMode + == WindowManagerPolicy.USER_ROTATION_LOCKED; // Ignore if we're unlocked. + } + + int revertOverriddenRotation() { + int savedRotation = mHalfFoldSavedRotation; + mHalfFoldSavedRotation = -1; + return savedRotation; + } + + void foldStateChanged(DeviceStateController.FoldState newState) { + ProtoLog.v(WM_DEBUG_ORIENTATION, + "foldStateChanged: displayId %d, halfFoldStateChanged %s, " + + "saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, " + + "mLastOrientation: %d, mRotation: %d", + mDisplayContent.getDisplayId(), newState.name(), mHalfFoldSavedRotation, + mUserRotation, mLastSensorRotation, mLastOrientation, mRotation); + if (mFoldState == DeviceStateController.FoldState.UNKNOWN) { + mFoldState = newState; + return; + } + if (newState == DeviceStateController.FoldState.HALF_FOLDED + && mFoldState != DeviceStateController.FoldState.HALF_FOLDED) { + // The device has transitioned to HALF_FOLDED state: save the current rotation and + // update the device rotation. + mHalfFoldSavedRotation = mRotation; + mFoldState = newState; + // Now mFoldState is set to HALF_FOLDED, the overrideFrozenRotation function will + // return true, so rotation is unlocked. + mService.updateRotation(false /* alwaysSendConfiguration */, + false /* forceRelayout */); + } else { + // Revert the rotation to our saved value if we transition from HALF_FOLDED. + mRotation = mHalfFoldSavedRotation; + // Tell the device to update its orientation (mFoldState is still HALF_FOLDED here + // so we will override USER_ROTATION_LOCKED and allow a rotation). + mService.updateRotation(false /* alwaysSendConfiguration */, + false /* forceRelayout */); + // Once we are rotated, set mFoldstate, effectively removing the lock override. + mFoldState = newState; + } + } + } + private class OrientationListener extends WindowOrientationListener implements Runnable { transient boolean mEnabled; diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index bf4b65da8b43..3a8fbbbaa77d 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -173,6 +173,7 @@ abstract class InsetsSourceProvider { mWindowContainer = windowContainer; // TODO: remove the frame provider for non-WindowState container. mFrameProvider = frameProvider; + mOverrideFrames.clear(); mOverrideFrameProviders = overrideFrameProviders; if (windowContainer == null) { setServerVisible(false); @@ -234,6 +235,8 @@ abstract class InsetsSourceProvider { updateSourceFrameForServerVisibility(); if (mOverrideFrameProviders != null) { + // Not necessary to clear the mOverrideFrames here. It will be cleared every time the + // override frame provider updates. for (int i = mOverrideFrameProviders.size() - 1; i >= 0; i--) { final int windowType = mOverrideFrameProviders.keyAt(i); final Rect overrideFrame; diff --git a/services/core/java/com/android/server/wm/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java index 7bd2a4a28e69..e74e5787ef5a 100644 --- a/services/core/java/com/android/server/wm/LaunchParamsController.java +++ b/services/core/java/com/android/server/wm/LaunchParamsController.java @@ -64,6 +64,10 @@ class LaunchParamsController { void registerDefaultModifiers(ActivityTaskSupervisor supervisor) { // {@link TaskLaunchParamsModifier} handles window layout preferences. registerModifier(new TaskLaunchParamsModifier(supervisor)); + if (DesktopModeLaunchParamsModifier.DESKTOP_MODE_SUPPORTED) { + // {@link DesktopModeLaunchParamsModifier} handles default task size changes + registerModifier(new DesktopModeLaunchParamsModifier()); + } } /** diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index a469c6b39e7f..c19353cb2676 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -17,14 +17,17 @@ package com.android.server.wm; import android.annotation.IntDef; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.graphics.Color; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.function.Function; /** Reads letterbox configs from resources and controls their overrides at runtime. */ final class LetterboxConfiguration { @@ -156,34 +159,25 @@ final class LetterboxConfiguration { // portrait device orientation. private boolean mIsVerticalReachabilityEnabled; - - // Horizontal position of a center of the letterboxed app window which is global to prevent - // "jumps" when switching between letterboxed apps. It's updated to reposition the app window - // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in - // LetterboxUiController#getHorizontalPositionMultiplier which is called from - // ActivityRecord#updateResolvedBoundsPosition. - // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from - // Overview after changing position in another app. - @LetterboxHorizontalReachabilityPosition - private volatile int mLetterboxPositionForHorizontalReachability; - - // Vertical position of a center of the letterboxed app window which is global to prevent - // "jumps" when switching between letterboxed apps. It's updated to reposition the app window - // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in - // LetterboxUiController#getVerticalPositionMultiplier which is called from - // ActivityRecord#updateResolvedBoundsPosition. - // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from - // Overview after changing position in another app. - @LetterboxVerticalReachabilityPosition - private volatile int mLetterboxPositionForVerticalReachability; - // Whether education is allowed for letterboxed fullscreen apps. private boolean mIsEducationEnabled; // Whether using split screen aspect ratio as a default aspect ratio for unresizable apps. private boolean mIsSplitScreenAspectRatioForUnresizableAppsEnabled; + // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier + @NonNull + private final LetterboxConfigurationPersister mLetterboxConfigurationPersister; + LetterboxConfiguration(Context systemUiContext) { + this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext, + () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext), + () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext))); + } + + @VisibleForTesting + LetterboxConfiguration(Context systemUiContext, + LetterboxConfigurationPersister letterboxConfigurationPersister) { mContext = systemUiContext; mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat( R.dimen.config_fixedOrientationLetterboxAspectRatio); @@ -206,14 +200,14 @@ final class LetterboxConfiguration { readLetterboxHorizontalReachabilityPositionFromConfig(mContext); mDefaultPositionForVerticalReachability = readLetterboxVerticalReachabilityPositionFromConfig(mContext); - mLetterboxPositionForHorizontalReachability = mDefaultPositionForHorizontalReachability; - mLetterboxPositionForVerticalReachability = mDefaultPositionForVerticalReachability; mIsEducationEnabled = mContext.getResources().getBoolean( R.bool.config_letterboxIsEducationEnabled); setDefaultMinAspectRatioForUnresizableApps(mContext.getResources().getFloat( R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps)); mIsSplitScreenAspectRatioForUnresizableAppsEnabled = mContext.getResources().getBoolean( R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled); + mLetterboxConfigurationPersister = letterboxConfigurationPersister; + mLetterboxConfigurationPersister.start(); } /** @@ -653,7 +647,9 @@ final class LetterboxConfiguration { * <p>The position multiplier is changed after each double tap in the letterbox area. */ float getHorizontalMultiplierForReachability() { - switch (mLetterboxPositionForHorizontalReachability) { + final int letterboxPositionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + switch (letterboxPositionForHorizontalReachability) { case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT: return 0.0f; case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER: @@ -662,10 +658,11 @@ final class LetterboxConfiguration { return 1.0f; default: throw new AssertionError( - "Unexpected letterbox position type: " - + mLetterboxPositionForHorizontalReachability); + "Unexpected letterbox position type: " + + letterboxPositionForHorizontalReachability); } } + /* * Gets vertical position of a center of the letterboxed app window when reachability * is enabled specified. 0 corresponds to the top side of the screen and 1 to the bottom side. @@ -673,7 +670,9 @@ final class LetterboxConfiguration { * <p>The position multiplier is changed after each double tap in the letterbox area. */ float getVerticalMultiplierForReachability() { - switch (mLetterboxPositionForVerticalReachability) { + final int letterboxPositionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + switch (letterboxPositionForVerticalReachability) { case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP: return 0.0f; case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER: @@ -683,7 +682,7 @@ final class LetterboxConfiguration { default: throw new AssertionError( "Unexpected letterbox position type: " - + mLetterboxPositionForVerticalReachability); + + letterboxPositionForVerticalReachability); } } @@ -693,7 +692,7 @@ final class LetterboxConfiguration { */ @LetterboxHorizontalReachabilityPosition int getLetterboxPositionForHorizontalReachability() { - return mLetterboxPositionForHorizontalReachability; + return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); } /* @@ -702,7 +701,7 @@ final class LetterboxConfiguration { */ @LetterboxVerticalReachabilityPosition int getLetterboxPositionForVerticalReachability() { - return mLetterboxPositionForVerticalReachability; + return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); } /** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */ @@ -742,9 +741,8 @@ final class LetterboxConfiguration { * right side. */ void movePositionForHorizontalReachabilityToNextRightStop() { - mLetterboxPositionForHorizontalReachability = Math.min( - mLetterboxPositionForHorizontalReachability + 1, - LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT); + updatePositionForHorizontalReachability(prev -> Math.min( + prev + 1, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT)); } /** @@ -752,8 +750,7 @@ final class LetterboxConfiguration { * side. */ void movePositionForHorizontalReachabilityToNextLeftStop() { - mLetterboxPositionForHorizontalReachability = - Math.max(mLetterboxPositionForHorizontalReachability - 1, 0); + updatePositionForHorizontalReachability(prev -> Math.max(prev - 1, 0)); } /** @@ -761,9 +758,8 @@ final class LetterboxConfiguration { * side. */ void movePositionForVerticalReachabilityToNextBottomStop() { - mLetterboxPositionForVerticalReachability = Math.min( - mLetterboxPositionForVerticalReachability + 1, - LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM); + updatePositionForVerticalReachability(prev -> Math.min( + prev + 1, LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM)); } /** @@ -771,8 +767,7 @@ final class LetterboxConfiguration { * side. */ void movePositionForVerticalReachabilityToNextTopStop() { - mLetterboxPositionForVerticalReachability = - Math.max(mLetterboxPositionForVerticalReachability - 1, 0); + updatePositionForVerticalReachability(prev -> Math.max(prev - 1, 0)); } /** @@ -822,4 +817,26 @@ final class LetterboxConfiguration { R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled); } + /** Calculates a new letterboxPositionForHorizontalReachability value and updates the store */ + private void updatePositionForHorizontalReachability( + Function<Integer, Integer> newHorizonalPositionFun) { + final int letterboxPositionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + final int nextHorizontalPosition = newHorizonalPositionFun.apply( + letterboxPositionForHorizontalReachability); + mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability( + nextHorizontalPosition); + } + + /** Calculates a new letterboxPositionForVerticalReachability value and updates the store */ + private void updatePositionForVerticalReachability( + Function<Integer, Integer> newVerticalPositionFun) { + final int letterboxPositionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + final int nextVerticalPosition = newVerticalPositionFun.apply( + letterboxPositionForVerticalReachability); + mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability( + nextVerticalPosition); + } + } diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java new file mode 100644 index 000000000000..70639b16c828 --- /dev/null +++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.Environment; +import android.util.AtomicFile; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.wm.LetterboxConfiguration.LetterboxHorizontalReachabilityPosition; +import com.android.server.wm.LetterboxConfiguration.LetterboxVerticalReachabilityPosition; +import com.android.server.wm.nano.WindowManagerProtos; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Persists the values of letterboxPositionForHorizontalReachability and + * letterboxPositionForVerticalReachability for {@link LetterboxConfiguration}. + */ +class LetterboxConfigurationPersister { + + private static final String TAG = + TAG_WITH_CLASS_NAME ? "LetterboxConfigurationPersister" : TAG_WM; + + @VisibleForTesting + static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config"; + + private final Context mContext; + private final Supplier<Integer> mDefaultHorizontalReachabilitySupplier; + private final Supplier<Integer> mDefaultVerticalReachabilitySupplier; + + // Horizontal position of a center of the letterboxed app window which is global to prevent + // "jumps" when switching between letterboxed apps. It's updated to reposition the app window + // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in + // LetterboxUiController#getHorizontalPositionMultiplier which is called from + // ActivityRecord#updateResolvedBoundsPosition. + @LetterboxHorizontalReachabilityPosition + private volatile int mLetterboxPositionForHorizontalReachability; + + // Vertical position of a center of the letterboxed app window which is global to prevent + // "jumps" when switching between letterboxed apps. It's updated to reposition the app window + // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in + // LetterboxUiController#getVerticalPositionMultiplier which is called from + // ActivityRecord#updateResolvedBoundsPosition. + @LetterboxVerticalReachabilityPosition + private volatile int mLetterboxPositionForVerticalReachability; + + @NonNull + private final AtomicFile mConfigurationFile; + + @Nullable + private final Consumer<String> mCompletionCallback; + + @NonNull + private final PersisterQueue mPersisterQueue; + + LetterboxConfigurationPersister(Context systemUiContext, + Supplier<Integer> defaultHorizontalReachabilitySupplier, + Supplier<Integer> defaultVerticalReachabilitySupplier) { + this(systemUiContext, defaultHorizontalReachabilitySupplier, + defaultVerticalReachabilitySupplier, + Environment.getDataSystemDirectory(), new PersisterQueue(), + /* completionCallback */ null); + } + + @VisibleForTesting + LetterboxConfigurationPersister(Context systemUiContext, + Supplier<Integer> defaultHorizontalReachabilitySupplier, + Supplier<Integer> defaultVerticalReachabilitySupplier, File configFolder, + PersisterQueue persisterQueue, @Nullable Consumer<String> completionCallback) { + mContext = systemUiContext.createDeviceProtectedStorageContext(); + mDefaultHorizontalReachabilitySupplier = defaultHorizontalReachabilitySupplier; + mDefaultVerticalReachabilitySupplier = defaultVerticalReachabilitySupplier; + mCompletionCallback = completionCallback; + final File prefFiles = new File(configFolder, LETTERBOX_CONFIGURATION_FILENAME); + mConfigurationFile = new AtomicFile(prefFiles); + mPersisterQueue = persisterQueue; + readCurrentConfiguration(); + } + + /** + * Startes the persistence queue + */ + void start() { + mPersisterQueue.startPersisting(); + } + + /* + * Gets the horizontal position of the letterboxed app window when horizontal reachability is + * enabled. + */ + @LetterboxHorizontalReachabilityPosition + int getLetterboxPositionForHorizontalReachability() { + return mLetterboxPositionForHorizontalReachability; + } + + /* + * Gets the vertical position of the letterboxed app window when vertical reachability is + * enabled. + */ + @LetterboxVerticalReachabilityPosition + int getLetterboxPositionForVerticalReachability() { + return mLetterboxPositionForVerticalReachability; + } + + /** + * Updates letterboxPositionForVerticalReachability if different from the current value + */ + void setLetterboxPositionForHorizontalReachability( + int letterboxPositionForHorizontalReachability) { + if (mLetterboxPositionForHorizontalReachability + != letterboxPositionForHorizontalReachability) { + mLetterboxPositionForHorizontalReachability = + letterboxPositionForHorizontalReachability; + updateConfiguration(); + } + } + + /** + * Updates letterboxPositionForVerticalReachability if different from the current value + */ + void setLetterboxPositionForVerticalReachability( + int letterboxPositionForVerticalReachability) { + if (mLetterboxPositionForVerticalReachability != letterboxPositionForVerticalReachability) { + mLetterboxPositionForVerticalReachability = letterboxPositionForVerticalReachability; + updateConfiguration(); + } + } + + @VisibleForTesting + void useDefaultValue() { + mLetterboxPositionForHorizontalReachability = mDefaultHorizontalReachabilitySupplier.get(); + mLetterboxPositionForVerticalReachability = mDefaultVerticalReachabilitySupplier.get(); + } + + private void readCurrentConfiguration() { + FileInputStream fis = null; + try { + fis = mConfigurationFile.openRead(); + byte[] protoData = readInputStream(fis); + final WindowManagerProtos.LetterboxProto letterboxData = + WindowManagerProtos.LetterboxProto.parseFrom(protoData); + mLetterboxPositionForHorizontalReachability = + letterboxData.letterboxPositionForHorizontalReachability; + mLetterboxPositionForVerticalReachability = + letterboxData.letterboxPositionForVerticalReachability; + } catch (IOException ioe) { + Slog.e(TAG, + "Error reading from LetterboxConfigurationPersister. " + + "Using default values!", ioe); + useDefaultValue(); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + useDefaultValue(); + Slog.e(TAG, "Error reading from LetterboxConfigurationPersister ", e); + } + } + } + } + + private void updateConfiguration() { + mPersisterQueue.addItem(new UpdateValuesCommand(mConfigurationFile, + mLetterboxPositionForHorizontalReachability, + mLetterboxPositionForVerticalReachability, + mCompletionCallback), /* flush */ true); + } + + private static byte[] readInputStream(InputStream in) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + byte[] buffer = new byte[1024]; + int size = in.read(buffer); + while (size > 0) { + outputStream.write(buffer, 0, size); + size = in.read(buffer); + } + return outputStream.toByteArray(); + } finally { + outputStream.close(); + } + } + + private static class UpdateValuesCommand implements + PersisterQueue.WriteQueueItem<UpdateValuesCommand> { + + @NonNull + private final AtomicFile mFileToUpdate; + @Nullable + private final Consumer<String> mOnComplete; + + + private final int mHorizontalReachability; + private final int mVerticalReachability; + + UpdateValuesCommand(@NonNull AtomicFile fileToUpdate, + int horizontalReachability, int verticalReachability, + @Nullable Consumer<String> onComplete) { + mFileToUpdate = fileToUpdate; + mHorizontalReachability = horizontalReachability; + mVerticalReachability = verticalReachability; + mOnComplete = onComplete; + } + + @Override + public void process() { + final WindowManagerProtos.LetterboxProto letterboxData = + new WindowManagerProtos.LetterboxProto(); + letterboxData.letterboxPositionForHorizontalReachability = mHorizontalReachability; + letterboxData.letterboxPositionForVerticalReachability = mVerticalReachability; + final byte[] bytes = WindowManagerProtos.LetterboxProto.toByteArray(letterboxData); + + FileOutputStream fos = null; + try { + fos = mFileToUpdate.startWrite(); + fos.write(bytes); + mFileToUpdate.finishWrite(fos); + } catch (IOException ioe) { + mFileToUpdate.failWrite(fos); + Slog.e(TAG, + "Error writing to LetterboxConfigurationPersister. " + + "Using default values!", ioe); + } finally { + if (mOnComplete != null) { + mOnComplete.accept("UpdateValuesCommand"); + } + } + } + } +} diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 317c93e63459..ea82417a2389 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -481,7 +481,7 @@ final class LetterboxUiController { } private void updateRoundedCorners(WindowState mainWindow) { - final SurfaceControl windowSurface = mainWindow.getClientViewRootSurface(); + final SurfaceControl windowSurface = mainWindow.getSurfaceControl(); if (windowSurface != null && windowSurface.isValid()) { final Transaction transaction = mActivityRecord.getSyncTransaction(); diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java index a89894db4b4b..30bdc3477edf 100644 --- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java +++ b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java @@ -24,10 +24,7 @@ import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.Context; import android.graphics.Rect; -import android.hardware.devicestate.DeviceStateManager; -import android.os.HandlerExecutor; import android.window.DisplayAreaInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; @@ -36,11 +33,8 @@ public class PhysicalDisplaySwitchTransitionLauncher { private final DisplayContent mDisplayContent; private final WindowManagerService mService; - private final DeviceStateManager mDeviceStateManager; private final TransitionController mTransitionController; - private DeviceStateListener mDeviceStateListener; - /** * If on a foldable device represents whether the device is folded or not */ @@ -52,21 +46,15 @@ public class PhysicalDisplaySwitchTransitionLauncher { mDisplayContent = displayContent; mService = displayContent.mWmService; mTransitionController = transitionController; - - mDeviceStateManager = mService.mContext.getSystemService(DeviceStateManager.class); - - if (mDeviceStateManager != null) { - mDeviceStateListener = new DeviceStateListener(mService.mContext); - mDeviceStateManager - .registerCallback(new HandlerExecutor(mDisplayContent.mWmService.mH), - mDeviceStateListener); - } } - public void destroy() { - if (mDeviceStateManager != null) { - mDeviceStateManager.unregisterCallback(mDeviceStateListener); - } + /** + * Called by the DeviceStateManager callback when the state changes. + */ + void foldStateChanged(DeviceStateController.FoldState newFoldState) { + // Ignore transitions to/from half-folded. + if (newFoldState == DeviceStateController.FoldState.HALF_FOLDED) return; + mIsFolded = newFoldState == DeviceStateController.FoldState.FOLDED; } /** @@ -143,10 +131,4 @@ public class PhysicalDisplaySwitchTransitionLauncher { mTransition = null; } - class DeviceStateListener extends DeviceStateManager.FoldStateListener { - - DeviceStateListener(Context context) { - super(context, newIsFolded -> mIsFolded = newIsFolded); - } - } } diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 4860762a5f7f..1fc061b2ca78 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -976,7 +976,7 @@ class RecentTasks { continue; } - res.add(createRecentTaskInfo(task, true /* stripExtras */)); + res.add(createRecentTaskInfo(task, true /* stripExtras */, getTasksAllowed)); } return res; } @@ -1895,7 +1895,8 @@ class RecentTasks { /** * Creates a new RecentTaskInfo from a Task. */ - ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras) { + ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras, + boolean getTasksAllowed) { final ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo(); // If the recent Task is detached, we consider it will be re-attached to the default // TaskDisplayArea because we currently only support recent overview in the default TDA. @@ -1907,6 +1908,9 @@ class RecentTasks { rti.id = rti.isRunning ? rti.taskId : INVALID_TASK_ID; rti.persistentId = rti.taskId; rti.lastSnapshotData.set(tr.mLastTaskSnapshotData); + if (!getTasksAllowed) { + Task.trimIneffectiveInfo(tr, rti); + } // Fill in organized child task info for the task created by organizer. if (tr.mCreatedByOrganizer) { diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 7f222423ec1c..2866f423dc07 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2021,7 +2021,12 @@ class RootWindowContainer extends WindowContainer<DisplayContent> // non-fullscreen bounds. Then when this new PIP task exits PIP, it can restore // to its previous freeform bounds. rootTask.setLastNonFullscreenBounds(task.mLastNonFullscreenBounds); - rootTask.setBounds(task.getBounds()); + // When creating a new Task for PiP, set its initial bounds as the TaskFragment in + // case the activity is embedded, so that it can be animated to PiP window from the + // current bounds. + // Use Task#setBoundsUnchecked to skip checking windowing mode as the windowing mode + // will be updated later after this is collected in transition. + rootTask.setBoundsUnchecked(r.getTaskFragment().getBounds()); // Move the last recents animation transaction from original task to the new one. if (task.mLastRecentsAnimationTransaction != null) { diff --git a/services/core/java/com/android/server/wm/RunningTasks.java b/services/core/java/com/android/server/wm/RunningTasks.java index 120fec0fe0e6..0e60274ba381 100644 --- a/services/core/java/com/android/server/wm/RunningTasks.java +++ b/services/core/java/com/android/server/wm/RunningTasks.java @@ -142,6 +142,10 @@ class RunningTasks { task.fillTaskInfo(rti, !mKeepIntentExtra); // Fill in some deprecated values rti.id = rti.taskId; + + if (!mAllowed) { + Task.trimIneffectiveInfo(task, rti); + } return rti; } } diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java index a638784390b6..8a6ddf658c2a 100644 --- a/services/core/java/com/android/server/wm/SafeActivityOptions.java +++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java @@ -119,13 +119,13 @@ public class SafeActivityOptions { /** * To ensure that two activities, one using this object, and the other using the - * SafeActivityOptions returned from this function, are launched into the same display through - * ActivityStartController#startActivities, all display-related information, i.e. - * displayAreaToken, launchDisplayId and callerDisplayId, are cloned. + * SafeActivityOptions returned from this function, are launched into the same display/root task + * through ActivityStartController#startActivities, all display-related information, i.e. + * displayAreaToken, launchDisplayId, callerDisplayId and the launch root task are cloned. */ - @Nullable SafeActivityOptions selectiveCloneDisplayOptions() { - final ActivityOptions options = cloneLaunchingDisplayOptions(mOriginalOptions); - final ActivityOptions callerOptions = cloneLaunchingDisplayOptions(mCallerOptions); + @Nullable SafeActivityOptions selectiveCloneLaunchOptions() { + final ActivityOptions options = cloneLaunchingOptions(mOriginalOptions); + final ActivityOptions callerOptions = cloneLaunchingOptions(mCallerOptions); if (options == null && callerOptions == null) { return null; } @@ -138,11 +138,12 @@ public class SafeActivityOptions { return safeOptions; } - private ActivityOptions cloneLaunchingDisplayOptions(ActivityOptions options) { + private ActivityOptions cloneLaunchingOptions(ActivityOptions options) { return options == null ? null : ActivityOptions.makeBasic() .setLaunchTaskDisplayArea(options.getLaunchTaskDisplayArea()) .setLaunchDisplayId(options.getLaunchDisplayId()) - .setCallerDisplayId((options.getCallerDisplayId())); + .setCallerDisplayId(options.getCallerDisplayId()) + .setLaunchRootTask(options.getLaunchRootTask()); } /** @@ -265,7 +266,7 @@ public class SafeActivityOptions { ActivityOptions options, int callingPid, int callingUid) { // If a launch task id is specified, then ensure that the caller is the recents // component or has the START_TASKS_FROM_RECENTS permission - if (options.getLaunchTaskId() != INVALID_TASK_ID + if ((options.getLaunchTaskId() != INVALID_TASK_ID || options.getDisableStartingWindow()) && !supervisor.mRecentTasks.isCallerRecents(callingUid)) { final int startInTaskPerm = ActivityTaskManagerService.checkPermission( START_TASKS_FROM_RECENTS, callingPid, callingUid); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index d6f295eb8f44..dd21c72c9ac9 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -21,7 +21,6 @@ import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED; import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION; import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN; import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; @@ -2618,6 +2617,13 @@ class Task extends TaskFragment { return boundsChange; } + /** Sets the requested bounds regardless of the windowing mode. */ + int setBoundsUnchecked(@NonNull Rect bounds) { + final int boundsChange = super.setBounds(bounds); + updateSurfaceBounds(); + return boundsChange; + } + @Override public boolean isCompatible(int windowingMode, int activityType) { // TODO: Should we just move this to ConfigurationContainer? @@ -3447,6 +3453,27 @@ class Task extends TaskFragment { info.isSleeping = shouldSleepActivities(); } + /** + * Removes the activity info if the activity belongs to a different uid, which is + * different from the app that hosts the task. + */ + static void trimIneffectiveInfo(Task task, TaskInfo info) { + final ActivityRecord baseActivity = task.getActivity(r -> !r.finishing, + false /* traverseTopToBottom */); + final int baseActivityUid = + baseActivity != null ? baseActivity.getUid() : task.effectiveUid; + + if (info.topActivityInfo != null + && task.effectiveUid != info.topActivityInfo.applicationInfo.uid) { + info.topActivity = null; + info.topActivityInfo = null; + } + + if (task.effectiveUid != baseActivityUid) { + info.baseActivity = null; + } + } + @Nullable PictureInPictureParams getPictureInPictureParams() { final Task topTask = getTopMostTask(); if (topTask == null) return null; @@ -3526,12 +3553,16 @@ class Task extends TaskFragment { * {@link android.window.TaskFragmentOrganizer} */ TaskFragmentParentInfo getTaskFragmentParentInfo() { - return new TaskFragmentParentInfo(getConfiguration(), getDisplayId(), isVisibleRequested()); + return new TaskFragmentParentInfo(getConfiguration(), getDisplayId(), + shouldBeVisible(null /* starting */)); } @Override void onActivityVisibleRequestedChanged() { - if (mVisibleRequested != isVisibleRequested()) { + final boolean prevVisibleRequested = mVisibleRequested; + // mVisibleRequested is updated in super method. + super.onActivityVisibleRequestedChanged(); + if (prevVisibleRequested != mVisibleRequested) { sendTaskFragmentParentInfoChangedIfNeeded(); } } @@ -5050,6 +5081,9 @@ class Task extends TaskFragment { == ActivityOptions.ANIM_SCENE_TRANSITION) { doShow = false; } + if (options != null && options.getDisableStartingWindow()) { + doShow = false; + } if (r.mLaunchTaskBehind) { // Don't do a starting window for mLaunchTaskBehind. More importantly make sure we // tell WindowManager that r is visible even though it is at the back of the root @@ -5309,8 +5343,9 @@ class Task extends TaskFragment { return false; } - boolean navigateUpTo(ActivityRecord srec, Intent destIntent, NeededUriGrants destGrants, - int resultCode, Intent resultData, NeededUriGrants resultGrants) { + boolean navigateUpTo(ActivityRecord srec, Intent destIntent, String resolvedType, + NeededUriGrants destGrants, int resultCode, Intent resultData, + NeededUriGrants resultGrants) { if (!srec.attachedToProcess()) { // Nothing to do if the caller is not attached, because this method should be called // from an alive activity. @@ -5403,28 +5438,22 @@ class Task extends TaskFragment { srec.packageName); } } else { - try { - ActivityInfo aInfo = AppGlobals.getPackageManager().getActivityInfo( - destIntent.getComponent(), ActivityManagerService.STOCK_PM_FLAGS, - srec.mUserId); - // TODO(b/64750076): Check if calling pid should really be -1. - final int res = mAtmService.getActivityStartController() - .obtainStarter(destIntent, "navigateUpTo") - .setCaller(srec.app.getThread()) - .setActivityInfo(aInfo) - .setResultTo(parent.token) - .setCallingPid(-1) - .setCallingUid(callingUid) - .setCallingPackage(srec.packageName) - .setCallingFeatureId(parent.launchedFromFeatureId) - .setRealCallingPid(-1) - .setRealCallingUid(callingUid) - .setComponentSpecified(true) - .execute(); - foundParentInTask = res == ActivityManager.START_SUCCESS; - } catch (RemoteException e) { - foundParentInTask = false; - } + // TODO(b/64750076): Check if calling pid should really be -1. + final int res = mAtmService.getActivityStartController() + .obtainStarter(destIntent, "navigateUpTo") + .setResolvedType(resolvedType) + .setUserId(srec.mUserId) + .setCaller(srec.app.getThread()) + .setResultTo(parent.token) + .setCallingPid(-1) + .setCallingUid(callingUid) + .setCallingPackage(srec.packageName) + .setCallingFeatureId(parent.launchedFromFeatureId) + .setRealCallingPid(-1) + .setRealCallingUid(callingUid) + .setComponentSpecified(true) + .execute(); + foundParentInTask = res == ActivityManager.START_SUCCESS; parent.finishIfPossible(resultCode, resultData, resultGrants, "navigate-top", true /* oomAdj */); } @@ -5826,12 +5855,10 @@ class Task extends TaskFragment { return false; } - // Existing Tasks can be reused if a new root task will be created anyway, or for the - // Dream - because there can only ever be one DreamActivity. + // Existing Tasks can be reused if a new root task will be created anyway. final int windowingMode = getWindowingMode(); final int activityType = getActivityType(); - return DisplayContent.alwaysCreateRootTask(windowingMode, activityType) - || activityType == ACTIVITY_TYPE_DREAM; + return DisplayContent.alwaysCreateRootTask(windowingMode, activityType); } void addChild(WindowContainer child, final boolean toTop, boolean showForAllUsers) { @@ -5898,10 +5925,7 @@ class Task extends TaskFragment { return BOUNDS_CHANGE_NONE; } - final int result = super.setBounds(!inMultiWindowMode() ? null : bounds); - - updateSurfaceBounds(); - return result; + return setBoundsUnchecked(!inMultiWindowMode() ? null : bounds); } @Override diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index ec6741464a80..3404e7b70429 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -2690,12 +2690,26 @@ class TaskFragment extends WindowContainer<WindowContainer> { return; } mVisibleRequested = isVisibleRequested; - final TaskFragment parentTf = getParent().asTaskFragment(); + final WindowContainer<?> parent = getParent(); + if (parent == null) { + return; + } + final TaskFragment parentTf = parent.asTaskFragment(); if (parentTf != null) { parentTf.onActivityVisibleRequestedChanged(); } } + @Nullable + @Override + TaskFragment getTaskFragment(Predicate<TaskFragment> callback) { + final TaskFragment taskFragment = super.getTaskFragment(callback); + if (taskFragment != null) { + return taskFragment; + } + return callback.test(this) ? this : null; + } + String toFullString() { final StringBuilder sb = new StringBuilder(128); sb.append(this); diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 2d5c9897a82c..509b1e6f41ca 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -184,19 +184,30 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } void dispose() { - while (!mOrganizedTaskFragments.isEmpty()) { - final TaskFragment taskFragment = mOrganizedTaskFragments.get(0); - // Cleanup before remove to prevent it from sending any additional event, such as - // #onTaskFragmentVanished, to the removed organizer. + for (int i = mOrganizedTaskFragments.size() - 1; i >= 0; i--) { + // Cleanup the TaskFragmentOrganizer from all TaskFragments it organized before + // removing the windows to prevent it from adding any additional TaskFragment + // pending event. + final TaskFragment taskFragment = mOrganizedTaskFragments.get(i); taskFragment.onTaskFragmentOrganizerRemoved(); - taskFragment.removeImmediately(); - mOrganizedTaskFragments.remove(taskFragment); } + + // Defer to avoid unnecessary layout when there are multiple TaskFragments removal. + mAtmService.deferWindowLayout(); + try { + while (!mOrganizedTaskFragments.isEmpty()) { + final TaskFragment taskFragment = mOrganizedTaskFragments.remove(0); + taskFragment.removeImmediately(); + } + } finally { + mAtmService.continueWindowLayout(); + } + for (int i = mDeferredTransitions.size() - 1; i >= 0; i--) { // Cleanup any running transaction to unblock the current transition. onTransactionFinished(mDeferredTransitions.keyAt(i)); } - mOrganizer.asBinder().unlinkToDeath(this, 0 /*flags*/); + mOrganizer.asBinder().unlinkToDeath(this, 0 /* flags */); } @NonNull @@ -426,7 +437,6 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr @Override public void unregisterOrganizer(@NonNull ITaskFragmentOrganizer organizer) { - validateAndGetState(organizer); final int pid = Binder.getCallingPid(); final long uid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); @@ -607,6 +617,13 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr int opType, @NonNull Throwable exception) { validateAndGetState(organizer); Slog.w(TAG, "onTaskFragmentError ", exception); + final PendingTaskFragmentEvent vanishedEvent = taskFragment != null + ? getPendingTaskFragmentEvent(taskFragment, PendingTaskFragmentEvent.EVENT_VANISHED) + : null; + if (vanishedEvent != null) { + // No need to notify if the TaskFragment has been removed. + return; + } addPendingEvent(new PendingTaskFragmentEvent.Builder( PendingTaskFragmentEvent.EVENT_ERROR, organizer) .setErrorCallbackToken(errorCallbackToken) @@ -690,11 +707,17 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } private void removeOrganizer(@NonNull ITaskFragmentOrganizer organizer) { - final TaskFragmentOrganizerState state = validateAndGetState(organizer); + final TaskFragmentOrganizerState state = mTaskFragmentOrganizerState.get( + organizer.asBinder()); + if (state == null) { + Slog.w(TAG, "The organizer has already been removed."); + return; + } + // Remove any pending event of this organizer first because state.dispose() may trigger + // event dispatch as result of surface placement. + mPendingTaskFragmentEvents.remove(organizer.asBinder()); // remove all of the children of the organized TaskFragment state.dispose(); - // Remove any pending event of this organizer. - mPendingTaskFragmentEvents.remove(organizer.asBinder()); mTaskFragmentOrganizerState.remove(organizer.asBinder()); } @@ -878,23 +901,6 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr return null; } - private boolean shouldSendEventWhenTaskInvisible(@NonNull PendingTaskFragmentEvent event) { - if (event.mEventType == PendingTaskFragmentEvent.EVENT_ERROR - // Always send parent info changed to update task visibility - || event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) { - return true; - } - - final TaskFragmentOrganizerState state = - mTaskFragmentOrganizerState.get(event.mTaskFragmentOrg.asBinder()); - final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos.get(event.mTaskFragment); - final TaskFragmentInfo info = event.mTaskFragment.getTaskFragmentInfo(); - // Send an info changed callback if this event is for the last activities to finish in a - // TaskFragment so that the {@link TaskFragmentOrganizer} can delete this TaskFragment. - return event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED - && lastInfo != null && lastInfo.hasRunningActivity() && info.isEmpty(); - } - void dispatchPendingEvents() { if (mAtmService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred() || mPendingTaskFragmentEvents.isEmpty()) { @@ -908,36 +914,19 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } } - void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state, + private void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state, @NonNull List<PendingTaskFragmentEvent> pendingEvents) { if (pendingEvents.isEmpty()) { return; } - - final ArrayList<Task> visibleTasks = new ArrayList<>(); - final ArrayList<Task> invisibleTasks = new ArrayList<>(); - final ArrayList<PendingTaskFragmentEvent> candidateEvents = new ArrayList<>(); - for (int i = 0, n = pendingEvents.size(); i < n; i++) { - final PendingTaskFragmentEvent event = pendingEvents.get(i); - final Task task = event.mTaskFragment != null ? event.mTaskFragment.getTask() : null; - if (task != null && (task.lastActiveTime <= event.mDeferTime - || !(isTaskVisible(task, visibleTasks, invisibleTasks) - || shouldSendEventWhenTaskInvisible(event)))) { - // Defer sending events to the TaskFragment until the host task is active again. - event.mDeferTime = task.lastActiveTime; - continue; - } - candidateEvents.add(event); - } - final int numEvents = candidateEvents.size(); - if (numEvents == 0) { + if (shouldDeferPendingEvents(state, pendingEvents)) { return; } - mTmpTaskSet.clear(); + final int numEvents = pendingEvents.size(); final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); for (int i = 0; i < numEvents; i++) { - final PendingTaskFragmentEvent event = candidateEvents.get(i); + final PendingTaskFragmentEvent event = pendingEvents.get(i); if (event.mEventType == PendingTaskFragmentEvent.EVENT_APPEARED || event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) { final Task task = event.mTaskFragment.getTask(); @@ -953,7 +942,47 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } mTmpTaskSet.clear(); state.dispatchTransaction(transaction); - pendingEvents.removeAll(candidateEvents); + pendingEvents.clear(); + } + + /** + * Whether or not to defer sending the events to the organizer to avoid waking the app process + * when it is in background. We want to either send all events or none to avoid inconsistency. + */ + private boolean shouldDeferPendingEvents(@NonNull TaskFragmentOrganizerState state, + @NonNull List<PendingTaskFragmentEvent> pendingEvents) { + final ArrayList<Task> visibleTasks = new ArrayList<>(); + final ArrayList<Task> invisibleTasks = new ArrayList<>(); + for (int i = 0, n = pendingEvents.size(); i < n; i++) { + final PendingTaskFragmentEvent event = pendingEvents.get(i); + if (event.mEventType != PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED + && event.mEventType != PendingTaskFragmentEvent.EVENT_INFO_CHANGED + && event.mEventType != PendingTaskFragmentEvent.EVENT_APPEARED) { + // Send events for any other types. + return false; + } + + // Check if we should send the event given the Task visibility and events. + final Task task; + if (event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) { + task = event.mTask; + } else { + task = event.mTaskFragment.getTask(); + } + if (task.lastActiveTime > event.mDeferTime + && isTaskVisible(task, visibleTasks, invisibleTasks)) { + // Send events when the app has at least one visible Task. + return false; + } else if (shouldSendEventWhenTaskInvisible(task, state, event)) { + // Sent events even if the Task is invisible. + return false; + } + + // Defer sending events to the organizer until the host task is active (visible) again. + event.mDeferTime = task.lastActiveTime; + } + // Defer for invisible Task. + return true; } private static boolean isTaskVisible(@NonNull Task task, @@ -974,6 +1003,28 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } } + private boolean shouldSendEventWhenTaskInvisible(@NonNull Task task, + @NonNull TaskFragmentOrganizerState state, + @NonNull PendingTaskFragmentEvent event) { + final TaskFragmentParentInfo lastParentInfo = state.mLastSentTaskFragmentParentInfos + .get(task.mTaskId); + if (lastParentInfo == null || lastParentInfo.isVisible()) { + // When the Task was visible, or when there was no Task info changed sent (in which case + // the organizer will consider it as visible by default), always send the event to + // update the Task visibility. + return true; + } + if (event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) { + // Send info changed if the TaskFragment is becoming empty/non-empty so the + // organizer can choose whether or not to remove the TaskFragment. + final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos + .get(event.mTaskFragment); + final boolean isEmpty = event.mTaskFragment.getNonFinishingActivityCount() == 0; + return lastInfo == null || lastInfo.isEmpty() != isEmpty; + } + return false; + } + void dispatchPendingInfoChangedEvent(@NonNull TaskFragment taskFragment) { final PendingTaskFragmentEvent event = getPendingTaskFragmentEvent(taskFragment, PendingTaskFragmentEvent.EVENT_INFO_CHANGED); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 147c9cb18315..32f61978d730 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -55,7 +55,6 @@ import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_OCCLUDES_KEYGUARD; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; -import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static android.window.TransitionInfo.FLAG_WILL_IME_SHOWN; @@ -1331,7 +1330,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " sibling is a participant with mode %s", TransitionInfo.modeToString(siblingMode)); - if (mode != siblingMode) { + if (reduceMode(mode) != reduceMode(siblingMode)) { ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " SKIP: common mode mismatch. was %s", TransitionInfo.modeToString(mode)); @@ -1341,6 +1340,16 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return true; } + /** "reduces" a mode into a smaller set of modes that uniquely represents visibility change. */ + @TransitionInfo.TransitionMode + private static int reduceMode(@TransitionInfo.TransitionMode int mode) { + switch (mode) { + case TRANSIT_TO_BACK: return TRANSIT_CLOSE; + case TRANSIT_TO_FRONT: return TRANSIT_OPEN; + default: return mode; + } + } + /** * Go through topTargets and try to promote (see {@link #canPromote}) one of them. * @@ -1586,10 +1595,18 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe if (info.mEndParent != null) { change.setParent(info.mEndParent.mRemoteToken.toWindowContainerToken()); } + if (info.mStartParent != null && info.mStartParent.mRemoteToken != null + && target.getParent() != info.mStartParent) { + change.setLastParent(info.mStartParent.mRemoteToken.toWindowContainerToken()); + } change.setMode(info.getTransitMode(target)); change.setStartAbsBounds(info.mAbsoluteBounds); change.setFlags(info.getChangeFlags(target)); + final Task task = target.asTask(); + final TaskFragment taskFragment = target.asTaskFragment(); + final ActivityRecord activityRecord = target.asActivityRecord(); + if (task != null) { final ActivityManager.RunningTaskInfo tinfo = new ActivityManager.RunningTaskInfo(); task.fillTaskInfo(tinfo); @@ -1623,12 +1640,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe change.setEndRelOffset(bounds.left - parentBounds.left, bounds.top - parentBounds.top); int endRotation = target.getWindowConfiguration().getRotation(); - final ActivityRecord activityRecord = target.asActivityRecord(); if (activityRecord != null) { - final Task arTask = activityRecord.getTask(); - final int backgroundColor = ColorUtils.setAlphaComponent( - arTask.getTaskDescription().getBackgroundColor(), 255); - change.setBackgroundColor(backgroundColor); // TODO(b/227427984): Shell needs to aware letterbox. // Always use parent bounds of activity because letterbox area (e.g. fixed aspect // ratio or size compat mode) should be included in the animation. @@ -1641,6 +1653,18 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe } else { change.setEndAbsBounds(bounds); } + + if (activityRecord != null || (taskFragment != null && taskFragment.isEmbedded())) { + // Set background color to Task theme color for activity and embedded TaskFragment + // in case we want to show background during the animation. + final Task parentTask = activityRecord != null + ? activityRecord.getTask() + : taskFragment.getTask(); + final int backgroundColor = ColorUtils.setAlphaComponent( + parentTask.getTaskDescription().getBackgroundColor(), 255); + change.setBackgroundColor(backgroundColor); + } + change.setRotation(info.mRotation, endRotation); if (info.mSnapshot != null) { change.setSnapshot(info.mSnapshot, info.mSnapshotLuma); @@ -1842,7 +1866,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe @TransitionInfo.TransitionMode int getTransitMode(@NonNull WindowContainer wc) { if ((mFlags & ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH) != 0) { - return TRANSIT_CLOSE; + return mExistenceChanged ? TRANSIT_CLOSE : TRANSIT_TO_BACK; } final boolean nowVisible = wc.isVisibleRequested(); if (nowVisible == mVisible) { @@ -1865,26 +1889,24 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe flags |= FLAG_TRANSLUCENT; } final Task task = wc.asTask(); - if (task != null && task.voiceSession != null) { - flags |= FLAG_IS_VOICE_INTERACTION; - } if (task != null) { final ActivityRecord topActivity = task.getTopNonFinishingActivity(); if (topActivity != null && topActivity.mStartingData != null && topActivity.mStartingData.hasImeSurface()) { flags |= FLAG_WILL_IME_SHOWN; } + if (task.voiceSession != null) { + flags |= FLAG_IS_VOICE_INTERACTION; + } } Task parentTask = null; final ActivityRecord record = wc.asActivityRecord(); if (record != null) { parentTask = record.getTask(); - if (record.mUseTransferredAnimation) { - flags |= FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; - } if (record.mVoiceInteraction) { flags |= FLAG_IS_VOICE_INTERACTION; } + flags |= record.mTransitionChangeFlags; } final TaskFragment taskFragment = wc.asTaskFragment(); if (taskFragment != null && task == null) { @@ -1903,20 +1925,26 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe // Whether the container fills its parent Task bounds. flags |= FLAG_FILLS_TASK; } - } - final DisplayContent dc = wc.asDisplayContent(); - if (dc != null) { - flags |= FLAG_IS_DISPLAY; - if (dc.hasAlertWindowSurfaces()) { - flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS; + } else { + final DisplayContent dc = wc.asDisplayContent(); + if (dc != null) { + flags |= FLAG_IS_DISPLAY; + if (dc.hasAlertWindowSurfaces()) { + flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS; + } + } else if (isWallpaper(wc)) { + flags |= FLAG_IS_WALLPAPER; + } else if (isInputMethod(wc)) { + flags |= FLAG_IS_INPUT_METHOD; + } else { + // In this condition, the wc can only be WindowToken or DisplayArea. + final int type = wc.getWindowType(); + if (type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW + && type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) { + flags |= TransitionInfo.FLAG_IS_SYSTEM_WINDOW; + } } } - if (isWallpaper(wc)) { - flags |= FLAG_IS_WALLPAPER; - } - if (isInputMethod(wc)) { - flags |= FLAG_IS_INPUT_METHOD; - } if (occludesKeyguard(wc)) { flags |= FLAG_OCCLUDES_KEYGUARD; } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index e8682f7a3b22..26ce4ae8415c 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -126,19 +126,27 @@ class TransitionController { mTransitionTracer = transitionTracer; mTransitionPlayerDeath = () -> { synchronized (mAtm.mGlobalLock) { - // Clean-up/finish any playing transitions. - for (int i = 0; i < mPlayingTransitions.size(); ++i) { - mPlayingTransitions.get(i).cleanUpOnFailure(); - } - mPlayingTransitions.clear(); - mTransitionPlayer = null; - mTransitionPlayerProc = null; - mRemotePlayer.clear(); - mRunningLock.doNotifyLocked(); + detachPlayer(); } }; } + private void detachPlayer() { + if (mTransitionPlayer == null) return; + // Clean-up/finish any playing transitions. + for (int i = 0; i < mPlayingTransitions.size(); ++i) { + mPlayingTransitions.get(i).cleanUpOnFailure(); + } + mPlayingTransitions.clear(); + if (mCollectingTransition != null) { + mCollectingTransition.abort(); + } + mTransitionPlayer = null; + mTransitionPlayerProc = null; + mRemotePlayer.clear(); + mRunningLock.doNotifyLocked(); + } + /** @see #createTransition(int, int) */ @NonNull Transition createTransition(int type) { @@ -193,7 +201,7 @@ class TransitionController { if (mTransitionPlayer.asBinder() != null) { mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0); } - mTransitionPlayer = null; + detachPlayer(); } if (player.asBinder() != null) { player.asBinder().linkToDeath(mTransitionPlayerDeath, 0); diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 07ae167f5e66..bece47613b92 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -41,6 +41,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; import static com.android.server.wm.AppTransition.MAX_APP_TRANSITION_DURATION; import static com.android.server.wm.AppTransition.isActivityTransitOld; +import static com.android.server.wm.AppTransition.isTaskFragmentTransitOld; import static com.android.server.wm.AppTransition.isTaskTransitOld; import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; import static com.android.server.wm.IdentifierProto.HASH_CODE; @@ -2985,10 +2986,17 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // {@link Activity#overridePendingTransition(int, int, int)}. @ColorInt int backdropColor = 0; if (controller.isFromActivityEmbedding()) { - final int animAttr = AppTransition.mapOpenCloseTransitTypes(transit, enter); - final Animation a = animAttr != 0 - ? appTransition.loadAnimationAttr(lp, animAttr, transit) : null; - showBackdrop = a != null && a.getShowBackdrop(); + if (isChanging) { + // When there are more than one changing containers, it may leave part of the + // screen empty. Show background color to cover that. + showBackdrop = getDisplayContent().mChangingContainers.size() > 1; + } else { + // Check whether or not to show backdrop for open/close transition. + final int animAttr = AppTransition.mapOpenCloseTransitTypes(transit, enter); + final Animation a = animAttr != 0 + ? appTransition.loadAnimationAttr(lp, animAttr, transit) : null; + showBackdrop = a != null && a.getShowBackdrop(); + } backdropColor = appTransition.getNextAppTransitionBackgroundColor(); } final Rect localBounds = new Rect(mTmpRect); @@ -3091,9 +3099,16 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } } + // Check if the animation requests to show background color for Activity and embedded + // TaskFragment. final ActivityRecord activityRecord = asActivityRecord(); - if (activityRecord != null && isActivityTransitOld(transit) - && adapter.getShowBackground()) { + final TaskFragment taskFragment = asTaskFragment(); + if (adapter.getShowBackground() + // Check if it is Activity transition. + && ((activityRecord != null && isActivityTransitOld(transit)) + // Check if it is embedded TaskFragment transition. + || (taskFragment != null && taskFragment.isEmbedded() + && isTaskFragmentTransitOld(transit)))) { final @ColorInt int backgroundColorForTransition; if (adapter.getBackgroundColor() != 0) { // If available use the background color provided through getBackgroundColor @@ -3103,9 +3118,11 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // Otherwise default to the window's background color if provided through // the theme as the background color for the animation - the top most window // with a valid background color and showBackground set takes precedence. - final Task arTask = activityRecord.getTask(); + final Task parentTask = activityRecord != null + ? activityRecord.getTask() + : taskFragment.getTask(); backgroundColorForTransition = ColorUtils.setAlphaComponent( - arTask.getTaskDescription().getBackgroundColor(), 255); + parentTask.getTaskDescription().getBackgroundColor(), 255); } animationRunnerBuilder.setTaskBackgroundColor(backgroundColorForTransition); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 4d37e0816639..ac720be90563 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -6007,7 +6007,11 @@ public class WindowManagerService extends IWindowManager.Stub if (mFrozenDisplayId != INVALID_DISPLAY && mFrozenDisplayId == w.getDisplayId() && mWindowsFreezingScreen != WINDOWS_FREEZING_SCREENS_TIMEOUT) { ProtoLog.v(WM_DEBUG_ORIENTATION, "Changing surface while display frozen: %s", w); - w.setOrientationChanging(true); + // WindowsState#reportResized won't tell invisible requested window to redraw, + // so do not set it as changing orientation to avoid affecting draw state. + if (w.isVisibleRequested()) { + w.setOrientationChanging(true); + } if (mWindowsFreezingScreen == WINDOWS_FREEZING_SCREENS_NONE) { mWindowsFreezingScreen = WINDOWS_FREEZING_SCREENS_ACTIVE; // XXX should probably keep timeout from diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 2e1477ddf0f1..32a110ea530e 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -46,6 +46,7 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER; import static com.android.server.wm.ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED; +import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission; import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; @@ -242,8 +243,18 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } @Override - public IBinder startTransition(int type, @Nullable IBinder transitionToken, + public IBinder startNewTransition(int type, @Nullable WindowContainerTransaction t) { + return startTransition(type, null /* transitionToken */, t); + } + + @Override + public void startTransition(@NonNull IBinder transitionToken, @Nullable WindowContainerTransaction t) { + startTransition(-1 /* unused type */, transitionToken, t); + } + + private IBinder startTransition(@WindowManager.TransitionType int type, + @Nullable IBinder transitionToken, @Nullable WindowContainerTransaction t) { enforceTaskPermission("startTransition()"); final CallerInfo caller = new CallerInfo(); final long ident = Binder.clearCallingIdentity(); @@ -1125,10 +1136,13 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final LauncherAppsServiceInternal launcherApps = LocalServices.getService( LauncherAppsServiceInternal.class); - launcherApps.startShortcut(caller.mUid, caller.mPid, callingPackage, - hop.getShortcutInfo().getPackage(), null /* default featureId */, + final boolean success = launcherApps.startShortcut(caller.mUid, caller.mPid, + callingPackage, hop.getShortcutInfo().getPackage(), null /* featureId */, hop.getShortcutInfo().getId(), null /* sourceBounds */, launchOpts, hop.getShortcutInfo().getUserId()); + if (success) { + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } break; } case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: { @@ -1557,10 +1571,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return (cfgChanges & CONTROLLABLE_CONFIGS) == 0; } - private void enforceTaskPermission(String func) { - mService.enforceTaskPermission(func); - } - private boolean isValidTransaction(@NonNull WindowContainerTransaction t) { if (t.getTaskFragmentOrganizer() != null && !mTaskFragmentOrganizerController .isOrganizerRegistered(t.getTaskFragmentOrganizer())) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 42d28612c83f..c161a9b26f59 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -3869,8 +3869,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // configuration update when the window has requested to be hidden. Doing so can lead to // the client erroneously accepting a configuration that would have otherwise caused an // activity restart. We instead hand back the last reported {@link MergedConfiguration}. - if (useLatestConfig || (relayoutVisible && (shouldCheckTokenVisibleRequested() - || mToken.isVisibleRequested()))) { + if (useLatestConfig || (relayoutVisible && (mActivityRecord == null + || mActivityRecord.mVisibleRequested))) { final Configuration globalConfig = getProcessGlobalConfiguration(); final Configuration overrideConfig = getMergedOverrideConfiguration(); outMergedConfiguration.setConfiguration(globalConfig, overrideConfig); diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index 267cff6652bb..f53a1cfcfb3c 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -351,9 +351,39 @@ <xs:annotation name="nonnull"/> <xs:annotation name="final"/> </xs:element> + <xs:sequence> + <!-- Thresholds as tenths of percent of current brightness level, at each level of + brightness --> + <xs:element name="brightnessThresholdPoints" type="thresholdPoints" maxOccurs="1" minOccurs="0"> + <xs:annotation name="final"/> + </xs:element> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="thresholdPoints"> + <xs:sequence> + <xs:element type="thresholdPoint" name="brightnessThresholdPoint" maxOccurs="unbounded" minOccurs="1"> + <xs:annotation name="nonnull"/> + <xs:annotation name="final"/> + </xs:element> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="thresholdPoint"> + <xs:sequence> + <xs:element type="nonNegativeDecimal" name="threshold"> + <xs:annotation name="nonnull"/> + <xs:annotation name="final"/> + </xs:element> + <xs:element type="nonNegativeDecimal" name="percentage"> + <xs:annotation name="nonnull"/> + <xs:annotation name="final"/> + </xs:element> + </xs:sequence> </xs:complexType> <xs:complexType name="autoBrightness"> + <xs:attribute name="enabled" type="xs:boolean" use="optional" default="true"/> <xs:sequence> <!-- Sets the debounce for autoBrightness brightening in millis--> <xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger" diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index f8bff757f1ac..d89bd7cc9aa2 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -6,14 +6,18 @@ package com.android.server.display.config { method public final java.math.BigInteger getBrighteningLightDebounceMillis(); method public final java.math.BigInteger getDarkeningLightDebounceMillis(); method public final com.android.server.display.config.DisplayBrightnessMapping getDisplayBrightnessMapping(); + method public boolean getEnabled(); method public final void setBrighteningLightDebounceMillis(java.math.BigInteger); method public final void setDarkeningLightDebounceMillis(java.math.BigInteger); method public final void setDisplayBrightnessMapping(com.android.server.display.config.DisplayBrightnessMapping); + method public void setEnabled(boolean); } public class BrightnessThresholds { ctor public BrightnessThresholds(); + method public final com.android.server.display.config.ThresholdPoints getBrightnessThresholdPoints(); method @NonNull public final java.math.BigDecimal getMinimum(); + method public final void setBrightnessThresholdPoints(com.android.server.display.config.ThresholdPoints); method public final void setMinimum(@NonNull java.math.BigDecimal); } @@ -204,6 +208,19 @@ package com.android.server.display.config { method public final void setBrightnessThrottlingMap(@NonNull com.android.server.display.config.BrightnessThrottlingMap); } + public class ThresholdPoint { + ctor public ThresholdPoint(); + method @NonNull public final java.math.BigDecimal getPercentage(); + method @NonNull public final java.math.BigDecimal getThreshold(); + method public final void setPercentage(@NonNull java.math.BigDecimal); + method public final void setThreshold(@NonNull java.math.BigDecimal); + } + + public class ThresholdPoints { + ctor public ThresholdPoints(); + method @NonNull public final java.util.List<com.android.server.display.config.ThresholdPoint> getBrightnessThresholdPoint(); + } + public class Thresholds { ctor public Thresholds(); method @NonNull public final com.android.server.display.config.BrightnessThresholds getBrighteningThresholds(); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 20d9cce9a622..75c8b7d4b9ef 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -7783,6 +7783,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Preconditions.checkCallAuthorization( isProfileOwner(caller) || isDefaultDeviceOwner(caller) || hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY)); + Preconditions.checkCallAuthorization(hasCrossUsersPermission(caller, userId)); synchronized (getLockObject()) { if (mOwners.hasProfileOwner(userId) || mOwners.hasDeviceOwner()) { @@ -7823,6 +7824,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Preconditions.checkCallAuthorization( isProfileOwner(caller) || isDefaultDeviceOwner(caller) || hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY)); + Preconditions.checkCallAuthorization(hasCrossUsersPermission(caller, userId)); synchronized (getLockObject()) { if (mOwners.hasProfileOwner(userId) || mOwners.hasDeviceOwner()) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index ac0b7acf182e..42a49c9a08b2 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1821,7 +1821,8 @@ public final class SystemServer implements Dumpable { t.traceBegin("StartStatusBarManagerService"); try { statusBar = new StatusBarManagerService(context); - ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar); + ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar, false, + DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO); } catch (Throwable e) { reportWtf("starting StatusBarManagerService", e); } diff --git a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java index bccd8a0b14b4..9ae892286e55 100644 --- a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java @@ -18,6 +18,7 @@ package com.android.server.backup; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -61,6 +62,7 @@ import java.util.function.IntConsumer; public class UserBackupManagerServiceTest { private static final String TEST_PACKAGE = "package1"; private static final String[] TEST_PACKAGES = new String[] { TEST_PACKAGE }; + private static final int WORKER_THREAD_TIMEOUT_MILLISECONDS = 1; @Mock Context mContext; @Mock IBackupManagerMonitor mBackupManagerMonitor; @@ -179,6 +181,7 @@ public class UserBackupManagerServiceTest { mService.agentDisconnected("com.android.foo"); + mService.waitForAsyncOperation(); verify(mOperationStorage).cancelOperation(eq(123), eq(true), any(IntConsumer.class)); verify(mOperationStorage).cancelOperation(eq(456), eq(true), any()); verify(mOperationStorage).cancelOperation(eq(789), eq(true), any()); @@ -207,6 +210,8 @@ public class UserBackupManagerServiceTest { boolean isEnabledStatePersisted = false; boolean shouldUseNewBackupEligibilityRules = false; + private volatile Thread mWorkerThread = null; + TestBackupService(Context context, PackageManager packageManager, LifecycleOperationStorage operationStorage) { super(context, packageManager, operationStorage); @@ -229,5 +234,23 @@ public class UserBackupManagerServiceTest { boolean shouldUseNewBackupEligibilityRules() { return shouldUseNewBackupEligibilityRules; } + + @Override + Thread getThreadForAsyncOperation(String operationName, Runnable operation) { + mWorkerThread = super.getThreadForAsyncOperation(operationName, operation); + return mWorkerThread; + } + + private void waitForAsyncOperation() { + if (mWorkerThread == null) { + return; + } + + try { + mWorkerThread.join(/* millis */ WORKER_THREAD_TIMEOUT_MILLISECONDS); + } catch (InterruptedException e) { + fail("Failed waiting for worker thread to complete: " + e.getMessage()); + } + } } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java index 10f0a5cbbc40..0cff4f14bf23 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -50,6 +51,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + @Presubmit @SmallTest @RunWith(AndroidTestingRunner.class) @@ -93,7 +97,7 @@ public class ALSProbeTest { mSensorEventListenerCaptor.getValue().onSensorChanged( new SensorEvent(mLightSensor, 1, 2, new float[]{value})); - assertThat(mProbe.getCurrentLux()).isEqualTo(value); + assertThat(mProbe.getMostRecentLux()).isEqualTo(value); } @Test @@ -121,13 +125,17 @@ public class ALSProbeTest { mProbe.destroy(); mProbe.enable(); + AtomicInteger lux = new AtomicInteger(10); + mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); + verify(mSensorManager, never()).registerListener(any(), any(), anyInt()); verifyNoMoreInteractions(mSensorManager); + assertThat(lux.get()).isLessThan(0); } @Test public void testDisabledReportsNegativeValue() { - assertThat(mProbe.getCurrentLux()).isLessThan(0f); + assertThat(mProbe.getMostRecentLux()).isLessThan(0f); mProbe.enable(); verify(mSensorManager).registerListener( @@ -136,7 +144,7 @@ public class ALSProbeTest { new SensorEvent(mLightSensor, 1, 1, new float[]{4.0f})); mProbe.disable(); - assertThat(mProbe.getCurrentLux()).isLessThan(0f); + assertThat(mProbe.getMostRecentLux()).isLessThan(0f); } @Test @@ -150,7 +158,7 @@ public class ALSProbeTest { verify(mSensorManager).unregisterListener(eq(mSensorEventListenerCaptor.getValue())); verifyNoMoreInteractions(mSensorManager); - assertThat(mProbe.getCurrentLux()).isLessThan(0f); + assertThat(mProbe.getMostRecentLux()).isLessThan(0f); } @Test @@ -166,7 +174,165 @@ public class ALSProbeTest { verify(mSensorManager).unregisterListener(any(SensorEventListener.class)); verifyNoMoreInteractions(mSensorManager); - assertThat(mProbe.getCurrentLux()).isLessThan(0f); + assertThat(mProbe.getMostRecentLux()).isLessThan(0f); + } + + @Test + public void testWatchDogCompletesAwait() { + mProbe.enable(); + + AtomicInteger lux = new AtomicInteger(-9); + mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); + + verify(mSensorManager).registerListener( + mSensorEventListenerCaptor.capture(), any(), anyInt()); + + moveTimeBy(TIMEOUT_MS); + + assertThat(lux.get()).isEqualTo(-1); + verify(mSensorManager).unregisterListener(any(SensorEventListener.class)); + verifyNoMoreInteractions(mSensorManager); + } + + @Test + public void testNextLuxWhenAlreadyEnabledAndNotAvailable() { + testNextLuxWhenAlreadyEnabled(false /* dataIsAvailable */); + } + + @Test + public void testNextLuxWhenAlreadyEnabledAndAvailable() { + testNextLuxWhenAlreadyEnabled(true /* dataIsAvailable */); + } + + private void testNextLuxWhenAlreadyEnabled(boolean dataIsAvailable) { + final List<Integer> values = List.of(1, 2, 3, 4, 6); + mProbe.enable(); + + verify(mSensorManager).registerListener( + mSensorEventListenerCaptor.capture(), any(), anyInt()); + + if (dataIsAvailable) { + for (int v : values) { + mSensorEventListenerCaptor.getValue().onSensorChanged( + new SensorEvent(mLightSensor, 1, 1, new float[]{v})); + } + } + AtomicInteger lux = new AtomicInteger(-1); + mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); + if (!dataIsAvailable) { + for (int v : values) { + mSensorEventListenerCaptor.getValue().onSensorChanged( + new SensorEvent(mLightSensor, 1, 1, new float[]{v})); + } + } + + mSensorEventListenerCaptor.getValue().onSensorChanged( + new SensorEvent(mLightSensor, 1, 1, new float[]{200f})); + + // should remain enabled + assertThat(lux.get()).isEqualTo(values.get(dataIsAvailable ? values.size() - 1 : 0)); + verify(mSensorManager, never()).unregisterListener(any(SensorEventListener.class)); + verifyNoMoreInteractions(mSensorManager); + + final int anotherValue = 12; + mSensorEventListenerCaptor.getValue().onSensorChanged( + new SensorEvent(mLightSensor, 1, 1, new float[]{12})); + assertThat(mProbe.getMostRecentLux()).isEqualTo(anotherValue); + } + + @Test + public void testNextLuxWhenNotEnabled() { + testNextLuxWhenNotEnabled(false /* enableWhileWaiting */); + } + + @Test + public void testNextLuxWhenNotEnabledButEnabledLater() { + testNextLuxWhenNotEnabled(true /* enableWhileWaiting */); + } + + private void testNextLuxWhenNotEnabled(boolean enableWhileWaiting) { + final List<Integer> values = List.of(1, 2, 3, 4, 6); + mProbe.disable(); + + AtomicInteger lux = new AtomicInteger(-1); + mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); + + if (enableWhileWaiting) { + mProbe.enable(); + } + + verify(mSensorManager).registerListener( + mSensorEventListenerCaptor.capture(), any(), anyInt()); + for (int v : values) { + mSensorEventListenerCaptor.getValue().onSensorChanged( + new SensorEvent(mLightSensor, 1, 1, new float[]{v})); + } + + // should restore the disabled state + assertThat(lux.get()).isEqualTo(values.get(0)); + verify(mSensorManager, enableWhileWaiting ? never() : times(1)).unregisterListener( + any(SensorEventListener.class)); + verifyNoMoreInteractions(mSensorManager); + } + + @Test + public void testNextLuxIsNotCanceledByDisableOrDestroy() { + final int value = 7; + AtomicInteger lux = new AtomicInteger(-1); + mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); + + verify(mSensorManager).registerListener( + mSensorEventListenerCaptor.capture(), any(), anyInt()); + + mProbe.destroy(); + mProbe.disable(); + + assertThat(lux.get()).isEqualTo(-1); + + mSensorEventListenerCaptor.getValue().onSensorChanged( + new SensorEvent(mLightSensor, 1, 1, new float[]{value})); + + assertThat(lux.get()).isEqualTo(value); + + mSensorEventListenerCaptor.getValue().onSensorChanged( + new SensorEvent(mLightSensor, 1, 1, new float[]{value + 1})); + + // should remain destroyed + mProbe.enable(); + + assertThat(lux.get()).isEqualTo(value); + verify(mSensorManager).unregisterListener(any(SensorEventListener.class)); + verifyNoMoreInteractions(mSensorManager); + } + + @Test + public void testMultipleNextConsumers() { + final int value = 7; + AtomicInteger lux = new AtomicInteger(-1); + AtomicInteger lux2 = new AtomicInteger(-1); + mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); + mProbe.awaitNextLux((v) -> lux2.set(Math.round(v)), null /* handler */); + + verify(mSensorManager).registerListener( + mSensorEventListenerCaptor.capture(), any(), anyInt()); + mSensorEventListenerCaptor.getValue().onSensorChanged( + new SensorEvent(mLightSensor, 1, 1, new float[]{value})); + + assertThat(lux.get()).isEqualTo(value); + assertThat(lux2.get()).isEqualTo(value); + } + + @Test + public void testNoNextLuxWhenDestroyed() { + mProbe.destroy(); + + AtomicInteger lux = new AtomicInteger(-20); + mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */); + + assertThat(lux.get()).isEqualTo(-1); + verify(mSensorManager, never()).registerListener( + mSensorEventListenerCaptor.capture(), any(), anyInt()); + verifyNoMoreInteractions(mSensorManager); } private void moveTimeBy(long millis) { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java index 60dc2eb6081d..88a9646cac8a 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java @@ -121,7 +121,7 @@ public class BiometricLoggerTest { verify(mSink).authenticate(eq(mOpContext), eq(DEFAULT_MODALITY), eq(DEFAULT_ACTION), eq(DEFAULT_CLIENT), anyBoolean(), anyLong(), anyInt(), eq(requireConfirmation), - eq(targetUserId), anyFloat()); + eq(targetUserId), any()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index dea4d4fb7c64..606f4864643c 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.same; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -40,6 +41,7 @@ import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.fingerprint.ISession; import android.hardware.biometrics.fingerprint.PointerContext; import android.hardware.fingerprint.Fingerprint; +import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.ISidefpsController; import android.hardware.fingerprint.IUdfpsOverlayController; @@ -72,6 +74,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.time.Clock; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -127,6 +130,8 @@ public class FingerprintAuthenticationClientTest { private ICancellationSignal mCancellationSignal; @Mock private Probe mLuxProbe; + @Mock + private Clock mClock; @Captor private ArgumentCaptor<OperationContext> mOperationContextCaptor; @Captor @@ -215,7 +220,7 @@ public class FingerprintAuthenticationClientTest { @Test public void luxProbeWhenAwake() throws RemoteException { - when(mBiometricContext.isAwake()).thenReturn(false, true, false); + when(mBiometricContext.isAwake()).thenReturn(false); when(mBiometricContext.isAod()).thenReturn(false); final FingerprintAuthenticationClient client = createClient(); client.start(mCallback); @@ -228,15 +233,38 @@ public class FingerprintAuthenticationClientTest { verify(mLuxProbe, never()).enable(); reset(mLuxProbe); + when(mBiometricContext.isAwake()).thenReturn(true); + mContextInjector.getValue().accept(opContext); verify(mLuxProbe).enable(); verify(mLuxProbe, never()).disable(); + when(mBiometricContext.isAwake()).thenReturn(false); + mContextInjector.getValue().accept(opContext); verify(mLuxProbe).disable(); } @Test + public void luxProbeEnabledOnStartWhenWake() throws RemoteException { + luxProbeEnabledOnStart(true /* isAwake */); + } + + @Test + public void luxProbeNotEnabledOnStartWhenNotWake() throws RemoteException { + luxProbeEnabledOnStart(false /* isAwake */); + } + + private void luxProbeEnabledOnStart(boolean isAwake) throws RemoteException { + when(mBiometricContext.isAwake()).thenReturn(isAwake); + when(mBiometricContext.isAod()).thenReturn(false); + final FingerprintAuthenticationClient client = createClient(); + client.start(mCallback); + + verify(mLuxProbe, isAwake ? times(1) : never()).enable(); + } + + @Test public void luxProbeDisabledOnAod() throws RemoteException { when(mBiometricContext.isAwake()).thenReturn(false); when(mBiometricContext.isAod()).thenReturn(true); @@ -423,6 +451,52 @@ public class FingerprintAuthenticationClientTest { } @Test + public void sideFingerprintSkipsWindowIfVendorMessageMatch() throws Exception { + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + final int vendorAcquireMessage = 1234; + + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, + FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage, + vendorAcquireMessage); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + mLooper.dispatchAll(); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, vendorAcquireMessage); + mLooper.dispatchAll(); + + verify(mCallback).onClientFinished(any(), eq(true)); + } + + @Test + public void sideFingerprintDoesNotSkipWindowOnVendorErrorMismatch() throws Exception { + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + final int vendorAcquireMessage = 1234; + + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, + FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage, + vendorAcquireMessage); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + mLooper.dispatchAll(); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 1); + mLooper.dispatchAll(); + + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + } + + @Test public void sideFingerprintSendsAuthIfFingerUp() throws Exception { when(mSensorProps.isAnySidefpsType()).thenReturn(true); @@ -469,6 +543,79 @@ public class FingerprintAuthenticationClientTest { verify(mCallback).onClientFinished(any(), eq(true)); } + @Test + public void sideFingerprintPowerWindowStartsOnAcquireStart() throws Exception { + final int powerWindow = 500; + final long authStart = 300; + + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsBpPowerPressWindow, powerWindow); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + + // Acquire start occurs at time = 0ms + when(mClock.millis()).thenReturn(0L); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); + + // Auth occurs at time = 300 + when(mClock.millis()).thenReturn(authStart); + // At this point the delay should be 500 - (300 - 0) == 200 milliseconds. + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + mLooper.dispatchAll(); + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + + // After waiting 200 milliseconds, auth should succeed. + mLooper.moveTimeForward(powerWindow - authStart); + mLooper.dispatchAll(); + verify(mCallback).onClientFinished(any(), eq(true)); + } + + @Test + public void sideFingerprintPowerWindowStartsOnLastAcquireStart() throws Exception { + final int powerWindow = 500; + + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + mContext.getOrCreateTestableResources().addOverride( + R.integer.config_sidefpsBpPowerPressWindow, powerWindow); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + // Acquire start occurs at time = 0ms + when(mClock.millis()).thenReturn(0L); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); + + // Auth reject occurs at time = 300ms + when(mClock.millis()).thenReturn(300L); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + false /* authenticated */, new ArrayList<>()); + mLooper.dispatchAll(); + + mLooper.moveTimeForward(300); + mLooper.dispatchAll(); + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + + when(mClock.millis()).thenReturn(1300L); + client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); + + // If code is correct, the new acquired start timestamp should be used + // and the code should only have to wait 500 - (1500-1300)ms. + when(mClock.millis()).thenReturn(1500L); + client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + mLooper.dispatchAll(); + + mLooper.moveTimeForward(299); + mLooper.dispatchAll(); + verify(mCallback, never()).onClientFinished(any(), anyBoolean()); + + mLooper.moveTimeForward(1); + mLooper.dispatchAll(); + verify(mCallback).onClientFinished(any(), eq(true)); + } + private FingerprintAuthenticationClient createClient() throws RemoteException { return createClient(100 /* version */, true /* allowBackgroundAuthentication */); } @@ -496,7 +643,7 @@ public class FingerprintAuthenticationClientTest { null /* taskStackListener */, mLockoutCache, mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication, mSensorProps, - new Handler(mLooper.getLooper())) { + new Handler(mLooper.getLooper()), mClock) { @Override protected ActivityTaskManager getActivityTaskManager() { return mActivityTaskManager; diff --git a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java index 8280fc6c962f..4f2b613d7aed 100644 --- a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java @@ -422,13 +422,13 @@ public class AutomaticBrightnessControllerTest { @Test public void testHysteresisLevels() { - int[] ambientBrighteningThresholds = {100, 200}; - int[] ambientDarkeningThresholds = {400, 500}; - int[] ambientThresholdLevels = {500}; + float[] ambientBrighteningThresholds = {50, 100}; + float[] ambientDarkeningThresholds = {10, 20}; + float[] ambientThresholdLevels = {0, 500}; float ambientDarkeningMinChangeThreshold = 3.0f; float ambientBrighteningMinChangeThreshold = 1.5f; HysteresisLevels hysteresisLevels = new HysteresisLevels(ambientBrighteningThresholds, - ambientDarkeningThresholds, ambientThresholdLevels, + ambientDarkeningThresholds, ambientThresholdLevels, ambientThresholdLevels, ambientDarkeningMinChangeThreshold, ambientBrighteningMinChangeThreshold); // test low, activate minimum change thresholds. @@ -437,16 +437,17 @@ public class AutomaticBrightnessControllerTest { assertEquals(1f, hysteresisLevels.getDarkeningThreshold(4.0f), EPSILON); // test max - assertEquals(12000f, hysteresisLevels.getBrighteningThreshold(10000.0f), EPSILON); - assertEquals(5000f, hysteresisLevels.getDarkeningThreshold(10000.0f), EPSILON); + // epsilon is x2 here, since the next floating point value about 20,000 is 0.0019531 greater + assertEquals(20000f, hysteresisLevels.getBrighteningThreshold(10000.0f), EPSILON * 2); + assertEquals(8000f, hysteresisLevels.getDarkeningThreshold(10000.0f), EPSILON); // test just below threshold - assertEquals(548.9f, hysteresisLevels.getBrighteningThreshold(499f), EPSILON); - assertEquals(299.4f, hysteresisLevels.getDarkeningThreshold(499f), EPSILON); + assertEquals(748.5f, hysteresisLevels.getBrighteningThreshold(499f), EPSILON); + assertEquals(449.1f, hysteresisLevels.getDarkeningThreshold(499f), EPSILON); // test at (considered above) threshold - assertEquals(600f, hysteresisLevels.getBrighteningThreshold(500f), EPSILON); - assertEquals(250f, hysteresisLevels.getDarkeningThreshold(500f), EPSILON); + assertEquals(1000f, hysteresisLevels.getBrighteningThreshold(500f), EPSILON); + assertEquals(400f, hysteresisLevels.getDarkeningThreshold(500f), EPSILON); } @Test diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java index 0a5df410bcdb..c2e8417f2ff0 100644 --- a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -885,6 +886,29 @@ public class BrightnessTrackerTest { assertNull(mInjector.mLightSensor); } + @Test + public void testOnlyOneReceiverRegistered() { + assertNull(mInjector.mLightSensor); + assertNull(mInjector.mSensorListener); + startTracker(mTracker, 0.3f, false); + + assertNotNull(mInjector.mLightSensor); + assertNotNull(mInjector.mSensorListener); + Sensor registeredLightSensor = mInjector.mLightSensor; + SensorEventListener registeredSensorListener = mInjector.mSensorListener; + + mTracker.start(0.3f); + assertSame(registeredLightSensor, mInjector.mLightSensor); + assertSame(registeredSensorListener, mInjector.mSensorListener); + + mTracker.stop(); + assertNull(mInjector.mLightSensor); + assertNull(mInjector.mSensorListener); + + // mInjector asserts that we aren't removing a null receiver + mTracker.stop(); + } + private InputStream getInputStream(String data) { return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); } diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java index 66420ad4572e..30024fb5c221 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -32,6 +32,8 @@ import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.R; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -47,7 +49,17 @@ import java.nio.file.Path; @Presubmit @RunWith(AndroidJUnit4.class) public final class DisplayDeviceConfigTest { + private static final int DEFAULT_PEAK_REFRESH_RATE = 75; + private static final int DEFAULT_REFRESH_RATE = 120; + private static final int[] LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{10, 30}; + private static final int[] LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{1, 21}; + private static final int[] HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{160}; + private static final int[] HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{30000}; + private DisplayDeviceConfig mDisplayDeviceConfig; + private static final float ZERO_DELTA = 0.0f; + private static final float SMALL_DELTA = 0.0001f; + @Mock private Context mContext; @@ -69,26 +81,74 @@ public final class DisplayDeviceConfigTest { assertEquals(mDisplayDeviceConfig.getAmbientHorizonShort(), 50); assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000); assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000); - assertEquals(mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), 10.0f, 0.0f); - assertEquals(mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), 2.0f, 0.0f); - assertEquals(mDisplayDeviceConfig.getBrightnessRampFastDecrease(), 0.01f, 0.0f); - assertEquals(mDisplayDeviceConfig.getBrightnessRampFastIncrease(), 0.02f, 0.0f); - assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), 0.04f, 0.0f); - assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), 0.03f, 0.0f); - assertEquals(mDisplayDeviceConfig.getBrightnessDefault(), 0.5f, 0.0f); + assertEquals(mDisplayDeviceConfig.getBrightnessRampFastDecrease(), 0.01f, ZERO_DELTA); + assertEquals(mDisplayDeviceConfig.getBrightnessRampFastIncrease(), 0.02f, ZERO_DELTA); + assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowIncrease(), 0.04f, ZERO_DELTA); + assertEquals(mDisplayDeviceConfig.getBrightnessRampSlowDecrease(), 0.03f, ZERO_DELTA); + assertEquals(mDisplayDeviceConfig.getBrightnessDefault(), 0.5f, ZERO_DELTA); assertArrayEquals(mDisplayDeviceConfig.getBrightness(), new float[]{0.0f, 0.62f, 1.0f}, - 0.0f); - assertArrayEquals(mDisplayDeviceConfig.getNits(), new float[]{2.0f, 500.0f, 800.0f}, 0.0f); + ZERO_DELTA); + assertArrayEquals(mDisplayDeviceConfig.getNits(), new float[]{2.0f, 500.0f, 800.0f}, + ZERO_DELTA); assertArrayEquals(mDisplayDeviceConfig.getBacklight(), new float[]{0.0f, 0.62f, 1.0f}, - 0.0f); - assertEquals(mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), 0.001, 0.000001f); - assertEquals(mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), 0.002, 0.000001f); + ZERO_DELTA); assertEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLightDebounce(), 2000); assertEquals(mDisplayDeviceConfig.getAutoBrightnessDarkeningLightDebounce(), 1000); assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new - float[]{0.0f, 50.0f, 80.0f}, 0.0f); + float[]{0.0f, 50.0f, 80.0f}, ZERO_DELTA); assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new - float[]{45.32f, 75.43f}, 0.0f); + float[]{45.32f, 75.43f}, ZERO_DELTA); + + // Test thresholds + assertEquals(10, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), + ZERO_DELTA); + assertEquals(20, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(), + ZERO_DELTA); + assertEquals(30, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA); + assertEquals(40, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA); + + assertEquals(0.1f, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA); + assertEquals(0.2f, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA); + assertEquals(0.3f, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA); + assertEquals(0.4f, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA); + + assertArrayEquals(new float[]{0, 0.10f, 0.20f}, + mDisplayDeviceConfig.getScreenBrighteningLevels(), ZERO_DELTA); + assertArrayEquals(new float[]{9, 10, 11}, + mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA); + assertArrayEquals(new float[]{0, 0.11f, 0.21f}, + mDisplayDeviceConfig.getScreenDarkeningLevels(), ZERO_DELTA); + assertArrayEquals(new float[]{11, 12, 13}, + mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA); + + assertArrayEquals(new float[]{0, 100, 200}, + mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA); + assertArrayEquals(new float[]{13, 14, 15}, + mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA); + assertArrayEquals(new float[]{0, 300, 400}, + mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA); + assertArrayEquals(new float[]{15, 16, 17}, + mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA); + + assertArrayEquals(new float[]{0, 0.12f, 0.22f}, + mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), ZERO_DELTA); + assertArrayEquals(new float[]{17, 18, 19}, + mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA); + assertArrayEquals(new float[]{0, 0.13f, 0.23f}, + mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), ZERO_DELTA); + assertArrayEquals(new float[]{19, 20, 21}, + mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA); + + assertArrayEquals(new float[]{0, 500, 600}, + mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA); + assertArrayEquals(new float[]{21, 22, 23}, + mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA); + assertArrayEquals(new float[]{0, 700, 800}, + mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA); + assertArrayEquals(new float[]{23, 24, 25}, + mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA); + + // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping, // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor. } @@ -97,9 +157,70 @@ public final class DisplayDeviceConfigTest { public void testConfigValuesFromConfigResource() { setupDisplayDeviceConfigFromConfigResourceFile(); assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new - float[]{2.0f, 200.0f, 600.0f}, 0.0f); + float[]{2.0f, 200.0f, 600.0f}, ZERO_DELTA); assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new - float[]{0.0f, 0.0f, 110.0f, 500.0f}, 0.0f); + float[]{0.0f, 0.0f, 110.0f, 500.0f}, ZERO_DELTA); + + // Test thresholds + assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold(), ZERO_DELTA); + assertEquals(0, mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle(), + ZERO_DELTA); + assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold(), ZERO_DELTA); + assertEquals(0, mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle(), ZERO_DELTA); + + assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), ZERO_DELTA); + assertEquals(0, mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle(), ZERO_DELTA); + assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), ZERO_DELTA); + assertEquals(0, mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle(), ZERO_DELTA); + + // screen levels will be considered "old screen brightness scale" + // and therefore will divide by 255 + assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f}, + mDisplayDeviceConfig.getScreenBrighteningLevels(), SMALL_DELTA); + assertArrayEquals(new float[]{35, 36, 37}, + mDisplayDeviceConfig.getScreenBrighteningPercentages(), ZERO_DELTA); + assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f}, + mDisplayDeviceConfig.getScreenDarkeningLevels(), SMALL_DELTA); + assertArrayEquals(new float[]{37, 38, 39}, + mDisplayDeviceConfig.getScreenDarkeningPercentages(), ZERO_DELTA); + + assertArrayEquals(new float[]{0, 30, 31}, + mDisplayDeviceConfig.getAmbientBrighteningLevels(), ZERO_DELTA); + assertArrayEquals(new float[]{27, 28, 29}, + mDisplayDeviceConfig.getAmbientBrighteningPercentages(), ZERO_DELTA); + assertArrayEquals(new float[]{0, 30, 31}, + mDisplayDeviceConfig.getAmbientDarkeningLevels(), ZERO_DELTA); + assertArrayEquals(new float[]{29, 30, 31}, + mDisplayDeviceConfig.getAmbientDarkeningPercentages(), ZERO_DELTA); + + assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f}, + mDisplayDeviceConfig.getScreenBrighteningLevelsIdle(), SMALL_DELTA); + assertArrayEquals(new float[]{35, 36, 37}, + mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle(), ZERO_DELTA); + assertArrayEquals(new float[]{0, 42 / 255f, 43 / 255f}, + mDisplayDeviceConfig.getScreenDarkeningLevelsIdle(), SMALL_DELTA); + assertArrayEquals(new float[]{37, 38, 39}, + mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle(), ZERO_DELTA); + + assertArrayEquals(new float[]{0, 30, 31}, + mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle(), ZERO_DELTA); + assertArrayEquals(new float[]{27, 28, 29}, + mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle(), ZERO_DELTA); + assertArrayEquals(new float[]{0, 30, 31}, + mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA); + assertArrayEquals(new float[]{29, 30, 31}, + mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA); + + assertEquals(mDisplayDeviceConfig.getDefaultRefreshRate(), DEFAULT_REFRESH_RATE); + assertEquals(mDisplayDeviceConfig.getDefaultPeakRefreshRate(), DEFAULT_PEAK_REFRESH_RATE); + assertArrayEquals(mDisplayDeviceConfig.getLowDisplayBrightnessThresholds(), + LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); + assertArrayEquals(mDisplayDeviceConfig.getLowAmbientBrightnessThresholds(), + LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE); + assertArrayEquals(mDisplayDeviceConfig.getHighDisplayBrightnessThresholds(), + HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); + assertArrayEquals(mDisplayDeviceConfig.getHighAmbientBrightnessThresholds(), + HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE); // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping, // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor. } @@ -154,11 +275,126 @@ public final class DisplayDeviceConfigTest { + "<ambientBrightnessChangeThresholds>\n" + "<brighteningThresholds>\n" + "<minimum>10</minimum>\n" + + "<brightnessThresholdPoints>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0</threshold><percentage>13</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>100</threshold><percentage>14</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>200</threshold><percentage>15</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "</brightnessThresholdPoints>\n" + "</brighteningThresholds>\n" + "<darkeningThresholds>\n" - + "<minimum>2</minimum>\n" + + "<minimum>30</minimum>\n" + + "<brightnessThresholdPoints>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0</threshold><percentage>15</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>300</threshold><percentage>16</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>400</threshold><percentage>17</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "</brightnessThresholdPoints>\n" + "</darkeningThresholds>\n" + "</ambientBrightnessChangeThresholds>\n" + + "<displayBrightnessChangeThresholds>\n" + + "<brighteningThresholds>\n" + + "<minimum>0.1</minimum>\n" + + "<brightnessThresholdPoints>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0</threshold>\n" + + "<percentage>9</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0.10</threshold>\n" + + "<percentage>10</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0.20</threshold>\n" + + "<percentage>11</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "</brightnessThresholdPoints>\n" + + "</brighteningThresholds>\n" + + "<darkeningThresholds>\n" + + "<minimum>0.3</minimum>\n" + + "<brightnessThresholdPoints>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0</threshold><percentage>11</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0.11</threshold><percentage>12</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0.21</threshold><percentage>13</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "</brightnessThresholdPoints>\n" + + "</darkeningThresholds>\n" + + "</displayBrightnessChangeThresholds>\n" + + "<ambientBrightnessChangeThresholdsIdle>\n" + + "<brighteningThresholds>\n" + + "<minimum>20</minimum>\n" + + "<brightnessThresholdPoints>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0</threshold><percentage>21</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>500</threshold><percentage>22</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>600</threshold><percentage>23</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "</brightnessThresholdPoints>\n" + + "</brighteningThresholds>\n" + + "<darkeningThresholds>\n" + + "<minimum>40</minimum>\n" + + "<brightnessThresholdPoints>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0</threshold><percentage>23</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>700</threshold><percentage>24</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>800</threshold><percentage>25</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "</brightnessThresholdPoints>\n" + + "</darkeningThresholds>\n" + + "</ambientBrightnessChangeThresholdsIdle>\n" + + "<displayBrightnessChangeThresholdsIdle>\n" + + "<brighteningThresholds>\n" + + "<minimum>0.2</minimum>\n" + + "<brightnessThresholdPoints>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0</threshold><percentage>17</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0.12</threshold><percentage>18</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0.22</threshold><percentage>19</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "</brightnessThresholdPoints>\n" + + "</brighteningThresholds>\n" + + "<darkeningThresholds>\n" + + "<minimum>0.4</minimum>\n" + + "<brightnessThresholdPoints>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0</threshold><percentage>19</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0.13</threshold><percentage>20</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "<brightnessThresholdPoint>\n" + + "<threshold>0.23</threshold><percentage>21</percentage>\n" + + "</brightnessThresholdPoint>\n" + + "</brightnessThresholdPoints>\n" + + "</darkeningThresholds>\n" + + "</displayBrightnessChangeThresholdsIdle>\n" + "<screenBrightnessRampFastDecrease>0.01</screenBrightnessRampFastDecrease> " + "<screenBrightnessRampFastIncrease>0.02</screenBrightnessRampFastIncrease> " + "<screenBrightnessRampSlowDecrease>0.03</screenBrightnessRampSlowDecrease>" @@ -171,18 +407,6 @@ public final class DisplayDeviceConfigTest { + "</screenBrightnessRampDecreaseMaxMillis>" + "<ambientLightHorizonLong>5000</ambientLightHorizonLong>\n" + "<ambientLightHorizonShort>50</ambientLightHorizonShort>\n" - + "<displayBrightnessChangeThresholds>" - + "<brighteningThresholds>" - + "<minimum>" - + "0.001" - + "</minimum>" - + "</brighteningThresholds>" - + "<darkeningThresholds>" - + "<minimum>" - + "0.002" - + "</minimum>" - + "</darkeningThresholds>" - + "</displayBrightnessChangeThresholds>" + "<screenBrightnessRampIncreaseMaxMillis>" + "2000" + "</screenBrightnessRampIncreaseMaxMillis>\n" @@ -241,8 +465,39 @@ public final class DisplayDeviceConfigTest { com.android.internal.R.array.config_autoBrightnessLevels)) .thenReturn(screenBrightnessLevelLux); - mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true); + // Thresholds + // Config.xml requires the levels arrays to be of length N and the thresholds arrays to be + // of length N+1 + when(mResources.getIntArray(com.android.internal.R.array.config_ambientThresholdLevels)) + .thenReturn(new int[]{30, 31}); + when(mResources.getIntArray(com.android.internal.R.array.config_screenThresholdLevels)) + .thenReturn(new int[]{42, 43}); + when(mResources.getIntArray( + com.android.internal.R.array.config_ambientBrighteningThresholds)) + .thenReturn(new int[]{270, 280, 290}); + when(mResources.getIntArray(com.android.internal.R.array.config_ambientDarkeningThresholds)) + .thenReturn(new int[]{290, 300, 310}); + when(mResources.getIntArray(R.array.config_screenBrighteningThresholds)) + .thenReturn(new int[]{350, 360, 370}); + when(mResources.getIntArray(R.array.config_screenDarkeningThresholds)) + .thenReturn(new int[]{370, 380, 390}); + // Configs related to refresh rates and blocking zones + when(mResources.getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate)) + .thenReturn(DEFAULT_PEAK_REFRESH_RATE); + when(mResources.getInteger(com.android.internal.R.integer.config_defaultRefreshRate)) + .thenReturn(DEFAULT_REFRESH_RATE); + when(mResources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate)) + .thenReturn(LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); + when(mResources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate)) + .thenReturn(LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE); + when(mResources.getIntArray( + R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate)) + .thenReturn(HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); + when(mResources.getIntArray( + R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate)) + .thenReturn(HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE); + mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true); } private TypedArray createFloatTypedArray(float[] vals) { diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java index 968e1d8c546b..18dd264558ac 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java @@ -1853,6 +1853,20 @@ public class DisplayModeDirectorTest { assertNull(vote); } + @Test + public void testNotifyDefaultDisplayDeviceUpdated() { + DisplayDeviceConfig displayDeviceConfig = mock(DisplayDeviceConfig.class); + when(displayDeviceConfig.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{}); + when(displayDeviceConfig.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{}); + when(displayDeviceConfig.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{}); + when(displayDeviceConfig.getHighAmbientBrightnessThresholds()).thenReturn(new int[]{}); + DisplayModeDirector director = + createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0); + director.defaultDisplayDeviceUpdated(displayDeviceConfig); + verify(displayDeviceConfig).getDefaultRefreshRate(); + verify(displayDeviceConfig).getDefaultPeakRefreshRate(); + } + private Temperature getSkinTemp(@Temperature.ThrottlingStatus int status) { return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status); } diff --git a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java index 9fe8609c85a1..3b0a22f80c30 100644 --- a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java @@ -275,6 +275,75 @@ public class PersistentDataStoreTest { assertNull(mDataStore.getBrightnessConfiguration(userSerial)); } + @Test + public void testStoreAndRestoreResolution() { + final String uniqueDisplayId = "test:123"; + DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) { + @Override + public boolean hasStableUniqueId() { + return true; + } + + @Override + public DisplayDeviceInfo getDisplayDeviceInfoLocked() { + return null; + } + }; + int width = 35; + int height = 45; + mDataStore.loadIfNeeded(); + mDataStore.setUserPreferredResolution(testDisplayDevice, width, height); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + mInjector.setWriteStream(baos); + mDataStore.saveIfNeeded(); + mTestLooper.dispatchAll(); + assertTrue(mInjector.wasWriteSuccessful()); + TestInjector newInjector = new TestInjector(); + PersistentDataStore newDataStore = new PersistentDataStore(newInjector); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + newInjector.setReadStream(bais); + newDataStore.loadIfNeeded(); + assertNotNull(newDataStore.getUserPreferredResolution(testDisplayDevice)); + assertEquals(35, newDataStore.getUserPreferredResolution(testDisplayDevice).x); + assertEquals(35, mDataStore.getUserPreferredResolution(testDisplayDevice).x); + assertEquals(45, newDataStore.getUserPreferredResolution(testDisplayDevice).y); + assertEquals(45, mDataStore.getUserPreferredResolution(testDisplayDevice).y); + } + + @Test + public void testStoreAndRestoreRefreshRate() { + final String uniqueDisplayId = "test:123"; + DisplayDevice testDisplayDevice = new DisplayDevice(null, null, uniqueDisplayId, null) { + @Override + public boolean hasStableUniqueId() { + return true; + } + + @Override + public DisplayDeviceInfo getDisplayDeviceInfoLocked() { + return null; + } + }; + float refreshRate = 85.3f; + mDataStore.loadIfNeeded(); + mDataStore.setUserPreferredRefreshRate(testDisplayDevice, refreshRate); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + mInjector.setWriteStream(baos); + mDataStore.saveIfNeeded(); + mTestLooper.dispatchAll(); + assertTrue(mInjector.wasWriteSuccessful()); + TestInjector newInjector = new TestInjector(); + PersistentDataStore newDataStore = new PersistentDataStore(newInjector); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + newInjector.setReadStream(bais); + newDataStore.loadIfNeeded(); + assertNotNull(newDataStore.getUserPreferredRefreshRate(testDisplayDevice)); + assertEquals(85.3f, mDataStore.getUserPreferredRefreshRate(testDisplayDevice), 01.f); + assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f); + } + public class TestInjector extends PersistentDataStore.Injector { private InputStream mReadStream; private OutputStream mWriteStream; diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java new file mode 100644 index 000000000000..303a370b0ba9 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.dreams; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.IRemoteCallback; +import android.os.RemoteException; +import android.os.test.TestLooper; +import android.service.dreams.IDreamService; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DreamControllerTest { + @Mock + private DreamController.Listener mListener; + @Mock + private Context mContext; + @Mock + private IBinder mIBinder; + @Mock + private IDreamService mIDreamService; + + @Captor + private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor; + @Captor + private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor; + + private final TestLooper mLooper = new TestLooper(); + private final Handler mHandler = new Handler(mLooper.getLooper()); + + private DreamController mDreamController; + + private Binder mToken; + private ComponentName mDreamName; + private ComponentName mOverlayName; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(mIDreamService.asBinder()).thenReturn(mIBinder); + when(mIBinder.queryLocalInterface(anyString())).thenReturn(mIDreamService); + when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true); + + mToken = new Binder(); + mDreamName = ComponentName.unflattenFromString("dream"); + mOverlayName = ComponentName.unflattenFromString("dream_overlay"); + mDreamController = new DreamController(mContext, mHandler, mListener); + } + + @Test + public void startDream_attachOnServiceConnected() throws RemoteException { + // Call dream controller to start dreaming. + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + + // Mock service connected. + final ServiceConnection serviceConnection = captureServiceConnection(); + serviceConnection.onServiceConnected(mDreamName, mIBinder); + mLooper.dispatchAll(); + + // Verify that dream service is called to attach. + verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/, any()); + } + + @Test + public void startDream_startASecondDream_detachOldDreamOnceNewDreamIsStarted() + throws RemoteException { + // Start first dream. + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + captureServiceConnection().onServiceConnected(mDreamName, mIBinder); + mLooper.dispatchAll(); + clearInvocations(mContext); + + // Set up second dream. + final Binder newToken = new Binder(); + final ComponentName newDreamName = ComponentName.unflattenFromString("new_dream"); + final ComponentName newOverlayName = ComponentName.unflattenFromString("new_dream_overlay"); + final IDreamService newDreamService = mock(IDreamService.class); + final IBinder newBinder = mock(IBinder.class); + when(newDreamService.asBinder()).thenReturn(newBinder); + when(newBinder.queryLocalInterface(anyString())).thenReturn(newDreamService); + + // Start second dream. + mDreamController.startDream(newToken, newDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, newOverlayName, "test" /*reason*/); + captureServiceConnection().onServiceConnected(newDreamName, newBinder); + mLooper.dispatchAll(); + + // Mock second dream started. + verify(newDreamService).attach(eq(newToken), eq(false) /*doze*/, + mRemoteCallbackCaptor.capture()); + mRemoteCallbackCaptor.getValue().sendResult(null /*data*/); + mLooper.dispatchAll(); + + // Verify that the first dream is called to detach. + verify(mIDreamService).detach(); + } + + @Test + public void stopDream_detachFromService() throws RemoteException { + // Start dream. + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + captureServiceConnection().onServiceConnected(mDreamName, mIBinder); + mLooper.dispatchAll(); + + // Stop dream. + mDreamController.stopDream(true /*immediate*/, "test stop dream" /*reason*/); + + // Verify that dream service is called to detach. + verify(mIDreamService).detach(); + } + + private ServiceConnection captureServiceConnection() { + verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(), + any()); + return mServiceConnectionACaptor.getValue(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt new file mode 100644 index 000000000000..246aa90ea173 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input + +import android.content.Context +import android.content.ContextWrapper +import android.hardware.BatteryState.STATUS_CHARGING +import android.hardware.BatteryState.STATUS_FULL +import android.hardware.input.IInputDeviceBatteryListener +import android.hardware.input.IInputManager +import android.hardware.input.InputManager +import android.os.Binder +import android.os.IBinder +import android.platform.test.annotations.Presubmit +import android.view.InputDevice +import androidx.test.InstrumentationRegistry +import org.junit.After +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.notNull +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyLong +import org.mockito.Mockito.eq +import org.mockito.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.Mockito.`when` +import org.mockito.junit.MockitoJUnit + +/** + * Tests for {@link InputDeviceBatteryController}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:InputDeviceBatteryControllerTests + */ +@Presubmit +class BatteryControllerTests { + companion object { + const val PID = 42 + const val DEVICE_ID = 13 + const val SECOND_DEVICE_ID = 11 + } + + @get:Rule + val rule = MockitoJUnit.rule()!! + + @Mock + private lateinit var native: NativeInputManagerService + @Mock + private lateinit var iInputManager: IInputManager + + private lateinit var batteryController: BatteryController + private lateinit var context: Context + + @Before + fun setup() { + context = spy(ContextWrapper(InstrumentationRegistry.getContext())) + val inputManager = InputManager.resetInstance(iInputManager) + `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) + `when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, SECOND_DEVICE_ID)) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(createInputDevice(DEVICE_ID)) + `when`(iInputManager.getInputDevice(SECOND_DEVICE_ID)) + .thenReturn(createInputDevice(SECOND_DEVICE_ID)) + + batteryController = BatteryController(context, native) + } + + private fun createInputDevice(deviceId: Int): InputDevice = + InputDevice(deviceId, 0 /*generation*/, 0 /*controllerNumber*/, + "Device $deviceId" /*name*/, 0 /*vendorId*/, 0 /*productId*/, "descriptor$deviceId", + true /*isExternal*/, 0 /*sources*/, 0 /*keyboardType*/, null /*keyCharacterMap*/, + false /*hasVibrator*/, false /*hasMicrophone*/, false /*hasButtonUnderPad*/, + false /*hasSensor*/, true /*hasBattery*/) + + @After + fun tearDown() { + InputManager.clearInstance() + } + + private fun createMockListener(): IInputDeviceBatteryListener { + val listener = mock(IInputDeviceBatteryListener::class.java) + val binder = mock(Binder::class.java) + `when`(listener.asBinder()).thenReturn(binder) + return listener + } + + @Test + fun testRegisterAndUnregisterBinderLifecycle() { + val listener = createMockListener() + // Ensure the binder lifecycle is tracked when registering a listener. + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder()).linkToDeath(notNull(), anyInt()) + batteryController.registerBatteryListener(SECOND_DEVICE_ID, listener, PID) + verify(listener.asBinder(), times(1)).linkToDeath(notNull(), anyInt()) + + // Ensure the binder lifecycle stops being tracked when all devices stopped being monitored. + batteryController.unregisterBatteryListener(SECOND_DEVICE_ID, listener, PID) + verify(listener.asBinder(), never()).unlinkToDeath(notNull(), anyInt()) + batteryController.unregisterBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder()).unlinkToDeath(notNull(), anyInt()) + } + + @Test + fun testOneListenerPerProcess() { + val listener1 = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener1, PID) + verify(listener1.asBinder()).linkToDeath(notNull(), anyInt()) + + // If a second listener is added for the same process, a security exception is thrown. + val listener2 = createMockListener() + try { + batteryController.registerBatteryListener(DEVICE_ID, listener2, PID) + fail("Expected security exception when registering more than one listener per process") + } catch (ignored: SecurityException) { + } + } + + @Test + fun testProcessDeathRemovesListener() { + val deathRecipient = ArgumentCaptor.forClass(IBinder.DeathRecipient::class.java) + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder()).linkToDeath(deathRecipient.capture(), anyInt()) + + // When the binder dies, the callback is unregistered. + deathRecipient.value!!.binderDied(listener.asBinder()) + verify(listener.asBinder()).unlinkToDeath(notNull(), anyInt()) + + // It is now possible to register the same listener again. + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder(), times(2)).linkToDeath(notNull(), anyInt()) + } + + @Test + fun testRegisteringListenerNotifiesStateImmediately() { + `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_FULL) + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(100) + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), + eq(STATUS_FULL), eq(1f), anyLong()) + + `when`(native.getBatteryStatus(SECOND_DEVICE_ID)).thenReturn(STATUS_CHARGING) + `when`(native.getBatteryCapacity(SECOND_DEVICE_ID)).thenReturn(78) + batteryController.registerBatteryListener(SECOND_DEVICE_ID, listener, PID) + verify(listener).onBatteryStateChanged(eq(SECOND_DEVICE_ID), eq(true /*isPresent*/), + eq(STATUS_CHARGING), eq(0.78f), anyLong()) + } +} diff --git a/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java b/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java index d8c9c3433313..e3ca1707ae0c 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java @@ -116,7 +116,7 @@ public class PowerGroupTest { @Test public void testDreamPowerGroup() { assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); - mPowerGroup.dreamLocked(TIMESTAMP1, UID); + mPowerGroup.dreamLocked(TIMESTAMP1, UID, /* allowWake= */ false); assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); assertThat(mPowerGroup.isSandmanSummonedLocked()).isTrue(); verify(mWakefulnessCallbackMock).onWakefulnessChangedLocked(eq(GROUP_ID), @@ -172,7 +172,7 @@ public class PowerGroupTest { eq(UID), /* opUid= */ anyInt(), /* opPackageName= */ isNull(), /* details= */ isNull()); assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DOZING); - assertThat(mPowerGroup.dreamLocked(TIMESTAMP2, UID)).isFalse(); + assertThat(mPowerGroup.dreamLocked(TIMESTAMP2, UID, /* allowWake= */ false)).isFalse(); assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DOZING); verify(mWakefulnessCallbackMock, never()).onWakefulnessChangedLocked( eq(GROUP_ID), /* wakefulness= */ eq(WAKEFULNESS_DREAMING), eq(TIMESTAMP2), @@ -181,6 +181,22 @@ public class PowerGroupTest { } @Test + public void testDreamPowerGroupWhenNotAwakeShouldWake() { + mPowerGroup.dozeLocked(TIMESTAMP1, UID, GO_TO_SLEEP_REASON_TIMEOUT); + verify(mWakefulnessCallbackMock).onWakefulnessChangedLocked(eq(GROUP_ID), + eq(WAKEFULNESS_DOZING), eq(TIMESTAMP1), eq(GO_TO_SLEEP_REASON_TIMEOUT), + eq(UID), /* opUid= */ anyInt(), /* opPackageName= */ isNull(), + /* details= */ isNull()); + assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DOZING); + assertThat(mPowerGroup.dreamLocked(TIMESTAMP2, UID, /* allowWake= */ true)).isTrue(); + assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); + verify(mWakefulnessCallbackMock).onWakefulnessChangedLocked( + eq(GROUP_ID), /* wakefulness= */ eq(WAKEFULNESS_DREAMING), eq(TIMESTAMP2), + /* reason= */ anyInt(), eq(UID), /* opUid= */ anyInt(), /* opPackageName= */ any(), + /* details= */ any()); + } + + @Test public void testLastWakeAndSleepTimeIsUpdated() { assertThat(mPowerGroup.getLastWakeTimeLocked()).isEqualTo(TIMESTAMP_CREATE); assertThat(mPowerGroup.getLastSleepTimeLocked()).isEqualTo(TIMESTAMP_CREATE); @@ -514,7 +530,7 @@ public class PowerGroupTest { .setBatterySaverEnabled(batterySaverEnabled) .setBrightnessFactor(brightnessFactor) .build(); - mPowerGroup.dreamLocked(TIMESTAMP1, UID); + mPowerGroup.dreamLocked(TIMESTAMP1, UID, /* allowWake= */ false); assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); mPowerGroup.setWakeLockSummaryLocked(WAKE_LOCK_SCREEN_BRIGHT); mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java index 9ff7d69e09a6..f5ed41a7fa8b 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java @@ -393,6 +393,12 @@ public class PowerManagerServiceTest { .thenReturn(minimumScreenOffTimeoutConfigMillis); } + private void setDreamsDisabledByAmbientModeSuppressionConfig(boolean disable) { + when(mResourcesSpy.getBoolean( + com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig)) + .thenReturn(disable); + } + private void advanceTime(long timeMs) { mClock.fastForward(timeMs); mTestLooper.dispatchAll(); @@ -612,6 +618,31 @@ public class PowerManagerServiceTest { assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); } + /** + * Tests that dreaming continues when undocking and configured to do so. + */ + @Test + public void testWakefulnessDream_shouldKeepDreamingWhenUndocked() { + createService(); + startSystem(); + + when(mResourcesSpy.getBoolean( + com.android.internal.R.bool.config_keepDreamingWhenUndocking)) + .thenReturn(true); + mService.readConfigurationLocked(); + + when(mBatteryManagerInternalMock.getPlugType()) + .thenReturn(BatteryManager.BATTERY_PLUGGED_DOCK); + setPluggedIn(true); + + forceAwake(); // Needs to be awake first before it can dream. + forceDream(); + when(mBatteryManagerInternalMock.getPlugType()).thenReturn(0); + setPluggedIn(false); + + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); + } + @Test public void testWakefulnessDoze_goToSleep() { createService(); @@ -731,7 +762,7 @@ public class PowerManagerServiceTest { doAnswer(inv -> { when(mDreamManagerInternalMock.isDreaming()).thenReturn(true); return null; - }).when(mDreamManagerInternalMock).startDream(anyBoolean()); + }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString()); setMinimumScreenOffTimeoutConfig(5); createService(); @@ -753,7 +784,7 @@ public class PowerManagerServiceTest { doAnswer(inv -> { when(mDreamManagerInternalMock.isDreaming()).thenReturn(true); return null; - }).when(mDreamManagerInternalMock).startDream(anyBoolean()); + }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString()); setMinimumScreenOffTimeoutConfig(5); createService(); @@ -765,6 +796,91 @@ public class PowerManagerServiceTest { assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); } + @SuppressWarnings("GuardedBy") + @Test + public void testAmbientSuppression_disablesDreamingAndWakesDevice() { + Settings.Secure.putInt(mContextSpy.getContentResolver(), + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1); + Settings.Secure.putInt(mContextSpy.getContentResolver(), + Settings.Secure.SCREENSAVER_ENABLED, 1); + + setDreamsDisabledByAmbientModeSuppressionConfig(true); + setMinimumScreenOffTimeoutConfig(10000); + createService(); + startSystem(); + + doAnswer(inv -> { + when(mDreamManagerInternalMock.isDreaming()).thenReturn(true); + return null; + }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString()); + + setPluggedIn(true); + // Allow asynchronous sandman calls to execute. + advanceTime(10000); + + forceDream(); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); + mService.getBinderServiceInstance().suppressAmbientDisplay("test", true); + advanceTime(50); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); + } + + @SuppressWarnings("GuardedBy") + @Test + public void testAmbientSuppressionDisabled_shouldNotWakeDevice() { + Settings.Secure.putInt(mContextSpy.getContentResolver(), + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1); + Settings.Secure.putInt(mContextSpy.getContentResolver(), + Settings.Secure.SCREENSAVER_ENABLED, 1); + + setDreamsDisabledByAmbientModeSuppressionConfig(false); + setMinimumScreenOffTimeoutConfig(10000); + createService(); + startSystem(); + + doAnswer(inv -> { + when(mDreamManagerInternalMock.isDreaming()).thenReturn(true); + return null; + }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString()); + + setPluggedIn(true); + // Allow asynchronous sandman calls to execute. + advanceTime(10000); + + forceDream(); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); + mService.getBinderServiceInstance().suppressAmbientDisplay("test", true); + advanceTime(50); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); + } + + @Test + public void testAmbientSuppression_doesNotAffectDreamForcing() { + Settings.Secure.putInt(mContextSpy.getContentResolver(), + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1); + Settings.Secure.putInt(mContextSpy.getContentResolver(), + Settings.Secure.SCREENSAVER_ENABLED, 1); + + setDreamsDisabledByAmbientModeSuppressionConfig(true); + setMinimumScreenOffTimeoutConfig(10000); + createService(); + startSystem(); + + doAnswer(inv -> { + when(mDreamManagerInternalMock.isDreaming()).thenReturn(true); + return null; + }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString()); + + mService.getBinderServiceInstance().suppressAmbientDisplay("test", true); + setPluggedIn(true); + // Allow asynchronous sandman calls to execute. + advanceTime(10000); + + // Verify that forcing dream still works even though ambient display is suppressed + forceDream(); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); + } + @Test public void testSetDozeOverrideFromDreamManager_triggersSuspendBlocker() { final String suspendBlockerName = "PowerManagerService.Display"; @@ -1168,7 +1284,7 @@ public class PowerManagerServiceTest { doAnswer(inv -> { when(mDreamManagerInternalMock.isDreaming()).thenReturn(true); return null; - }).when(mDreamManagerInternalMock).startDream(anyBoolean()); + }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString()); final String pkg = mContextSpy.getOpPackageName(); final Binder token = new Binder(); @@ -1662,7 +1778,7 @@ public class PowerManagerServiceTest { forceDozing(); // Allow handleSandman() to be called asynchronously advanceTime(500); - verify(mDreamManagerInternalMock).startDream(eq(true)); + verify(mDreamManagerInternalMock).startDream(eq(true), anyString()); } @Test @@ -1700,7 +1816,7 @@ public class PowerManagerServiceTest { // Allow handleSandman() to be called asynchronously advanceTime(500); - verify(mDreamManagerInternalMock).startDream(eq(true)); + verify(mDreamManagerInternalMock).startDream(eq(true), anyString()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java index 235849c1cd8b..c484f457faea 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java @@ -53,7 +53,8 @@ final class FakeVibratorControllerProvider { private boolean mIsAvailable = true; private boolean mIsInfoLoadSuccessful = true; - private long mLatency; + private long mOnLatency; + private long mOffLatency; private int mOffCount; private int mCapabilities; @@ -97,7 +98,7 @@ final class FakeVibratorControllerProvider { public long on(long milliseconds, long vibrationId) { recordEffectSegment(vibrationId, new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, /* frequencyHz= */ 0, (int) milliseconds)); - applyLatency(); + applyLatency(mOnLatency); scheduleListener(milliseconds, vibrationId); return milliseconds; } @@ -105,12 +106,13 @@ final class FakeVibratorControllerProvider { @Override public void off() { mOffCount++; + applyLatency(mOffLatency); } @Override public void setAmplitude(float amplitude) { mAmplitudes.add(amplitude); - applyLatency(); + applyLatency(mOnLatency); } @Override @@ -121,7 +123,7 @@ final class FakeVibratorControllerProvider { } recordEffectSegment(vibrationId, new PrebakedSegment((int) effect, false, (int) strength)); - applyLatency(); + applyLatency(mOnLatency); scheduleListener(EFFECT_DURATION, vibrationId); return EFFECT_DURATION; } @@ -141,7 +143,7 @@ final class FakeVibratorControllerProvider { duration += EFFECT_DURATION + primitive.getDelay(); recordEffectSegment(vibrationId, primitive); } - applyLatency(); + applyLatency(mOnLatency); scheduleListener(duration, vibrationId); return duration; } @@ -154,7 +156,7 @@ final class FakeVibratorControllerProvider { recordEffectSegment(vibrationId, primitive); } recordBraking(vibrationId, braking); - applyLatency(); + applyLatency(mOnLatency); scheduleListener(duration, vibrationId); return duration; } @@ -193,10 +195,10 @@ final class FakeVibratorControllerProvider { return mIsInfoLoadSuccessful; } - private void applyLatency() { + private void applyLatency(long latencyMillis) { try { - if (mLatency > 0) { - Thread.sleep(mLatency); + if (latencyMillis > 0) { + Thread.sleep(latencyMillis); } } catch (InterruptedException e) { } @@ -240,10 +242,15 @@ final class FakeVibratorControllerProvider { /** * Sets the latency this controller should fake for turning the vibrator hardware on or setting - * it's vibration amplitude. + * the vibration amplitude. */ - public void setLatency(long millis) { - mLatency = millis; + public void setOnLatency(long millis) { + mOnLatency = millis; + } + + /** Sets the latency this controller should fake for turning the vibrator off. */ + public void setOffLatency(long millis) { + mOffLatency = millis; } /** Set the capabilities of the fake vibrator hardware. */ diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java index 0551bfc70bda..42a2c10a24d6 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java @@ -1118,7 +1118,7 @@ public class VibrationThreadTest { // 25% of the first waveform step will be spent on the native on() call. // 25% of each waveform step will be spent on the native setAmplitude() call.. - mVibratorProviders.get(VIBRATOR_ID).setLatency(stepDuration / 4); + mVibratorProviders.get(VIBRATOR_ID).setOnLatency(stepDuration / 4); mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); int stepCount = totalDuration / stepDuration; @@ -1149,7 +1149,7 @@ public class VibrationThreadTest { fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); long latency = 5_000; // 5s - fakeVibrator.setLatency(latency); + fakeVibrator.setOnLatency(latency); long vibrationId = 1; VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); @@ -1163,8 +1163,7 @@ public class VibrationThreadTest { // fail at waitForCompletion(cancellingThread). Thread cancellingThread = new Thread( () -> conductor.notifyCancelled( - new Vibration.EndInfo( - Vibration.Status.CANCELLED_BY_USER), + new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER), /* immediate= */ false)); cancellingThread.start(); diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java index fe0a79c2d944..039e159a347b 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -817,6 +817,39 @@ public class VibratorManagerServiceTest { verify(mBatteryStatsMock).noteVibratorOn(UID, 5000); // The second vibration shouldn't have recorded that the vibrators were turned on. verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong()); + // No segment played is the prebaked CLICK from the second vibration. + assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream() + .anyMatch(PrebakedSegment.class::isInstance)); + // Clean up repeating effect. + service.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, service); + } + + @Test + public void vibrate_withOngoingRepeatingVibrationBeingCancelled_playsAfterPreviousIsCancelled() + throws Exception { + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setOffLatency(50); // Add latency so cancellation is slow. + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); + VibratorManagerService service = createSystemReadyService(); + + VibrationEffect repeatingEffect = VibrationEffect.createWaveform( + new long[]{10, 10_000}, new int[]{255, 0}, 1); + vibrate(service, repeatingEffect, ALARM_ATTRS); + + // VibrationThread will start this vibration async, wait until the off waveform step. + assertTrue(waitUntil(s -> fakeVibrator.getOffCount() > 0, service, TEST_TIMEOUT_MILLIS)); + + // Cancel vibration right before requesting a new one. + // This should trigger slow IVibrator.off before setting the vibration status to cancelled. + service.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, service); + vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), + ALARM_ATTRS); + + // Check that second vibration was played. + assertTrue(fakeVibrator.getAllEffectSegments().stream() + .anyMatch(PrebakedSegment.class::isInstance)); } @Test @@ -867,6 +900,11 @@ public class VibratorManagerServiceTest { // The second vibration shouldn't have recorded that the vibrators were turned on. verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong()); + // The second vibration shouldn't have played any prebaked segment. + assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream() + .anyMatch(PrebakedSegment.class::isInstance)); + // Clean up long effect. + service.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, service); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java index 617a34f44bff..8e81e2d8997c 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -54,6 +54,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -83,7 +84,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; -import android.service.dreams.DreamManagerInternal; import android.test.mock.MockContentResolver; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -101,6 +101,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Spy; import java.time.LocalDateTime; import java.time.LocalTime; @@ -137,8 +138,8 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { private PackageManager mPackageManager; @Mock private IBinder mBinder; - @Mock - private DreamManagerInternal mDreamManager; + @Spy + private TestInjector mInjector; @Captor private ArgumentCaptor<Intent> mOrderedBroadcastIntent; @Captor @@ -207,10 +208,10 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { addLocalService(WindowManagerInternal.class, mWindowManager); addLocalService(PowerManagerInternal.class, mLocalPowerManager); addLocalService(TwilightManager.class, mTwilightManager); - addLocalService(DreamManagerInternal.class, mDreamManager); - + + mInjector = spy(new TestInjector()); mUiManagerService = new UiModeManagerService(mContext, /* setupWizardComplete= */ true, - mTwilightManager, new TestInjector()); + mTwilightManager, mInjector); try { mUiManagerService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); } catch (SecurityException e) {/* ignore for permission denial */} @@ -1321,84 +1322,86 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { @Test public void dreamWhenDocked() { - setScreensaverActivateOnDock(true); - setScreensaverEnabled(true); - triggerDockIntent(); verifyAndSendResultBroadcast(); - verify(mDreamManager).requestDream(); + verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); } @Test - public void noDreamWhenDocked_dreamsDisabled() { - setScreensaverActivateOnDock(true); - setScreensaverEnabled(false); + public void noDreamWhenDocked_keyguardNotShowing_interactive() { + mUiManagerService.setStartDreamImmediatelyOnDock(false); + when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(false); + when(mPowerManager.isInteractive()).thenReturn(true); triggerDockIntent(); verifyAndSendResultBroadcast(); - verify(mDreamManager, never()).requestDream(); + verify(mInjector, never()).startDreamWhenDockedIfAppropriate(mContext); } @Test - public void noDreamWhenDocked_dreamsWhenDockedDisabled() { - setScreensaverActivateOnDock(false); - setScreensaverEnabled(true); + public void dreamWhenDocked_keyguardShowing_interactive() { + mUiManagerService.setStartDreamImmediatelyOnDock(false); + when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(true); + when(mPowerManager.isInteractive()).thenReturn(false); triggerDockIntent(); verifyAndSendResultBroadcast(); - verify(mDreamManager, never()).requestDream(); + verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); } @Test - public void noDreamWhenDocked_keyguardNotShowing_interactive() { - setScreensaverActivateOnDock(true); - setScreensaverEnabled(true); + public void dreamWhenDocked_keyguardNotShowing_notInteractive() { mUiManagerService.setStartDreamImmediatelyOnDock(false); when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(false); - when(mPowerManager.isInteractive()).thenReturn(true); + when(mPowerManager.isInteractive()).thenReturn(false); triggerDockIntent(); verifyAndSendResultBroadcast(); - verify(mDreamManager, never()).requestDream(); + verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); } @Test - public void dreamWhenDocked_keyguardShowing_interactive() { - setScreensaverActivateOnDock(true); - setScreensaverEnabled(true); + public void dreamWhenDocked_keyguardShowing_notInteractive() { mUiManagerService.setStartDreamImmediatelyOnDock(false); when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(true); when(mPowerManager.isInteractive()).thenReturn(false); triggerDockIntent(); verifyAndSendResultBroadcast(); - verify(mDreamManager).requestDream(); + verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); } @Test - public void dreamWhenDocked_keyguardNotShowing_notInteractive() { - setScreensaverActivateOnDock(true); - setScreensaverEnabled(true); - mUiManagerService.setStartDreamImmediatelyOnDock(false); - when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(false); - when(mPowerManager.isInteractive()).thenReturn(false); + public void dreamWhenDocked_ambientModeSuppressed_suppressionEnabled() { + mUiManagerService.setStartDreamImmediatelyOnDock(true); + mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true); + when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true); triggerDockIntent(); verifyAndSendResultBroadcast(); - verify(mDreamManager).requestDream(); + verify(mInjector, never()).startDreamWhenDockedIfAppropriate(mContext); } @Test - public void dreamWhenDocked_keyguardShowing_notInteractive() { - setScreensaverActivateOnDock(true); - setScreensaverEnabled(true); - mUiManagerService.setStartDreamImmediatelyOnDock(false); - when(mWindowManager.isKeyguardShowingAndNotOccluded()).thenReturn(true); - when(mPowerManager.isInteractive()).thenReturn(false); + public void dreamWhenDocked_ambientModeSuppressed_suppressionDisabled() { + mUiManagerService.setStartDreamImmediatelyOnDock(true); + mUiManagerService.setDreamsDisabledByAmbientModeSuppression(false); + + when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true); + triggerDockIntent(); + verifyAndSendResultBroadcast(); + verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); + } + + @Test + public void dreamWhenDocked_ambientModeNotSuppressed_suppressionEnabled() { + mUiManagerService.setStartDreamImmediatelyOnDock(true); + mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true); + when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(false); triggerDockIntent(); verifyAndSendResultBroadcast(); - verify(mDreamManager).requestDream(); + verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); } private void triggerDockIntent() { @@ -1435,22 +1438,6 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { mOrderedBroadcastIntent.getValue()); } - private void setScreensaverEnabled(boolean enable) { - Settings.Secure.putIntForUser( - mContentResolver, - Settings.Secure.SCREENSAVER_ENABLED, - enable ? 1 : 0, - UserHandle.USER_CURRENT); - } - - private void setScreensaverActivateOnDock(boolean enable) { - Settings.Secure.putIntForUser( - mContentResolver, - Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, - enable ? 1 : 0, - UserHandle.USER_CURRENT); - } - private void requestAllPossibleProjectionTypes() throws RemoteException { for (int i = 0; i < Integer.SIZE; ++i) { mService.requestProjection(mBinder, 1 << i, PACKAGE_NAME); @@ -1467,11 +1454,17 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { } public TestInjector(int callingUid) { - this.callingUid = callingUid; + this.callingUid = callingUid; } + @Override public int getCallingUid() { return callingUid; } + + @Override + public void startDreamWhenDockedIfAppropriate(Context context) { + // do nothing + } } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java index c5131c8f8c1d..bf66dee936fe 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java @@ -15,6 +15,7 @@ */ package com.android.server.notification; +import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; @@ -30,9 +31,11 @@ import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.intThat; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -49,6 +52,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.content.pm.VersionedPackage; +import android.content.res.Resources; import android.os.Bundle; import android.os.UserHandle; import android.service.notification.NotificationListenerFilter; @@ -69,6 +73,7 @@ import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.internal.util.reflection.FieldSetter; @@ -77,6 +82,7 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.util.Arrays; import java.util.List; public class NotificationListenersTest extends UiServiceTestCase { @@ -85,6 +91,8 @@ public class NotificationListenersTest extends UiServiceTestCase { private PackageManager mPm; @Mock private IPackageManager miPm; + @Mock + private Resources mResources; @Mock NotificationManagerService mNm; @@ -96,7 +104,8 @@ public class NotificationListenersTest extends UiServiceTestCase { private ComponentName mCn1 = new ComponentName("pkg", "pkg.cmp"); private ComponentName mCn2 = new ComponentName("pkg2", "pkg2.cmp2"); - + private ComponentName mUninstalledComponent = new ComponentName("pkg3", + "pkg3.NotificationListenerService"); @Before public void setUp() throws Exception { @@ -111,7 +120,7 @@ public class NotificationListenersTest extends UiServiceTestCase { @Test public void testReadExtraTag() throws Exception { - String xml = "<" + TAG_REQUESTED_LISTENERS+ ">" + String xml = "<" + TAG_REQUESTED_LISTENERS + ">" + "<listener component=\"" + mCn1.flattenToString() + "\" user=\"0\">" + "<allowed types=\"7\" />" + "</listener>" @@ -131,11 +140,55 @@ public class NotificationListenersTest extends UiServiceTestCase { } @Test + public void loadDefaultsFromConfig_forHeadlessSystemUser_loadUninstalled() throws Exception { + // setup with headless system user mode + mListeners = spy(mNm.new NotificationListeners( + mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm, + /* isHeadlessSystemUserMode= */ true)); + mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent); + + mListeners.loadDefaultsFromConfig(); + + assertThat(mListeners.getDefaultComponents()).contains(mUninstalledComponent); + } + + @Test + public void loadDefaultsFromConfig_forNonHeadlessSystemUser_ignoreUninstalled() + throws Exception { + // setup without headless system user mode + mListeners = spy(mNm.new NotificationListeners( + mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm, + /* isHeadlessSystemUserMode= */ false)); + mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent); + + mListeners.loadDefaultsFromConfig(); + + assertThat(mListeners.getDefaultComponents()).doesNotContain(mUninstalledComponent); + } + + private void mockDefaultListenerConfigForUninstalledComponent(ComponentName componentName) { + ArraySet<ComponentName> components = new ArraySet<>(Arrays.asList(componentName)); + when(mResources + .getString( + com.android.internal.R.string.config_defaultListenerAccessPackages)) + .thenReturn(componentName.getPackageName()); + when(mContext.getResources()).thenReturn(mResources); + doReturn(components).when(mListeners).queryPackageForServices( + eq(componentName.getPackageName()), + intThat(hasIntBitFlag(MATCH_ANY_USER)), + anyInt()); + } + + public static ArgumentMatcher<Integer> hasIntBitFlag(int flag) { + return arg -> arg != null && ((arg & flag) == flag); + } + + @Test public void testWriteExtraTag() throws Exception { NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>()); VersionedPackage a1 = new VersionedPackage("pkg1", 243); NotificationListenerFilter nlf2 = - new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1})); + new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[]{a1})); mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf); mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2); @@ -435,63 +488,112 @@ public class NotificationListenersTest extends UiServiceTestCase { @Test public void testNotifyPostedLockedInLockdownMode() { - NotificationRecord r = mock(NotificationRecord.class); - NotificationRecord old = mock(NotificationRecord.class); - - // before the lockdown mode - when(mNm.isInLockDownMode()).thenReturn(false); - mListeners.notifyPostedLocked(r, old, true); - mListeners.notifyPostedLocked(r, old, false); - verify(r, atLeast(2)).getSbn(); - - // in the lockdown mode - reset(r); - reset(old); - when(mNm.isInLockDownMode()).thenReturn(true); - mListeners.notifyPostedLocked(r, old, true); - mListeners.notifyPostedLocked(r, old, false); - verify(r, never()).getSbn(); - } - - @Test - public void testnotifyRankingUpdateLockedInLockdownMode() { - List chn = mock(List.class); - - // before the lockdown mode - when(mNm.isInLockDownMode()).thenReturn(false); - mListeners.notifyRankingUpdateLocked(chn); - verify(chn, atLeast(1)).size(); - - // in the lockdown mode - reset(chn); - when(mNm.isInLockDownMode()).thenReturn(true); - mListeners.notifyRankingUpdateLocked(chn); - verify(chn, never()).size(); + NotificationRecord r0 = mock(NotificationRecord.class); + NotificationRecord old0 = mock(NotificationRecord.class); + UserHandle uh0 = mock(UserHandle.class); + + NotificationRecord r1 = mock(NotificationRecord.class); + NotificationRecord old1 = mock(NotificationRecord.class); + UserHandle uh1 = mock(UserHandle.class); + + // Neither user0 and user1 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(false); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + + mListeners.notifyPostedLocked(r0, old0, true); + mListeners.notifyPostedLocked(r0, old0, false); + verify(r0, atLeast(2)).getSbn(); + + mListeners.notifyPostedLocked(r1, old1, true); + mListeners.notifyPostedLocked(r1, old1, false); + verify(r1, atLeast(2)).getSbn(); + + // Reset + reset(r0); + reset(old0); + reset(r1); + reset(old1); + + // Only user 0 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(true); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + + mListeners.notifyPostedLocked(r0, old0, true); + mListeners.notifyPostedLocked(r0, old0, false); + verify(r0, never()).getSbn(); + + mListeners.notifyPostedLocked(r1, old1, true); + mListeners.notifyPostedLocked(r1, old1, false); + verify(r1, atLeast(2)).getSbn(); } @Test public void testNotifyRemovedLockedInLockdownMode() throws NoSuchFieldException { - NotificationRecord r = mock(NotificationRecord.class); - NotificationStats rs = mock(NotificationStats.class); + NotificationRecord r0 = mock(NotificationRecord.class); + NotificationStats rs0 = mock(NotificationStats.class); + UserHandle uh0 = mock(UserHandle.class); + + NotificationRecord r1 = mock(NotificationRecord.class); + NotificationStats rs1 = mock(NotificationStats.class); + UserHandle uh1 = mock(UserHandle.class); + StatusBarNotification sbn = mock(StatusBarNotification.class); FieldSetter.setField(mNm, NotificationManagerService.class.getDeclaredField("mHandler"), mock(NotificationManagerService.WorkerHandler.class)); - // before the lockdown mode - when(mNm.isInLockDownMode()).thenReturn(false); - when(r.getSbn()).thenReturn(sbn); - mListeners.notifyRemovedLocked(r, 0, rs); - mListeners.notifyRemovedLocked(r, 0, rs); - verify(r, atLeast(2)).getSbn(); - - // in the lockdown mode - reset(r); - reset(rs); - when(mNm.isInLockDownMode()).thenReturn(true); - when(r.getSbn()).thenReturn(sbn); - mListeners.notifyRemovedLocked(r, 0, rs); - mListeners.notifyRemovedLocked(r, 0, rs); - verify(r, never()).getSbn(); + // Neither user0 and user1 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(false); + when(r0.getSbn()).thenReturn(sbn); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + when(r1.getSbn()).thenReturn(sbn); + + mListeners.notifyRemovedLocked(r0, 0, rs0); + mListeners.notifyRemovedLocked(r0, 0, rs0); + verify(r0, atLeast(2)).getSbn(); + + mListeners.notifyRemovedLocked(r1, 0, rs1); + mListeners.notifyRemovedLocked(r1, 0, rs1); + verify(r1, atLeast(2)).getSbn(); + + // Reset + reset(r0); + reset(rs0); + reset(r1); + reset(rs1); + + // Only user 0 is in the lockdown mode + when(r0.getUser()).thenReturn(uh0); + when(uh0.getIdentifier()).thenReturn(0); + when(mNm.isInLockDownMode(0)).thenReturn(true); + when(r0.getSbn()).thenReturn(sbn); + + when(r1.getUser()).thenReturn(uh1); + when(uh1.getIdentifier()).thenReturn(1); + when(mNm.isInLockDownMode(1)).thenReturn(false); + when(r1.getSbn()).thenReturn(sbn); + + mListeners.notifyRemovedLocked(r0, 0, rs0); + mListeners.notifyRemovedLocked(r0, 0, rs0); + verify(r0, never()).getSbn(); + + mListeners.notifyRemovedLocked(r1, 0, rs1); + mListeners.notifyRemovedLocked(r1, 0, rs1); + verify(r1, atLeast(2)).getSbn(); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 4d1c78643f6d..52a1ae2d6555 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -174,6 +174,7 @@ import android.service.notification.Adjustment; import android.service.notification.ConversationChannelWrapper; import android.service.notification.NotificationListenerFilter; import android.service.notification.NotificationListenerService; +import android.service.notification.NotificationRankingUpdate; import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.service.notification.ZenPolicy; @@ -9837,10 +9838,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mStrongAuthTracker.setGetStrongAuthForUserReturnValue( STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); - assertTrue(mStrongAuthTracker.isInLockDownMode()); - mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0); + assertTrue(mStrongAuthTracker.isInLockDownMode(mContext.getUserId())); + mStrongAuthTracker.setGetStrongAuthForUserReturnValue(mContext.getUserId()); mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); - assertFalse(mStrongAuthTracker.isInLockDownMode()); + assertFalse(mStrongAuthTracker.isInLockDownMode(mContext.getUserId())); } @Test @@ -9856,8 +9857,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // when entering the lockdown mode, cancel the 2 notifications. mStrongAuthTracker.setGetStrongAuthForUserReturnValue( STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); - mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); - assertTrue(mStrongAuthTracker.isInLockDownMode()); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertTrue(mStrongAuthTracker.isInLockDownMode(0)); // the notifyRemovedLocked function is called twice due to REASON_CANCEL_ALL. ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class); @@ -9866,10 +9867,46 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // exit lockdown mode. mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0); - mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId()); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertFalse(mStrongAuthTracker.isInLockDownMode(0)); // the notifyPostedLocked function is called twice. - verify(mListeners, times(2)).notifyPostedLocked(any(), any()); + verify(mWorkerHandler, times(2)).postDelayed(any(Runnable.class), anyLong()); + //verify(mListeners, times(2)).notifyPostedLocked(any(), any()); + } + + @Test + public void testMakeRankingUpdateLockedInLockDownMode() { + // post 2 notifications from a same package + NotificationRecord pkgA = new NotificationRecord(mContext, + generateSbn("a", 1000, 9, 0), mTestNotificationChannel); + mService.addNotification(pkgA); + NotificationRecord pkgB = new NotificationRecord(mContext, + generateSbn("a", 1000, 9, 1), mTestNotificationChannel); + mService.addNotification(pkgB); + + mService.setIsVisibleToListenerReturnValue(true); + NotificationRankingUpdate nru = mService.makeRankingUpdateLocked(null); + assertEquals(2, nru.getRankingMap().getOrderedKeys().length); + + // when only user 0 entering the lockdown mode, its notification will be suppressed. + mStrongAuthTracker.setGetStrongAuthForUserReturnValue( + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertTrue(mStrongAuthTracker.isInLockDownMode(0)); + assertFalse(mStrongAuthTracker.isInLockDownMode(1)); + + nru = mService.makeRankingUpdateLocked(null); + assertEquals(1, nru.getRankingMap().getOrderedKeys().length); + + // User 0 exits lockdown mode. Its notification will be resumed. + mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0); + mStrongAuthTracker.onStrongAuthRequiredChanged(0); + assertFalse(mStrongAuthTracker.isInLockDownMode(0)); + assertFalse(mStrongAuthTracker.isInLockDownMode(1)); + + nru = mService.makeRankingUpdateLocked(null); + assertEquals(2, nru.getRankingMap().getOrderedKeys().length); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java deleted file mode 100644 index d7650420788c..000000000000 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java +++ /dev/null @@ -1,551 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.server.notification; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertNull; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import android.app.ActivityManager; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Person; -import android.app.RemoteInput; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Typeface; -import android.graphics.drawable.Icon; -import android.net.Uri; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.style.StyleSpan; -import android.util.Pair; -import android.widget.RemoteViews; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.server.UiServiceTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class NotificationTest extends UiServiceTestCase { - - @Mock - ActivityManager mAm; - - @Mock - Resources mResources; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testDoesNotStripsExtenders() { - Notification.Builder nb = new Notification.Builder(mContext, "channel"); - nb.extend(new Notification.CarExtender().setColor(Color.RED)); - nb.extend(new Notification.TvExtender().setChannelId("different channel")); - nb.extend(new Notification.WearableExtender().setDismissalId("dismiss")); - Notification before = nb.build(); - Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before); - - assertTrue(before == after); - - assertEquals("different channel", new Notification.TvExtender(before).getChannelId()); - assertEquals(Color.RED, new Notification.CarExtender(before).getColor()); - assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId()); - } - - @Test - public void testStyleChangeVisiblyDifferent_noStyles() { - Notification.Builder n1 = new Notification.Builder(mContext, "test"); - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - - assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_noStyleToStyle() { - Notification.Builder n1 = new Notification.Builder(mContext, "test"); - Notification.Builder n2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_styleToNoStyle() { - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testStyleChangeVisiblyDifferent_changeStyle() { - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle()); - Notification.Builder n2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle()); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2)); - } - - @Test - public void testInboxTextChange() { - Notification.Builder nInbox1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle().addLine("a").addLine("b")); - Notification.Builder nInbox2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.InboxStyle().addLine("b").addLine("c")); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2)); - } - - @Test - public void testBigTextTextChange() { - Notification.Builder nBigText1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle().bigText("something")); - Notification.Builder nBigText2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigTextStyle().bigText("else")); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2)); - } - - @Test - public void testBigPictureChange() { - Bitmap bitA = mock(Bitmap.class); - when(bitA.getGenerationId()).thenReturn(100); - Bitmap bitB = mock(Bitmap.class); - when(bitB.getGenerationId()).thenReturn(200); - - Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigPictureStyle().bigPicture(bitA)); - Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.BigPictureStyle().bigPicture(bitB)); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2)); - } - - @Test - public void testMessagingChange_text() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class))) - .addMessage(new Notification.MessagingStyle.Message( - "b", 100, mock(Person.class))) - ); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_data() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)) - .setData("text", mock(Uri.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_sender() { - Person a = mock(Person.class); - when(a.getName()).thenReturn("A"); - Person b = mock(Person.class); - when(b.getName()).thenReturn("b"); - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_key() { - Person a = mock(Person.class); - when(a.getKey()).thenReturn("A"); - Person b = mock(Person.class); - when(b.getKey()).thenReturn("b"); - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, a))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message("a", 100, b))); - - assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testMessagingChange_ignoreTimeChange() { - Notification.Builder nM1 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 100, mock(Person.class)))); - Notification.Builder nM2 = new Notification.Builder(mContext, "test") - .setStyle(new Notification.MessagingStyle("") - .addMessage(new Notification.MessagingStyle.Message( - "a", 1000, mock(Person.class))) - ); - - assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2)); - } - - @Test - public void testRemoteViews_nullChange() { - Notification.Builder n1 = new Notification.Builder(mContext, "test") - .setContent(mock(RemoteViews.class)); - Notification.Builder n2 = new Notification.Builder(mContext, "test"); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test") - .setContent(mock(RemoteViews.class)); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test") - .setCustomBigContentView(mock(RemoteViews.class)); - n2 = new Notification.Builder(mContext, "test"); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test") - .setCustomBigContentView(mock(RemoteViews.class)); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test"); - n2 = new Notification.Builder(mContext, "test"); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_layoutChange() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(189); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_layoutSame() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_sequenceChange() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - when(a.getSequenceNumber()).thenReturn(1); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - when(b.getSequenceNumber()).thenReturn(2); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertTrue(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testRemoteViews_sequenceSame() { - RemoteViews a = mock(RemoteViews.class); - when(a.getLayoutId()).thenReturn(234); - when(a.getSequenceNumber()).thenReturn(1); - RemoteViews b = mock(RemoteViews.class); - when(b.getLayoutId()).thenReturn(234); - when(b.getSequenceNumber()).thenReturn(1); - - Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a); - Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - - n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a); - n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b); - assertFalse(Notification.areRemoteViewsChanged(n1, n2)); - } - - @Test - public void testActionsDifferent_null() { - Notification n1 = new Notification.Builder(mContext, "test") - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentSame() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentText() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) - .build(); - - assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentSpannables() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, - new SpannableStringBuilder().append("test1", - new StyleSpan(Typeface.BOLD), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE), - intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "test1", intent).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentNumber() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build()) - .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build()) - .build(); - - assertTrue(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsDifferentIntent() { - PendingIntent intent1 = mock(PendingIntent.class); - PendingIntent intent2 = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testActionsIgnoresRemoteInputs() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - Notification n1 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(new RemoteInput.Builder("a") - .setChoices(new CharSequence[] {"i", "m"}) - .build()) - .build()) - .build(); - Notification n2 = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(new RemoteInput.Builder("a") - .setChoices(new CharSequence[] {"t", "m"}) - .build()) - .build()) - .build(); - - assertFalse(Notification.areActionsVisiblyDifferent(n1, n2)); - } - - @Test - public void testFreeformRemoteInputActionPair_noRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - Notification notification = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .build()) - .build(); - assertNull(notification.findRemoteInputActionPair(false)); - } - - @Test - public void testFreeformRemoteInputActionPair_hasRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - RemoteInput remoteInput = new RemoteInput.Builder("a").build(); - - Notification.Action actionWithRemoteInput = - new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(remoteInput) - .addRemoteInput(remoteInput) - .build(); - - Notification.Action actionWithoutRemoteInput = - new Notification.Action.Builder(icon, "TEXT 2", intent) - .build(); - - Notification notification = new Notification.Builder(mContext, "test") - .addAction(actionWithoutRemoteInput) - .addAction(actionWithRemoteInput) - .build(); - - Pair<RemoteInput, Notification.Action> remoteInputActionPair = - notification.findRemoteInputActionPair(false); - - assertNotNull(remoteInputActionPair); - assertEquals(remoteInput, remoteInputActionPair.first); - assertEquals(actionWithRemoteInput, remoteInputActionPair.second); - } - - @Test - public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - Notification notification = new Notification.Builder(mContext, "test") - .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput( - new RemoteInput.Builder("a") - .setAllowFreeFormInput(false).build()) - .build()) - .build(); - assertNull(notification.findRemoteInputActionPair(true)); - } - - @Test - public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() { - PendingIntent intent = mock(PendingIntent.class); - Icon icon = mock(Icon.class); - - RemoteInput remoteInput = - new RemoteInput.Builder("a").setAllowFreeFormInput(false).build(); - RemoteInput freeformRemoteInput = - new RemoteInput.Builder("b").setAllowFreeFormInput(true).build(); - - Notification.Action actionWithFreeformRemoteInput = - new Notification.Action.Builder(icon, "TEXT 1", intent) - .addRemoteInput(remoteInput) - .addRemoteInput(freeformRemoteInput) - .build(); - - Notification.Action actionWithoutFreeformRemoteInput = - new Notification.Action.Builder(icon, "TEXT 2", intent) - .addRemoteInput(remoteInput) - .build(); - - Notification notification = new Notification.Builder(mContext, "test") - .addAction(actionWithoutFreeformRemoteInput) - .addAction(actionWithFreeformRemoteInput) - .build(); - - Pair<RemoteInput, Notification.Action> remoteInputActionPair = - notification.findRemoteInputActionPair(true); - - assertNotNull(remoteInputActionPair); - assertEquals(freeformRemoteInput, remoteInputActionPair.first); - assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second); - } -} - diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java index b49e5cbfa9dc..8cf74fbf88b7 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java @@ -19,10 +19,12 @@ package com.android.server.notification; import android.companion.ICompanionDeviceManager; import android.content.ComponentName; import android.content.Context; +import android.service.notification.StatusBarNotification; import androidx.annotation.Nullable; import com.android.internal.logging.InstanceIdSequence; +import com.android.server.notification.ManagedServices.ManagedServiceInfo; import java.util.HashSet; import java.util.Set; @@ -37,6 +39,9 @@ public class TestableNotificationManagerService extends NotificationManagerServi @Nullable NotificationAssistantAccessGrantedCallback mNotificationAssistantAccessGrantedCallback; + @Nullable + Boolean mIsVisibleToListenerReturnValue = null; + TestableNotificationManagerService(Context context, NotificationRecordLogger logger, InstanceIdSequence notificationInstanceIdSequence) { super(context, logger, notificationInstanceIdSequence); @@ -119,6 +124,19 @@ public class TestableNotificationManagerService extends NotificationManagerServi mShowReviewPermissionsNotification = setting; } + protected void setIsVisibleToListenerReturnValue(boolean value) { + mIsVisibleToListenerReturnValue = value; + } + + @Override + boolean isVisibleToListener(StatusBarNotification sbn, int notificationType, + ManagedServiceInfo listener) { + if (mIsVisibleToListenerReturnValue != null) { + return mIsVisibleToListenerReturnValue; + } + return super.isVisibleToListener(sbn, notificationType, listener); + } + public class StrongAuthTrackerFake extends NotificationManagerService.StrongAuthTracker { private int mGetStrongAuthForUserReturnValue = 0; StrongAuthTrackerFake(Context context) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index d5447447a7b2..462957a88a6c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2319,6 +2319,22 @@ public class ActivityRecordTests extends WindowTestsBase { assertTrue(activity1.getTask().getTaskInfo().launchCookies.contains(launchCookie)); } + @Test + public void testOrientationForScreenOrientationBehind() { + final Task task = createTask(mDisplayContent); + // Activity below + new ActivityBuilder(mAtm) + .setTask(task) + .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) + .build(); + final ActivityRecord activityTop = new ActivityBuilder(mAtm) + .setTask(task) + .setScreenOrientation(SCREEN_ORIENTATION_BEHIND) + .build(); + final int topOrientation = activityTop.getRequestedConfigurationOrientation(); + assertEquals(SCREEN_ORIENTATION_PORTRAIT, topOrientation); + } + private void verifyProcessInfoUpdate(ActivityRecord activity, State state, boolean shouldUpdate, boolean activityChange) { reset(activity.app); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java index d5e336b1cf2f..eed32d7d815c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java @@ -40,14 +40,18 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; +import android.app.ActivityOptions; import android.app.WaitResult; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.os.Binder; import android.os.ConditionVariable; +import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.view.Display; @@ -308,4 +312,40 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase { waitHandlerIdle(mAtm.mH); verify(mRootWindowContainer, timeout(TIMEOUT_MS)).startHomeOnEmptyDisplays("userUnlocked"); } + + /** Verifies that launch from recents sets the launch cookie on the activity. */ + @Test + public void testStartActivityFromRecents_withLaunchCookie() { + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + + IBinder launchCookie = new Binder("test_launch_cookie"); + ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchCookie(launchCookie); + SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(options.toBundle()); + + doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(), + anyInt(), any()); + + mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions); + + assertThat(activity.mLaunchCookie).isEqualTo(launchCookie); + verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions)); + } + + /** Verifies that launch from recents doesn't set the launch cookie on the activity. */ + @Test + public void testStartActivityFromRecents_withoutLaunchCookie() { + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + + SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle( + ActivityOptions.makeBasic().toBundle()); + + doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(), + anyInt(), any()); + + mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions); + + assertThat(activity.mLaunchCookie).isNull(); + verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions)); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index 1cd0b198ff5a..e30e5dbcaf46 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -242,7 +242,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { private IOnBackInvokedCallback createOnBackInvokedCallback() { return new IOnBackInvokedCallback.Stub() { @Override - public void onBackStarted() { + public void onBackStarted(BackEvent backEvent) { } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java new file mode 100644 index 000000000000..7830e9094796 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.util.DisplayMetrics.DENSITY_DEFAULT; + +import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS; +import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY; +import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_DONE; +import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import com.android.server.wm.LaunchParamsController.LaunchParamsModifier.Result; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for desktop mode task bounds. + * + * Build/Install/Run: + * atest WmTests:DesktopModeLaunchParamsModifierTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { + + private ActivityRecord mActivity; + + private DesktopModeLaunchParamsModifier mTarget; + + private LaunchParamsController.LaunchParams mCurrent; + private LaunchParamsController.LaunchParams mResult; + + @Before + public void setUp() throws Exception { + mActivity = new ActivityBuilder(mAtm).build(); + mTarget = new DesktopModeLaunchParamsModifier(); + mCurrent = new LaunchParamsController.LaunchParams(); + mCurrent.reset(); + mResult = new LaunchParamsController.LaunchParams(); + mResult.reset(); + } + + @Test + public void testReturnsSkipIfTaskIsNull() { + assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(null).calculate()); + } + + @Test + public void testReturnsSkipIfNotBoundsPhase() { + final Task task = new TaskBuilder(mSupervisor).build(); + assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).setPhase( + PHASE_DISPLAY).calculate()); + } + + @Test + public void testReturnsSkipIfTaskNotInFreeform() { + final Task task = new TaskBuilder(mSupervisor).setWindowingMode( + WINDOWING_MODE_FULLSCREEN).build(); + assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).calculate()); + } + + @Test + public void testReturnsSkipIfCurrentParamsHasBounds() { + final Task task = new TaskBuilder(mSupervisor).setWindowingMode( + WINDOWING_MODE_FREEFORM).build(); + mCurrent.mBounds.set(/* left */ 0, /* top */ 0, /* right */ 100, /* bottom */ 100); + assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).calculate()); + } + + @Test + public void testUsesDefaultBounds() { + final Task task = new TaskBuilder(mSupervisor).setWindowingMode( + WINDOWING_MODE_FREEFORM).build(); + assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(dpiToPx(task, 840), mResult.mBounds.width()); + assertEquals(dpiToPx(task, 630), mResult.mBounds.height()); + } + + @Test + public void testUsesDisplayAreaAndWindowingModeFromSource() { + final Task task = new TaskBuilder(mSupervisor).setWindowingMode( + WINDOWING_MODE_FREEFORM).build(); + TaskDisplayArea mockTaskDisplayArea = mock(TaskDisplayArea.class); + mCurrent.mPreferredTaskDisplayArea = mockTaskDisplayArea; + mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; + + assertEquals(RESULT_DONE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(mockTaskDisplayArea, mResult.mPreferredTaskDisplayArea); + assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode); + } + + private int dpiToPx(Task task, int dpi) { + float density = (float) task.getConfiguration().densityDpi / DENSITY_DEFAULT; + return (int) (dpi * density + 0.5f); + } + + private class CalculateRequestBuilder { + private Task mTask; + private int mPhase = PHASE_BOUNDS; + private final ActivityRecord mActivity = + DesktopModeLaunchParamsModifierTests.this.mActivity; + private final LaunchParamsController.LaunchParams mCurrentParams = mCurrent; + private final LaunchParamsController.LaunchParams mOutParams = mResult; + + private CalculateRequestBuilder setTask(Task task) { + mTask = task; + return this; + } + + private CalculateRequestBuilder setPhase(int phase) { + mPhase = phase; + return this; + } + + @Result + private int calculate() { + return mTarget.onCalculate(mTask, /* layout*/ null, mActivity, /* source */ + null, /* options */ null, /* request */ null, mPhase, mCurrentParams, + mOutParams); + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java new file mode 100644 index 000000000000..86732c9a3f46 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.content.res.Resources; +import android.hardware.devicestate.DeviceStateManager; +import android.os.Handler; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.function.Consumer; + +/** + * Test class for {@link DeviceStateController}. + * + * Build/Install/Run: + * atest WmTests:DeviceStateControllerTests + */ +@SmallTest +@Presubmit +public class DeviceStateControllerTests { + + private DeviceStateController.FoldStateListener mFoldStateListener; + private DeviceStateController mTarget; + private DeviceStateControllerBuilder mBuilder; + + private Context mMockContext; + private Handler mMockHandler; + private Resources mMockRes; + private DeviceStateManager mMockDeviceStateManager; + + private Consumer<DeviceStateController.FoldState> mDelegate; + private DeviceStateController.FoldState mCurrentState = DeviceStateController.FoldState.UNKNOWN; + + @Before + public void setUp() { + mBuilder = new DeviceStateControllerBuilder(); + mCurrentState = DeviceStateController.FoldState.UNKNOWN; + } + + private void initialize(boolean supportFold, boolean supportHalfFold) throws Exception { + mBuilder.setSupportFold(supportFold, supportHalfFold); + mDelegate = (newFoldState) -> { + mCurrentState = newFoldState; + }; + mBuilder.setDelegate(mDelegate); + mBuilder.build(); + verifyFoldStateListenerRegistration(1); + } + + @Test + public void testInitialization() throws Exception { + initialize(true /* supportFold */, true /* supportHalfFolded */); + mFoldStateListener.onStateChanged(mUnfoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); + } + + @Test + public void testInitializationWithNoFoldSupport() throws Exception { + initialize(false /* supportFold */, false /* supportHalfFolded */); + mFoldStateListener.onStateChanged(mFoldedStates[0]); + // Note that the folded state is ignored. + assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); + } + + @Test + public void testWithFoldSupported() throws Exception { + initialize(true /* supportFold */, false /* supportHalfFolded */); + mFoldStateListener.onStateChanged(mUnfoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); + mFoldStateListener.onStateChanged(mFoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED); + mFoldStateListener.onStateChanged(mHalfFoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); // Ignored + } + + @Test + public void testWithHalfFoldSupported() throws Exception { + initialize(true /* supportFold */, true /* supportHalfFolded */); + mFoldStateListener.onStateChanged(mUnfoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); + mFoldStateListener.onStateChanged(mFoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED); + mFoldStateListener.onStateChanged(mHalfFoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.HALF_FOLDED); + } + + + private final int[] mFoldedStates = {0}; + private final int[] mUnfoldedStates = {1}; + private final int[] mHalfFoldedStates = {2}; + + + private void verifyFoldStateListenerRegistration(int numOfInvocation) { + final ArgumentCaptor<DeviceStateController.FoldStateListener> listenerCaptor = + ArgumentCaptor.forClass(DeviceStateController.FoldStateListener.class); + verify(mMockDeviceStateManager, times(numOfInvocation)).registerCallback( + any(), + listenerCaptor.capture()); + if (numOfInvocation > 0) { + mFoldStateListener = listenerCaptor.getValue(); + } + } + + private class DeviceStateControllerBuilder { + private boolean mSupportFold = false; + private boolean mSupportHalfFold = false; + private Consumer<DeviceStateController.FoldState> mDelegate; + + DeviceStateControllerBuilder setSupportFold( + boolean supportFold, boolean supportHalfFold) { + mSupportFold = supportFold; + mSupportHalfFold = supportHalfFold; + return this; + } + + DeviceStateControllerBuilder setDelegate( + Consumer<DeviceStateController.FoldState> delegate) { + mDelegate = delegate; + return this; + } + + private void mockFold(boolean enableFold, boolean enableHalfFold) { + if (enableFold) { + when(mMockContext.getResources().getIntArray( + com.android.internal.R.array.config_foldedDeviceStates)) + .thenReturn(mFoldedStates); + } + if (enableHalfFold) { + when(mMockContext.getResources().getIntArray( + com.android.internal.R.array.config_halfFoldedDeviceStates)) + .thenReturn(mHalfFoldedStates); + } + } + + private void build() throws Exception { + mMockContext = mock(Context.class); + mMockRes = mock(Resources.class); + when(mMockContext.getResources()).thenReturn((mMockRes)); + mMockDeviceStateManager = mock(DeviceStateManager.class); + when(mMockContext.getSystemService(DeviceStateManager.class)) + .thenReturn(mMockDeviceStateManager); + mockFold(mSupportFold, mSupportHalfFold); + mMockHandler = mock(Handler.class); + mTarget = new DeviceStateController(mMockContext, mMockHandler, mDelegate); + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java index 89f71110f3c2..b45c37f9da0c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java @@ -28,6 +28,7 @@ import static android.view.IWindowManager.FIXED_TO_USER_ROTATION_ENABLED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.atLeast; import static com.android.dx.mockito.inline.extended.ExtendedMockito.atMost; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; @@ -103,7 +104,7 @@ public class DisplayRotationTests { private Context mMockContext; private Resources mMockRes; private SensorManager mMockSensorManager; - private Sensor mFakeSensor; + private Sensor mFakeOrientationSensor; private DisplayWindowSettings mMockDisplayWindowSettings; private ContentResolver mMockResolver; private FakeSettingsProvider mFakeSettingsProvider; @@ -323,7 +324,7 @@ public class DisplayRotationTests { waitForUiHandler(); verify(mMockSensorManager, times(numOfInvocation)).registerListener( listenerCaptor.capture(), - same(mFakeSensor), + same(mFakeOrientationSensor), anyInt(), any()); if (numOfInvocation > 0) { @@ -460,7 +461,7 @@ public class DisplayRotationTests { SensorEvent.class.getDeclaredConstructor(int.class); constructor.setAccessible(true); final SensorEvent event = constructor.newInstance(1); - event.sensor = mFakeSensor; + event.sensor = mFakeOrientationSensor; event.values[0] = rotation; event.timestamp = SystemClock.elapsedRealtimeNanos(); return event; @@ -691,6 +692,43 @@ public class DisplayRotationTests { SCREEN_ORIENTATION_SENSOR, Surface.ROTATION_0)); } + // ==================================================== + // Tests for half-fold auto-rotate override of rotation + // ==================================================== + @Test + public void testUpdatesRotationWhenSensorUpdates_RotationLocked_HalfFolded() throws Exception { + mBuilder.setSupportHalfFoldAutoRotateOverride(true); + mBuilder.build(); + configureDisplayRotation(SCREEN_ORIENTATION_LANDSCAPE, false, false); + + enableOrientationSensor(); + + mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN); + freezeRotation(Surface.ROTATION_270); + + mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_0)); + assertTrue(waitForUiHandler()); + // No rotation... + assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation( + SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0)); + + // ... until half-fold + mTarget.foldStateChanged(DeviceStateController.FoldState.HALF_FOLDED); + assertTrue(waitForUiHandler()); + verify(sMockWm).updateRotation(false, false); + assertTrue(waitForUiHandler()); + assertEquals(Surface.ROTATION_0, mTarget.rotationForOrientation( + SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0)); + + // ... then transition back to flat + mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN); + assertTrue(waitForUiHandler()); + verify(sMockWm, atLeast(1)).updateRotation(false, false); + assertTrue(waitForUiHandler()); + assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation( + SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0)); + } + // ================================= // Tests for Policy based Rotation // ================================= @@ -884,6 +922,7 @@ public class DisplayRotationTests { private class DisplayRotationBuilder { private boolean mIsDefaultDisplay = true; private boolean mSupportAutoRotation = true; + private boolean mSupportHalfFoldAutoRotateOverride = false; private int mLidOpenRotation = WindowManagerPolicy.WindowManagerFuncs.LID_ABSENT; private int mCarDockRotation; @@ -920,6 +959,12 @@ public class DisplayRotationTests { return this; } + private DisplayRotationBuilder setSupportHalfFoldAutoRotateOverride( + boolean supportHalfFoldAutoRotateOverride) { + mSupportHalfFoldAutoRotateOverride = supportHalfFoldAutoRotateOverride; + return this; + } + private void captureObservers() { ArgumentCaptor<ContentObserver> captor = ArgumentCaptor.forClass( ContentObserver.class); @@ -1032,9 +1077,13 @@ public class DisplayRotationTests { mMockSensorManager = mock(SensorManager.class); when(mMockContext.getSystemService(Context.SENSOR_SERVICE)) .thenReturn(mMockSensorManager); - mFakeSensor = createSensor(Sensor.TYPE_DEVICE_ORIENTATION); + mFakeOrientationSensor = createSensor(Sensor.TYPE_DEVICE_ORIENTATION); when(mMockSensorManager.getSensorList(Sensor.TYPE_DEVICE_ORIENTATION)).thenReturn( - Collections.singletonList(mFakeSensor)); + Collections.singletonList(mFakeOrientationSensor)); + + when(mMockContext.getResources().getBoolean( + com.android.internal.R.bool.config_windowManagerHalfFoldAutoRotateOverride)) + .thenReturn(mSupportHalfFoldAutoRotateOverride); mMockResolver = mock(ContentResolver.class); when(mMockContext.getContentResolver()).thenReturn(mMockResolver); diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java new file mode 100644 index 000000000000..1246d1ee46e8 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; +import static com.android.server.wm.LetterboxConfigurationPersister.LETTERBOX_CONFIGURATION_FILENAME; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.platform.test.annotations.Presubmit; +import android.util.AtomicFile; + +import androidx.test.filters.SmallTest; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +@SmallTest +@Presubmit +public class LetterboxConfigurationPersisterTest { + + private static final long TIMEOUT = 2000L; // 2 secs + + private LetterboxConfigurationPersister mLetterboxConfigurationPersister; + private Context mContext; + private PersisterQueue mPersisterQueue; + private QueueState mQueueState; + private PersisterQueue.Listener mQueueListener; + private File mConfigFolder; + + @Before + public void setUp() throws Exception { + mContext = getInstrumentation().getTargetContext(); + mConfigFolder = mContext.getFilesDir(); + mPersisterQueue = new PersisterQueue(); + mQueueState = new QueueState(); + mLetterboxConfigurationPersister = new LetterboxConfigurationPersister(mContext, + () -> mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForHorizontalReachability), + () -> mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForVerticalReachability), + mConfigFolder, mPersisterQueue, mQueueState); + mQueueListener = queueEmpty -> mQueueState.onItemAdded(); + mPersisterQueue.addListener(mQueueListener); + mLetterboxConfigurationPersister.start(); + } + + public void tearDown() throws InterruptedException { + deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue); + waitForCompletion(mPersisterQueue); + mPersisterQueue.removeListener(mQueueListener); + stopPersisterSafe(mPersisterQueue); + } + + @Test + public void test_whenStoreIsCreated_valuesAreDefaults() { + final int positionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + final int defaultPositionForHorizontalReachability = + mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForHorizontalReachability); + Assert.assertEquals(defaultPositionForHorizontalReachability, + positionForHorizontalReachability); + final int positionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + final int defaultPositionForVerticalReachability = + mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForVerticalReachability); + Assert.assertEquals(defaultPositionForVerticalReachability, + positionForVerticalReachability); + } + + @Test + public void test_whenUpdatedWithNewValues_valuesAreWritten() { + mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability( + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); + mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability( + LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); + waitForCompletion(mPersisterQueue); + final int newPositionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + final int newPositionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + newPositionForHorizontalReachability); + Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + newPositionForVerticalReachability); + } + + @Test + public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() { + final PersisterQueue firstPersisterQueue = new PersisterQueue(); + final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister( + mContext, () -> -1, () -> -1, mContext.getFilesDir(), firstPersisterQueue, + mQueueState); + firstPersister.start(); + firstPersister.setLetterboxPositionForHorizontalReachability( + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); + firstPersister.setLetterboxPositionForVerticalReachability( + LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); + waitForCompletion(firstPersisterQueue); + stopPersisterSafe(firstPersisterQueue); + final PersisterQueue secondPersisterQueue = new PersisterQueue(); + final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister( + mContext, () -> -1, () -> -1, mContext.getFilesDir(), secondPersisterQueue, + mQueueState); + secondPersister.start(); + final int newPositionForHorizontalReachability = + secondPersister.getLetterboxPositionForHorizontalReachability(); + final int newPositionForVerticalReachability = + secondPersister.getLetterboxPositionForVerticalReachability(); + Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + newPositionForHorizontalReachability); + Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + newPositionForVerticalReachability); + deleteConfiguration(secondPersister, secondPersisterQueue); + waitForCompletion(secondPersisterQueue); + stopPersisterSafe(secondPersisterQueue); + } + + @Test + public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() { + mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability( + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); + mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability( + LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); + waitForCompletion(mPersisterQueue); + final int newPositionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + final int newPositionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + newPositionForHorizontalReachability); + Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + newPositionForVerticalReachability); + deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue); + waitForCompletion(mPersisterQueue); + final int positionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + final int defaultPositionForHorizontalReachability = + mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForHorizontalReachability); + Assert.assertEquals(defaultPositionForHorizontalReachability, + positionForHorizontalReachability); + final int positionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + final int defaultPositionForVerticalReachability = + mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForVerticalReachability); + Assert.assertEquals(defaultPositionForVerticalReachability, + positionForVerticalReachability); + } + + private void stopPersisterSafe(PersisterQueue persisterQueue) { + try { + persisterQueue.stopPersisting(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private void waitForCompletion(PersisterQueue persisterQueue) { + final long endTime = System.currentTimeMillis() + TIMEOUT; + // The queue could be empty but the last item still processing and not completed. For this + // reason the completion happens when there are not more items to process and the last one + // has completed. + while (System.currentTimeMillis() < endTime && (!isQueueEmpty(persisterQueue) + || !hasLastItemCompleted())) { + try { + Thread.sleep(100); + } catch (InterruptedException ie) { /* Nope */} + } + } + + private boolean isQueueEmpty(PersisterQueue persisterQueue) { + return persisterQueue.findLastItem( + writeQueueItem -> true, PersisterQueue.WriteQueueItem.class) != null; + } + + private boolean hasLastItemCompleted() { + return mQueueState.isEmpty(); + } + + private void deleteConfiguration(LetterboxConfigurationPersister persister, + PersisterQueue persisterQueue) { + final AtomicFile fileToDelete = new AtomicFile( + new File(mConfigFolder, LETTERBOX_CONFIGURATION_FILENAME)); + persisterQueue.addItem( + new DeleteFileCommand(fileToDelete, mQueueState.andThen( + s -> persister.useDefaultValue())), true); + } + + private static class DeleteFileCommand implements + PersisterQueue.WriteQueueItem<DeleteFileCommand> { + + @NonNull + private final AtomicFile mFileToDelete; + @Nullable + private final Consumer<String> mOnComplete; + + DeleteFileCommand(@NonNull AtomicFile fileToDelete, Consumer<String> onComplete) { + mFileToDelete = fileToDelete; + mOnComplete = onComplete; + } + + @Override + public void process() { + mFileToDelete.delete(); + if (mOnComplete != null) { + mOnComplete.accept("DeleteFileCommand"); + } + } + } + + // Contains the current length of the persister queue + private static class QueueState implements Consumer<String> { + + // The current number of commands in the queue + @VisibleForTesting + private final AtomicInteger mCounter = new AtomicInteger(0); + + @Override + public void accept(String s) { + mCounter.decrementAndGet(); + } + + void onItemAdded() { + mCounter.incrementAndGet(); + } + + boolean isEmpty() { + return mCounter.get() == 0; + } + + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java new file mode 100644 index 000000000000..c927f9e449d5 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; + +import java.util.function.Consumer; + +@SmallTest +@Presubmit +public class LetterboxConfigurationTest { + + private LetterboxConfiguration mLetterboxConfiguration; + private LetterboxConfigurationPersister mLetterboxConfigurationPersister; + + @Before + public void setUp() throws Exception { + Context context = getInstrumentation().getTargetContext(); + mLetterboxConfigurationPersister = mock(LetterboxConfigurationPersister.class); + mLetterboxConfiguration = new LetterboxConfiguration(context, + mLetterboxConfigurationPersister); + } + + @Test + public void test_whenReadingValues_storeIsInvoked() { + mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(); + verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability(); + mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(); + verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability(); + } + + @Test + public void test_whenSettingValues_updateConfigurationIsInvoked() { + mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(); + verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability( + anyInt()); + mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(); + verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability( + anyInt()); + } + + @Test + public void test_whenMovedHorizontally_updatePositionAccordingly() { + // Starting from center + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop); + // Starting from left + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop); + // Starting from right + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop); + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); + } + + @Test + public void test_whenMovedVertically_updatePositionAccordingly() { + // Starting from center + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop); + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop); + // Starting from top + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop); + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop); + // Starting from bottom + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop); + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop); + } + + private void assertForHorizontalMove(int from, int expected, int expectedTime, + Consumer<LetterboxConfiguration> move) { + // We are in the current position + when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()) + .thenReturn(from); + move.accept(mLetterboxConfiguration); + verify(mLetterboxConfigurationPersister, + times(expectedTime)).setLetterboxPositionForHorizontalReachability( + expected); + } + + private void assertForVerticalMove(int from, int expected, int expectedTime, + Consumer<LetterboxConfiguration> move) { + // We are in the current position + when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()) + .thenReturn(from); + move.accept(mLetterboxConfiguration); + verify(mLetterboxConfigurationPersister, + times(expectedTime)).setLetterboxPositionForVerticalReachability( + expected); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index adf694c2a88d..0462e1be7a5f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -30,6 +30,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE; import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.os.Process.NOBODY_UID; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -1220,20 +1221,34 @@ public class RecentTasksTest extends WindowTestsBase { @Test public void testCreateRecentTaskInfo_detachedTask() { - final Task task = createTaskBuilder(".Task").setCreateActivity(true).build(); + final Task task = createTaskBuilder(".Task").build(); + new ActivityBuilder(mSupervisor.mService) + .setTask(task) + .setUid(NOBODY_UID) + .setComponent(getUniqueComponentName()) + .build(); final TaskDisplayArea tda = task.getDisplayArea(); assertTrue(task.isAttached()); assertTrue(task.supportsMultiWindow()); - RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true); + RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, + true /* getTasksAllowed */); assertTrue(info.supportsMultiWindow); + info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, + false /* getTasksAllowed */); + + assertTrue(info.topActivity == null); + assertTrue(info.topActivityInfo == null); + assertTrue(info.baseActivity == null); + // The task can be put in split screen even if it is not attached now. task.removeImmediately(); - info = mRecentTasks.createRecentTaskInfo(task, true); + info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, + true /* getTasksAllowed */); assertTrue(info.supportsMultiWindow); @@ -1242,7 +1257,8 @@ public class RecentTasksTest extends WindowTestsBase { doReturn(false).when(tda).supportsNonResizableMultiWindow(); doReturn(false).when(task).isResizeable(); - info = mRecentTasks.createRecentTaskInfo(task, true); + info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, + true /* getTasksAllowed */); assertFalse(info.supportsMultiWindow); @@ -1250,7 +1266,8 @@ public class RecentTasksTest extends WindowTestsBase { // the device supports it. doReturn(true).when(tda).supportsNonResizableMultiWindow(); - info = mRecentTasks.createRecentTaskInfo(task, true); + info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, + true /* getTasksAllowed */); assertTrue(info.supportsMultiWindow); } diff --git a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java index 6128428dcb7d..b46e90da3944 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java @@ -1312,13 +1312,15 @@ public class RootTaskTests extends WindowTestsBase { secondActivity.app.setThread(null); // This should do nothing from a non-attached caller. assertFalse(task.navigateUpTo(secondActivity /* source record */, - firstActivity.intent /* destIntent */, null /* destGrants */, - 0 /* resultCode */, null /* resultData */, null /* resultGrants */)); + firstActivity.intent /* destIntent */, null /* resolvedType */, + null /* destGrants */, 0 /* resultCode */, null /* resultData */, + null /* resultGrants */)); secondActivity.app.setThread(thread); assertTrue(task.navigateUpTo(secondActivity /* source record */, - firstActivity.intent /* destIntent */, null /* destGrants */, - 0 /* resultCode */, null /* resultData */, null /* resultGrants */)); + firstActivity.intent /* destIntent */, null /* resolvedType */, + null /* destGrants */, 0 /* resultCode */, null /* resultData */, + null /* resultGrants */)); // The firstActivity uses default launch mode, so the activities between it and itself will // be finished. assertTrue(secondActivity.finishing); diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java index 601cf154b3bf..64c1e05da2cd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java @@ -72,6 +72,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Rect; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import android.util.MergedConfiguration; @@ -377,6 +378,33 @@ public class RootWindowContainerTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_FULLSCREEN, fullscreenTask.getWindowingMode()); } + @Test + public void testMovingEmbeddedActivityToPip() { + final Rect taskBounds = new Rect(0, 0, 800, 1000); + final Rect taskFragmentBounds = new Rect(0, 0, 400, 1000); + final Task task = mRootWindowContainer.getDefaultTaskDisplayArea().createRootTask( + WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, true /* onTop */); + task.setBounds(taskBounds); + assertEquals(taskBounds, task.getBounds()); + final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .createActivityCount(2) + .setBounds(taskFragmentBounds) + .build(); + assertEquals(taskFragmentBounds, taskFragment.getBounds()); + final ActivityRecord topActivity = taskFragment.getTopMostActivity(); + + // Move the top activity to pinned root task. + mRootWindowContainer.moveActivityToPinnedRootTask(topActivity, + null /* launchIntoPipHostActivity */, "test"); + + final Task pinnedRootTask = task.getDisplayArea().getRootPinnedTask(); + + // Ensure the initial bounds of the PiP Task is the same as the TaskFragment. + ensureTaskPlacement(pinnedRootTask, topActivity); + assertEquals(taskFragmentBounds, pinnedRootTask.getBounds()); + } + private static void ensureTaskPlacement(Task task, ActivityRecord... activities) { final ArrayList<ActivityRecord> taskActivities = new ArrayList<>(); diff --git a/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java index e57ad5d9ff8c..24e932f36f80 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java @@ -57,10 +57,20 @@ public class SafeActivityOptionsTest { .setLaunchTaskDisplayArea(token) .setLaunchDisplayId(launchDisplayId) .setCallerDisplayId(callerDisplayId)) - .selectiveCloneDisplayOptions(); + .selectiveCloneLaunchOptions(); assertSame(clone.getOriginalOptions().getLaunchTaskDisplayArea(), token); assertEquals(clone.getOriginalOptions().getLaunchDisplayId(), launchDisplayId); assertEquals(clone.getOriginalOptions().getCallerDisplayId(), callerDisplayId); } + + @Test + public void test_selectiveCloneLunchRootTask() { + final WindowContainerToken token = mock(WindowContainerToken.class); + final SafeActivityOptions clone = new SafeActivityOptions(ActivityOptions.makeBasic() + .setLaunchRootTask(token)) + .selectiveCloneLaunchOptions(); + + assertSame(clone.getOriginalOptions().getLaunchRootTask(), token); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 1404de253476..4202f46c188c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -56,6 +56,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -91,6 +92,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; import java.util.List; @@ -195,6 +197,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment); mController.dispatchPendingEvents(); + assertTaskFragmentParentInfoChangedTransaction(mTask); assertTaskFragmentAppearedTransaction(); } @@ -365,6 +368,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { mController.onActivityReparentedToTask(activity); mController.dispatchPendingEvents(); + assertTaskFragmentParentInfoChangedTransaction(task); assertActivityReparentedToTaskTransaction(task.mTaskId, activity.intent, activity.token); } @@ -760,6 +764,50 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test + public void testOrganizerRemovedWithPendingEvents() { + final TaskFragment tf0 = new TaskFragmentBuilder(mAtm) + .setCreateParentTask() + .setOrganizer(mOrganizer) + .setFragmentToken(mFragmentToken) + .build(); + final TaskFragment tf1 = new TaskFragmentBuilder(mAtm) + .setCreateParentTask() + .setOrganizer(mOrganizer) + .setFragmentToken(new Binder()) + .build(); + assertTrue(tf0.isOrganizedTaskFragment()); + assertTrue(tf1.isOrganizedTaskFragment()); + assertTrue(tf0.isAttached()); + assertTrue(tf0.isAttached()); + + // Mock the behavior that remove TaskFragment can trigger event dispatch. + final Answer<Void> removeImmediately = invocation -> { + invocation.callRealMethod(); + mController.dispatchPendingEvents(); + return null; + }; + doAnswer(removeImmediately).when(tf0).removeImmediately(); + doAnswer(removeImmediately).when(tf1).removeImmediately(); + + // Add pending events. + mController.onTaskFragmentAppeared(mIOrganizer, tf0); + mController.onTaskFragmentAppeared(mIOrganizer, tf1); + + // Remove organizer. + mController.unregisterOrganizer(mIOrganizer); + mController.dispatchPendingEvents(); + + // Nothing should happen after the organizer is removed. + verify(mOrganizer, never()).onTransactionReady(any()); + + // TaskFragments should be removed. + assertFalse(tf0.isOrganizedTaskFragment()); + assertFalse(tf1.isOrganizedTaskFragment()); + assertFalse(tf0.isAttached()); + assertFalse(tf0.isAttached()); + } + + @Test public void testTaskFragmentInPip_startActivityInTaskFragment() { setupTaskFragmentInPip(); final ActivityRecord activity = mTaskFragment.getTopMostActivity(); @@ -872,29 +920,87 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { @Test public void testDeferPendingTaskFragmentEventsOfInvisibleTask() { - // Task - TaskFragment - Activity. final Task task = createTask(mDisplayContent); final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) .setOrganizer(mOrganizer) .setFragmentToken(mFragmentToken) .build(); - - // Mock the task to invisible doReturn(false).when(task).shouldBeVisible(any()); - // Sending events - taskFragment.mTaskFragmentAppearedSent = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + // Dispatch the initial event in the Task to update the Task visibility to the organizer. + mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); + verify(mOrganizer).onTransactionReady(any()); - // Verifies that event was not sent + // Verify that events were not sent when the Task is in background. + clearInvocations(mOrganizer); + final Rect bounds = new Rect(0, 0, 500, 1000); + task.setBoundsUnchecked(bounds); + mController.onTaskFragmentParentInfoChanged(mIOrganizer, task); + mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + mController.dispatchPendingEvents(); verify(mOrganizer, never()).onTransactionReady(any()); + + // Verify that the events were sent when the Task becomes visible. + doReturn(true).when(task).shouldBeVisible(any()); + task.lastActiveTime++; + mController.dispatchPendingEvents(); + verify(mOrganizer).onTransactionReady(any()); + } + + @Test + public void testSendAllPendingTaskFragmentEventsWhenAnyTaskIsVisible() { + // Invisible Task. + final Task invisibleTask = createTask(mDisplayContent); + final TaskFragment invisibleTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(invisibleTask) + .setOrganizer(mOrganizer) + .setFragmentToken(mFragmentToken) + .build(); + doReturn(false).when(invisibleTask).shouldBeVisible(any()); + + // Visible Task. + final IBinder fragmentToken = new Binder(); + final Task visibleTask = createTask(mDisplayContent); + final TaskFragment visibleTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(visibleTask) + .setOrganizer(mOrganizer) + .setFragmentToken(fragmentToken) + .build(); + doReturn(true).when(invisibleTask).shouldBeVisible(any()); + + // Sending events + invisibleTaskFragment.mTaskFragmentAppearedSent = true; + visibleTaskFragment.mTaskFragmentAppearedSent = true; + mController.onTaskFragmentInfoChanged(mIOrganizer, invisibleTaskFragment); + mController.onTaskFragmentInfoChanged(mIOrganizer, visibleTaskFragment); + mController.dispatchPendingEvents(); + + // Verify that both events are sent. + verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); + final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); + final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); + + // There should be two Task info changed with two TaskFragment info changed. + assertEquals(4, changes.size()); + // Invisible Task info changed + assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(0).getType()); + assertEquals(invisibleTask.mTaskId, changes.get(0).getTaskId()); + // Invisible TaskFragment info changed + assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(1).getType()); + assertEquals(invisibleTaskFragment.getFragmentToken(), + changes.get(1).getTaskFragmentToken()); + // Visible Task info changed + assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(2).getType()); + assertEquals(visibleTask.mTaskId, changes.get(2).getTaskId()); + // Visible TaskFragment info changed + assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(3).getType()); + assertEquals(visibleTaskFragment.getFragmentToken(), changes.get(3).getTaskFragmentToken()); } @Test public void testCanSendPendingTaskFragmentEventsAfterActivityResumed() { - // Task - TaskFragment - Activity. final Task task = createTask(mDisplayContent); final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) @@ -903,24 +1009,26 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { .createActivityCount(1) .build(); final ActivityRecord activity = taskFragment.getTopMostActivity(); - - // Mock the task to invisible doReturn(false).when(task).shouldBeVisible(any()); taskFragment.setResumedActivity(null, "test"); - // Sending events - taskFragment.mTaskFragmentAppearedSent = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + // Dispatch the initial event in the Task to update the Task visibility to the organizer. + mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); + verify(mOrganizer).onTransactionReady(any()); - // Verifies that event was not sent + // Verify the info changed event is not sent because the Task is invisible + clearInvocations(mOrganizer); + final Rect bounds = new Rect(0, 0, 500, 1000); + task.setBoundsUnchecked(bounds); + mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + mController.dispatchPendingEvents(); verify(mOrganizer, never()).onTransactionReady(any()); - // Mock the task becomes visible, and activity resumed + // Mock the task becomes visible, and activity resumed. Verify the info changed event is + // sent. doReturn(true).when(task).shouldBeVisible(any()); taskFragment.setResumedActivity(activity, "test"); - - // Verifies that event is sent. mController.dispatchPendingEvents(); verify(mOrganizer).onTransactionReady(any()); } @@ -975,25 +1083,24 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity(); // Add another activity in the Task so that it always contains a non-finishing activity. createActivityRecord(task); - assertTrue(task.shouldBeVisible(null)); + doReturn(false).when(task).shouldBeVisible(any()); - // Dispatch pending info changed event from creating the activity - taskFragment.mTaskFragmentAppearedSent = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); + // Dispatch the initial event in the Task to update the Task visibility to the organizer. + mController.onTaskFragmentAppeared(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); verify(mOrganizer).onTransactionReady(any()); - // Verify the info changed callback is not called when the task is invisible + // Verify the info changed event is not sent because the Task is invisible clearInvocations(mOrganizer); - doReturn(false).when(task).shouldBeVisible(any()); + final Rect bounds = new Rect(0, 0, 500, 1000); + task.setBoundsUnchecked(bounds); mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); verify(mOrganizer, never()).onTransactionReady(any()); - // Finish the embedded activity, and verify the info changed callback is called because the + // Finish the embedded activity, and verify the info changed event is sent because the // TaskFragment is becoming empty. embeddedActivity.finishing = true; - mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment); mController.dispatchPendingEvents(); verify(mOrganizer).onTransactionReady(any()); } @@ -1205,7 +1312,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { /** * Creates a {@link TaskFragment} with the {@link WindowContainerTransaction}. Calls - * {@link WindowOrganizerController#applyTransaction} to apply the transaction, + * {@link WindowOrganizerController#applyTransaction(WindowContainerTransaction)} to apply the + * transaction, */ private void createTaskFragmentFromOrganizer(WindowContainerTransaction wct, ActivityRecord ownerActivity, IBinder fragmentToken) { @@ -1239,8 +1347,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); assertFalse(changes.isEmpty()); - // Appeared will come with parent info changed. - final TaskFragmentTransaction.Change change = changes.get(changes.size() - 1); + // Use remove to verify multiple transaction changes. + final TaskFragmentTransaction.Change change = changes.remove(0); assertEquals(TYPE_TASK_FRAGMENT_APPEARED, change.getType()); assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo()); assertEquals(mFragmentToken, change.getTaskFragmentToken()); @@ -1253,8 +1361,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); assertFalse(changes.isEmpty()); - // InfoChanged may come with parent info changed. - final TaskFragmentTransaction.Change change = changes.get(changes.size() - 1); + // Use remove to verify multiple transaction changes. + final TaskFragmentTransaction.Change change = changes.remove(0); assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, change.getType()); assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo()); assertEquals(mFragmentToken, change.getTaskFragmentToken()); @@ -1266,7 +1374,9 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); assertFalse(changes.isEmpty()); - final TaskFragmentTransaction.Change change = changes.get(0); + + // Use remove to verify multiple transaction changes. + final TaskFragmentTransaction.Change change = changes.remove(0); assertEquals(TYPE_TASK_FRAGMENT_VANISHED, change.getType()); assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo()); assertEquals(mFragmentToken, change.getTaskFragmentToken()); @@ -1278,7 +1388,9 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); assertFalse(changes.isEmpty()); - final TaskFragmentTransaction.Change change = changes.get(0); + + // Use remove to verify multiple transaction changes. + final TaskFragmentTransaction.Change change = changes.remove(0); assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, change.getType()); assertEquals(task.mTaskId, change.getTaskId()); assertEquals(task.getTaskFragmentParentInfo(), change.getTaskFragmentParentInfo()); @@ -1290,7 +1402,9 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); assertFalse(changes.isEmpty()); - final TaskFragmentTransaction.Change change = changes.get(0); + + // Use remove to verify multiple transaction changes. + final TaskFragmentTransaction.Change change = changes.remove(0); assertEquals(TYPE_TASK_FRAGMENT_ERROR, change.getType()); assertEquals(mErrorToken, change.getErrorCallbackToken()); final Bundle errorBundle = change.getErrorBundle(); @@ -1306,7 +1420,9 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); assertFalse(changes.isEmpty()); - final TaskFragmentTransaction.Change change = changes.get(0); + + // Use remove to verify multiple transaction changes. + final TaskFragmentTransaction.Change change = changes.remove(0); assertEquals(TYPE_ACTIVITY_REPARENTED_TO_TASK, change.getType()); assertEquals(taskId, change.getTaskId()); assertEquals(intent, change.getActivityIntent()); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 76cd19be19b0..68ac1d6f2e01 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -1454,6 +1454,21 @@ public class TaskTests extends WindowTestsBase { verify(tfBehind, never()).resumeTopActivity(any(), any(), anyBoolean()); } + @Test + public void testGetTaskFragment() { + final Task parentTask = createTask(mDisplayContent); + final TaskFragment tf0 = createTaskFragmentWithParentTask(parentTask); + final TaskFragment tf1 = createTaskFragmentWithParentTask(parentTask); + + assertNull("Could not find it because there's no organized TaskFragment", + parentTask.getTaskFragment(TaskFragment::isOrganizedTaskFragment)); + + doReturn(true).when(tf0).isOrganizedTaskFragment(); + + assertEquals("tf0 must be return because it's the organized TaskFragment.", + tf0, parentTask.getTaskFragment(TaskFragment::isOrganizedTaskFragment)); + } + private Task getTestTask() { final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); return task.getBottomMostTask(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 45d8e226c64f..66e46a2bb187 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -19,7 +19,6 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; @@ -60,7 +59,9 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.app.ActivityManager; import android.content.res.Configuration; +import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; @@ -80,6 +81,8 @@ import android.window.TransitionInfo; import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; +import com.android.internal.graphics.ColorUtils; + import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -393,6 +396,70 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testCreateInfo_PromoteSimilarClose() { + final Transition transition = createTestTransition(TRANSIT_CLOSE); + ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; + ArraySet<WindowContainer> participants = transition.mParticipants; + + final Task topTask = createTask(mDisplayContent); + final Task belowTask = createTask(mDisplayContent); + final ActivityRecord showing = createActivityRecord(belowTask); + final ActivityRecord hiding = createActivityRecord(topTask); + final ActivityRecord closing = createActivityRecord(topTask); + // Start states. + changes.put(topTask, new Transition.ChangeInfo(true /* vis */, false /* exChg */)); + changes.put(belowTask, new Transition.ChangeInfo(false /* vis */, false /* exChg */)); + changes.put(showing, new Transition.ChangeInfo(false /* vis */, false /* exChg */)); + changes.put(hiding, new Transition.ChangeInfo(true /* vis */, false /* exChg */)); + changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */)); + fillChangeMap(changes, topTask); + // End states. + showing.mVisibleRequested = true; + closing.mVisibleRequested = false; + hiding.mVisibleRequested = false; + + participants.add(belowTask); + participants.add(hiding); + participants.add(closing); + ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes); + assertEquals(2, targets.size()); + assertTrue(targets.contains(belowTask)); + assertTrue(targets.contains(topTask)); + } + + @Test + public void testCreateInfo_PromoteSimilarOpen() { + final Transition transition = createTestTransition(TRANSIT_OPEN); + ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; + ArraySet<WindowContainer> participants = transition.mParticipants; + + final Task topTask = createTask(mDisplayContent); + final Task belowTask = createTask(mDisplayContent); + final ActivityRecord showing = createActivityRecord(topTask); + final ActivityRecord opening = createActivityRecord(topTask); + final ActivityRecord closing = createActivityRecord(belowTask); + // Start states. + changes.put(topTask, new Transition.ChangeInfo(false /* vis */, false /* exChg */)); + changes.put(belowTask, new Transition.ChangeInfo(true /* vis */, false /* exChg */)); + changes.put(showing, new Transition.ChangeInfo(false /* vis */, false /* exChg */)); + changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */)); + changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */)); + fillChangeMap(changes, topTask); + // End states. + showing.mVisibleRequested = true; + opening.mVisibleRequested = true; + closing.mVisibleRequested = false; + + participants.add(belowTask); + participants.add(showing); + participants.add(opening); + ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes); + assertEquals(2, targets.size()); + assertTrue(targets.contains(belowTask)); + assertTrue(targets.contains(topTask)); + } + + @Test public void testTargets_noIntermediatesToWallpaper() { final Transition transition = createTestTransition(TRANSIT_OPEN); @@ -732,6 +799,11 @@ public class TransitionTests extends WindowTestsBase { assertTrue(asyncRotationController.isTargetToken(decorToken)); assertShouldFreezeInsetsPosition(asyncRotationController, statusBar, true); + if (TransitionController.SYNC_METHOD != BLASTSyncEngine.METHOD_BLAST) { + // Only seamless window syncs its draw transaction with transition. + assertFalse(asyncRotationController.handleFinishDrawing(statusBar, mMockT)); + assertTrue(asyncRotationController.handleFinishDrawing(screenDecor, mMockT)); + } screenDecor.setOrientationChanging(false); // Status bar finishes drawing before the start transaction. Its fade-in animation will be // executed until the transaction is committed, so it is still in target tokens. @@ -1160,10 +1232,8 @@ public class TransitionTests extends WindowTestsBase { final ArraySet<WindowContainer> participants = transition.mParticipants; final Task task = createTask(mDisplayContent); - // Set to multi-windowing mode in order to set bounds. - task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); final Rect taskBounds = new Rect(0, 0, 500, 1000); - task.setBounds(taskBounds); + task.getConfiguration().windowConfiguration.setBounds(taskBounds); final ActivityRecord nonEmbeddedActivity = createActivityRecord(task); final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); mAtm.mTaskFragmentOrganizerController.registerOrganizer( @@ -1205,10 +1275,8 @@ public class TransitionTests extends WindowTestsBase { final ArraySet<WindowContainer> participants = transition.mParticipants; final Task task = createTask(mDisplayContent); - // Set to multi-windowing mode in order to set bounds. - task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); final Rect taskBounds = new Rect(0, 0, 500, 1000); - task.setBounds(taskBounds); + task.getConfiguration().windowConfiguration.setBounds(taskBounds); final ActivityRecord activity = createActivityRecord(task); // Start states: set bounds to make sure the start bounds is ignored if it is not visible. activity.getConfiguration().windowConfiguration.setBounds(new Rect(0, 0, 250, 500)); @@ -1236,10 +1304,8 @@ public class TransitionTests extends WindowTestsBase { final ArraySet<WindowContainer> participants = transition.mParticipants; final Task task = createTask(mDisplayContent); - // Set to multi-windowing mode in order to set bounds. - task.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); final Rect taskBounds = new Rect(0, 0, 500, 1000); - task.setBounds(taskBounds); + task.getConfiguration().windowConfiguration.setBounds(taskBounds); final ActivityRecord activity = createActivityRecord(task); // Start states: fills Task without override. activity.mVisibleRequested = true; @@ -1260,6 +1326,35 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testReparentChangeLastParent() { + final Transition transition = createTestTransition(TRANSIT_CHANGE); + final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; + final ArraySet<WindowContainer> participants = transition.mParticipants; + + // Reparent activity in transition. + final Task lastParent = createTask(mDisplayContent); + final Task newParent = createTask(mDisplayContent); + final ActivityRecord activity = createActivityRecord(lastParent); + activity.mVisibleRequested = true; + // Skip manipulate the SurfaceControl. + doNothing().when(activity).setDropInputMode(anyInt()); + changes.put(activity, new Transition.ChangeInfo(activity)); + activity.reparent(newParent, POSITION_TOP); + activity.mVisibleRequested = false; + + participants.add(activity); + final ArrayList<WindowContainer> targets = Transition.calculateTargets( + participants, changes); + final TransitionInfo info = Transition.calculateTransitionInfo( + transition.mType, 0 /* flags */, targets, changes, mMockT); + + // Change contains last parent info. + assertEquals(1, info.getChanges().size()); + assertEquals(lastParent.mRemoteToken.toWindowContainerToken(), + info.getChanges().get(0).getLastParent()); + } + + @Test public void testIncludeEmbeddedActivityReparent() { final Transition transition = createTestTransition(TRANSIT_OPEN); final Task task = createTask(mDisplayContent); @@ -1293,6 +1388,50 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testChangeSetBackgroundColor() { + final Transition transition = createTestTransition(TRANSIT_CHANGE); + final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; + final ArraySet<WindowContainer> participants = transition.mParticipants; + + // Test background color for Activity and embedded TaskFragment. + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + mAtm.mTaskFragmentOrganizerController.registerOrganizer( + ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder())); + final Task task = createTask(mDisplayContent); + final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer); + final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity(); + final ActivityRecord nonEmbeddedActivity = createActivityRecord(task); + final ActivityManager.TaskDescription taskDescription = + new ActivityManager.TaskDescription.Builder() + .setBackgroundColor(Color.YELLOW) + .build(); + task.setTaskDescription(taskDescription); + + // Start states: + embeddedActivity.mVisibleRequested = true; + nonEmbeddedActivity.mVisibleRequested = false; + changes.put(embeddedTf, new Transition.ChangeInfo(embeddedTf)); + changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(nonEmbeddedActivity)); + // End states: + embeddedActivity.mVisibleRequested = false; + nonEmbeddedActivity.mVisibleRequested = true; + + participants.add(embeddedTf); + participants.add(nonEmbeddedActivity); + final ArrayList<WindowContainer> targets = Transition.calculateTargets( + participants, changes); + final TransitionInfo info = Transition.calculateTransitionInfo(transition.mType, + 0 /* flags */, targets, changes, mMockT); + + // Background color should be set on both Activity and embedded TaskFragment. + final int expectedBackgroundColor = ColorUtils.setAlphaComponent( + taskDescription.getBackgroundColor(), 255); + assertEquals(2, info.getChanges().size()); + assertEquals(expectedBackgroundColor, info.getChanges().get(0).getBackgroundColor()); + assertEquals(expectedBackgroundColor, info.getChanges().get(1).getBackgroundColor()); + } + + @Test public void testTransitionVisibleChange() { registerTestTransitionPlayer(); final ActivityRecord app = createActivityRecord(mDisplayContent); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index b8da8cc77ec6..b0d7ed660837 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -43,6 +43,7 @@ import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_ import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; @@ -195,14 +196,31 @@ public class WindowManagerServiceTests extends WindowTestsBase { mWm.mWindowMap.put(win.mClient.asBinder(), win); final int w = 100; final int h = 200; + final ClientWindowFrames outFrames = new ClientWindowFrames(); + final MergedConfiguration outConfig = new MergedConfiguration(); + final SurfaceControl outSurfaceControl = new SurfaceControl(); + final InsetsState outInsetsState = new InsetsState(); + final InsetsSourceControl[] outControls = new InsetsSourceControl[0]; + final Bundle outBundle = new Bundle(); mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.GONE, 0, 0, 0, - new ClientWindowFrames(), new MergedConfiguration(), new SurfaceControl(), - new InsetsState(), new InsetsSourceControl[0], new Bundle()); + outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle); // Because the window is already invisible, it doesn't need to apply exiting animation // and WMS#tryStartExitingAnimation() will destroy the surface directly. assertFalse(win.mAnimatingExit); assertFalse(win.mHasSurface); assertNull(win.mWinAnimator.mSurfaceController); + + doReturn(mSystemServicesTestRule.mTransaction).when(SurfaceControl::getGlobalTransaction); + // Invisible requested activity should not get the last config even if its view is visible. + mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.VISIBLE, 0, 0, 0, + outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle); + assertEquals(0, outConfig.getMergedConfiguration().densityDpi); + // Non activity window can still get the last config. + win.mActivityRecord = null; + win.fillClientWindowFramesAndConfiguration(outFrames, outConfig, + false /* useLatestConfig */, true /* relayoutVisible */); + assertEquals(win.getConfiguration().densityDpi, + outConfig.getMergedConfiguration().densityDpi); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index cfc0da7a4a15..9bcc1367f8ab 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -731,6 +731,15 @@ public class WindowStateTests extends WindowTestsBase { assertTrue(mWm.mResizingWindows.contains(startingApp)); assertTrue(startingApp.isDrawn()); assertFalse(startingApp.getOrientationChanging()); + + // Even if the display is frozen, invisible requested window should not be affected. + startingApp.mActivityRecord.mVisibleRequested = false; + mWm.startFreezingDisplay(0, 0, mDisplayContent); + doReturn(true).when(mWm.mPolicy).isScreenOn(); + startingApp.getWindowFrames().setInsetsChanged(true); + startingApp.updateResizingWindowIfNeeded(); + assertTrue(startingApp.isDrawn()); + assertFalse(startingApp.getOrientationChanging()); } @UseTestDisplay(addWindows = W_ABOVE_ACTIVITY) diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index ef532f5732eb..b99fd1606f55 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -718,6 +718,10 @@ class WindowTestsBase extends SystemServiceTestsBase { activity.mVisibleRequested = true; } + static TaskFragment createTaskFragmentWithParentTask(@NonNull Task parentTask) { + return createTaskFragmentWithParentTask(parentTask, false /* createEmbeddedTask */); + } + /** * Creates a {@link TaskFragment} and attach it to the {@code parentTask}. * @@ -1727,7 +1731,7 @@ class WindowTestsBase extends SystemServiceTestsBase { } void startTransition() { - mOrganizer.startTransition(mLastRequest.getType(), mLastTransit, null); + mOrganizer.startTransition(mLastTransit, null); } void onTransactionReady(SurfaceControl.Transaction t) { diff --git a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java index eafcef2f1d38..1e74451a8d4d 100644 --- a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java +++ b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java @@ -210,21 +210,15 @@ final class TranslationManagerServiceImpl extends final int translatedAppUid = getAppUidByComponentName(getContext(), componentName, getUserId()); final String packageName = componentName.getPackageName(); - if (activityDestroyed) { - // In the Activity destroy case, we only calls onTranslationFinished() in - // non-finisTranslation() state. If there is a finisTranslation() calls by apps, we - // should remove the waiting callback to avoid callback twice. + // In the Activity destroyed case, we only call onTranslationFinished() in + // non-finishTranslation() state. If there is a finishTranslation() call by apps, we + // should remove the waiting callback to avoid invoking callbacks twice. + if (activityDestroyed || mWaitingFinishedCallbackActivities.contains(token)) { invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, /* sourceSpec= */ null, /* targetSpec= */ null, packageName, translatedAppUid); mWaitingFinishedCallbackActivities.remove(token); - } else { - if (mWaitingFinishedCallbackActivities.contains(token)) { - invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, - /* sourceSpec= */ null, /* targetSpec= */ null, - packageName, translatedAppUid); - mWaitingFinishedCallbackActivities.remove(token); - } + mActiveTranslations.remove(token); } } @@ -237,6 +231,9 @@ final class TranslationManagerServiceImpl extends // Activity is the new Activity, the original Activity is paused in the same task. // To make sure the operation still work, we use the token to find the target Activity in // this task, not the top Activity only. + // + // Note: getAttachedNonFinishingActivityForTask() takes the shareable activity token. We + // call this method so that we can get the regular activity token below. ActivityTokens candidateActivityTokens = mActivityTaskManagerInternal.getAttachedNonFinishingActivityForTask(taskId, token); if (candidateActivityTokens == null) { @@ -263,27 +260,27 @@ final class TranslationManagerServiceImpl extends getAppUidByComponentName(getContext(), componentName, getUserId()); String packageName = componentName.getPackageName(); - invokeCallbacksIfNecessaryLocked(state, sourceSpec, targetSpec, packageName, activityToken, + invokeCallbacksIfNecessaryLocked(state, sourceSpec, targetSpec, packageName, token, translatedAppUid); - updateActiveTranslationsLocked(state, sourceSpec, targetSpec, packageName, activityToken, + updateActiveTranslationsLocked(state, sourceSpec, targetSpec, packageName, token, translatedAppUid); } @GuardedBy("mLock") private void updateActiveTranslationsLocked(int state, TranslationSpec sourceSpec, - TranslationSpec targetSpec, String packageName, IBinder activityToken, + TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken, int translatedAppUid) { // We keep track of active translations and their state so that we can: // 1. Trigger callbacks that are registered after translation has started. // See registerUiTranslationStateCallbackLocked(). // 2. NOT trigger callbacks when the state didn't change. // See invokeCallbacksIfNecessaryLocked(). - ActiveTranslation activeTranslation = mActiveTranslations.get(activityToken); + ActiveTranslation activeTranslation = mActiveTranslations.get(shareableActivityToken); switch (state) { case STATE_UI_TRANSLATION_STARTED: { if (activeTranslation == null) { try { - activityToken.linkToDeath(this, /* flags= */ 0); + shareableActivityToken.linkToDeath(this, /* flags= */ 0); } catch (RemoteException e) { Slog.w(TAG, "Failed to call linkToDeath for translated app with uid=" + translatedAppUid + "; activity is already dead", e); @@ -294,7 +291,7 @@ final class TranslationManagerServiceImpl extends packageName, translatedAppUid); return; } - mActiveTranslations.put(activityToken, + mActiveTranslations.put(shareableActivityToken, new ActiveTranslation(sourceSpec, targetSpec, translatedAppUid, packageName)); } @@ -317,7 +314,7 @@ final class TranslationManagerServiceImpl extends case STATE_UI_TRANSLATION_FINISHED: { if (activeTranslation != null) { - mActiveTranslations.remove(activityToken); + mActiveTranslations.remove(shareableActivityToken); } break; } @@ -332,12 +329,12 @@ final class TranslationManagerServiceImpl extends @GuardedBy("mLock") private void invokeCallbacksIfNecessaryLocked(int state, TranslationSpec sourceSpec, - TranslationSpec targetSpec, String packageName, IBinder activityToken, + TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken, int translatedAppUid) { boolean shouldInvokeCallbacks = true; int stateForCallbackInvocation = state; - ActiveTranslation activeTranslation = mActiveTranslations.get(activityToken); + ActiveTranslation activeTranslation = mActiveTranslations.get(shareableActivityToken); if (activeTranslation == null) { if (state != STATE_UI_TRANSLATION_STARTED) { shouldInvokeCallbacks = false; @@ -403,14 +400,6 @@ final class TranslationManagerServiceImpl extends } } - if (DEBUG) { - Slog.d(TAG, - (shouldInvokeCallbacks ? "" : "NOT ") - + "Invoking callbacks for translation state=" - + stateForCallbackInvocation + " for app with uid=" + translatedAppUid - + " packageName=" + packageName); - } - if (shouldInvokeCallbacks) { invokeCallbacks(stateForCallbackInvocation, sourceSpec, targetSpec, packageName, translatedAppUid); @@ -448,7 +437,7 @@ final class TranslationManagerServiceImpl extends pw.println(waitingFinishCallbackSize); for (IBinder activityToken : mWaitingFinishedCallbackActivities) { pw.print(prefix); - pw.print("activityToken: "); + pw.print("shareableActivityToken: "); pw.println(activityToken); } } @@ -458,7 +447,14 @@ final class TranslationManagerServiceImpl extends int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName, int translatedAppUid) { Bundle result = createResultForCallback(state, sourceSpec, targetSpec, packageName); - if (mCallbacks.getRegisteredCallbackCount() == 0) { + int registeredCallbackCount = mCallbacks.getRegisteredCallbackCount(); + if (DEBUG) { + Slog.d(TAG, "Invoking " + registeredCallbackCount + " callbacks for translation state=" + + state + " for app with uid=" + translatedAppUid + + " packageName=" + packageName); + } + + if (registeredCallbackCount == 0) { return; } List<InputMethodInfo> enabledInputMethods = getEnabledInputMethods(); @@ -521,8 +517,10 @@ final class TranslationManagerServiceImpl extends @GuardedBy("mLock") public void registerUiTranslationStateCallbackLocked(IRemoteCallback callback, int sourceUid) { mCallbacks.register(callback, sourceUid); - - if (mActiveTranslations.size() == 0) { + int numActiveTranslations = mActiveTranslations.size(); + Slog.i(TAG, "New registered callback for sourceUid=" + sourceUid + " with currently " + + numActiveTranslations + " active translations"); + if (numActiveTranslations == 0) { return; } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 3da47110fb49..352d8d115b6a 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -136,9 +136,6 @@ public class VoiceInteractionManagerService extends SystemService { private final RemoteCallbackList<IVoiceInteractionSessionListener> mVoiceInteractionSessionListeners = new RemoteCallbackList<>(); - // TODO(b/226201975): remove once RoleService supports pre-created users - private final ArrayList<UserHandle> mIgnoredPreCreatedUsers = new ArrayList<>(); - public VoiceInteractionManagerService(Context context) { super(context); mContext = context; @@ -308,24 +305,14 @@ public class VoiceInteractionManagerService extends SystemService { return hotwordDetectionConnection.mIdentity; } + // TODO(b/226201975): remove this method once RoleService supports pre-created users @Override public void onPreCreatedUserConversion(int userId) { - Slogf.d(TAG, "onPreCreatedUserConversion(%d)", userId); - - for (int i = 0; i < mIgnoredPreCreatedUsers.size(); i++) { - UserHandle preCreatedUser = mIgnoredPreCreatedUsers.get(i); - if (preCreatedUser.getIdentifier() == userId) { - Slogf.d(TAG, "Updating role on pre-created user %d", userId); - mServiceStub.mRoleObserver.onRoleHoldersChanged(RoleManager.ROLE_ASSISTANT, - preCreatedUser); - mIgnoredPreCreatedUsers.remove(i); - return; - } - } - Slogf.w(TAG, "onPreCreatedUserConversion(%d): not available on " - + "mIgnoredPreCreatedUserIds (%s)", userId, mIgnoredPreCreatedUsers); + Slogf.d(TAG, "onPreCreatedUserConversion(%d): calling onRoleHoldersChanged() again", + userId); + mServiceStub.mRoleObserver.onRoleHoldersChanged(RoleManager.ROLE_ASSISTANT, + UserHandle.of(userId)); } - } // implementation entry point and binder service @@ -807,8 +794,10 @@ public class VoiceInteractionManagerService extends SystemService { if (TextUtils.isEmpty(curInteractor)) { return null; } - if (DEBUG) Slog.d(TAG, "getCurInteractor curInteractor=" + curInteractor + if (DEBUG) { + Slog.d(TAG, "getCurInteractor curInteractor=" + curInteractor + " user=" + userHandle); + } return ComponentName.unflattenFromString(curInteractor); } @@ -816,8 +805,9 @@ public class VoiceInteractionManagerService extends SystemService { Settings.Secure.putStringForUser(mContext.getContentResolver(), Settings.Secure.VOICE_INTERACTION_SERVICE, comp != null ? comp.flattenToShortString() : "", userHandle); - if (DEBUG) Slog.d(TAG, "setCurInteractor comp=" + comp - + " user=" + userHandle); + if (DEBUG) { + Slog.d(TAG, "setCurInteractor comp=" + comp + " user=" + userHandle); + } } ComponentName findAvailRecognizer(String prefPackage, int userHandle) { @@ -1912,7 +1902,6 @@ public class VoiceInteractionManagerService extends SystemService { pw.println(" mTemporarilyDisabled: " + mTemporarilyDisabled); pw.println(" mCurUser: " + mCurUser); pw.println(" mCurUserSupported: " + mCurUserSupported); - pw.println(" mIgnoredPreCreatedUsers: " + mIgnoredPreCreatedUsers); dumpSupportedUsers(pw, " "); mDbHelper.dump(pw); if (mImpl == null) { @@ -2026,6 +2015,11 @@ public class VoiceInteractionManagerService extends SystemService { List<String> roleHolders = mRm.getRoleHoldersAsUser(roleName, user); + if (DEBUG) { + Slogf.d(TAG, "onRoleHoldersChanged(%s, %s): roleHolders=%s", roleName, user, + roleHolders); + } + // TODO(b/226201975): this method is beling called when a pre-created user is added, // at which point it doesn't have any role holders. But it's not called again when // the actual user is added (i.e., when the pre-created user is converted), so we @@ -2036,9 +2030,9 @@ public class VoiceInteractionManagerService extends SystemService { if (roleHolders.isEmpty()) { UserInfo userInfo = mUserManagerInternal.getUserInfo(user.getIdentifier()); if (userInfo != null && userInfo.preCreated) { - Slogf.d(TAG, "onRoleHoldersChanged(): ignoring pre-created user %s for now", - userInfo.toFullString()); - mIgnoredPreCreatedUsers.add(user); + Slogf.d(TAG, "onRoleHoldersChanged(): ignoring pre-created user %s for now," + + " this method will be called again when it's converted to a real" + + " user", userInfo.toFullString()); return; } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AssistantAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AssistantAppHelper.kt new file mode 100644 index 000000000000..efb92f208bde --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/AssistantAppHelper.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.helpers + +import android.app.Instrumentation +import android.content.ComponentName +import android.provider.Settings +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.testapp.ActivityOptions +import org.junit.Assert.assertNotNull + +class AssistantAppHelper @JvmOverloads constructor( + val instr: Instrumentation, + val component: ComponentName = ActivityOptions.ASSISTANT_SERVICE_COMPONENT_NAME, +) { + protected val uiDevice: UiDevice = UiDevice.getInstance(instr) + protected val defaultAssistant: String? = Settings.Secure.getString( + instr.targetContext.contentResolver, + Settings.Secure.ASSISTANT) + protected val defaultVoiceInteractionService: String? = Settings.Secure.getString( + instr.targetContext.contentResolver, + Settings.Secure.VOICE_INTERACTION_SERVICE) + + fun setDefaultAssistant() { + Settings.Secure.putString( + instr.targetContext.contentResolver, + Settings.Secure.VOICE_INTERACTION_SERVICE, + component.flattenToString()) + Settings.Secure.putString( + instr.targetContext.contentResolver, + Settings.Secure.ASSISTANT, + component.flattenToString()) + } + + fun resetDefaultAssistant() { + Settings.Secure.putString( + instr.targetContext.contentResolver, + Settings.Secure.VOICE_INTERACTION_SERVICE, + defaultVoiceInteractionService) + Settings.Secure.putString( + instr.targetContext.contentResolver, + Settings.Secure.ASSISTANT, + defaultAssistant) + } + + /** + * Open Assistance UI. + * + * @param longpress open the UI by long pressing power button. + * Otherwise open the UI through vioceinteraction shell command directly. + */ + @JvmOverloads + fun openUI(longpress: Boolean = false) { + if (longpress) { + uiDevice.executeShellCommand("input keyevent --longpress KEYCODE_POWER") + } else { + uiDevice.executeShellCommand("cmd voiceinteraction show") + } + val ui = uiDevice.wait( + Until.findObject(By.res(ActivityOptions.FLICKER_APP_PACKAGE, "vis_frame")), + FIND_TIMEOUT) + assertNotNull("Can't find Assistant UI after long pressing power button.", ui) + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt index d11ca4950d16..fa83f227e80d 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt @@ -64,7 +64,7 @@ class GameAppHelper @JvmOverloads constructor( wmHelper: WindowManagerStateHelper, direction: Direction ): Boolean { - val ratioForScreenBottom = 0.97 + val ratioForScreenBottom = 0.99 val fullView = wmHelper.getWindowRegion(component) require(!fullView.isEmpty) { "Target $component view not found." } diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java index 45a47303990c..e90eed15ecfe 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java @@ -80,20 +80,21 @@ public class ActivityOptions { public static final String SHOW_WHEN_LOCKED_ACTIVITY_LAUNCHER_NAME = "ShowWhenLockedApp"; public static final ComponentName SHOW_WHEN_LOCKED_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".ShowWhenLockedActivity"); + new ComponentName(FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".ShowWhenLockedActivity"); public static final String NOTIFICATION_ACTIVITY_LAUNCHER_NAME = "NotificationApp"; public static final ComponentName NOTIFICATION_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".NotificationActivity"); + new ComponentName(FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".NotificationActivity"); public static final String MAIL_ACTIVITY_LAUNCHER_NAME = "MailActivity"; - public static final ComponentName MAIL_ACTIVITY_COMPONENT_NAME = new ComponentName( - FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".MailActivity"); + public static final ComponentName MAIL_ACTIVITY_COMPONENT_NAME = + new ComponentName(FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".MailActivity"); public static final String GAME_ACTIVITY_LAUNCHER_NAME = "GameApp"; public static final ComponentName GAME_ACTIVITY_COMPONENT_NAME = - new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".GameActivity"); + new ComponentName(FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".GameActivity"); + + public static final ComponentName ASSISTANT_SERVICE_COMPONENT_NAME = + new ComponentName( + FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".AssistantInteractionService"); } |