diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index a5aff241237d3..37953653117c5 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -652,7 +652,7 @@ public SharedPreferences getSharedPreferences(File file, int mode) { + "if UserManager is not available. " + "(e.g. from inside an isolated process)"); } - if (!um.isUserUnlockingOrUnlocked(UserHandle.myUserId())) { + if (!um.isUserUnlockingOrUnlocked(getUserId())) { throw new IllegalStateException("SharedPreferences in " + "credential encrypted storage are not available until after " + "user (id " + UserHandle.myUserId() + ") is unlocked"); diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index f08e78c861e35..f68083e28c3bd 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -182,6 +182,7 @@ interface IActivityTaskManager { List getAppTasks(in String callingPackage); void startSystemLockTaskMode(int taskId); void stopSystemLockTaskMode(); + void rebuildSystemLockTaskPinnedMode(); void finishVoiceTask(in IVoiceInteractionSession session); int addAppTask(in IBinder activityToken, in Intent intent, in ActivityManager.TaskDescription description, in Bitmap thumbnail); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 206c62380673f..6dfae8ca4b936 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -3236,8 +3236,8 @@ public void visitUris(@NonNull Consumer visitor) { person.visitUris(visitor); } - final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES, - Parcelable.class); + final Bundle[] messages = + getParcelableArrayFromBundle(extras, EXTRA_MESSAGES, Bundle.class); if (!ArrayUtils.isEmpty(messages)) { for (MessagingStyle.Message message : MessagingStyle.Message .getMessagesFromBundleArray(messages)) { @@ -3245,8 +3245,8 @@ public void visitUris(@NonNull Consumer visitor) { } } - final Parcelable[] historic = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES, - Parcelable.class); + final Parcelable[] historic = + getParcelableArrayFromBundle(extras, EXTRA_HISTORIC_MESSAGES, Bundle.class); if (!ArrayUtils.isEmpty(historic)) { for (MessagingStyle.Message message : MessagingStyle.Message .getMessagesFromBundleArray(historic)) { @@ -8501,8 +8501,8 @@ public boolean showsChronometer() { */ public boolean hasImage() { if (isStyle(MessagingStyle.class) && extras != null) { - final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES, - Parcelable.class); + final Bundle[] messages = + getParcelableArrayFromBundle(extras, EXTRA_MESSAGES, Bundle.class); if (!ArrayUtils.isEmpty(messages)) { for (MessagingStyle.Message m : MessagingStyle.Message .getMessagesFromBundleArray(messages)) { @@ -9794,10 +9794,10 @@ protected void restoreFromExtras(Bundle extras) { mUser = user; } mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE); - Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES, Parcelable.class); + Bundle[] messages = getParcelableArrayFromBundle(extras, EXTRA_MESSAGES, Bundle.class); mMessages = Message.getMessagesFromBundleArray(messages); - Parcelable[] histMessages = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES, - Parcelable.class); + Bundle[] histMessages = getParcelableArrayFromBundle( + extras, EXTRA_HISTORIC_MESSAGES, Bundle.class); mHistoricMessages = Message.getMessagesFromBundleArray(histMessages); mIsGroupConversation = extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION); mUnreadMessageCount = extras.getInt(EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT); diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index 523849e6914ca..f2939324afaa2 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -1782,7 +1782,7 @@ private void appendLibAssetsLocked(@NonNull SharedLibraryAssets libAssets) { final WeakReference weakImplRef = mResourceImpls.valueAt(i); final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null; if (impl == null) { - Slog.w(TAG, "Found a null ResourcesImpl, skipped."); + if (DEBUG) Slog.w(TAG, "Found a null ResourcesImpl, skipped."); continue; } diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java index 7b46db16f80a0..7616d805c31ca 100644 --- a/core/java/android/app/admin/DeviceAdminInfo.java +++ b/core/java/android/app/admin/DeviceAdminInfo.java @@ -473,8 +473,12 @@ public CharSequence loadLabel(PackageManager pm) { */ public CharSequence loadDescription(PackageManager pm) throws NotFoundException { if (mActivityInfo.descriptionRes != 0) { - return pm.getText(mActivityInfo.packageName, + try { + return pm.getText(mActivityInfo.packageName, mActivityInfo.descriptionRes, mActivityInfo.applicationInfo); + } catch (OutOfMemoryError e) { + throw new NotFoundException(); + } } throw new NotFoundException(); } diff --git a/core/java/android/location/Location.java b/core/java/android/location/Location.java index fd3e5a22e969e..fea7bc9315bd7 100644 --- a/core/java/android/location/Location.java +++ b/core/java/android/location/Location.java @@ -28,6 +28,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; +import android.os.SystemProperties; import android.util.Printer; import android.util.TimeUtils; @@ -805,6 +806,13 @@ public void setIsFromMockProvider(boolean isFromMockProvider) { * @see LocationManager#addTestProvider */ public boolean isMock() { + // Check if mock location override is enabled via Settings + boolean overrideMockDetection = SystemProperties.getBoolean( + "persist.sys.override_mock_location", false); + + if (overrideMockDetection) { + return false; + } return (mFieldsMask & HAS_MOCK_PROVIDER_MASK) != 0; } diff --git a/core/java/android/net/NetworkPolicy.java b/core/java/android/net/NetworkPolicy.java index 570211ecc88d0..e5995d3005a58 100644 --- a/core/java/android/net/NetworkPolicy.java +++ b/core/java/android/net/NetworkPolicy.java @@ -103,6 +103,22 @@ public static RecurrenceRule buildRule(int cycleDay, ZoneId cycleTimezone) { } } + public static RecurrenceRule buildWeeklyRule(int cycleDay, ZoneId cycleTimezone) { + if (cycleDay != NetworkPolicy.CYCLE_NONE) { + return RecurrenceRule.buildRecurringWeekly(cycleDay, cycleTimezone); + } else { + return RecurrenceRule.buildNever(); + } + } + + public static RecurrenceRule buildDailyRule(int cycleDay, ZoneId cycleTimezone) { + if (cycleDay != NetworkPolicy.CYCLE_NONE) { + return RecurrenceRule.buildRecurringDaily(cycleDay, cycleTimezone); + } else { + return RecurrenceRule.buildNever(); + } + } + @Deprecated public NetworkPolicy(NetworkTemplate template, int cycleDay, String cycleTimezone, long warningBytes, long limitBytes, boolean metered) { diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java index 12928596efc92..d631bea8158e0 100644 --- a/core/java/android/os/StrictMode.java +++ b/core/java/android/os/StrictMode.java @@ -1559,6 +1559,8 @@ public static void initVmDefaults(ApplicationInfo ai) { if (targetSdkVersion >= Build.VERSION_CODES.N) { builder.detectFileUriExposure(); builder.penaltyDeathOnFileUriExposure(); + builder.detectActivityLeaks(); + builder.detectLeakedRegistrationObjects(); } if (Build.IS_USER || Build.IS_USERDEBUG || DISABLE || SystemProperties.getBoolean(DISABLE_PROPERTY, false)) { @@ -2655,7 +2657,9 @@ private static void clampViolationTimeMap(final @NonNull SparseLongArray violati /** @hide */ public static void onVmPolicyViolation(Violation originStack) { - onVmPolicyViolation(originStack, false); + boolean forceDeath = originStack instanceof IntentReceiverLeakedViolation + || originStack instanceof ServiceConnectionLeakedViolation; + onVmPolicyViolation(originStack, forceDeath); } /** @hide */ diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index fbffe0743a746..fe6d299f69678 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7120,6 +7120,13 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean public static final String STATUSBAR_BATTERY_BAR_BLEND_COLOR_REVERSE = "statusbar_battery_bar_blend_color_reverse"; + /** + * Whether to show remaining charging time on the lockscreen while charging + * @hide + */ + @Readable + public static final String LOCKSCREEN_CHARGING_TIME = "lockscreen_charging_time"; + /** * Enable/disable Bluetooth Battery bar * @hide @@ -7170,6 +7177,11 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String SHOW_FOURG_ICON = "show_fourg_icon"; + /** + * @hide + */ + public static final String DISABLE_STACKED_MOBILE_ICONS = "disable_stacked_mobile_icons"; + /** * @hide */ @@ -7324,6 +7336,11 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String FP_ERROR_VIBRATE = "fp_error_vibrate"; + /** + * @hide + */ + public static final String RECENTS_LOCKED_TASKS = "recents_locked_tasks"; + /** * Whether to show the carrier name on the lockscreen * @hide @@ -7411,12 +7428,32 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String QS_TILE_SHAPE = "qs_tile_shape"; + /** + * @hide + */ + public static final String QS_TILE_STYLE_MINIMAL = "qs_tile_style_minimal"; + + /** + * @hide + */ + public static final String QS_USE_MODIFIED_TILE_SPACING = "qs_use_modified_tile_spacing"; + + /** + * @hide + */ + public static final String QS_TILE_STYLE_MINIMAL_INVERT = "qs_tile_style_minimal_invert"; + /** * Customize Brightness slider shape. * @hide */ public static final String QS_BRIGHTNESS_SLIDER_SHAPE = "qs_brightness_slider_shape"; + /** + * @hide + */ + public static final String QS_BRIGHTNESS_SLIDER_STYLE = "qs_brightness_slider_style"; + /** * Haptic feedback on QS tiles * @hide @@ -7757,6 +7794,91 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean @Readable public static final String BLOCK_WALLPAPER_DIMMING = "block_wallpaper_dimming"; + /** + * What to show at the bottom of the Ambient display + * 0: Nothing. + * 1: Battery Percentage. + * 2: Battery Temperature. + * 3: Battery Percentage & Temperature Together. + * @hide + */ + public static final String AMBIENT_SHOW_SETTINGS = "ambient_show_settings"; + + /** + * Ambient settings show icons + * @hide + */ + public static final String AMBIENT_SHOW_SETTINGS_ICONS = "ambient_show_settings_icons"; + + /** + * Show Settings icon in QS Footer + * @hide + */ + public static final String QS_FOOTER_SHOW_SETTINGS = "qs_footer_show_settings"; + + /** + * Show Edit icon in QS Footer. + * @hide + */ + public static final String QS_FOOTER_SHOW_EDIT = "qs_footer_show_edit"; + + /** + * Show power menu icon in QS Footer. + * @hide + */ + public static final String QS_FOOTER_SHOW_POWER_MENU = "qs_footer_show_power_menu"; + + /** + * Gradient on QS tiles + * @hide + */ + public static final String QS_TILE_GRADIENT = "qs_tile_gradient"; + + /** + * Gradient on QS brightness slider + * @hide + */ + public static final String QS_BRIGHTNESS_SLIDER_GRADIENT = "qs_brightness_slider_gradient"; + + /** + * Gradient on Volume slider + * @hide + */ + public static final String VOLUME_SLIDER_GRADIENT = "volume_slider_gradient"; + + /** + * Gradient color mode + * @hide + */ + public static final String CUSTOM_GRADIENT_COLOR_MODE = "custom_gradient_color_mode"; + + /** + * Gradient start color + * @hide + */ + public static final String CUSTOM_GRADIENT_START_COLOR = "custom_gradient_start_color"; + + /** + * Gradient end color + * @hide + */ + public static final String CUSTOM_GRADIENT_END_COLOR = "custom_gradient_end_color"; + + /** + * @hide + */ + public static final String ONGOING_ACTION_CHIP = "ongoing_action_chip"; + + /** + * @hide + */ + public static final String ONGOING_MEDIA_PROGRESS = "ongoing_media_progress"; + + /** + * @hide + */ + public static final String ONGOING_COMPACT_MODE = "ongoing_compact_mode"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. @@ -14451,20 +14573,6 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val @Readable public static final String KEYBOX_DATA = "keybox_data"; - /** - * Store vboot key. - * @hide - */ - @Readable - public static final String VBOOT_KEY = "vboot_key"; - - /** - * Store vboot hash. - * @hide - */ - @Readable - public static final String VBOOT_HASH = "vboot_hash"; - /** * Whether to show privacy indicator for location * @hide @@ -14578,6 +14686,18 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String GAME_OVERLAY = "game_overlay"; + /** + * Whether to use system accent color for lock screen clock text + * @hide + */ + public static final String CLOCK_TEXT_ACCENT_COLOR = "clock_text_accent_color"; + + /** + * Lock screen clock text opacity (0-100) + * @hide + */ + public static final String CLOCK_TEXT_OPACITY = "clock_text_opacity"; + /** * Whether to show an overlay in the bottom corner of the screen on copying stuff * into the clipboard. @@ -14690,6 +14810,13 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String NOTIFICATION_ROW_TRANSPARENCY_LOCKSCREEN = "notification_row_transparency_lockscreen"; + /** + * Control which apps to hide from other user apps. + * @hide + */ + @Readable + public static final String HIDE_APPLIST = "hide_applist"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/core/java/android/util/RecurrenceRule.java b/core/java/android/util/RecurrenceRule.java index 9ef9c723ca278..fc373ee9c9ce4 100644 --- a/core/java/android/util/RecurrenceRule.java +++ b/core/java/android/util/RecurrenceRule.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.net.ProtocolException; import java.time.Clock; +import java.time.DayOfWeek; import java.time.LocalTime; import java.time.Period; import java.time.ZoneId; @@ -80,6 +81,26 @@ public static RecurrenceRule buildRecurringMonthly(int dayOfMonth, ZoneId zone) return new RecurrenceRule(start, null, Period.ofMonths(1)); } + @UnsupportedAppUsage + public static RecurrenceRule buildRecurringWeekly(int dayOfWeek, ZoneId zone) { + final ZonedDateTime now = ZonedDateTime.now(sClock).withZoneSameInstant(zone); + final DayOfWeek dayNow = now.getDayOfWeek(); + final DayOfWeek dayStart = DayOfWeek.of(dayOfWeek); + final int minusDays = dayNow.getValue() - dayStart.getValue(); + final ZonedDateTime start = ZonedDateTime.of( + now.toLocalDate().minusWeeks(1).minusDays(minusDays), + LocalTime.MIDNIGHT, zone); + return new RecurrenceRule(start, null, Period.ofWeeks(1)); + } + + @UnsupportedAppUsage + public static RecurrenceRule buildRecurringDaily(int hourOfDay, ZoneId zone) { + final ZonedDateTime now = ZonedDateTime.now(sClock).withZoneSameInstant(zone); + final ZonedDateTime start = ZonedDateTime.of(now.toLocalDate(), + LocalTime.MIDNIGHT.plusHours(hourOfDay), zone); + return new RecurrenceRule(start, null, Period.ofDays(1)); + } + private RecurrenceRule(Parcel source) { start = convertZonedDateTime(source.readString()); end = convertZonedDateTime(source.readString()); @@ -168,6 +189,24 @@ public boolean isMonthly() { && period.getDays() == 0; } + @UnsupportedAppUsage + public boolean isWeekly() { + return start != null + && period != null + && period.getYears() == 0 + && period.getMonths() == 0 + && period.getDays() == 7; + } + + @UnsupportedAppUsage + public boolean isDaily() { + return start != null + && period != null + && period.getYears() == 0 + && period.getMonths() == 0 + && period.getDays() == 1; + } + public Iterator> cycleIterator() { if (period != null) { return new RecurringIterator(); diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index 37782ac7f2f23..3393f3d149635 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -44,6 +44,8 @@ import android.util.TimeUtils; import android.view.animation.AnimationUtils; +import com.android.internal.util.ScrollOptimizer; + import java.io.PrintWriter; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; @@ -367,6 +369,8 @@ private Choreographer(Looper looper, int vsyncSource, long layerHandle) { mLastFrameTimeNanos = Long.MIN_VALUE; mFrameIntervalNanos = (long)(1000000000 / getRefreshRate()); + + ScrollOptimizer.setFrameInterval(mFrameIntervalNanos); mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1]; for (int i = 0; i <= CALLBACK_LAST; i++) { @@ -872,7 +876,7 @@ public long getLatestExpectedPresentTimeNanos() { private void scheduleFrameLocked(long now) { if (!mFrameScheduled) { mFrameScheduled = true; - if (USE_VSYNC) { + if (ScrollOptimizer.shouldUseVsync()) { if (DEBUG_FRAMES) { Log.d(TAG, "Scheduling next frame on vsync."); } @@ -888,6 +892,7 @@ private void scheduleFrameLocked(long now) { mHandler.sendMessageAtFrontOfQueue(msg); } } else { + sFrameDelay = ScrollOptimizer.getFrameDelay(); final long nextFrameTime = Math.max( mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now); if (DEBUG_FRAMES) { @@ -1144,6 +1149,13 @@ void doFrame(long frameTimeNanos, int frame, mLastVsyncEventData.copyFrom(vsyncEventData); } + if (frameIntervalNanos > 0 && (Math.abs(frameIntervalNanos - mFrameIntervalNanos) + > TimeUtils.NANOS_PER_MS)) { + mFrameIntervalNanos = frameIntervalNanos; + ScrollOptimizer.setFrameInterval(mFrameIntervalNanos); + } + ScrollOptimizer.setUITaskStatus(true); + if (resynced && Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { String message = String.format("Choreographer#doFrame - resynced to %d in %.1fms", timeline.mVsyncId, (timeline.mDeadlineNanos - startNanos) * 0.000001f); @@ -1164,6 +1176,7 @@ void doFrame(long frameTimeNanos, int frame, doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameIntervalNanos); doCallbacks(Choreographer.CALLBACK_COMMIT, frameIntervalNanos); + ScrollOptimizer.setUITaskStatus(false); } finally { AnimationUtils.unlockAnimationClock(); mInDoFrameCallback = false; @@ -1599,6 +1612,7 @@ public void onVsync(long timestampNanos, long physicalDisplayId, int frame, mTimestampNanos = timestampNanos; mFrame = frame; mLastVsyncEventData.copyFrom(vsyncEventData); + ScrollOptimizer.setVsyncTime(mTimestampNanos); Message msg = Message.obtain(mHandler, this); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java index c26506eafe998..3242716bd8b9c 100644 --- a/core/java/android/view/SurfaceControlViewHost.java +++ b/core/java/android/view/SurfaceControlViewHost.java @@ -511,8 +511,10 @@ public void setView(@NonNull View view, @NonNull WindowManager.LayoutParams attr attrs.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; addWindowToken(attrs); view.setLayoutParams(attrs); - mViewRoot.setView(view, attrs, null); - mViewRoot.setBackKeyCallbackForWindowlessWindow(mWm::forwardBackKeyToParent); + if (mViewRoot.mDisplay != null) { + mViewRoot.setView(view, attrs, null); + mViewRoot.setBackKeyCallbackForWindowlessWindow(mWm::forwardBackKeyToParent); + } } /** diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c0e9e15f34314..66d15d3d466a3 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -297,6 +297,7 @@ import com.android.internal.policy.PhoneFallbackEventHandler; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.FastPrintWriter; +import com.android.internal.util.ScrollOptimizer; import com.android.internal.view.BaseSurfaceHolder; import com.android.internal.view.RootViewSurfaceTaker; import com.android.internal.view.SurfaceCallbackHelper; @@ -1558,8 +1559,12 @@ public void setView(View view, WindowManager.LayoutParams attrs, View panelParen attrs.setSurfaceInsets(view, false /*manual*/, true /*preservePrevious*/); } - CompatibilityInfo compatibilityInfo = - mDisplay.getDisplayAdjustments().getCompatibilityInfo(); + CompatibilityInfo compatibilityInfo; + if (mDisplay != null) { + compatibilityInfo = mDisplay.getDisplayAdjustments().getCompatibilityInfo(); + } else { + compatibilityInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; + } mTranslator = compatibilityInfo.getTranslator(); // If the application owns the surface, don't enable hardware acceleration @@ -2864,6 +2869,7 @@ void updateBlastSurfaceIfNeeded() { mBlastBufferQueue.destroy(); } mBlastBufferQueue = new BLASTBufferQueue(mTag, true /* updateDestinationFrame */); + ScrollOptimizer.setBLASTBufferQueue(mBlastBufferQueue); // If we create and destroy BBQ without recreating the SurfaceControl, we can end up // queuing buffers on multiple apply tokens causing out of order buffer submissions. We // fix this by setting the same apply token on all BBQs created by this VRI. @@ -6769,7 +6775,9 @@ private void performConfigurationChange(@NonNull MergedConfiguration mergedConfi + ", globalConfig: " + globalConfig + ", overrideConfig: " + overrideConfig); - final CompatibilityInfo ci = mDisplay.getDisplayAdjustments().getCompatibilityInfo(); + final CompatibilityInfo ci = mDisplay != null + ? mDisplay.getDisplayAdjustments().getCompatibilityInfo() + : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; if (!ci.equals(CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO)) { globalConfig = new Configuration(globalConfig); overrideConfig = new Configuration(overrideConfig); @@ -10644,6 +10652,7 @@ private void scheduleProcessInputEvents() { } void doProcessInputEvents() { + ScrollOptimizer.setBLASTBufferQueue(mBlastBufferQueue); // Deliver all pending input events in the queue. while (mPendingInputEventHead != null) { QueuedInputEvent q = mPendingInputEventHead; @@ -10658,6 +10667,10 @@ void doProcessInputEvents() { mPendingInputEventCount); mViewFrameInfo.setInputEvent(mInputEventAssigner.processEvent(q.mEvent)); + + if (q.mEvent instanceof MotionEvent) { + ScrollOptimizer.setMotionType(((MotionEvent)q.mEvent).getActionMasked()); + } deliverInputEvent(q); } diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index e563d1781b45a..e46cd9b9a3eef 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -59,6 +59,7 @@ import com.android.internal.os.ApplicationSharedMemory; import com.android.internal.policy.PhoneWindow; import com.android.internal.util.FastPrintWriter; +import com.android.internal.util.ScrollOptimizer; import java.io.FileDescriptor; import java.io.FileOutputStream; @@ -385,6 +386,36 @@ public View getRootView(String name) { return null; } + private int getVisibleRootCount (ArrayList roots) { + int visibleRootCount = 0; + int lastLeft = -1; + int lastTop = -1; + int lastWidth = 0; + int lastHeight = 0; + for (int i = roots.size() - 1; i >= 0; --i) { + View root_view = roots.get(i).getView(); + if (root_view != null && root_view.getVisibility() == View.VISIBLE) { + int left = root_view.getLeft(); + int top = root_view.getTop(); + int width = root_view.getRight() - root_view.getLeft() ; + int height = root_view.getBottom() - root_view.getTop() ; + // Filter the invalid visible views. + if (width != 0 && height != 0) { + // Filter the overwritten visible views. + if (lastWidth != width || lastHeight != height || + lastLeft != left || lastTop != top ) { + visibleRootCount++; + } + lastLeft = left; + lastTop = top; + lastWidth = width; + lastHeight = height; + } + } + } + return visibleRootCount; + } + public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow, int userId) { if (view == null) { @@ -491,6 +522,12 @@ public void addView(View view, ViewGroup.LayoutParams params, view.setLayoutParams(wparams); + int visibleRootCount = 0; + visibleRootCount = getVisibleRootCount(mRoots); + if (visibleRootCount > 1) { + ScrollOptimizer.disableOptimizer(true); + } + mViews.add(view); mRoots.add(root); mParams.add(wparams); @@ -623,6 +660,18 @@ void doRemoveView(ViewRootImpl root) { final View view = mViews.remove(index); mDyingViews.remove(view); } + // The visibleRootCount more than one means multi-layer, and multi-layer rendering + // can result in unexpected pending between UI thread and render thread with + // pre-rendering enabled. Need to disable pre-rendering for multi-layer cases. + int visibleRootCount = 0; + visibleRootCount = getVisibleRootCount(mRoots); + + if (visibleRootCount > 1) { + ScrollOptimizer.disableOptimizer(true); + } else if (visibleRootCount == 1) { + ScrollOptimizer.disableOptimizer(false); + } + allViewsRemoved = mRoots.isEmpty(); mWindowViewsListenerGroup.accept(getWindowViews()); diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java index 8ecd57179bad2..1ea19e8e68044 100644 --- a/core/java/android/view/animation/AnimationUtils.java +++ b/core/java/android/view/animation/AnimationUtils.java @@ -30,13 +30,17 @@ import android.content.res.Resources.Theme; import android.content.res.XmlResourceParser; import android.os.SystemClock; +import android.os.SystemProperties; import android.ravenwood.annotation.RavenwoodIgnore; import android.ravenwood.annotation.RavenwoodKeepPartialClass; import android.util.AttributeSet; +import android.util.DisplayMetrics; import android.util.TimeUtils; import android.util.Xml; import android.view.InflateException; +import com.android.internal.R; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -226,6 +230,20 @@ public static long getExpectedPresentationTimeMillis() { public static Animation loadAnimation(Context context, @AnimRes int id) throws NotFoundException { + if (SystemProperties.getBoolean("persist.sys.activity_anim_perf_override", false)) { + ActivityAnimations.maybeInit(context); + switch (id) { + case R.anim.activity_open_enter: + return ActivityAnimations.getOpenEnter(); + case R.anim.activity_open_exit: + return ActivityAnimations.getOpenExit(); + case R.anim.activity_close_enter: + return ActivityAnimations.getCloseEnter(); + case R.anim.activity_close_exit: + return ActivityAnimations.getCloseExit(); + } + } + XmlResourceParser parser = null; try { parser = context.getResources().getAnimation(id); @@ -506,4 +524,100 @@ private static Interpolator createInterpolatorFromXml( } return interpolator; } + + /** @hide */ + public final class ActivityAnimations { + + private static Animation sOpenEnter; + private static Animation sOpenExit; + private static Animation sCloseEnter; + private static Animation sCloseExit; + + private static Interpolator sFastOutExtraSlowInInterpolator; + + private static final float DISTANCE = 0.1f; + + private ActivityAnimations() {} + + /** @hide */ + public static void maybeInit(Context context) { + if (sFastOutExtraSlowInInterpolator == null) { + sFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator( + context, R.interpolator.fast_out_extra_slow_in); + } + } + + private static class ActivityAnimFactory { + private float fromX = 0f, toX = 0f; + private long duration = 200L; + + public ActivityAnimFactory fromX(float ratio) { + this.fromX = ratio; + return this; + } + + public ActivityAnimFactory toX(float ratio) { + this.toX = ratio; + return this; + } + + public Animation build() { + AnimationSet animationSet = new AnimationSet(false); + TranslateAnimation slide = new TranslateAnimation( + Animation.RELATIVE_TO_SELF, fromX, + Animation.RELATIVE_TO_SELF, toX, + Animation.RELATIVE_TO_SELF, 0f, + Animation.RELATIVE_TO_SELF, 0f + ); + slide.setDuration(duration); + slide.setInterpolator(sFastOutExtraSlowInInterpolator); + animationSet.addAnimation(slide); + return animationSet; + } + } + + /** @hide */ + public static Animation getOpenEnter() { + if (sOpenEnter == null) { + sOpenEnter = new ActivityAnimFactory() + .fromX(1.0f) + .toX(0.0f) + .build(); + } + return sOpenEnter; + } + + /** @hide */ + public static Animation getOpenExit() { + if (sOpenExit == null) { + sOpenExit = new ActivityAnimFactory() + .fromX(0.0f) + .toX(-DISTANCE) + .build(); + } + return sOpenExit; + } + + /** @hide */ + public static Animation getCloseEnter() { + if (sCloseEnter == null) { + sCloseEnter = new ActivityAnimFactory() + .fromX(-DISTANCE) + .toX(0.0f) + .build(); + } + return sCloseEnter; + } + + /** @hide */ + public static Animation getCloseExit() { + if (sCloseExit == null) { + sCloseExit = new ActivityAnimFactory() + .fromX(0.0f) + .toX(1.0f) + .build(); + } + return sCloseExit; + } + } } diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index fe5afe437834d..b679da84c24d1 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -44,6 +44,7 @@ import com.android.internal.inputmethod.IRemoteComputerControlInputConnection; import com.android.internal.inputmethod.IRemoteInputConnection; import com.android.internal.inputmethod.InputMethodInfoSafeList; +import com.android.internal.inputmethod.InputMethodSubtypeSafeList; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; @@ -250,8 +251,9 @@ static List getEnabledInputMethodSubtypeList(@Nullable Strin return new ArrayList<>(); } try { - return service.getEnabledInputMethodSubtypeList(imiId, - allowsImplicitlyEnabledSubtypes, userId); + return InputMethodSubtypeSafeList.extractFrom( + service.getEnabledInputMethodSubtypeList(imiId, + allowsImplicitlyEnabledSubtypes, userId)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index c9485d7d3b0f4..184fea5be0f57 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -50,6 +50,8 @@ import android.util.Xml; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; +import com.android.internal.annotations.VisibleForTesting; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -313,68 +315,70 @@ public InputMethodInfo(Context context, ResolveInfo service, "Meta-data does not start with input-method tag"); } - TypedArray sa = res.obtainAttributes(attrs, - com.android.internal.R.styleable.InputMethod); - settingsActivityComponent = sa.getString( - com.android.internal.R.styleable.InputMethod_settingsActivity); - languageSettingsActivityComponent = sa.getString( - com.android.internal.R.styleable.InputMethod_languageSettingsActivity); - if ((si.name != null && si.name.length() > COMPONENT_NAME_MAX_LENGTH) - || (settingsActivityComponent != null - && settingsActivityComponent.length() - > COMPONENT_NAME_MAX_LENGTH) - || (languageSettingsActivityComponent != null - && languageSettingsActivityComponent.length() - > COMPONENT_NAME_MAX_LENGTH)) { - throw new XmlPullParserException( - "Activity name exceeds maximum of 1000 characters"); + final MetadataReadBytesTracker readTracker = new MetadataReadBytesTracker(); + try (TypedArrayWrapper sa = TypedArrayWrapper.createForMethod( + res.obtainAttributes(attrs, com.android.internal.R.styleable.InputMethod), + readTracker)) { + settingsActivityComponent = sa.getString( + com.android.internal.R.styleable.InputMethod_settingsActivity); + languageSettingsActivityComponent = sa.getString( + com.android.internal.R.styleable.InputMethod_languageSettingsActivity); + isVrOnly = sa.getBoolean(com.android.internal.R.styleable.InputMethod_isVrOnly, + false); + isVirtualDeviceOnly = sa.getBoolean( + com.android.internal.R.styleable.InputMethod_isVirtualDeviceOnly, false); + isDefaultResId = sa.getResourceId( + com.android.internal.R.styleable.InputMethod_isDefault, 0); + supportsSwitchingToNextInputMethod = sa.getBoolean( + com.android.internal.R.styleable + .InputMethod_supportsSwitchingToNextInputMethod, + false); + inlineSuggestionsEnabled = sa.getBoolean( + com.android.internal.R.styleable.InputMethod_supportsInlineSuggestions, + false); + supportsInlineSuggestionsWithTouchExploration = sa.getBoolean( + com.android.internal.R.styleable + .InputMethod_supportsInlineSuggestionsWithTouchExploration, false); + suppressesSpellChecker = sa.getBoolean( + com.android.internal.R.styleable.InputMethod_suppressesSpellChecker, false); + showInInputMethodPicker = sa.getBoolean( + com.android.internal.R.styleable.InputMethod_showInInputMethodPicker, true); + mHandledConfigChanges = sa.getInt( + com.android.internal.R.styleable.InputMethod_configChanges, 0); + mSupportsStylusHandwriting = sa.getBoolean( + com.android.internal.R.styleable.InputMethod_supportsStylusHandwriting, + false); + mSupportsConnectionlessStylusHandwriting = sa.getBoolean( + com.android.internal.R.styleable + .InputMethod_supportsConnectionlessStylusHandwriting, false); + stylusHandwritingSettingsActivity = sa.getString( + com.android.internal.R.styleable + .InputMethod_stylusHandwritingSettingsActivity); } - isVrOnly = sa.getBoolean(com.android.internal.R.styleable.InputMethod_isVrOnly, false); - isVirtualDeviceOnly = sa.getBoolean( - com.android.internal.R.styleable.InputMethod_isVirtualDeviceOnly, false); - isDefaultResId = sa.getResourceId( - com.android.internal.R.styleable.InputMethod_isDefault, 0); - supportsSwitchingToNextInputMethod = sa.getBoolean( - com.android.internal.R.styleable.InputMethod_supportsSwitchingToNextInputMethod, - false); - inlineSuggestionsEnabled = sa.getBoolean( - com.android.internal.R.styleable.InputMethod_supportsInlineSuggestions, false); - supportsInlineSuggestionsWithTouchExploration = sa.getBoolean( - com.android.internal.R.styleable - .InputMethod_supportsInlineSuggestionsWithTouchExploration, false); - suppressesSpellChecker = sa.getBoolean( - com.android.internal.R.styleable.InputMethod_suppressesSpellChecker, false); - showInInputMethodPicker = sa.getBoolean( - com.android.internal.R.styleable.InputMethod_showInInputMethodPicker, true); - mHandledConfigChanges = sa.getInt( - com.android.internal.R.styleable.InputMethod_configChanges, 0); - mSupportsStylusHandwriting = sa.getBoolean( - com.android.internal.R.styleable.InputMethod_supportsStylusHandwriting, false); - mSupportsConnectionlessStylusHandwriting = sa.getBoolean( - com.android.internal.R.styleable - .InputMethod_supportsConnectionlessStylusHandwriting, false); - stylusHandwritingSettingsActivity = sa.getString( - com.android.internal.R.styleable.InputMethod_stylusHandwritingSettingsActivity); - sa.recycle(); - final int depth = parser.getDepth(); // Parse all subtypes while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { - if (type == XmlPullParser.START_TAG) { - nodeName = parser.getName(); - if (!"subtype".equals(nodeName)) { - throw new XmlPullParserException( - "Meta-data in input-method does not start with subtype tag"); - } - final TypedArray a = res.obtainAttributes( - attrs, com.android.internal.R.styleable.InputMethod_Subtype); + if (type != XmlPullParser.START_TAG) { + continue; + } + nodeName = parser.getName(); + if (!"subtype".equals(nodeName)) { + throw new XmlPullParserException( + "Meta-data in input-method does not start with subtype tag"); + } + + final InputMethodSubtype subtype; + try (TypedArrayWrapper a = TypedArrayWrapper.createForSubtype( + res.obtainAttributes(attrs, + com.android.internal.R.styleable.InputMethod_Subtype), + readTracker)) { String pkLanguageTag = a.getString(com.android.internal.R.styleable .InputMethod_Subtype_physicalKeyboardHintLanguageTag); String pkLayoutType = a.getString(com.android.internal.R.styleable .InputMethod_Subtype_physicalKeyboardHintLayoutType); - final InputMethodSubtype subtype = new InputMethodSubtypeBuilder() + subtype = new InputMethodSubtypeBuilder() .setSubtypeNameResId(a.getResourceId(com.android.internal.R.styleable .InputMethod_Subtype_label, 0)) .setSubtypeIconResId(a.getResourceId(com.android.internal.R.styleable @@ -399,12 +403,11 @@ public InputMethodInfo(Context context, ResolveInfo service, .InputMethod_Subtype_subtypeId, 0 /* use Arrays.hashCode */)) .setIsAsciiCapable(a.getBoolean(com.android.internal.R.styleable .InputMethod_Subtype_isAsciiCapable, false)).build(); - a.recycle(); - if (!subtype.isAuxiliary()) { - isAuxIme = false; - } - subtypes.add(subtype); } + if (!subtype.isAuxiliary()) { + isAuxIme = false; + } + subtypes.add(subtype); } } catch (NameNotFoundException | IndexOutOfBoundsException | NumberFormatException e) { throw new XmlPullParserException( @@ -468,6 +471,11 @@ private static void validateXmlMetaData(@NonNull ServiceInfo si, @NonNull Resour return; } + if (si.name != null && si.name.length() > COMPONENT_NAME_MAX_LENGTH) { + throw new XmlPullParserException( + "Input method name exceeds " + COMPONENT_NAME_MAX_LENGTH + " characters"); + } + // Validate file size using InputStream.skip() long totalBytesSkipped = 0; // Loop to ensure we skip the required number of bytes, as a single @@ -1128,4 +1136,162 @@ public InputMethodInfo[] newArray(int size) { public int describeContents() { return 0; } + + /** + * A wrapper class for {@link TypedArray} that enforces limits on the size of the metadata + * read from the XML. Methods throw an {@link XmlPullParserException} if the limit is surpassed. + * + *

This class works in conjunction with {@link MetadataReadBytesTracker} to: + *

    + *
  • Limit the length of individual string attributes. For + * {@code settingsActivity} and {@code languageSettingsActivity}, the maximum length is + * {@link #COMPONENT_NAME_MAX_LENGTH}. For other string attributes, the maximum length is + * {@link #STRING_ATTRIBUTES_MAX_CHAR_LENGTH}.
  • + *
  • Track the total amount of data read from the metadata XML. The + * {@link MetadataReadBytesTracker} ensures that the cumulative size of all attributes + * does not exceed {@link #MAX_METADATA_SIZE_BYTES}. + *
+ * + * @hide + */ + @VisibleForTesting + public static final class TypedArrayWrapper implements AutoCloseable { + /** The underlying {@link TypedArray} to read from. */ + @NonNull + private final TypedArray mTypedArray; + /** Tracker for enforcing metadata size limits. */ + @NonNull + private final MetadataReadBytesTracker mReadTracker; + /** {@code true} if parsing a {@code } tag, {@code false} otherwise. */ + private final boolean mIsReadingSubtype; + + /** + * Creates a {@link TypedArrayWrapper} for parsing attributes of the main + * {@code } tag. + * + * @param wrapped The {@link TypedArray} obtained for the {@code } tag. + * @param readTracker The tracker for monitoring data size. + * @return A new {@link TypedArrayWrapper} instance. + */ + @NonNull + @VisibleForTesting + public static TypedArrayWrapper createForMethod( + @NonNull TypedArray wrapped, @NonNull MetadataReadBytesTracker readTracker) { + return new TypedArrayWrapper(wrapped, readTracker, false); + } + + /** + * Creates a {@link TypedArrayWrapper} for parsing attributes of a {@code } tag. + * + * @param wrapped The {@link TypedArray} obtained for the {@code } tag. + * @param readTracker The tracker for monitoring data size. + * @return A new {@link TypedArrayWrapper} instance. + */ + @NonNull + @VisibleForTesting + public static TypedArrayWrapper createForSubtype( + @NonNull TypedArray wrapped, @NonNull MetadataReadBytesTracker readTracker) { + return new TypedArrayWrapper(wrapped, readTracker, true); + } + + /** + * Constructs a new wrapper. + */ + private TypedArrayWrapper(@NonNull TypedArray wrapped, + @NonNull MetadataReadBytesTracker readTracker, boolean isReadingSubtype) { + mTypedArray = wrapped; + mReadTracker = readTracker; + mIsReadingSubtype = isReadingSubtype; + } + + /** Retrieves an integer value for the attribute at {@code index}. */ + @VisibleForTesting + public int getInt(int index, int defaultValue) throws XmlPullParserException { + if (!mTypedArray.hasValue(index)) { + return defaultValue; + } + final int ret = mTypedArray.getInt(index, defaultValue); + mReadTracker.onReadBytes(Integer.BYTES); + return ret; + } + + /** Retrieves the string value for the attribute at {@code index}. */ + @VisibleForTesting + public String getString(int index) throws XmlPullParserException { + final String ret = mTypedArray.getString(index); + final int maxLen = getMaxLength(index); + if (ret != null && ret.length() > maxLen) { + throw new XmlPullParserException( + "String resources in input method exceed the length limit of " + + maxLen + " characters"); + } + mReadTracker.onReadBytes(ret == null ? 0 : ret.length() * Character.BYTES); + return ret; + } + + /** Retrieves a boolean value for the attribute at {@code index}. */ + @VisibleForTesting + public boolean getBoolean(int index, boolean defaultValue) throws XmlPullParserException { + if (!mTypedArray.hasValue(index)) { + return defaultValue; + } + final boolean ret = mTypedArray.getBoolean(index, defaultValue); + mReadTracker.onReadBytes(1); + return ret; + } + + /** Retrieves a resource identifier for the attribute at {@code index}. */ + @VisibleForTesting + public int getResourceId(int index, int defaultValue) throws XmlPullParserException { + if (!mTypedArray.hasValue(index)) { + return defaultValue; + } + final int ret = mTypedArray.getResourceId(index, defaultValue); + mReadTracker.onReadBytes(Integer.BYTES); + return ret; + } + + @Override + public void close() { + mTypedArray.recycle(); + } + + private int getMaxLength(int index) { + // Note that the Android resource has limit DEFAULT_MAX_STRING_ATTR_LENGTH = 32_768. + if (mIsReadingSubtype) { + // No limits for strings in subtype for now. + return Integer.MAX_VALUE; + } else { + return switch (index) { + // TODO(b/456008595): Consider to add + // InputMethod_stylusHandwritingSettingsActivity + case com.android.internal.R.styleable.InputMethod_settingsActivity, + com.android.internal.R.styleable.InputMethod_languageSettingsActivity -> + COMPONENT_NAME_MAX_LENGTH; + default -> + // TODO(b/456008595): Consider to introduce limits. + Integer.MAX_VALUE; + }; + } + } + } + + /** @hide */ + @VisibleForTesting + public static final class MetadataReadBytesTracker { + private int mRemainingBytes = MAX_METADATA_SIZE_BYTES; + + @VisibleForTesting + public MetadataReadBytesTracker() { + } + + private void onReadBytes(int bytes) throws XmlPullParserException { + mRemainingBytes -= bytes; + if (mRemainingBytes < 0) { + throw new XmlPullParserException( + "The input method service has metadata exceeds the " + + MAX_METADATA_SIZE_BYTES + " byte limit"); + } + } + } } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 61ade5fdad8b2..49c1c2f3535db 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -713,6 +713,7 @@ public abstract class AbsListView extends AdapterView implements Te private int mMinimumVelocity; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124051740) private int mMaximumVelocity; + private int mDecacheThreshold; private float mVelocityScale = 1.0f; final boolean[] mIsScrap = new boolean[1]; @@ -1029,6 +1030,7 @@ private void initAbsListView() { mVerticalScrollFactor = configuration.getScaledVerticalScrollFactor(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mDecacheThreshold = mMaximumVelocity / 2; mOverscrollDistance = configuration.getScaledOverscrollDistance(); mOverflingDistance = configuration.getScaledOverflingDistance(); @@ -4278,6 +4280,10 @@ private void onTouchUp(MotionEvent ev) { } mSelector.setHotspot(x, ev.getY()); } + if (!mDataChanged && !mIsDetaching + && isAttachedToWindow()) { + performClick.run(); + } if (mTouchModeReset != null) { removeCallbacks(mTouchModeReset); } @@ -4288,10 +4294,6 @@ public void run() { mTouchMode = TOUCH_MODE_REST; child.setPressed(false); setPressed(false); - if (!mDataChanged && !mIsDetaching - && isAttachedToWindow()) { - performClick.run(); - } } }; postDelayed(mTouchModeReset, @@ -4997,7 +4999,7 @@ public void run() { // Keep the fling alive a little longer postDelayed(this, FLYWHEEL_TIMEOUT); } else { - endFling(); + endFling(false); // Don't disable the scrolling cache right after it was enabled mTouchMode = TOUCH_MODE_SCROLL; reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } @@ -5017,6 +5019,11 @@ float getSplineFlingDistance(int velocity) { // Use AbsListView#fling(int) instead @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) void start(int initialVelocity) { + if (Math.abs(initialVelocity) > mDecacheThreshold) { + // For long flings, scrolling cache causes stutter, so don't use it + clearScrollingCache(); + } + int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0; mLastFlingY = initialY; mScroller.setInterpolator(null); @@ -5097,6 +5104,10 @@ void startScroll(int distance, int duration, boolean linear, // To interrupt a fling early you should use smoothScrollBy(0,0) instead @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) void endFling() { + endFling(true); + } + + void endFling(boolean clearCache) { mTouchMode = TOUCH_MODE_REST; removeCallbacks(this); @@ -5105,7 +5116,8 @@ void endFling() { if (!mSuppressIdleStateChangeCall) { reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } - clearScrollingCache(); + if (clearCache) + clearScrollingCache(); mScroller.abortAnimation(); if (mFlingStrictSpan != null) { diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java index df209fb876bab..6f31d4186da84 100644 --- a/core/java/android/widget/OverScroller.java +++ b/core/java/android/widget/OverScroller.java @@ -24,8 +24,122 @@ import android.util.Log; import android.view.ViewConfiguration; import android.view.animation.AnimationUtils; +import android.view.animation.BaseInterpolator; import android.view.animation.Interpolator; +import com.android.internal.util.ScrollOptimizer; + +/** + * @hide + */ +class AXUIInterpolator extends BaseInterpolator { + private static final double DEFAULT_STIFFNESS = 40.0; + private static final double DEFAULT_DAMPING_RATIO = 1.15; + private static final float DEFAULT_MAX_VELOCITY = 15000.0f; + + private final double mOmega; + private final double mDampingRatio; + private final double mInitialVelocityNorm; + private final float mDurationScale; + private final double mDampedFreq; + private final double mCoefficient; + private float mNormalizationFactor = -1.0f; + + public AXUIInterpolator(double stiffness, double dampingRatio, double velocity, + float durationScale, float maxVelocity) { + mOmega = Math.sqrt(stiffness <= 0.0 ? DEFAULT_STIFFNESS : stiffness); + mDampingRatio = dampingRatio <= 0.0 ? DEFAULT_DAMPING_RATIO : dampingRatio; + mInitialVelocityNorm = Math.abs(velocity) / (maxVelocity <= 0.0f ? DEFAULT_MAX_VELOCITY : maxVelocity); + mDurationScale = durationScale <= 0.0f ? 1.0f : durationScale; + + if (mDampingRatio < 1.0) { + // Underdamped + mDampedFreq = Math.sqrt(1.0 - (mDampingRatio * mDampingRatio)) * mOmega; + mCoefficient = ((mDampingRatio * mOmega) - mInitialVelocityNorm) / mDampedFreq; + } else if (Double.compare(1.0, mDampingRatio) == 0) { + // Critically damped + mDampedFreq = 0.0; + mCoefficient = (-mInitialVelocityNorm) + mOmega; + } else { + // Overdamped + mDampedFreq = 0.0; + mCoefficient = (-mInitialVelocityNorm) + (mDampingRatio * mOmega); + } + } + + private float calculatePosition(float t) { + if (t < 0.0f) t = 0.0f; + double time = t * mDurationScale; + double expTerm = Math.exp((-mDampingRatio) * mOmega * time); + double displacement; + + if (mDampingRatio < 1.0) { + // Underdamped oscillation + displacement = (Math.cos(mDampedFreq * time) + + (mCoefficient * Math.sin(mDampedFreq * time))) * expTerm; + } else if (Double.compare(1.0, mDampingRatio) == 0) { + // Critically damped + displacement = ((mCoefficient * time) + 1.0) * Math.exp((-mOmega) * time); + } else { + // Overdamped + double sqrtTerm = mOmega * Math.sqrt((mDampingRatio * mDampingRatio) - 1.0); + displacement = (expTerm / sqrtTerm) * + (((-mInitialVelocityNorm + (mOmega * mDampingRatio)) * + Math.sinh(mDampingRatio * time)) + + (Math.cosh(mDampingRatio * time) * sqrtTerm)); + } + return (float) (1.0 - displacement); + } + + @Override + public float getInterpolation(float input) { + if (mNormalizationFactor == -1.0f) { + float endValue = calculatePosition(1.0f); + mNormalizationFactor = endValue != 0.0f ? endValue : 1.0f; + } + return calculatePosition(input) / mNormalizationFactor; + } + + /** + */ + public float getVelocityRatio(float t) { + double time = t >= 0.0f ? t : 0.0f; + double expTerm = Math.exp((-mDurationScale) * mDampingRatio * mOmega * time); + double velocityMagnitude; + + if (mDampingRatio < 1.0) { + double sinTerm = Math.sin(mDurationScale * mDampedFreq * time); + double cosTerm = Math.cos(mDurationScale * mDampedFreq * time); + velocityMagnitude = Math.abs( + ((sinTerm * (-mDurationScale) * + ((mCoefficient * mDampingRatio * mOmega) + mDampedFreq)) + + (cosTerm * mDurationScale * + ((mCoefficient * mDampedFreq) - (mDampingRatio * mOmega)))) * expTerm); + } else if (Double.compare(1.0, mDampingRatio) != 0) { + // Overdamped + double sqrtTerm = mOmega * Math.sqrt((mDampingRatio * mDampingRatio) - 1.0); + double sqrtTermSq = sqrtTerm * sqrtTerm; + velocityMagnitude = Math.abs((expTerm / sqrtTerm) * + ((Math.sinh(mDurationScale * mDampingRatio * time) * mDurationScale * + ((sqrtTermSq + (mInitialVelocityNorm * mDampingRatio * mOmega)) - + ((mDampingRatio * mDampingRatio) * mOmega * mOmega))) + + (Math.cosh(mDurationScale * mDampingRatio * time) * mDurationScale * + mDampingRatio * (((mDampingRatio * mOmega) - mInitialVelocityNorm) - + (mOmega * sqrtTerm))))); + } else { + // Critically damped + velocityMagnitude = Math.abs(mDurationScale * + ((mCoefficient - mOmega) - (((mCoefficient * mDurationScale) * mOmega) * time)) * + Math.exp((-mDurationScale) * mOmega * time)); + } + return (float) velocityMagnitude; + } + + public float getDurationScale() { + return mDurationScale; + } +} + /** * This class encapsulates scrolling with the ability to overshoot the bounds * of a scrolling operation. This class is a drop-in replacement for @@ -163,6 +277,9 @@ public final boolean isFinished() { */ public final void forceFinished(boolean finished) { mScrollerX.mFinished = mScrollerY.mFinished = finished; + if (finished && mMode == FLING_MODE) { + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_END); + } } /** @@ -287,6 +404,9 @@ public void setFinalY(int newY) { */ public boolean computeScrollOffset() { if (isFinished()) { + if (mMode == FLING_MODE) { + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_END); + } return false; } @@ -326,6 +446,10 @@ public boolean computeScrollOffset() { } } + if (isFinished()) { + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_END); + } + break; } @@ -364,6 +488,7 @@ public void startScroll(int startX, int startY, int dx, int dy) { * @param duration Duration of the scroll in milliseconds. */ public void startScroll(int startX, int startY, int dx, int dy, int duration) { + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_END); mMode = SCROLL_MODE; mScrollerX.startScroll(startX, dx, duration); mScrollerY.startScroll(startY, dy, duration); @@ -434,6 +559,8 @@ public void fling(int startX, int startY, int velocityX, int velocityY, velocityY += oldVelocityY; } } + + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_START); mMode = FLING_MODE; mScrollerX.fling(startX, velocityX, minX, maxX, overX); @@ -502,6 +629,9 @@ public boolean isOverScrolled() { * @see #forceFinished(boolean) */ public void abortAnimation() { + if (mMode == FLING_MODE) { + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_END); + } mScrollerX.finish(); mScrollerY.finish(); } @@ -599,6 +729,101 @@ static class SplineOverScroller { private static final int CUBIC = 1; private static final int BALLISTIC = 2; + private static final double AX_STIFFNESS = 40.0; + private static final double AX_DAMPING_RATIO = 1.15; + private static final float AX_MAX_VELOCITY = 15000.0f; + private static final double AX_FRICTION_NORMAL = 14.0; + private static final double AX_FRICTION_SLOW = 12.0; + private static final double AX_FRICTION_MID = 16.0; + + private static final double VELOCITY_THRESHOLD_LOW = 5000.0; + private static final double VELOCITY_THRESHOLD_HIGH = 8000.0; + private static final double FRICTION_COEF_1 = 0.35; + private static final double FRICTION_COEF_2 = 0.2; + private static final double FRICTION_COEF_3 = 0.3; + + private static final double MIN_VELOCITY_ADJUST_FRICTION = 1000.0; + private static final double MID_VELOCITY_ADJUST_FRICTION = 4000.0; + private static final double MAX_VELOCITY_ADJUST_FRICTION = 10000.0; + + private static final float SCALE_FACTOR_HIGH = 1.2f; + private static final float SCALE_FACTOR_MID = 0.8f; + private static final float SCALE_FACTOR_LOW = 0.5f; + private static final float VELOCITY_6K = 6000.0f; + private static final float VELOCITY_10K = 10000.0f; + private static final float VELOCITY_20K = 20000.0f; + + private AXUIInterpolator mSpringInterpolator; + private boolean mUseSpringPhysics = true; + private int mSpringSplineDuration; + private int mSpringDistanceOld; + private int mSpringDistanceNew; + private double mMinSplineDelta = FRICTION_COEF_2; + private int mOvershootDistance; + private int mIterationCount = 0; + private boolean mFinishedEarly = false; + private float mDeltaTime; + private int mPhysicsIterations = 0; + private long mLastUpdateTime; + + private FrictionConfig mCurrentFriction; + private final FrictionConfig mPrimaryFriction = new FrictionConfig(AX_FRICTION_NORMAL, 0.0); + private final FrictionConfig mSpringFriction = new FrictionConfig(AX_FRICTION_NORMAL, AX_STIFFNESS); + + private final CurrentState mPhysicsState = new CurrentState(); + private final CurrentState mPhysicsState2 = new CurrentState(); + + /** + */ + private static class FrictionConfig { + double mDamping; + double mStiffness; + + FrictionConfig(double damping, double stiffness) { + mDamping = calculateDamping((float) damping); + mStiffness = calculateStiffness((float) stiffness); + } + + void setDampingFromVelocity(double value) { + mDamping = calculateDamping((float) value); + } + + void setStiffnessFromValue(double value) { + mStiffness = calculateStiffness((float) value); + } + + static float calculateDamping(float f) { + if (f == 0.0f) { + return 0.0f; + } + return ((f - 8.0f) * 3.0f) + 25.0f; + } + + static double calculateStiffness(float f) { + if (f == 0.0f) { + return 0.0; + } + return ((f - 30.0f) * 3.62f) + 194.0f; + } + } + + /** + */ + private static class CurrentState { + double mPosition; + double mVelocity; + + void reset() { + mPosition = 0.0; + mVelocity = 0.0; + } + + void set(double position, double velocity) { + mPosition = position; + mVelocity = velocity; + } + } + static { float x_min = 0.0f; float y_min = 0.0f; @@ -681,6 +906,117 @@ private void adjustDuration(int start, int oldFinal, int newFinal) { } } + + /** + */ + private double getVelocityFrictionCoef(float velocity) { + double absVel = Math.abs(velocity); + if (absVel <= VELOCITY_THRESHOLD_LOW) { + return FRICTION_COEF_2; + } else if (absVel > VELOCITY_THRESHOLD_HIGH) { + return FRICTION_COEF_1; + } else { + return FRICTION_COEF_3; + } + } + + /** + */ + private float getDurationScaleFactor(float velocity) { + float absVel = Math.abs(velocity); + if (absVel <= 2000.0f) { + return SCALE_FACTOR_HIGH; + } else if (absVel <= VELOCITY_6K) { + float ratio = (absVel - 2000.0f) / 4000.0f; + return SCALE_FACTOR_HIGH - (ratio * 0.4f); + } else if (absVel <= VELOCITY_10K) { + return SCALE_FACTOR_MID; + } else if (absVel > VELOCITY_20K) { + return SCALE_FACTOR_LOW; + } else { + float ratio = (absVel - VELOCITY_10K) / VELOCITY_10K; + return SCALE_FACTOR_MID - (ratio * 0.3f); + } + } + + /** + */ + private void initSpringPhysics(int position, int velocity) { + mPhysicsIterations = 1; + mFinishedEarly = false; + mPrimaryFriction.setDampingFromVelocity(AX_FRICTION_NORMAL); + mPrimaryFriction.setStiffnessFromValue(0.0); + mCurrentFriction = mPrimaryFriction; + mPhysicsState.set(position, velocity); + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mLastUpdateTime = mStartTime; + } + + /** + */ + private void updateSpringPhysics() { + double pos = mPhysicsState.mPosition; + double vel = mPhysicsState.mVelocity; + double pos2 = mPhysicsState2.mPosition; + + adjustFrictionForVelocity(); + + double stiffness = mCurrentFriction.mStiffness; + double target = mFinal; + double force = (target - pos2) * stiffness; + float dt = mDeltaTime; + + double midPos = pos + (dt * vel) / 2.0; + double midVel = vel + (dt * force) / 2.0; + double damping = mCurrentFriction.mDamping; + double midForce = ((target - midPos) * stiffness) - (damping * midVel); + double midVel2 = vel + (dt * midForce) / 2.0; + double midForce2 = ((target - (pos + (dt * midVel) / 2.0)) * stiffness) - (damping * midVel2); + double finalPos = pos + (dt * midVel2); + double finalVel = vel + (dt * midForce2); + double finalForce = (stiffness * (target - finalPos)) - (damping * finalVel); + + double avgVel = ((midVel + midVel2) * 2.0 + vel + finalVel) * 0.167; + mPhysicsState2.mVelocity = finalVel; + mPhysicsState2.mPosition = finalPos; + mPhysicsState.mVelocity = vel + (dt * (force + ((midForce + midForce2) * 2.0) + finalForce) * 0.167); + mPhysicsState.mPosition = pos + (dt * avgVel); + mPhysicsIterations++; + } + + /** + */ + private void adjustFrictionForVelocity() { + if (mPhysicsIterations != 1) { + return; + } + double absVel = Math.abs(mPhysicsState.mVelocity); + if (absVel > MID_VELOCITY_ADJUST_FRICTION && absVel < MAX_VELOCITY_ADJUST_FRICTION) { + mCurrentFriction.mDamping = FrictionConfig.calculateDamping((float) AX_FRICTION_MID); + } else if (absVel >= MAX_VELOCITY_ADJUST_FRICTION) { + mCurrentFriction.setDampingFromVelocity(AX_FRICTION_NORMAL); + } else { + mCurrentFriction.mDamping = FrictionConfig.calculateDamping((float) AX_FRICTION_SLOW); + } + } + + /** + */ + private boolean hasLostVelocity() { + return Math.abs(mPhysicsState.mVelocity) < 5.0; + } + + /** + */ + private void updateWithSpringInterpolator(float progress) { + if (mSpringInterpolator == null) return; + float posRatio = mSpringInterpolator.getInterpolation(progress); + float velRatio = mSpringInterpolator.getVelocityRatio(progress); + mPhysicsState.mPosition = (mSpringDistanceOld * posRatio) + mStart; + mPhysicsState.mVelocity = ((mSpringDistanceOld * velRatio) / mSpringSplineDuration) * 1000.0f; + mPhysicsIterations++; + } + void startScroll(int start, int distance, int duration) { mFinished = false; @@ -782,6 +1118,25 @@ void fling(int start, int velocity, int min, int max, int over) { adjustDuration(mStart, mFinal, max); mFinal = max; } + + if (mUseSpringPhysics && velocity != 0) { + mMinSplineDelta = getVelocityFrictionCoef(Math.abs(velocity)); + mOvershootDistance = over; + mIterationCount = 0; + mFinishedEarly = false; + + float durationScale = getDurationScaleFactor(Math.abs(velocity)); + mSpringInterpolator = new AXUIInterpolator( + AX_STIFFNESS, AX_DAMPING_RATIO, + velocity, durationScale, AX_MAX_VELOCITY); + + mSpringDistanceOld = mSplineDistance; + mSpringDistanceNew = mFinal - start; + mSpringSplineDuration = mSplineDuration; + + initSpringPhysics(start, velocity); + mPhysicsState2.reset(); + } } private double getSplineDeceleration(int velocity) { @@ -910,9 +1265,10 @@ boolean continueWhenFinished() { */ boolean update() { final long time = AnimationUtils.currentAnimationTimeMillis(); - final long currentTime = time - mStartTime; + final long adjustedTime = ScrollOptimizer.getAdjustedAnimationClock(time); + final long currentTime = adjustedTime - mStartTime; - if (currentTime == 0) { + if (currentTime <= 0) { // Skip work but report that we're still going if we have a nonzero duration. return mDuration > 0; } @@ -924,20 +1280,27 @@ boolean update() { switch (mState) { case SPLINE: { final float t = (float) currentTime / mSplineDuration; - final int index = (int) (NB_SAMPLES * t); - float distanceCoef = 1.f; - float velocityCoef = 0.f; - if (index < NB_SAMPLES) { - final float t_inf = (float) index / NB_SAMPLES; - final float t_sup = (float) (index + 1) / NB_SAMPLES; - final float d_inf = SPLINE_POSITION[index]; - final float d_sup = SPLINE_POSITION[index + 1]; - velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); - distanceCoef = d_inf + (t - t_inf) * velocityCoef; - } - distance = distanceCoef * mSplineDistance; - mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f; + if (mUseSpringPhysics && mSpringInterpolator != null && mSpringSplineDuration > 0) { + float posRatio = mSpringInterpolator.getInterpolation(t); + float velRatio = mSpringInterpolator.getVelocityRatio(t); + distance = posRatio * mSpringDistanceOld; + mCurrVelocity = ((mSpringDistanceOld * velRatio) / mSpringSplineDuration) * 1000.0f; + } else { + final int index = (int) (NB_SAMPLES * t); + float distanceCoef = 1.f; + float velocityCoef = 0.f; + if (index < NB_SAMPLES) { + final float t_inf = (float) index / NB_SAMPLES; + final float t_sup = (float) (index + 1) / NB_SAMPLES; + final float d_inf = SPLINE_POSITION[index]; + final float d_sup = SPLINE_POSITION[index + 1]; + velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); + distanceCoef = d_inf + (t - t_inf) * velocityCoef; + } + distance = distanceCoef * mSplineDistance; + mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f; + } break; } diff --git a/core/java/com/android/internal/inputmethod/AbstractSafeList.java b/core/java/com/android/internal/inputmethod/AbstractSafeList.java new file mode 100644 index 0000000000000..697b153afecfe --- /dev/null +++ b/core/java/com/android/internal/inputmethod/AbstractSafeList.java @@ -0,0 +1,127 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.inputmethod; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; + +/** + * An abstract base class for creating a {@link Parcelable} container that can hold an arbitrary + * number of {@link Parcelable} objects without worrying about + * {@link android.os.TransactionTooLargeException}. + * + * @see Parcel#readBlob() + * @see Parcel#writeBlob(byte[]) + * + * @param The type of the {@link Parcelable} objects. + */ +public abstract class AbstractSafeList implements Parcelable { + @Nullable + private byte[] mBuffer; + + protected AbstractSafeList(@Nullable List list) { + if (list != null && !list.isEmpty()) { + mBuffer = marshall(list); + } + } + + protected AbstractSafeList(@Nullable byte[] buffer) { + mBuffer = buffer; + } + + /** + * Extracts the list of {@link Parcelable} objects from a {@link AbstractSafeList}, and + * clears the internal buffer of the list. + * + * @param from The {@link AbstractSafeList} to extract from. + * @param creator The {@link Parcelable.Creator} for the {@link Parcelable} objects. + * @param The type of the {@link Parcelable} objects. + * @return The list of {@link Parcelable} objects. + */ + @NonNull + protected static List extractFrom( + @Nullable AbstractSafeList from, @NonNull Parcelable.Creator creator) { + if (from == null) { + return new ArrayList<>(); + } + final byte[] buf = from.mBuffer; + from.mBuffer = null; + if (buf != null) { + final List list = unmarshall(buf, creator); + if (list != null) { + return list; + } + } + return new ArrayList<>(); + } + + @Override + public int describeContents() { + // As long as the parcelled classes return 0, we can also return 0 here. + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeBlob(mBuffer); + } + + /** + * Marshalls a list of {@link Parcelable} objects into a byte array. + */ + @Nullable + @VisibleForTesting + public static byte[] marshall(@NonNull List list) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + parcel.writeTypedList(list); + return parcel.marshall(); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } + + /** + * Unmarshalls a byte array into a list of {@link Parcelable} objects. + */ + @Nullable + @VisibleForTesting + public static List unmarshall( + @NonNull byte[] data, @NonNull Parcelable.Creator creator) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + parcel.unmarshall(data, 0, data.length); + parcel.setDataPosition(0); + return parcel.createTypedArrayList(creator); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } +} diff --git a/core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.java b/core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.java index 9e720fb6cceea..a2ea5b08f13f3 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.java +++ b/core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.java @@ -19,24 +19,24 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Parcel; -import android.os.Parcelable; import android.view.inputmethod.InputMethodInfo; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** - * A {@link Parcelable} container that can holds an arbitrary number of {@link InputMethodInfo} - * without worrying about {@link android.os.TransactionTooLargeException} when passing across - * process boundary. - * - * @see Parcel#readBlob() - * @see Parcel#writeBlob(byte[]) + * A {@link android.os.Parcelable} container that can hold an arbitrary number of + * {@link InputMethodInfo} without worrying about + * {@link android.os.TransactionTooLargeException} when passing across process boundary. */ -public final class InputMethodInfoSafeList implements Parcelable { - @Nullable - private byte[] mBuffer; +public final class InputMethodInfoSafeList extends AbstractSafeList { + + private InputMethodInfoSafeList(@Nullable byte[] buffer) { + super(buffer); + } + + private InputMethodInfoSafeList(@Nullable List list) { + super(list); + } /** * Instantiates a list of {@link InputMethodInfo} from the given {@link InputMethodInfoSafeList} @@ -53,81 +53,20 @@ public final class InputMethodInfoSafeList implements Parcelable { */ @NonNull public static List extractFrom(@Nullable InputMethodInfoSafeList from) { - final byte[] buf = from.mBuffer; - from.mBuffer = null; - if (buf != null) { - final InputMethodInfo[] array = unmarshall(buf); - if (array != null) { - return new ArrayList<>(Arrays.asList(array)); - } - } - return new ArrayList<>(); - } - - @NonNull - private static InputMethodInfo[] toArray(@Nullable List original) { - if (original == null) { - return new InputMethodInfo[0]; - } - return original.toArray(new InputMethodInfo[0]); - } - - @Nullable - private static byte[] marshall(@NonNull InputMethodInfo[] array) { - Parcel parcel = null; - try { - parcel = Parcel.obtain(); - parcel.writeTypedArray(array, 0); - return parcel.marshall(); - } finally { - if (parcel != null) { - parcel.recycle(); - } - } - } - - @Nullable - private static InputMethodInfo[] unmarshall(byte[] data) { - Parcel parcel = null; - try { - parcel = Parcel.obtain(); - parcel.unmarshall(data, 0, data.length); - parcel.setDataPosition(0); - return parcel.createTypedArray(InputMethodInfo.CREATOR); - } finally { - if (parcel != null) { - parcel.recycle(); - } - } - } - - private InputMethodInfoSafeList(@Nullable byte[] blob) { - mBuffer = blob; + return AbstractSafeList.extractFrom(from, InputMethodInfo.CREATOR); } /** * Instantiates {@link InputMethodInfoSafeList} from the given list of {@link InputMethodInfo}. * * @param list list of {@link InputMethodInfo} from which {@link InputMethodInfoSafeList} will - * be created + * be created. Giving {@code null} will result in an empty + * {@link InputMethodInfoSafeList}. * @return {@link InputMethodInfoSafeList} that stores the given list of {@link InputMethodInfo} */ @NonNull public static InputMethodInfoSafeList create(@Nullable List list) { - if (list == null || list.isEmpty()) { - return empty(); - } - return new InputMethodInfoSafeList(marshall(toArray(list))); - } - - /** - * Creates an empty {@link InputMethodInfoSafeList}. - * - * @return {@link InputMethodInfoSafeList} that is empty - */ - @NonNull - public static InputMethodInfoSafeList empty() { - return new InputMethodInfoSafeList(null); + return new InputMethodInfoSafeList(list); } public static final Creator CREATOR = new Creator<>() { @@ -141,16 +80,4 @@ public InputMethodInfoSafeList[] newArray(int size) { return new InputMethodInfoSafeList[size]; } }; - - @Override - public int describeContents() { - // As long as InputMethodInfo#describeContents() is guaranteed to return 0, we can always - // return 0 here. - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeBlob(mBuffer); - } } diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.aidl b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.aidl new file mode 100644 index 0000000000000..11000632eba54 --- /dev/null +++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.inputmethod; + +parcelable InputMethodSubtypeSafeList; diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.java b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.java new file mode 100644 index 0000000000000..cd95088f5cf0d --- /dev/null +++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.java @@ -0,0 +1,87 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.inputmethod; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.view.inputmethod.InputMethodSubtype; + +import java.util.List; + +/** + * A {@link android.os.Parcelable} container that can hold an arbitrary number of + * {@link InputMethodSubtype} without worrying about + * {@link android.os.TransactionTooLargeException} when passing across process boundary. + */ +public final class InputMethodSubtypeSafeList extends AbstractSafeList { + + private InputMethodSubtypeSafeList(@Nullable byte[] buffer) { + super(buffer); + } + + private InputMethodSubtypeSafeList(@Nullable List list) { + super(list); + } + + /** + * Instantiates a list of {@link InputMethodSubtype} from the given + * {@link InputMethodSubtypeSafeList} then clears the internal buffer of + * {@link InputMethodSubtypeSafeList}. + * + *

Note that each {@link InputMethodSubtype} item is guaranteed to be a copy of the original + * {@link InputMethodSubtype} object.

+ * + *

Any subsequent call will return an empty list.

+ * + * @param from {@link InputMethodSubtypeSafeList} from which the list of + * {@link InputMethodSubtype} will be extracted + * @return list of {@link InputMethodSubtype} stored in the given + * {@link InputMethodSubtypeSafeList} + */ + @NonNull + public static List extractFrom(@Nullable InputMethodSubtypeSafeList from) { + return AbstractSafeList.extractFrom(from, InputMethodSubtype.CREATOR); + } + + /** + * Instantiates {@link InputMethodSubtypeSafeList} from the given list of + * {@link InputMethodSubtype}. + * + * @param list list of {@link InputMethodSubtype} from which + * {@link InputMethodSubtypeSafeList} will be created. Giving {@code null} will + * result in an empty {@link InputMethodSubtypeSafeList}. + * @return {@link InputMethodSubtypeSafeList} that stores the given list of + * {@link InputMethodSubtype} + */ + @NonNull + public static InputMethodSubtypeSafeList create(@Nullable List list) { + return new InputMethodSubtypeSafeList(list); + } + + public static final Creator CREATOR = new Creator<>() { + @Override + public InputMethodSubtypeSafeList createFromParcel(Parcel in) { + return new InputMethodSubtypeSafeList(in.readBlob()); + } + + @Override + public InputMethodSubtypeSafeList[] newArray(int size) { + return new InputMethodSubtypeSafeList[size]; + } + }; +} diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java index 7503fb1f99133..7f744b148f187 100644 --- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java +++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java @@ -264,7 +264,7 @@ private Boolean getFlagValueFromNewStorage(String flagPackageAndName) { try { return AconfigPackage.load(p); } catch (Exception e) { - Slog.e(LOG_TAG, "Failed to load aconfig package " + p, e); + //Slog.e(LOG_TAG, "Failed to load aconfig package " + p, e); return null; } }); diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedPermissionUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedPermissionUtils.java index 64cf311c74260..3af2a0410df6a 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedPermissionUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedPermissionUtils.java @@ -172,6 +172,8 @@ public static ParseResult parsePermission(ParsingPackage pkg, } } + permission.setName(permission.getName().trim()); + permission.setProtectionLevel( PermissionInfo.fixProtectionLevel(permission.getProtectionLevel())); @@ -236,6 +238,8 @@ public static ParseResult parsePermissionTree(ParsingPackage p sa.recycle(); } + permission.setName(permission.getName().trim()); + int index = permission.getName().indexOf('.'); if (index > 0) { index = permission.getName().indexOf('.', index + 1); @@ -285,7 +289,8 @@ public static ParseResult parsePermissionGroup(ParsingPac .setBackgroundRequestDetailRes(sa.getResourceId(R.styleable.AndroidManifestPermissionGroup_backgroundRequestDetail, 0)) .setRequestRes(sa.getResourceId(R.styleable.AndroidManifestPermissionGroup_request, 0)) .setPriority(sa.getInt(R.styleable.AndroidManifestPermissionGroup_priority, 0)) - .setFlags(sa.getInt(R.styleable.AndroidManifestPermissionGroup_permissionGroupFlags,0)); + .setFlags(sa.getInt(R.styleable.AndroidManifestPermissionGroup_permissionGroupFlags,0)) + .setName(permissionGroup.getName().trim()); // @formatter:on } finally { sa.recycle(); diff --git a/core/java/com/android/internal/policy/ScreenDecorationsUtils.java b/core/java/com/android/internal/policy/ScreenDecorationsUtils.java index b23515aa51f33..2d358433c6b7f 100644 --- a/core/java/com/android/internal/policy/ScreenDecorationsUtils.java +++ b/core/java/com/android/internal/policy/ScreenDecorationsUtils.java @@ -41,6 +41,10 @@ public class ScreenDecorationsUtils { * If the associated display is not internal, will return 0. */ public static float getWindowCornerRadius(Context context) { + String callingPackage = context.getPackageManager().getNameForUid(android.os.Binder.getCallingUid()); + if ("com.google.android.apps.nexuslauncher".equals(callingPackage)) { + return 32f; + } final Resources resources = context.getResources(); if (!supportsRoundedCornersOnWindows(resources)) { return 0f; diff --git a/core/java/com/android/internal/util/ScrollOptimizer.java b/core/java/com/android/internal/util/ScrollOptimizer.java new file mode 100644 index 0000000000000..17bb71a771d85 --- /dev/null +++ b/core/java/com/android/internal/util/ScrollOptimizer.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2025 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.graphics.BLASTBufferQueue; +import android.os.Process; +import android.os.StrictMode; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.Trace; +import android.util.Log; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Locale; + +public class ScrollOptimizer { + + /** @hide */ + public static final int FLING_START = 1; + /** @hide */ + public static final int FLING_END = 0; + + private static final String TAG = "AxPerf"; + + private static final String PROP_SCROLL_OPT = "persist.sys.perf.scroll_opt"; + private static final String PROP_SCROLL_OPT_HEAVY_APP = "persist.sys.perf.scroll_opt.heavy_app"; + private static final String PROP_DEBUG = "persist.sys.perf.scroll_opt_debug"; + + private static final String TIMER_SLACK_CONTENT = "50000"; + + private static final long FRAME_INTERVAL_THRESHOLD_NS = 10_000_000L; + private static final long DEFAULT_FRAME_DELAY_MS = 10L; + private static final long OPTIMIZED_FRAME_DELAY_MS = 3L; + private static final long FLING_END_TIMEOUT_MS = 3000L; + + private static int sInitialUndequeued = 4; + private static int sFallbackUndequeued = 3; + + private static final int PROP_UNSET = -1; + private static final int MOTION_TOUCH = 0; + private static final int MOTION_FLING = 1; + private static final int MOTION_SCROLL = 2; + private static final int APP_TYPE_NORMAL = 1; + private static final int APP_TYPE_HEAVY = 2; + + private static boolean sPrevUseVsync = true; + private static boolean sAdjustCalled = false; + private static boolean sTimerSlackUpdated = false; + private static boolean sPreRenderDone = false; + private static boolean sNeedUpdateBuffer = false; + + private static long sFrameIntervalNs = -1; + private static long sFrameIntervalMs = -1; + private static long sHalfFrameIntervalNs = -1; + private static long sHeavyFrameThresholdNs = -1; + private static long sLastVsyncTimeNs = -1; + private static long sLastAdjustedTimeNs = -1; + private static long sLastFlingStartMs = -1; + private static long sLastUIStartNs = -1; + private static long sLastUIEndNs = -1; + + private static int sPid = -1; + private static int sHeavyAppProp = -1; + private static int sHeavyApp = 0; + private static int sHeavyFrameCount = 0; + private static int sAppType = APP_TYPE_NORMAL; + private static int sMotionType = PROP_UNSET; + private static int mLastFlingFlg = 0; + + private static int sActualUndequeued = 0; + private static int sExpectedUndequeued = 0; + + private static BLASTBufferQueue sBlastQueue = null; + private static Method sSetUndequeuedMethod = null; + private static Method sGetUndequeuedMethod = null; + + private static FileOutputStream sTimerSlackStream = null; + + private static boolean sDebugEnabled = false; + private static boolean sInitCalled = false; + private static boolean sFeatureEnabled = false; + private static boolean sTemporarilyDisabled = false; + private static boolean sLastUseVsync = true; + + private static void logger(String msg) { + if (sDebugEnabled) { + Log.d(TAG, msg); + } + } + + private static int getUndequeuedBufferCount() { + int undequeued = 0; + if (sBlastQueue == null) { + logger("sBlastBufferQueue is null."); + sFeatureEnabled = false; + return 0; + } + try { + Object res = sGetUndequeuedMethod.invoke(sBlastQueue); + undequeued = res instanceof Integer ? (Integer) res : 0; + logger("undequeuedBufferCount: " + undequeued); + } catch (Exception e) { + undequeued = 0; + } + return undequeued; + } + + private static void initIfNeeded() { + try { + sFeatureEnabled = SystemProperties.getBoolean(PROP_SCROLL_OPT, true); + int prop = SystemProperties.getInt(PROP_SCROLL_OPT_HEAVY_APP, 1); + sHeavyAppProp = prop; + sHeavyApp = prop; + sDebugEnabled = SystemProperties.getBoolean(PROP_DEBUG, false); + + Class clazz = Class.forName("android.graphics.BLASTBufferQueue"); + sSetUndequeuedMethod = clazz.getMethod("setUndequeuedBufferCount", Integer.TYPE); + sGetUndequeuedMethod = clazz.getMethod("getUndequeuedBufferCount"); + + sPid = Process.myPid(); + sTimerSlackStream = new FileOutputStream( + String.format(Locale.US, "/proc/%d/timerslack_ns", sPid)); + + if (Process.myUid() == 1000) { + logger("Disable for system_server"); + sFeatureEnabled = false; + } + sInitCalled = true; + } catch (Exception e) { + Log.e(TAG, "Couldn't load BLASTBufferQueue Class"); + sInitCalled = true; + sFeatureEnabled = false; + } + + if (sHeavyApp == 1) { + logger("Heavy app detection is enabled."); + } + if (sSetUndequeuedMethod == null || sGetUndequeuedMethod == null) { + Log.e(TAG, "Couldn't find UndequeuedBufferCount functions"); + sFeatureEnabled = false; + } + } + + public static void disableOptimizer(boolean disable) { + boolean wasTemporarilyDisabled = sTemporarilyDisabled; + if (wasTemporarilyDisabled == disable) return; + if (wasTemporarilyDisabled) { + sFeatureEnabled = true; + sTemporarilyDisabled = false; + logger("enable ScrollOptimizer again."); + } else if (sFeatureEnabled) { + sFeatureEnabled = false; + sTemporarilyDisabled = true; + logger("disable ScrollOptimizer temperarily."); + } + } + + private static void resetFlingState() { + sLastUseVsync = true; + sAdjustCalled = false; + sAppType = APP_TYPE_NORMAL; + sHeavyFrameCount = 0; + sMotionType = PROP_UNSET; + mLastFlingFlg = 0; + sTimerSlackUpdated = false; + } + + private static void updateExpectedFromPreRender() { + if (sPreRenderDone) { + sPreRenderDone = false; + int val = getUndequeuedBufferCount(); + sActualUndequeued = val; + sExpectedUndequeued = val; + if (val > 1) { + sExpectedUndequeued = val - 1; + } else if (val < 1) { + sExpectedUndequeued = 1; + } + } + } + + public static long getAdjustedAnimationClock(long originalTimeNs) { + if (!sFeatureEnabled || Process.myTid() != sPid) { + return originalTimeNs; + } + if (sAdjustCalled) { + logger("unnecessary adjustClock is called!"); + if (originalTimeNs > sLastAdjustedTimeNs) { + sLastAdjustedTimeNs = originalTimeNs; + } + return sLastAdjustedTimeNs; + } + sAdjustCalled = true; + long candidate = sLastAdjustedTimeNs + sFrameIntervalMs; + if (mLastFlingFlg != 1) { + if (originalTimeNs >= candidate || + SystemClock.uptimeMillis() >= sLastFlingStartMs + FLING_END_TIMEOUT_MS) { + sLastAdjustedTimeNs = originalTimeNs; + return originalTimeNs; + } + logger("extended adjustedTime: " + candidate + ", originTime: " + originalTimeNs); + logger("extend clock adjustion"); + sLastAdjustedTimeNs = candidate; + return candidate; + } + if (candidate < originalTimeNs) { + candidate = originalTimeNs; + } else if (sPrevUseVsync) { + long offset = candidate - originalTimeNs; + if (offset > 0 && sFrameIntervalMs > 0) { + long rounds = Math.round((double) offset / (double) sFrameIntervalMs); + candidate = (sFrameIntervalMs * rounds) + originalTimeNs; + } + } + logger("adjustedTime: " + candidate + ", originTime: " + originalTimeNs); + sLastAdjustedTimeNs = candidate; + return candidate; + } + + public static long getFrameDelay() { + if (!sFeatureEnabled) { + return DEFAULT_FRAME_DELAY_MS; + } + return OPTIMIZED_FRAME_DELAY_MS; + } + + private static void setUndequeuedBufferCount(int count) { + if (sBlastQueue == null) { + logger("sBlastBufferQueue is null."); + sFeatureEnabled = false; + return; + } + try { + sSetUndequeuedMethod.invoke(sBlastQueue, count); + logger("setUndequeuedBufferCount: " + count); + } catch (Exception e) { + e.printStackTrace(); + sFeatureEnabled = false; + } + } + + private static void writeTimerSlack() { + if (sTimerSlackStream == null) { + sFeatureEnabled = false; + return; + } + StrictMode.ThreadPolicy old = StrictMode.allowThreadViolations(); + try { + try { + sTimerSlackStream.write(TIMER_SLACK_CONTENT.getBytes()); + sTimerSlackUpdated = true; + } catch (IOException e) { + e.printStackTrace(); + Log.w(TAG, "Failed to update timer slack!"); + sFeatureEnabled = false; + } + } finally { + StrictMode.setThreadPolicy(old); + } + } + + public static void setBLASTBufferQueue(BLASTBufferQueue queue) { + if (sFeatureEnabled && Process.myTid() == sPid && sBlastQueue != queue) { + sBlastQueue = queue; + sNeedUpdateBuffer = false; + setUndequeuedBufferCount(sInitialUndequeued); + } + } + + public static void setFlingFlag(int flingFlg) { + if (sFeatureEnabled && Process.myTid() == sPid) { + logger("setFlingFlag: " + flingFlg); + if (flingFlg != 1) { + if (flingFlg < 0) { + logger("Fling quit for unknown."); + } + if (mLastFlingFlg == 1) { + resetFlingState(); + logger("Fling end."); + } + return; + } + if (mLastFlingFlg == 1) { + resetFlingState(); + logger("avoid concurrent fling"); + return; + } + if (sMotionType == MOTION_FLING) { + mLastFlingFlg = flingFlg; + if (!sTimerSlackUpdated) { + writeTimerSlack(); + } + sNeedUpdateBuffer = false; + sLastFlingStartMs = SystemClock.uptimeMillis(); + logger("Fling start."); + } else { + logger("Fling without touch"); + } + sMotionType = PROP_UNSET; + } + } + + public static void setFrameInterval(long nanos) { + if (!sInitCalled) { + initIfNeeded(); + } + logger("frameIntervalNanos: " + nanos); + sFrameIntervalNs = nanos; + sFrameIntervalMs = nanos / 1_000_000; + long half = nanos / 2; + sHalfFrameIntervalNs = half; + sHeavyFrameThresholdNs = half * OPTIMIZED_FRAME_DELAY_MS; + if (nanos > FRAME_INTERVAL_THRESHOLD_NS) { + sInitialUndequeued = 3; + sFallbackUndequeued = 2; + } else { + sInitialUndequeued = 4; + sFallbackUndequeued = 3; + } + if (sHeavyAppProp == PROP_UNSET) { + if (nanos > FRAME_INTERVAL_THRESHOLD_NS) { + sHeavyApp = 0; + } else { + sHeavyApp = 1; + } + } + } + + public static void setMotionType(int motion) { + if (sFeatureEnabled && Process.myTid() == sPid) { + if (motion == MOTION_TOUCH) { + boolean wasFling = (mLastFlingFlg == 1); + resetFlingState(); + int curUndequeued = getUndequeuedBufferCount(); + sActualUndequeued = curUndequeued; + if (sNeedUpdateBuffer && curUndequeued != sFallbackUndequeued) { + if (curUndequeued > sFallbackUndequeued || + System.nanoTime() - sLastUIEndNs > (sFrameIntervalNs * 2) + 1_000_000) { + setUndequeuedBufferCount(sFallbackUndequeued); + sExpectedUndequeued = sFallbackUndequeued; + } + } else if (wasFling || sActualUndequeued > 0) { + sExpectedUndequeued = sActualUndequeued; + } else { + sExpectedUndequeued = 1; + } + } else if (motion == MOTION_SCROLL) { + sNeedUpdateBuffer = true; + int cur = getUndequeuedBufferCount(); + sActualUndequeued = cur; + if (sExpectedUndequeued > cur && cur > 0) { + sExpectedUndequeued = cur; + } + } + sMotionType = motion; + logger("setMotionType: " + motion); + } + } + + public static void setUITaskStatus(boolean running) { + if (sFeatureEnabled && Process.myTid() == sPid) { + long nowNs = System.nanoTime(); + long uiDurationNs; + if (running) { + sAdjustCalled = false; + if (mLastFlingFlg == 1) { + long durSinceUIStart = nowNs - sLastUIStartNs; + if (durSinceUIStart > (sFrameIntervalNs * 2) - 1_000_000) { + updateExpectedFromPreRender(); + } + long hf = sHalfFrameIntervalNs; + if (durSinceUIStart > (OPTIMIZED_FRAME_DELAY_MS * hf) - 1_000_000 && + nowNs - sLastUIEndNs > hf) { + sHeavyFrameCount++; + } + } + sLastUIStartNs = nowNs; + uiDurationNs = 0; + } else { + sLastUIEndNs = nowNs; + sPrevUseVsync = sLastUseVsync; + uiDurationNs = nowNs - sLastUIStartNs; + if (mLastFlingFlg == 1 && uiDurationNs > sFrameIntervalNs * 2) { + updateExpectedFromPreRender(); + } + } + int mode = sHeavyApp; + if (mode == 0) return; + if (mode == 2) { + sAppType = APP_TYPE_HEAVY; + return; + } + if (running) return; + if (sMotionType == MOTION_SCROLL || mLastFlingFlg == 1) { + if (uiDurationNs > sFrameIntervalNs) { + sHeavyFrameCount++; + } + if ((sHeavyFrameCount > 1 || uiDurationNs > sHeavyFrameThresholdNs) && + sAppType != APP_TYPE_HEAVY) { + sAppType = APP_TYPE_HEAVY; + logger("App type: heavy app"); + } + } + logger("UI duration: " + uiDurationNs); + } + } + + public static void setVsyncTime(long vsyncTimeNs) { + if (sFeatureEnabled) { + sLastVsyncTimeNs = vsyncTimeNs; + logger("setVsyncTime: " + sLastVsyncTimeNs); + } + } + + public static boolean shouldUseVsync() { + boolean result = true; + if (sFeatureEnabled && Process.myTid() == sPid) { + if (mLastFlingFlg != 1) { + sLastUseVsync = true; + return true; + } + if (sAppType == APP_TYPE_HEAVY) { + sLastUseVsync = false; + return false; + } + if (sPreRenderDone) { + logger("pre-render done"); + sLastUseVsync = true; + return true; + } + long interval = sFrameIntervalNs; + long timeToNext = interval - ((sLastUIStartNs - sLastVsyncTimeNs) % interval); + if (timeToNext < 3_000_000L) { + logger("too close to next vsync"); + sLastUseVsync = false; + if (sExpectedUndequeued > 0) { + sExpectedUndequeued = sExpectedUndequeued - 1; + } + return false; + } + if (!sLastUseVsync) { + logger("use vsync as last frame not use vsync"); + sLastUseVsync = true; + return true; + } + int undequeued = getUndequeuedBufferCount(); + sActualUndequeued = undequeued; + int expected = sExpectedUndequeued; + if (undequeued > expected) { + logger("align undequeued: " + sActualUndequeued + " with expected: " + sExpectedUndequeued); + sActualUndequeued = expected; + } else if (undequeued < 1 && expected > 0) { + sActualUndequeued = 1; + } + if (sActualUndequeued > 0) { + sExpectedUndequeued = sExpectedUndequeued - 1; + result = false; + } else { + sPreRenderDone = true; + logger("pre-render done"); + result = true; + } + sLastUseVsync = result; + } + return result; + } +} diff --git a/core/java/com/android/internal/util/lunaris/HideAppListUtils.java b/core/java/com/android/internal/util/lunaris/HideAppListUtils.java new file mode 100644 index 0000000000000..a55b6aae6404e --- /dev/null +++ b/core/java/com/android/internal/util/lunaris/HideAppListUtils.java @@ -0,0 +1,115 @@ +package com.android.internal.util.lunaris; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.SystemProperties; +import android.provider.Settings; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class HideAppListUtils { + enum Action { + ADD, + REMOVE, + SET + } + + private static boolean isBootCompleted() { + return SystemProperties.getBoolean("sys.boot_completed", false); + } + + public static boolean shouldHideAppList(Context context, String packageName) { + return shouldHideAppList(context.getContentResolver(), packageName); + } + + public static boolean shouldHideAppList(ContentResolver cr, String packageName) { + if (cr == null || packageName == null || !isBootCompleted()) { + return false; + } + + Set apps = getApps(cr); + if (apps.isEmpty()) { + return false; + } + + return apps.contains(packageName); + } + + public static Set getApps(Context context) { + if (context == null) { + return new HashSet<>(); + } + + return getApps(context.getContentResolver()); + } + + public static Set getApps(ContentResolver cr) { + if (cr == null) { + return new HashSet<>(); + } + + String apps = ""; + try { + apps = Settings.Secure.getString(cr, Settings.Secure.HIDE_APPLIST); + } catch (IllegalStateException e) { + return new HashSet<>(); + } + if (apps != null && !apps.isEmpty() && !apps.equals(",")) { + return new HashSet<>(Arrays.asList(apps.split(","))); + } + + return new HashSet<>(); + } + + private static void putAppsForUser( + Context context, String packageName, int userId, Action action) { + if (context == null || userId < 0) { + return; + } + + final Set apps = getApps(context); + switch (action) { + case ADD: + apps.add(packageName); + break; + case REMOVE: + apps.remove(packageName); + break; + case SET: + // Don't change + break; + } + + Settings.Secure.putStringForUser( + context.getContentResolver(), + Settings.Secure.HIDE_APPLIST, + String.join(",", apps), + userId); + } + + public void addApp(Context mContext, String packageName, int userId) { + if (mContext == null || packageName == null || userId < 0) { + return; + } + + putAppsForUser(mContext, packageName, userId, Action.ADD); + } + + public void removeApp(Context mContext, String packageName, int userId) { + if (mContext == null || packageName == null || userId < 0) { + return; + } + + putAppsForUser(mContext, packageName, userId, Action.REMOVE); + } + + public void setApps(Context mContext, int userId) { + if (mContext == null || userId < 0) { + return; + } + + putAppsForUser(mContext, null, userId, Action.SET); + } +} diff --git a/core/java/com/android/internal/util/lunaris/KeyboxChainGenerator.java b/core/java/com/android/internal/util/lunaris/KeyboxChainGenerator.java index 7b6b46a984c66..3820ab21af1a8 100644 --- a/core/java/com/android/internal/util/lunaris/KeyboxChainGenerator.java +++ b/core/java/com/android/internal/util/lunaris/KeyboxChainGenerator.java @@ -13,15 +13,14 @@ import android.hardware.security.keymint.Algorithm; import android.hardware.security.keymint.EcCurve; import android.hardware.security.keymint.KeyOrigin; +import android.os.SystemProperties; import android.hardware.security.keymint.KeyParameter; import android.hardware.security.keymint.Tag; import android.os.Binder; import android.os.Build; import android.os.UserHandle; -import android.provider.Settings; import android.security.keystore.KeyProperties; import android.system.keystore2.KeyDescriptor; -import android.util.Base64; import android.util.Log; import androidx.annotation.Nullable; @@ -146,35 +145,19 @@ private static ASN1Encodable[] fromIntList(List list) { private static Extension createExtension(KeyGenParameters params, int uid) { try { - Context context = ActivityThread.currentApplication(); - if (context == null) { - Log.e(TAG, "Context is null in createExtension"); - return null; - } - SecureRandom secureRandom = new SecureRandom(); - - String key = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.VBOOT_KEY); - byte[] verifiedBootKey; - if (key == null) { - byte[] randomBytes = new byte[32]; - secureRandom.nextBytes(randomBytes); - String encoded = Base64.encodeToString(randomBytes, Base64.NO_WRAP); - Settings.Secure.putString(context.getContentResolver(), Settings.Secure.VBOOT_KEY, encoded); - verifiedBootKey = randomBytes; - } else { - verifiedBootKey = Base64.decode(key, Base64.NO_WRAP); - } + SecureRandom random = new SecureRandom(); + + byte[] verifiedBootKey = new byte[32]; + random.nextBytes(verifiedBootKey); - String hash = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.VBOOT_HASH); byte[] verifiedBootHash; - if (hash == null) { - byte[] randomBytes = new byte[32]; - secureRandom.nextBytes(randomBytes); - String encoded = Base64.encodeToString(randomBytes, Base64.NO_WRAP); - Settings.Secure.putString(context.getContentResolver(), Settings.Secure.VBOOT_HASH, encoded); - verifiedBootHash = randomBytes; + String vbmetaProp = SystemProperties.get("ro.boot.vbmeta.digest", ""); + + if (vbmetaProp != null && vbmetaProp.length() == 64) { + verifiedBootHash = hexStringToByteArray(vbmetaProp); } else { - verifiedBootHash = Base64.decode(hash, Base64.NO_WRAP); + verifiedBootHash = new byte[32]; + random.nextBytes(verifiedBootHash); } ASN1Encodable[] rootOfTrustEncodables = { @@ -402,6 +385,18 @@ private static KeyPair buildRSAKeyPair(KeyGenParameters params) throws Exception return kpg.generateKeyPair(); } + private static byte[] hexStringToByteArray(String hex) { + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + int high = Character.digit(hex.charAt(i), 16); + int low = Character.digit(hex.charAt(i + 1), 16); + if (high == -1 || low == -1) throw new IllegalArgumentException("Invalid hex"); + data[i / 2] = (byte) ((high << 4) + low); + } + return data; + } + private static void dlog(String msg) { if (DEBUG) Log.d(TAG, msg); } diff --git a/core/java/com/android/internal/util/lunaris/SystemRestartUtils.java b/core/java/com/android/internal/util/lunaris/SystemRestartUtils.java new file mode 100644 index 0000000000000..7c74e33cd1a78 --- /dev/null +++ b/core/java/com/android/internal/util/lunaris/SystemRestartUtils.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2023 Rising OS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.lunaris; + +import android.app.AlertDialog; +import android.app.IActivityManager; +import android.app.ActivityManager; +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; + +import com.android.internal.R; +import com.android.internal.statusbar.IStatusBarService; + +import java.lang.ref.WeakReference; + +import java.util.List; + +public class SystemRestartUtils { + + private static final int RESTART_TIMEOUT = 1000; + + public static void showSystemRestartDialog(Context context) { + new AlertDialog.Builder(context) + .setTitle(R.string.system_restart_title) + .setMessage(R.string.system_restart_message) + .setPositiveButton(R.string.ok, (dialog, id) -> { + Handler handler = new Handler(); + handler.postDelayed(() -> restartSystem(context), RESTART_TIMEOUT); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + public static void powerOffSystem(Context context) { + new PowerOffSystemTask(context).execute(); + } + + private static class PowerOffSystemTask extends AsyncTask { + private final WeakReference mContext; + + PowerOffSystemTask(Context context) { + mContext = new WeakReference<>(context); + } + + @Override + protected Void doInBackground(Void... params) { + try { + IStatusBarService mBarService = IStatusBarService.Stub.asInterface( + ServiceManager.getService(Context.STATUS_BAR_SERVICE)); + if (mBarService != null) { + try { + Thread.sleep(RESTART_TIMEOUT); + mBarService.shutdown(); + } catch (RemoteException | InterruptedException e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + } + + public static void restartSystem(Context context) { + new RestartSystemTask(context).execute(); + } + + private static class RestartSystemTask extends AsyncTask { + private final WeakReference mContext; + + RestartSystemTask(Context context) { + mContext = new WeakReference<>(context); + } + + @Override + protected Void doInBackground(Void... params) { + try { + IStatusBarService mBarService = IStatusBarService.Stub.asInterface( + ServiceManager.getService(Context.STATUS_BAR_SERVICE)); + if (mBarService != null) { + try { + Thread.sleep(RESTART_TIMEOUT); + mBarService.reboot(false, null); + } catch (RemoteException | InterruptedException e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + } + + private static void showRestartDialog(Context context, int title, int message, Runnable action) { + new AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.ok, (dialog, id) -> { + Handler handler = new Handler(); + handler.postDelayed(action, RESTART_TIMEOUT); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + public static void restartProcess(Context context, String processName) { + new RestartTask(context, processName).execute(); + } + + private static class RestartTask extends AsyncTask { + private final WeakReference mContext; + private final String mPackageName; + + RestartTask(Context context, String packageName) { + mContext = new WeakReference<>(context); + mPackageName = packageName; + } + + @Override + protected Void doInBackground(Void... params) { + try { + ActivityManager am = (ActivityManager) mContext.get().getSystemService(Context.ACTIVITY_SERVICE); + if (am != null) { + List runningProcesses = am.getRunningAppProcesses(); + for (ActivityManager.RunningAppProcessInfo appProcess : runningProcesses) { + if (appProcess.pkgList != null) { + for (String pkg : appProcess.pkgList) { + if (pkg.equals(mPackageName)) { + Process.killProcess(appProcess.pid); + return null; + } + } + } + } + } + } catch (Exception e) {} + return null; + } + } + + public static void showSettingsRestartDialog(Context context) { + showRestartDialog(context, R.string.settings_restart_title, R.string.settings_restart_message, () -> restartProcess(context, "com.android.settings")); + } + + public static void showSystemUIRestartDialog(Context context) { + showRestartDialog(context, R.string.systemui_restart_title, R.string.systemui_restart_message, () -> restartProcess(context, "com.android.systemui")); + } +} diff --git a/core/java/com/android/internal/util/lunaris/ThemeUtils.java b/core/java/com/android/internal/util/lunaris/ThemeUtils.java index 0a4d229c48886..1fa8f310af73a 100644 --- a/core/java/com/android/internal/util/lunaris/ThemeUtils.java +++ b/core/java/com/android/internal/util/lunaris/ThemeUtils.java @@ -22,6 +22,8 @@ import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.om.IOverlayManager; import android.content.om.OverlayInfo; import android.content.pm.PackageManager; @@ -38,6 +40,7 @@ import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.PathShape; import android.net.Uri; +import android.os.BatteryManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -48,6 +51,12 @@ import org.json.JSONException; import org.json.JSONObject; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -244,4 +253,76 @@ public boolean isDefaultOverlay(String category) { return getOverlayPackagesForCategory(category).stream() .noneMatch(pkg -> isOverlayEnabled(pkg)); } + + public static String batteryTemperature(Context context, Boolean ForC) { + Intent intent = context.registerReceiver(null, new IntentFilter( + Intent.ACTION_BATTERY_CHANGED)); + float temp = ((float) (intent != null ? intent.getIntExtra( + BatteryManager.EXTRA_TEMPERATURE, 0) : 0)) / 10; + // Round up to nearest number + int c = (int) ((temp) + 0.5f); + float n = temp + 0.5f; + // Use boolean to determine celsius or fahrenheit + return String.valueOf((n - c) % 2 == 0 ? (int) temp : + ForC ? c * 9/5 + 32 + "°F" :c + "°C"); + } + + public static String getCPUTemp(Context context) { + String value = null; + String cpuTempPath = context.getResources().getString( + com.android.internal.R.string.config_cpu_temp_path); + + if (fileExists(cpuTempPath)) { + value = readOneLine(cpuTempPath); + } + + if (value == null || value.isEmpty()) { + return "N/A"; + } + + try { + int cpuTempMultiplier = context.getResources().getInteger( + com.android.internal.R.integer.config_sysCPUTempMultiplier); + + if (cpuTempMultiplier == 0) { + cpuTempMultiplier = 1; + } + + int temp = Integer.parseInt(value.trim()) / cpuTempMultiplier; + return String.format("%d°C", temp); + } catch (NumberFormatException | Resources.NotFoundException e) { + Log.w("ThemeUtils", "Failed to parse CPU temperature: " + value, e); + return "N/A"; + } + } + + public static boolean fileExists(String filename) { + if (filename == null || filename.isEmpty()) { + return false; + } + return new File(filename).exists(); + } + + public static String readOneLine(String fname) { + if (fname == null || fname.isEmpty()) { + return null; + } + + BufferedReader br = null; + try { + br = new BufferedReader(new FileReader(fname), 512); + return br.readLine(); + } catch (Exception e) { + Log.w("ThemeUtils", "Failed to read file: " + fname, e); + return null; + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + // Ignore close errors + } + } + } + } } diff --git a/core/java/com/android/internal/util/lunaris/Utils.java b/core/java/com/android/internal/util/lunaris/Utils.java index 8a77e69496915..c4131fd8b8059 100644 --- a/core/java/com/android/internal/util/lunaris/Utils.java +++ b/core/java/com/android/internal/util/lunaris/Utils.java @@ -16,17 +16,28 @@ package com.android.internal.util.lunaris; +import android.app.ActivityManager; +import android.app.IActivityManager; +import android.content.Context; import android.content.Context; import android.content.Intent; +import android.content.om.OverlayManager; +import android.content.om.OverlayManagerTransaction; +import android.content.om.OverlayIdentifier; +import android.content.om.OverlayInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.os.AsyncTask; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; +import android.os.UserHandle; + +import android.util.Log; import com.android.internal.statusbar.IStatusBarService; @@ -35,6 +46,41 @@ public class Utils { + private static final String TAG = "Utils"; + + public static void restartApp(String appName, Context context) { + new RestartAppTask(appName, context).execute(); + } + + private static class RestartAppTask extends AsyncTask { + private Context mContext; + private String mApp; + + public RestartAppTask(String appName, Context context) { + super(); + mContext = context; + mApp = appName; + } + + @Override + protected Void doInBackground(Void... params) { + try { + ActivityManager am = + (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + IActivityManager ams = ActivityManager.getService(); + for (ActivityManager.RunningAppProcessInfo app: am.getRunningAppProcesses()) { + if (mApp.equals(app.processName)) { + ams.killApplicationProcess(app.processName, app.uid); + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + } + public static boolean isPackageInstalled(Context context, String packageName, boolean ignoreState) { if (packageName != null) { try { @@ -111,4 +157,45 @@ public static void restartSystemUI() { } catch (RemoteException e) { } } + + public static void toggleOverlay(Context context, String overlayName, boolean enable) { + OverlayManager overlayManager = context.getSystemService(OverlayManager.class); + if (overlayManager == null) { + Log.e(TAG, "OverlayManager is not available"); + return; + } + + OverlayIdentifier overlayId = getOverlayID(overlayManager, overlayName); + if (overlayId == null) { + Log.e(TAG, "Overlay ID not found for " + overlayName); + return; + } + + OverlayManagerTransaction.Builder transaction = new OverlayManagerTransaction.Builder(); + transaction.setEnabled(overlayId, enable, UserHandle.USER_CURRENT); + + try { + overlayManager.commit(transaction.build()); + } catch (Exception e) { + Log.e(TAG, "Error toggling overlay", e); + } + } + + private static OverlayIdentifier getOverlayID(OverlayManager overlayManager, String name) { + try { + if (name.contains(":")) { + String[] parts = name.split(":"); + List infos = overlayManager.getOverlayInfosForTarget(parts[0], UserHandle.CURRENT); + for (OverlayInfo info : infos) { + if (parts[1].equals(info.getOverlayName())) return info.getOverlayIdentifier(); + } + } else { + OverlayInfo info = overlayManager.getOverlayInfo(name, UserHandle.CURRENT); + if (info != null) return info.getOverlayIdentifier(); + } + } catch (Exception e) { + Log.e(TAG, "Error retrieving overlay ID", e); + } + return null; + } } diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 29363a533a036..3f6098c270d47 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -32,6 +32,7 @@ import com.android.internal.inputmethod.IRemoteComputerControlInputConnection; import com.android.internal.inputmethod.IRemoteInputConnection; import com.android.internal.inputmethod.InputBindResult; import com.android.internal.inputmethod.InputMethodInfoSafeList; +import com.android.internal.inputmethod.InputMethodSubtypeSafeList; /** * Public interface to the global input method manager, used by all client applications. @@ -67,7 +68,7 @@ interface IInputMethodManager { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") - List getEnabledInputMethodSubtypeList(in @nullable String imiId, + InputMethodSubtypeSafeList getEnabledInputMethodSubtypeList(in @nullable String imiId, boolean allowsImplicitlyEnabledSubtypes, int userId); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp index e9e5b7f6c13ac..ea4605eb09d95 100644 --- a/core/jni/android_graphics_BLASTBufferQueue.cpp +++ b/core/jni/android_graphics_BLASTBufferQueue.cpp @@ -188,6 +188,18 @@ static jobject nativeGetSurface(JNIEnv* env, jclass clazz, jlong ptr, queue->getSurface(includeSurfaceControlHandle)); } +static void nativeSetUndequeuedBufferCount(JNIEnv* env, jclass clazz, jlong ptr, jint count) { + auto queue = reinterpret_cast(ptr); + if (queue == nullptr) return; + queue->qtiSetUndequeuedBufferCount(count); +} + +static jint nativeGetUndequeuedBufferCount(JNIEnv* env, jclass clazz, jlong ptr) { + auto queue = reinterpret_cast(ptr); + if (queue == nullptr) return -1; + return queue->qtiGetUndequeuedBufferCount(); +} + class JGlobalRefHolder { public: JGlobalRefHolder(JavaVM* vm, jobject object) : mVm(vm), mObject(object) {} @@ -330,6 +342,8 @@ static const JNINativeMethod gMethods[] = { // clang-format off {"nativeCreate", "(Ljava/lang/String;Z)J", (void*)nativeCreate}, {"nativeGetSurface", "(JZ)Landroid/view/Surface;", (void*)nativeGetSurface}, + {"nativeSetUndequeuedBufferCount", "(JI)V", (void*)nativeSetUndequeuedBufferCount}, + {"nativeGetUndequeuedBufferCount", "(J)I", (void*)nativeGetUndequeuedBufferCount}, {"nativeDestroy", "(J)V", (void*)nativeDestroy}, {"nativeSyncNextTransaction", "(JLjava/util/function/Consumer;Z)Z", (void*)nativeSyncNextTransaction}, {"nativeStopContinuousSyncTransaction", "(J)V", (void*)nativeStopContinuousSyncTransaction}, diff --git a/core/res/res/values-zh-rTW/lunaris_strings.xml b/core/res/res/values-zh-rTW/lunaris_strings.xml new file mode 100644 index 0000000000000..75a28b2b91035 --- /dev/null +++ b/core/res/res/values-zh-rTW/lunaris_strings.xml @@ -0,0 +1,42 @@ + + + + + + 再次滑動即可解鎖手勢 + + + 已將「%s」新增至遊戲空間 + + + 解鎖 %1$s + + + 口袋模式已啟動 + 口袋模式示意圖 + 如要使用手機: + 1. 確保上方顯示的藍色區域(距離感測器)未受遮擋,且沒有髒污或灰塵。 + 2. 長按電源鍵即可退出口袋模式。 + + + 有來電和通知時會響鈴 + + + 需要重新啟動「設定」 + 必須重新啟動「設定」才能套用所有變更,要立即重新啟動嗎? + + + 需要重新啟動「系統」 + 必須重新啟動「系統」才能套用所有變更,要立即重新啟動嗎? + + + 需要重新啟動「系統 UI」 + 必須重新啟動「系統 UI」才能套用所有變更,要立即重新啟動嗎? + + + 需要重新啟動「啟動器」 + 必須重新啟動「啟動器」才能套用所有變更,要立即重新啟動嗎? + diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 00e01e0b1eba3..8253142016487 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4224,7 +4224,7 @@ - 320dp + 260dp false diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 60a0cff5c41f8..6aa31ad0050a3 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -1306,4 +1306,9 @@ 24dp + + + 50dp + 150dp + 86dp diff --git a/core/res/res/values/lunaris_arrays.xml b/core/res/res/values/lunaris_arrays.xml index cbc67262316e5..384921bc490f5 100644 --- a/core/res/res/values/lunaris_arrays.xml +++ b/core/res/res/values/lunaris_arrays.xml @@ -4,16 +4,6 @@ SPDX-License-Identifier: Apache-2.0 --> - - com.google.android.apps.nexuslauncher/com.android.quickstep.RecentsActivity - com.android.launcher3/com.android.quickstep.RecentsActivity - - - - com.google.android.apps.nexuslauncher - com.android.launcher3 - - diff --git a/core/res/res/values/lunaris_config.xml b/core/res/res/values/lunaris_config.xml index 2b5d8553965c7..389ceb6d77b97 100644 --- a/core/res/res/values/lunaris_config.xml +++ b/core/res/res/values/lunaris_config.xml @@ -105,4 +105,12 @@ false + + + /sys/class/thermal/thermal_zone0/temp + 1 + 1 + + + com.android.systemui/com.android.systemui.usb.UsbFunctionActivity diff --git a/core/res/res/values/lunaris_strings.xml b/core/res/res/values/lunaris_strings.xml index 2c7ee6651d417..8ad829b665bce 100644 --- a/core/res/res/values/lunaris_strings.xml +++ b/core/res/res/values/lunaris_strings.xml @@ -23,4 +23,20 @@ Calls and notifications will ring + + + Settings restart required + For all changes to take effect, a Settings app restart is required. Restart Settings app now? + + + System restart required + For all the changes to take effect, a system restart is required. Perform system restart now? + + + SystemUI restart required + For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? + + + Launcher restart required + For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values/lunaris_symbols.xml b/core/res/res/values/lunaris_symbols.xml index 2ab46a7ca6526..7cf47b7541b81 100644 --- a/core/res/res/values/lunaris_symbols.xml +++ b/core/res/res/values/lunaris_symbols.xml @@ -99,4 +99,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/res/res/values/quickswitch_arrays.xml b/core/res/res/values/quickswitch_arrays.xml new file mode 100644 index 0000000000000..f38a024a4335c --- /dev/null +++ b/core/res/res/values/quickswitch_arrays.xml @@ -0,0 +1,30 @@ + + + + + com.android.launcher3/com.android.quickstep.RecentsActivity + com.google.android.apps.nexuslauncher/com.android.quickstep.RecentsActivity + + + + com.android.launcher3 + com.google.android.apps.nexuslauncher + + diff --git a/core/res/res/values/quickswitch_symbols.xml b/core/res/res/values/quickswitch_symbols.xml new file mode 100644 index 0000000000000..87da280f46f7b --- /dev/null +++ b/core/res/res/values/quickswitch_symbols.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index ec82d658425b2..122bcca8c789b 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -6347,4 +6347,9 @@ + + + + + diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java index 87333dd31b8d3..1ccc9e5483298 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java @@ -16,18 +16,27 @@ package android.view.inputmethod; +import static android.view.inputmethod.InputMethodInfo.COMPONENT_NAME_MAX_LENGTH; + import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.annotation.XmlRes; import android.content.Context; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.res.TypedArray; import android.os.Bundle; import android.os.Parcel; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; +import android.view.inputmethod.InputMethodInfo.MetadataReadBytesTracker; +import android.view.inputmethod.InputMethodInfo.TypedArrayWrapper; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -38,6 +47,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.xmlpull.v1.XmlPullParserException; @SmallTest @RunWith(AndroidJUnit4.class) @@ -131,6 +141,94 @@ public void testIsVirtualDeviceOnly() throws Exception { assertThat(clone.isVirtualDeviceOnly(), is(true)); } + @Test + public void testTypedArrayWrapper() throws Exception { + final TypedArray mockTypedArray = mock(TypedArray.class); + when(mockTypedArray.hasValue(0)).thenReturn(true); + when(mockTypedArray.getInt(0, 0)).thenReturn(123); + when(mockTypedArray.getString(1)).thenReturn("hello"); + when(mockTypedArray.hasValue(2)).thenReturn(true); + when(mockTypedArray.getBoolean(2, false)).thenReturn(true); + when(mockTypedArray.hasValue(3)).thenReturn(true); + when(mockTypedArray.getResourceId(3, 0)).thenReturn(456); + + try (TypedArrayWrapper wrapper = TypedArrayWrapper.createForMethod(mockTypedArray, + new MetadataReadBytesTracker())) { + assertThat(wrapper.getInt(0, 0), is(123)); + assertThat(wrapper.getString(1), is("hello")); + assertThat(wrapper.getBoolean(2, false), is(true)); + assertThat(wrapper.getResourceId(3, 0), is(456)); + } + } + + @Test + public void testTypedArrayWrapper_getString_throwsExceptionWhenStringTooLong() + throws Exception { + final TypedArray mockTypedArray = mock(TypedArray.class); + final String longStringA = "a".repeat(COMPONENT_NAME_MAX_LENGTH + 1); + final String longStringB = "b".repeat(COMPONENT_NAME_MAX_LENGTH + 1); + when(mockTypedArray.getString( + com.android.internal.R.styleable.InputMethod_settingsActivity)) + .thenReturn(longStringA); + when(mockTypedArray.getString( + com.android.internal.R.styleable.InputMethod_languageSettingsActivity)) + .thenReturn(longStringB); + + try (TypedArrayWrapper wrapper = TypedArrayWrapper.createForMethod(mockTypedArray, + new MetadataReadBytesTracker())) { + assertThrows( + XmlPullParserException.class, + () -> wrapper.getString( + com.android.internal.R.styleable.InputMethod_settingsActivity)); + assertThrows( + XmlPullParserException.class, + () -> wrapper.getString( + com.android.internal.R.styleable.InputMethod_languageSettingsActivity)); + } + + // The same index can be used for method and subtype for different attributes. + // This verifies the same index returns the correct string for subtypes. + try (TypedArrayWrapper wrapper = TypedArrayWrapper.createForSubtype(mockTypedArray, + new MetadataReadBytesTracker())) { + assertThat(wrapper.getString( + com.android.internal.R.styleable.InputMethod_settingsActivity), + is(longStringA)); + assertThat(wrapper.getString( + com.android.internal.R.styleable.InputMethod_languageSettingsActivity), + is(longStringB)); + } + } + + @Test + public void testTypedArrayWrapper_closeRecyclesTypedArray() { + final TypedArray mockTypedArray = mock(TypedArray.class); + final TypedArrayWrapper wrapper = TypedArrayWrapper.createForMethod(mockTypedArray, + new MetadataReadBytesTracker()); + + wrapper.close(); + + verify(mockTypedArray).recycle(); + } + + @Test + public void testTypedArrayWrapper_metadataReadBytesTracker_throwsExceptionWhenLimitExceeded() { + final TypedArray mockTypedArray = mock(TypedArray.class); + final String longString = "a".repeat(1000); + when(mockTypedArray.getString(0)).thenReturn(longString); + + try (TypedArrayWrapper wrapper = TypedArrayWrapper.createForMethod(mockTypedArray, + new MetadataReadBytesTracker())) { + assertThrows(XmlPullParserException.class, () -> { + // Each character is 2 bytes. 1000 chars * 2 = 2000 bytes per call. + // Limit is 200 * 1024 = 204800 bytes. + // 204800 / 2000 = 102.4. So 103 calls will exceed the limit. + for (int i = 0; i < 103; ++i) { + wrapper.getString(0); + } + }); + } + } + private InputMethodInfo buildInputMethodForTest(final @XmlRes int metaDataRes) throws Exception { final Context context = InstrumentationRegistry.getInstrumentation().getContext(); diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/AbstractSafeListTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/AbstractSafeListTest.java new file mode 100644 index 0000000000000..0f72f095dbe3c --- /dev/null +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/AbstractSafeListTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.inputmethod; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.os.Parcel; +import android.os.Parcelable; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class AbstractSafeListTest { + + private static class TestParcelable implements Parcelable { + final int mData; + + TestParcelable(int data) { + mData = data; + } + + TestParcelable(Parcel parcel) { + mData = parcel.readInt(); + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mData); + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("EffectivelyPrivate") // Parcelable must have CREATOR. + public static final Creator CREATOR = new Creator() { + @Override + public TestParcelable createFromParcel(Parcel parcel) { + return new TestParcelable(parcel); + } + + @Override + public TestParcelable[] newArray(int size) { + return new TestParcelable[size]; + } + }; + } + + @Test + public void testMarshallThenUnmarshall() { + List originalArray = List.of(new TestParcelable(1), new TestParcelable(2)); + byte[] marshalled = AbstractSafeList.marshall(originalArray); + assertNotNull(marshalled); + List unmarshalled = + AbstractSafeList.unmarshall(marshalled, TestParcelable.CREATOR); + assertNotNull(unmarshalled); + assertEquals(originalArray.size(), unmarshalled.size()); + for (int i = 0; i < originalArray.size(); i++) { + assertEquals(originalArray.get(i).mData, unmarshalled.get(i).mData); + } + } + + @Test + public void testMarshallEmptyArray() { + List originalArray = List.of(); + byte[] marshalled = AbstractSafeList.marshall(originalArray); + assertNotNull(marshalled); + List unmarshalled = + AbstractSafeList.unmarshall(marshalled, TestParcelable.CREATOR); + assertNotNull(unmarshalled); + assertEquals(0, unmarshalled.size()); + } +} diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodSubtypeSafeListTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodSubtypeSafeListTest.java new file mode 100644 index 0000000000000..089ffb80d7a90 --- /dev/null +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodSubtypeSafeListTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.inputmethod; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import android.os.Parcel; +import android.platform.test.annotations.Presubmit; +import android.view.inputmethod.InputMethodSubtype; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class InputMethodSubtypeSafeListTest { + + private static InputMethodSubtype createFakeInputMethodSubtype(String locale, String mode) { + return new InputMethodSubtype.InputMethodSubtypeBuilder() + .setSubtypeLocale(locale) + .setSubtypeMode(mode) + .build(); + } + + private static List createTestInputMethodSubtypeList() { + List list = new ArrayList<>(); + list.add(createFakeInputMethodSubtype("en_US", "keyboard")); + list.add(createFakeInputMethodSubtype("ja_JP", "keyboard")); + list.add(createFakeInputMethodSubtype("en_GB", "voice")); + return list; + } + + private static void assertItemsAfterExtract( + List originals, + Function, InputMethodSubtypeSafeList> factory) { + InputMethodSubtypeSafeList list = factory.apply(originals); + List extracted = InputMethodSubtypeSafeList.extractFrom(list); + assertEquals(originals.size(), extracted.size()); + for (int i = 0; i < originals.size(); i++) { + assertNotSame( + "InputMethodSubtypeSafeList.extractFrom() must clone each instance", + originals.get(i), extracted.get(i)); + assertEquals( + "Verify the cloned instances have the equal locale", + originals.get(i).getLocale(), extracted.get(i).getLocale()); + assertEquals( + "Verify the cloned instances have the equal mode", + originals.get(i).getMode(), extracted.get(i).getMode()); + } + + // Subsequent calls of InputMethodSubtypeSafeList.extractFrom() return an empty list. + List extracted2 = InputMethodSubtypeSafeList.extractFrom(list); + assertTrue(extracted2.isEmpty()); + } + + private static InputMethodSubtypeSafeList cloneViaParcel(InputMethodSubtypeSafeList original) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + original.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + InputMethodSubtypeSafeList newInstance = + InputMethodSubtypeSafeList.CREATOR.createFromParcel(parcel); + assertNotNull(newInstance); + return newInstance; + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } + + @Test + public void testCreate() { + assertNotNull(InputMethodSubtypeSafeList.create(createTestInputMethodSubtypeList())); + } + + @Test + public void testExtract() { + assertItemsAfterExtract( + createTestInputMethodSubtypeList(), + InputMethodSubtypeSafeList::create); + } + + @Test + public void testExtractAfterParceling() { + assertItemsAfterExtract( + createTestInputMethodSubtypeList(), + originals -> cloneViaParcel(InputMethodSubtypeSafeList.create(originals))); + } + + @Test + public void testExtractEmptyList() { + assertItemsAfterExtract(Collections.emptyList(), InputMethodSubtypeSafeList::create); + } + + @Test + public void testExtractAfterParcelingEmptyList() { + assertItemsAfterExtract(Collections.emptyList(), + originals -> cloneViaParcel(InputMethodSubtypeSafeList.create(originals))); + } +} diff --git a/data/etc/com.android.dialer.xml b/data/etc/com.android.dialer.xml index 405279f8b1a4f..488b04cd46fd0 100644 --- a/data/etc/com.android.dialer.xml +++ b/data/etc/com.android.dialer.xml @@ -26,5 +26,8 @@ + + + diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index 7f89fd5fad513..4d8c29a816ecc 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -100,5 +100,6 @@ + diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java index 2f40e7f1a4592..44be74194b214 100644 --- a/graphics/java/android/graphics/BLASTBufferQueue.java +++ b/graphics/java/android/graphics/BLASTBufferQueue.java @@ -33,6 +33,8 @@ public final class BLASTBufferQueue { private static native long nativeCreate(String name, boolean updateDestinationFrame); private static native void nativeDestroy(long ptr); + private static native void nativeSetUndequeuedBufferCount(long ptr, int count); + private static native int nativeGetUndequeuedBufferCount(long ptr); private static native Surface nativeGetSurface(long ptr, boolean includeSurfaceControlHandle); private static native boolean nativeSyncNextTransaction(long ptr, Consumer callback, boolean acquireSingleBuffer); @@ -106,6 +108,20 @@ public Surface createSurfaceWithHandle() { return nativeGetSurface(mNativeObject, true /* includeSurfaceControlHandle */); } + /** + * Set undequeued buffer count + */ + public void setUndequeuedBufferCount(int count) { + nativeSetUndequeuedBufferCount(mNativeObject, count); + } + + /** + * @return the count of undequeued buffer + */ + public int getUndequeuedBufferCount() { + return nativeGetUndequeuedBufferCount(mNativeObject); + } + /** * Send a callback that accepts a transaction to BBQ. BBQ will acquire buffers into the a * transaction it created and will eventually send the transaction into the callback 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 index 4c3cf8742e598..8d485c649cead 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java @@ -54,7 +54,7 @@ default void executeRemoteCallWithTaskPermission(RemoteCallable controlle default void executeRemoteCallWithTaskPermission(RemoteCallable controllerInstance, String log, Consumer callback, boolean blocking) { if (controllerInstance == null) return; - + final RemoteCallable controller = controllerInstance; if (!com.android.internal.util.lunaris.PixelPropsUtils.shouldBypassManageActivityTaskPermission( controllerInstance.getContext())) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java index bd23a058e76d7..5e369529fbd88 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java @@ -17,7 +17,7 @@ package com.android.wm.shell.dagger; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; -import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY; import static android.os.Process.THREAD_PRIORITY_FOREGROUND; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; @@ -96,7 +96,7 @@ public static ShellExecutor provideSysUIMainExecutor( * See {@link com.android.systemui.SystemUIFactory#init(Context, boolean)}. */ public static HandlerThread createShellMainThread() { - HandlerThread mainThread = new HandlerThread("wmshell.main", THREAD_PRIORITY_DISPLAY); + HandlerThread mainThread = new HandlerThread("wmshell.main", THREAD_PRIORITY_URGENT_DISPLAY); return mainThread; } @@ -167,7 +167,7 @@ public static Choreographer provideShellMainChoreographer( @Provides @ShellAnimationThread public static Handler provideShellAnimationHandler() { - HandlerThread animThread = new HandlerThread("wmshell.anim", THREAD_PRIORITY_DISPLAY); + HandlerThread animThread = new HandlerThread("wmshell.anim", THREAD_PRIORITY_URGENT_DISPLAY); animThread.start(); if (Build.IS_DEBUGGABLE) { animThread.getLooper().setTraceTag(Trace.TRACE_TAG_WINDOW_MANAGER); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java index 3d211516f6bba..3c193f2baaf32 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java @@ -72,6 +72,9 @@ void makeTaskSnapshotWindow(StartingWindowInfo info, SurfaceControl rootSurface, return; } final Display display = mDisplayManager.getDisplay(runningTaskInfo.displayId); + if (display == null) { + return; + } final StartingSurfaceDrawer.WindowlessStartingWindow wlw = new StartingSurfaceDrawer.WindowlessStartingWindow( mContext.getResources().getConfiguration(), rootSurface); diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 5b3012875ac58..a529618ecf629 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -272,6 +272,14 @@ cc_test { ":FrameworkResourcesNotSparseTestApp", ], test_suites: ["device-tests"], + test_options: { + test_runner_options: [ + { + name: "native-test-timeout", + value: "1m", + } + ] + }, } cc_benchmark { diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 1186772cac865..8d922486ebd35 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -1256,6 +1256,33 @@ base::expected AssetManager2::GetBag( return base::unexpected(entry.error()); } + // Resolve reference resources recursively + auto final_resid = resid; + std::vector referenced_ids; + while (!(entry->entry_flags & ResTable_entry::FLAG_COMPLEX)) { + if (final_resid == 0U || + std::find(referenced_ids.begin(), referenced_ids.end(), + final_resid) != referenced_ids.end()) { + return base::unexpected(std::nullopt); + } + referenced_ids.push_back(final_resid); + auto value = GetResource(final_resid, false, 0); + if (value.has_value() && value->type == Res_value::TYPE_REFERENCE) { + // Follow the reference + final_resid = value->data; + // Re-fetch the entry for the target of the reference + entry = FindEntry(final_resid, 0u, false, false); + + if (!entry.has_value()) { + // Target is still not a bag, can't resolve as a bag + return base::unexpected(std::nullopt); + } + } else { + // Not a bag and not a reference to one + return base::unexpected(std::nullopt); + } + } + auto entry_map = std::get_if>(&entry->entry); if (entry_map == nullptr) { // Not a bag, nothing to do. diff --git a/libs/androidfw/tests/AssetManager2_test.cpp b/libs/androidfw/tests/AssetManager2_test.cpp index 7e1a0add672f2..26186a461836c 100644 --- a/libs/androidfw/tests/AssetManager2_test.cpp +++ b/libs/androidfw/tests/AssetManager2_test.cpp @@ -889,4 +889,100 @@ TEST_F(AssetManager2Test, GetFlaggedAssets) { EXPECT_TRUE(value->entry_flags & ResTable_entry::FLAG_USES_FEATURE_FLAGS); } +TEST_F(AssetManager2Test, FindsBagThroughConcreteStringArray) { + AssetManager2 assetmanager; + assetmanager.SetApkAssets({basic_assets_}); + + auto bag = assetmanager.GetBag(basic::R::array::concrete_string_array); + ASSERT_TRUE(bag.has_value()); + + ASSERT_EQ(3u, (*bag)->entry_count); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[0].value.dataType); + EXPECT_EQ(std::string("a"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[0].cookie), (*bag)->entries[0].value.data)); + EXPECT_EQ(0, (*bag)->entries[0].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[1].value.dataType); + EXPECT_EQ(std::string("b"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[1].cookie), (*bag)->entries[1].value.data)); + EXPECT_EQ(0, (*bag)->entries[1].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[2].value.dataType); + EXPECT_EQ(std::string("c"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[2].cookie), (*bag)->entries[2].value.data)); + EXPECT_EQ(0, (*bag)->entries[2].cookie); +} + +TEST_F(AssetManager2Test, FindsBagThroughSingleReference) { + AssetManager2 assetmanager; + assetmanager.SetApkAssets({basic_assets_}); + + auto bag = assetmanager.GetBag(basic::R::array::aliased_string_array); + ASSERT_TRUE(bag.has_value()); + + ASSERT_EQ(3u, (*bag)->entry_count); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[0].value.dataType); + EXPECT_EQ(std::string("a"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[0].cookie), (*bag)->entries[0].value.data)); + EXPECT_EQ(0, (*bag)->entries[0].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[1].value.dataType); + EXPECT_EQ(std::string("b"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[1].cookie), (*bag)->entries[1].value.data)); + EXPECT_EQ(0, (*bag)->entries[1].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[2].value.dataType); + EXPECT_EQ(std::string("c"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[2].cookie), (*bag)->entries[2].value.data)); + EXPECT_EQ(0, (*bag)->entries[2].cookie); +} + +TEST_F(AssetManager2Test, FindsBagThroughDoubleReference) { + AssetManager2 assetmanager; + assetmanager.SetApkAssets({basic_assets_}); + + auto bag = assetmanager.GetBag(basic::R::array::double_aliased_string_array); + ASSERT_TRUE(bag.has_value()); + + ASSERT_EQ(3u, (*bag)->entry_count); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[0].value.dataType); + EXPECT_EQ(std::string("a"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[0].cookie), (*bag)->entries[0].value.data)); + EXPECT_EQ(0, (*bag)->entries[0].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[1].value.dataType); + EXPECT_EQ(std::string("b"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[1].cookie), (*bag)->entries[1].value.data)); + EXPECT_EQ(0, (*bag)->entries[1].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[2].value.dataType); + EXPECT_EQ(std::string("c"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[2].cookie), (*bag)->entries[2].value.data)); + EXPECT_EQ(0, (*bag)->entries[2].cookie); +} + +TEST_F(AssetManager2Test, DetectsCircularBag) { + // If the test fails because it is hung you will see the following: + // Result: died but not with expected exit code: + // Terminated by signal 14 + // caused by the alarm(1) timeout. + EXPECT_EXIT({ + alarm(1); + AssetManager2 assetmanager; + assetmanager.SetApkAssets({basic_assets_}); + + auto bag = assetmanager.GetBag(basic::R::array::circular_aliased_string_array1); + if (!bag.has_value()) { + // Success, a circular reference should not return a bag + exit(0); + } + // Fail, we returned a bag + fprintf(stderr, "GetBag returned a bag from a circular reference!"); + exit(1); + }, ::testing::ExitedWithCode(0), ""); +} + } // namespace android diff --git a/libs/androidfw/tests/LoadedArsc_test.cpp b/libs/androidfw/tests/LoadedArsc_test.cpp index eb90765cd17a4..3bf4ade500844 100644 --- a/libs/androidfw/tests/LoadedArsc_test.cpp +++ b/libs/androidfw/tests/LoadedArsc_test.cpp @@ -288,6 +288,12 @@ TEST(LoadedArscTest, ResourceIdentifierIterator) { ASSERT_EQ(0x7f050000u, *iter++); ASSERT_EQ(0x7f050001u, *iter++); ASSERT_EQ(0x7f060000u, *iter++); + ASSERT_EQ(0x7f060001u, *iter++); + ASSERT_EQ(0x7f060002u, *iter++); + ASSERT_EQ(0x7f060003u, *iter++); + ASSERT_EQ(0x7f060004u, *iter++); + ASSERT_EQ(0x7f060005u, *iter++); + ASSERT_EQ(0x7f060006u, *iter++); ASSERT_EQ(0x7f070000u, *iter++); ASSERT_EQ(0x7f070001u, *iter++); ASSERT_EQ(0x7f070002u, *iter++); diff --git a/libs/androidfw/tests/data/basic/R.h b/libs/androidfw/tests/data/basic/R.h index b7e814fea079b..d6eb0e41e461f 100644 --- a/libs/androidfw/tests/data/basic/R.h +++ b/libs/androidfw/tests/data/basic/R.h @@ -73,6 +73,10 @@ struct R { struct array { enum : uint32_t { integerArray1 = 0x7f060000, + concrete_string_array = 0x7f060001, + aliased_string_array = 0x7f060002, + double_aliased_string_array = 0x7f060003, + circular_aliased_string_array1 = 0x7f060004, }; }; }; diff --git a/libs/androidfw/tests/data/basic/basic.apk b/libs/androidfw/tests/data/basic/basic.apk index b721ebfde4459..42a50eaf70d30 100644 Binary files a/libs/androidfw/tests/data/basic/basic.apk and b/libs/androidfw/tests/data/basic/basic.apk differ diff --git a/libs/androidfw/tests/data/basic/basic_de_fr.apk b/libs/androidfw/tests/data/basic/basic_de_fr.apk index 767dff6fcfa5f..a3f52d600c97d 100644 Binary files a/libs/androidfw/tests/data/basic/basic_de_fr.apk and b/libs/androidfw/tests/data/basic/basic_de_fr.apk differ diff --git a/libs/androidfw/tests/data/basic/basic_hdpi-v4.apk b/libs/androidfw/tests/data/basic/basic_hdpi-v4.apk index 58953f56d736d..474f63fc1a095 100644 Binary files a/libs/androidfw/tests/data/basic/basic_hdpi-v4.apk and b/libs/androidfw/tests/data/basic/basic_hdpi-v4.apk differ diff --git a/libs/androidfw/tests/data/basic/basic_xhdpi-v4.apk b/libs/androidfw/tests/data/basic/basic_xhdpi-v4.apk index 103f6565bb066..225c04e9b4010 100644 Binary files a/libs/androidfw/tests/data/basic/basic_xhdpi-v4.apk and b/libs/androidfw/tests/data/basic/basic_xhdpi-v4.apk differ diff --git a/libs/androidfw/tests/data/basic/basic_xxhdpi-v4.apk b/libs/androidfw/tests/data/basic/basic_xxhdpi-v4.apk index 61369d5067862..e10788980613a 100644 Binary files a/libs/androidfw/tests/data/basic/basic_xxhdpi-v4.apk and b/libs/androidfw/tests/data/basic/basic_xxhdpi-v4.apk differ diff --git a/libs/androidfw/tests/data/basic/res/values/values.xml b/libs/androidfw/tests/data/basic/res/values/values.xml index d4b2683c62e18..beb64b11161b7 100644 --- a/libs/androidfw/tests/data/basic/res/values/values.xml +++ b/libs/androidfw/tests/data/basic/res/values/values.xml @@ -74,12 +74,29 @@ 3 - - - + + + + + @id/middle_ref @id/low_ref + + + + a + b + c + + + @array/concrete_string_array + + @array/aliased_string_array + + @array/circular_aliased_string_array2 + @array/circular_aliased_string_array3 + @array/circular_aliased_string_array1 diff --git a/libs/hwui/platform/android/thread/CommonPoolBase.h b/libs/hwui/platform/android/thread/CommonPoolBase.h index 8f836b6124407..42d0d113ff170 100644 --- a/libs/hwui/platform/android/thread/CommonPoolBase.h +++ b/libs/hwui/platform/android/thread/CommonPoolBase.h @@ -41,7 +41,7 @@ class CommonPoolBase { tids[i] = pthread_gettid_np(self); tidConditionVars[i].notify_one(); } - setpriority(PRIO_PROCESS, 0, PRIORITY_FOREGROUND); + setpriority(PRIO_PROCESS, 0, PRIORITY_URGENT_DISPLAY); auto startHook = renderthread::RenderThread::getOnStartHook(); if (startHook) { startHook(name.data()); diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java index 917640d993473..61ef04d1762b5 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -101,7 +101,7 @@ */ public class ExifInterface { private static final String TAG = "ExifInterface"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final boolean DEBUG = false; // The Exif tag names. See Tiff 6.0 Section 3 and Section 8. /** Type is String. */ @@ -2037,10 +2037,12 @@ private void loadAttributes(@NonNull InputStream in) { // Ignore exceptions in order to keep the compatibility with the old versions of // ExifInterface. mIsSupportedFile = false; - Log.d( - TAG, - "Invalid image: ExifInterface got an unsupported or corrupted image file", - e); + if (DEBUG) { + Log.d( + TAG, + "Invalid image: ExifInterface got an unsupported or corrupted image file", + e); + } } finally { addDefaultValuesForCompatibility(); diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index a0bf7faa3151c..161da450fa4d8 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -204,7 +204,7 @@ public MediaSession(@NonNull Context context, @NonNull String tag, int bitmapSize = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize); - mMaxBitmapSize = Math.min(bitmapSize, 500); + mMaxBitmapSize = Math.min(bitmapSize, 460); mCbStub = new CallbackStub(this); MediaSessionManager manager = (MediaSessionManager) context diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp index 1e48c1f6d7bbb..97da7b8577501 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -443,7 +443,7 @@ int APerformanceHintManager::createSessionUsingConfig(ASessionCreationConfig* se sessionCreationConfig->layerTokens.clear(); if (!ret.isOk() || !returnValue.session) { - ALOGE("%s: PerformanceHint cannot create session. %s", __FUNCTION__, ret.getMessage()); + //ALOGE("%s: PerformanceHint cannot create session. %s", __FUNCTION__, ret.getMessage()); switch (ret.getExceptionCode()) { case binder::Status::EX_UNSUPPORTED_OPERATION: return ENOTSUP; diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java index d28b15fae4548..d1642719470ec 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java @@ -131,7 +131,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { boolean isDocumentsManager = checkPermission(Manifest.permission.MANAGE_DOCUMENTS, -1, callingUid) == PackageManager.PERMISSION_GRANTED; boolean isSystemDownloadsProvider = PackageUtil.getSystemDownloadsProviderInfo( - mPackageManager, callingUid) != null; + mPackageManager, callingUid) != null; // By default, the originatingUid is callingUid. If the caller is the system download // provider or the documents manager, we parse the originatingUid from the @@ -151,7 +151,18 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { && checkPermission(Manifest.permission.INSTALL_PACKAGES, /* pid= */ -1, originatingUid) == PackageManager.PERMISSION_GRANTED; - boolean isTrustedSource = isPrivilegedAndKnown || isInstallPkgPermissionGranted; + // Bypass the unknown source user restrictions check when either of the following + // two conditions is met: + // 1. An installer with the INSTALL_PACKAGES permission initiated the + // installation via the PackageInstaller APIs and not via an + // ACTION_VIEW or ACTION_INSTALL_PACKAGE intent. + // 2. An installer is a privileged app and it has set the + // EXTRA_NOT_UNKNOWN_SOURCE flag to be true in the intent. + final boolean isIntentInstall = + Intent.ACTION_VIEW.equals(intentAction) + || Intent.ACTION_INSTALL_PACKAGE.equals(intentAction); + final boolean isTrustedSource = + (!isIntentInstall && isInstallPkgPermissionGranted) || isPrivilegedAndKnown; // In general case, the originatingUid is callingUid. If callingUid is INVALID_UID, return // InstallAborted in the check above. When the originatingUid is INVALID_UID here, it means @@ -346,7 +357,7 @@ private boolean isCallerSessionOwner(int callingUid, int sessionId) { private void checkDevicePolicyRestrictions(boolean isTrustedSource) { String[] restrictions; - if(isTrustedSource) { + if (isTrustedSource) { restrictions = new String[] { UserManager.DISALLOW_INSTALL_APPS }; } else { restrictions = new String[] { diff --git a/packages/SettingsLib/SettingsTheme/res/values/themes.xml b/packages/SettingsLib/SettingsTheme/res/values/themes.xml index 2d881d1a8a7b3..0cbab1439550b 100644 --- a/packages/SettingsLib/SettingsTheme/res/values/themes.xml +++ b/packages/SettingsLib/SettingsTheme/res/values/themes.xml @@ -32,11 +32,9 @@ + + diff --git a/packages/SystemUI/res/values/lineage_dimens.xml b/packages/SystemUI/res/values/lineage_dimens.xml index 4a24b9d357147..2b695422554e7 100644 --- a/packages/SystemUI/res/values/lineage_dimens.xml +++ b/packages/SystemUI/res/values/lineage_dimens.xml @@ -18,4 +18,7 @@ 24dp + + + 24dp diff --git a/packages/SystemUI/res/values/lunaris_attrs.xml b/packages/SystemUI/res/values/lunaris_attrs.xml index 30f90e3916764..46f9d23364b0c 100644 --- a/packages/SystemUI/res/values/lunaris_attrs.xml +++ b/packages/SystemUI/res/values/lunaris_attrs.xml @@ -3,7 +3,7 @@ Copyright (C) 2024-2026 Lunaris AOSP SPDX-License-Identifier: Apache-2.0 --> - + @@ -16,4 +16,12 @@ + + + + + + + + diff --git a/packages/SystemUI/res/values/lunaris_dimens.xml b/packages/SystemUI/res/values/lunaris_dimens.xml index b3a04ce2e7681..7f095106957ee 100644 --- a/packages/SystemUI/res/values/lunaris_dimens.xml +++ b/packages/SystemUI/res/values/lunaris_dimens.xml @@ -122,4 +122,21 @@ 28dp 0dp 8dp + + + 180dp + 0dp + 0dp + + + 70dp + 18sp + 10sp + 10sp + 4sp + 5dp + + + 28dp + 16dp diff --git a/packages/SystemUI/res/values/lunaris_strings.xml b/packages/SystemUI/res/values/lunaris_strings.xml index f248b2473038a..f30dd0da74c70 100644 --- a/packages/SystemUI/res/values/lunaris_strings.xml +++ b/packages/SystemUI/res/values/lunaris_strings.xml @@ -25,6 +25,9 @@ Data disabled icon + + Disable combined data icon + 4G icon @@ -200,4 +203,23 @@ Re-evaluating system theme.. Please wait for a few seconds + + Lunaris AOSP + + + No data transfer + File Transfer + PTP + Webcam + USB tethering + MIDI + Use USB for + + + VPN tethering + VPN tethering turned off. + VPN tethering turned on. + + + null diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml index c16725682a82a..b7ca14c166414 100644 --- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml +++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml @@ -52,6 +52,16 @@ app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> + + + + 1f - keyguardTransitionInteractor.getStartedState() -> 1f + when { + keyguardTransitionInteractor.getCurrentState() == AOD -> 1f + keyguardTransitionInteractor.getStartedState() == AOD -> 1f + keyguardTransitionInteractor.getCurrentState() == DOZING -> 1f + keyguardTransitionInteractor.getStartedState() == DOZING -> 1f else -> 0f } ) @@ -618,6 +620,10 @@ constructor( it.copy(value = 1f - it.value) }, keyguardTransitionInteractor.transition(Edge.create(LOCKSCREEN, AOD)), + keyguardTransitionInteractor.transition(Edge.create(DOZING, LOCKSCREEN)).map { + it.copy(value = 1f - it.value) + }, + keyguardTransitionInteractor.transition(Edge.create(LOCKSCREEN, DOZING)), ) .filter { it.transitionState != TransitionState.FINISHED } .collect { handleDoze(it.value) } @@ -646,7 +652,7 @@ constructor( keyguardTransitionInteractor .transition(Edge.create(to = LOCKSCREEN)) .filter { it.transitionState == TransitionState.STARTED } - .filter { it.from != AOD } + .filter { it.from != AOD && it.from != DOZING } .filter { !com.android.systemui.Flags.newDozingKeyguardStates() || it.from != DOZING } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java index 2cccb62d62992..b286accef3884 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java @@ -87,7 +87,7 @@ private void updateEmergencyButtonVisibility() { boolean showButton = Settings.Secure.getInt( mContext.getContentResolver(), Settings.Secure.LOCKSCREEN_SHOW_EMERGENCY_BUTTON, - 0) == 1; + 1) == 1; mEcaView.setVisibility(showButton ? View.VISIBLE : View.GONE); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java index a46b4443dcac7..1d0daa58bd5fe 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java @@ -276,7 +276,7 @@ private void updateEmergencyButtonVisibility() { boolean showButton = Settings.Secure.getInt( mContext.getContentResolver(), Settings.Secure.LOCKSCREEN_SHOW_EMERGENCY_BUTTON, - 0) == 1; + 1) == 1; mEcaView.setVisibility(showButton ? View.VISIBLE : View.GONE); } } diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index db2956a6b23cf..42be88397bda2 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -41,6 +41,7 @@ import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.rotation.RotationPolicyWrapper; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; @@ -55,6 +56,16 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; import com.android.systemui.tuner.TunerService; +import com.android.systemui.statusbar.policy.HotspotController; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.bluetooth.ui.viewModel.BluetoothDetailsContentViewModel; +import com.android.systemui.statusbar.connectivity.AccessPointController; +import com.android.systemui.statusbar.connectivity.NetworkController; +import com.android.systemui.qs.tiles.dialog.InternetDialogManager; +import com.android.systemui.media.dialog.MediaOutputDialogManager; +import com.android.systemui.statusbar.phone.ScrimController; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.FlashlightController; import dagger.Lazy; @@ -152,6 +163,16 @@ public class Dependency { @Inject Lazy mUserTrackerLazy; @Inject Lazy mStatusBarWindowControllerStoreLazy; @Inject Lazy mSysUIStateDisplaysInteractor; + @Inject Lazy mRotationPolicyWrapperLazy; + @Inject Lazy mActivityStarter; + @Inject Lazy mAccessPointController; + @Inject Lazy mNetworkController; + @Inject Lazy mInternetDialogManager; + @Inject Lazy mMediaOutputDialogManager; + @Inject Lazy mConfigurationController; + @Inject Lazy mFlashlightController; + @Inject Lazy mBluetoothDetailsContentViewModel; + @Inject Lazy mHotspotController; @Inject public Dependency() { @@ -199,6 +220,16 @@ protected void start() { mProviders.put(SysUIStateDisplaysInteractor.class, mSysUIStateDisplaysInteractor::get); mProviders.put( StatusBarWindowControllerStore.class, mStatusBarWindowControllerStoreLazy::get); + mProviders.put(RotationPolicyWrapper.class, mRotationPolicyWrapperLazy::get); + mProviders.put(MediaOutputDialogManager.class, mMediaOutputDialogManager::get); + mProviders.put(AccessPointController.class, mAccessPointController::get); + mProviders.put(NetworkController.class, mNetworkController::get); + mProviders.put(InternetDialogManager.class, mInternetDialogManager::get); + mProviders.put(ConfigurationController.class, mConfigurationController::get); + mProviders.put(FlashlightController.class, mFlashlightController::get); + mProviders.put(BluetoothDetailsContentViewModel.class, mBluetoothDetailsContentViewModel::get); + mProviders.put(ActivityStarter.class, mActivityStarter::get); + mProviders.put(HotspotController.class, mHotspotController::get); Dependency.setInstance(this); } diff --git a/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java b/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java index b25a362fb924c..fc25854f3768e 100644 --- a/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java @@ -835,8 +835,9 @@ public LauncherProxyService(Context context, mShadeDisplayPolicy = shadeDisplayPolicy; mUserTracker = userTracker; mConnectionBackoffAttempts = 0; - mRecentsComponentName = ComponentName.unflattenFromString(context.getString( - com.android.internal.R.string.config_recentsComponentName)); + int defaultLauncher = android.os.SystemProperties.getInt("persist.sys.default_launcher", 0); + String[] launcherComponents = context.getResources().getStringArray(com.android.internal.R.array.config_launcherComponents); + mRecentsComponentName = ComponentName.unflattenFromString(launcherComponents[defaultLauncher]); mQuickStepIntent = new Intent(ACTION_QUICKSTEP) .setPackage(mRecentsComponentName.getPackageName()); mPerDisplaySysUiStateRepository = perDisplaySysUiStateRepository; diff --git a/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt index 3a4ca954f6257..172943642f7b9 100644 --- a/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + package com.android.systemui.axion.volume import android.content.Context @@ -49,8 +51,14 @@ class AxionVolumeDialog @Inject constructor( init { with(window!!) { + clearFlags( + WindowManager.LayoutParams.FLAG_DIM_BEHIND or + WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR + ) addFlags( - WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED ) @@ -129,11 +137,13 @@ class AxionVolumeDialog @Inject constructor( } } - MaterialTheme( - colorScheme = if (isSystemInDarkTheme()) + MaterialExpressiveTheme( + colorScheme = if (isSystemInDarkTheme()) { dynamicDarkColorScheme(LocalContext.current) - else + } else { dynamicLightColorScheme(LocalContext.current) + }, + MotionScheme.expressive() ) { Box( modifier = Modifier diff --git a/packages/SystemUI/src/com/android/systemui/axion/volume/repository/VolumeRepository.kt b/packages/SystemUI/src/com/android/systemui/axion/volume/repository/VolumeRepository.kt index 1705e5d57f604..49492405d73b7 100644 --- a/packages/SystemUI/src/com/android/systemui/axion/volume/repository/VolumeRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/axion/volume/repository/VolumeRepository.kt @@ -37,6 +37,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.plugins.VolumeDialogController +import com.android.systemui.volume.VolumeDialogControllerImpl import javax.inject.Inject import kotlinx.coroutines.* import kotlinx.coroutines.channels.awaitClose @@ -315,7 +316,8 @@ class AxionVolumeRepositoryImpl @Inject constructor( AudioManager.STREAM_VOICE_CALL ) - if (state.activeStream != -1 && state.activeStream !in excludedActiveStreams) { + if (state.activeStream != -1 && state.activeStream !in excludedActiveStreams && + state.states.get(state.activeStream) != null) { streams.add(state.activeStream) } @@ -332,7 +334,13 @@ class AxionVolumeRepositoryImpl @Inject constructor( val max = getMaxVolume(streamType) val min = getMinVolume(streamType) val volume = (min + (level * (max - min))).toInt().coerceIn(min, max) - audioManager.setStreamVolume(streamType, volume, flags) + + if (streamType >= VolumeDialogControllerImpl.DYNAMIC_STREAM_REMOTE_START_INDEX || + streamType == VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST) { + controller.setStreamVolume(streamType, volume, false) + } else { + audioManager.setStreamVolume(streamType, volume, flags) + } } override fun setMute(streamType: Int, muted: Boolean) { @@ -365,11 +373,27 @@ class AxionVolumeRepositoryImpl @Inject constructor( controller.userActivity() } - override fun getMaxVolume(streamType: Int): Int = - audioManager.getStreamMaxVolume(streamType) + override fun getMaxVolume(streamType: Int): Int { + val state = controllerState.value + if (state != null) { + val ss = state.states.get(streamType) + if (ss != null) { + return ss.levelMax + } + } + return audioManager.getStreamMaxVolume(streamType) + } - override fun getMinVolume(streamType: Int): Int = - audioManager.getStreamMinVolume(streamType) + override fun getMinVolume(streamType: Int): Int { + val state = controllerState.value + if (state != null) { + val ss = state.states.get(streamType) + if (ss != null) { + return ss.levelMin + } + } + return audioManager.getStreamMinVolume(streamType) + } override fun isStreamMuted(streamType: Int): Boolean = audioManager.isStreamMute(streamType) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt index 5b665ed550e10..895779840b805 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt @@ -32,7 +32,7 @@ import com.android.app.animation.Interpolators import com.android.settingslib.Utils import com.android.systemui.surfaceeffects.ripple.RippleShader -private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f +private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.7f /** * Handles two ripple effects: dwell ripple and unlocked ripple @@ -76,7 +76,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at private var radius: Float = 0f set(value) { field = value * .9f - rippleShader.rippleSize.setMaxSize(field * 2f, field * 2f) + rippleShader.rippleSize.setMaxSize(field * 2.2f, field * 2.2f) } private var origin: Point = Point() set(value) { @@ -95,7 +95,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at dwellShader.color = 0xffffffff.toInt() // default color dwellShader.progress = 0f - dwellShader.distortionStrength = .4f + dwellShader.distortionStrength = .8f dwellPaint.shader = dwellShader visibility = GONE } @@ -109,7 +109,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at origin = location radius = maxOf(location.x, location.y, width - location.x, height - location.y).toFloat() dwellOrigin = location - dwellRadius = sensorRadius * 1.5f + dwellRadius = sensorRadius * 1.7f } /** diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index 1ba29a25f010c..df60421bd0680 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -34,6 +34,7 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -73,16 +74,21 @@ import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView @@ -99,6 +105,8 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.compose.PlatformSlider +import com.android.compose.PlatformSliderDefaults import com.android.compose.modifiers.padding import com.android.compose.modifiers.sliderPercentage import com.android.compose.modifiers.thenIf @@ -156,6 +164,8 @@ fun BrightnessSlider( val cr = context.contentResolver var hapticsEnabled by remember { mutableStateOf(readEnableHaptics(cr)) } + var useAxStyle by remember { mutableStateOf(readUseAxStyle(cr)) } + var showAutoBrightness by remember { mutableStateOf(readShowAutoBrightness(cr)) } val shapeMode = rememberSliderShapeMode() val trackCornerDp: Dp = when (shapeMode) { @@ -165,6 +175,8 @@ fun BrightnessSlider( else -> Dimensions.SliderTrackRoundedCorner } + val brightnessGradient = brightnessSliderGradient() + var value by remember(gammaValue) { mutableIntStateOf(gammaValue) } val animatedValue by animateFloatAsState(targetValue = value.toFloat(), label = "BrightnessSliderAnimatedValue") @@ -173,6 +185,7 @@ fun BrightnessSlider( val enabled = !isRestricted val contentDescription = stringResource(R.string.accessibility_brightness) val interactionSource = remember { MutableInteractionSource() } + val hapticsViewModel: SliderHapticsViewModel? = if (hapticsEnabled) { rememberViewModel(traceName = "SliderHapticsViewModel") { @@ -189,7 +202,136 @@ fun BrightnessSlider( } else { null } - val colors = colors() + + val hasAutoBrightness = context.resources.getBoolean( + com.android.internal.R.bool.config_automatic_brightness_available + ) + + DisposableEffect(Unit) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + hapticsEnabled = readEnableHaptics(cr) + useAxStyle = readUseAxStyle(cr) + showAutoBrightness = readShowAutoBrightness(cr) + } + } + } + + cr.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_HAPTIC), + false, observer, UserHandle.USER_ALL + ) + + cr.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_STYLE), + false, observer, UserHandle.USER_ALL + ) + + cr.registerContentObserver( + LineageSettings.Secure.getUriFor(LineageSettings.Secure.QS_SHOW_AUTO_BRIGHTNESS), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + cr.unregisterContentObserver(observer) + } + } + + if (useAxStyle) { + val iconRes by + remember(gammaValue, valueRange) { + derivedStateOf { + val percentage = + (value - valueRange.first) * 100f / (valueRange.last - valueRange.first) + iconResProvider(percentage) + } + } + val axIconRes = if (autoMode) R.drawable.ic_qs_brightness_auto_on else iconRes + val axIconSize = 56.dp + val iconTapScope = rememberCoroutineScope() + val axGradientTrackColor: Color = if (brightnessGradient != null) { + val mode = rememberGradientColorMode() + if (mode == 1) rememberGradientCustomColors().first + else MaterialTheme.colorScheme.primary + } else { + CustomColorScheme.current.qsTileColor + } + val sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors().copy( + trackColor = axGradientTrackColor, + ) + + Box(modifier = modifier) { + PlatformSlider( + value = animatedValue, + onValueChange = { + if (enabled && !overriddenByAppState) { + hapticsViewModel?.onValueChange(it) + value = it.toInt() + onDrag(value) + } + }, + onValueChangeFinished = { + if (enabled && !overriddenByAppState) { + hapticsViewModel?.onValueChangeEnded() + onStop(value) + } + }, + valueRange = floatValueRange, + enabled = enabled, + interactionSource = interactionSource, + colors = sliderColors, + modifier = Modifier + .fillMaxWidth() + .height(axIconSize) + .sysuiResTag("slider") + .semantics(mergeDescendants = true) { + this.text = AnnotatedString(contentDescription) + } + .sliderPercentage { + (value - valueRange.first).toFloat() / (valueRange.last - valueRange.first) + } + .thenIf(isRestricted) { + Modifier.clickable { + if (restriction is PolicyRestriction.Restricted) { + onRestrictedClick(restriction) + } + } + }, + icon = { _ -> + Icon( + painter = painterResource(axIconRes), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + }, + ) + + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .size(axIconSize) + .clip(CircleShape) + .pointerInput(Unit) { + detectTapGestures { + iconTapScope.launch { onIconClick() } + } + } + ) + } + + val currentShowToast by rememberUpdatedState(showToast) + LaunchedEffect(interactionSource, overriddenByAppState) { + interactionSource.interactions.collect { interaction -> + if (interaction is DragInteraction.Start && overriddenByAppState) { + currentShowToast() + } + } + } + return + } + + val colors = colors(brightnessGradient) // The value state is recreated every time gammaValue changes, so we recreate this derivedState // We have to use value as that's the value that changes when the user is dragging (gammaValue @@ -236,36 +378,6 @@ fun BrightnessSlider( } } - val hasAutoBrightness = context.resources.getBoolean( - com.android.internal.R.bool.config_automatic_brightness_available - ) - var showAutoBrightness by remember { mutableStateOf(readShowAutoBrightness(cr)) } - - DisposableEffect(Unit) { - val observer = object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) { - context.mainExecutor.execute { - showAutoBrightness = readShowAutoBrightness(cr) - hapticsEnabled = readEnableHaptics(cr) - } - } - } - - cr.registerContentObserver( - LineageSettings.Secure.getUriFor(LineageSettings.Secure.QS_SHOW_AUTO_BRIGHTNESS), - false, observer, UserHandle.USER_ALL - ) - - cr.registerContentObserver( - Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_HAPTIC), - false, observer, UserHandle.USER_ALL - ) - - onDispose { - cr.unregisterContentObserver(observer) - } - } - Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier @@ -370,12 +482,34 @@ fun BrightnessSlider( ) if (activeTrackEnd > 0f) { - drawRoundRect( - color = colors.activeTrackColor, - topLeft = Offset(0f, 0f), - size = Size(activeTrackEnd, trackHeight), - cornerRadius = cornerRadius - ) + val gradient = brightnessGradient + if (gradient != null) { + val clipPath = Path().apply { + addRoundRect( + RoundRect( + left = 0f, + top = 0f, + right = activeTrackEnd.coerceAtMost(trackWidth), + bottom = trackHeight, + cornerRadius = cornerRadius, + ) + ) + } + clipPath(clipPath) { + drawRect( + brush = gradient.brush, + topLeft = Offset.Zero, + size = Size(trackWidth, trackHeight), + ) + } + } else { + drawRoundRect( + color = colors.activeTrackColor, + topLeft = Offset(0f, 0f), + size = Size(activeTrackEnd, trackHeight), + cornerRadius = cornerRadius + ) + } } val yOffset = trackHeight / 2 - IconSize.toSize().height / 2 @@ -412,6 +546,7 @@ fun BrightnessSlider( drawAutoBrightnessButton( autoMode = autoMode, hapticsEnabled = hapticsEnabled, + brightnessGradient = brightnessGradient, onIconClick = onIconClick ) } @@ -469,6 +604,127 @@ fun rememberSliderShapeMode(): Int { return shapeMode } +private data class BrightnessGradient(val brush: Brush) + +@Composable +private fun rememberSliderGradient(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readEnabled(): Boolean = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_BRIGHTNESS_SLIDER_GRADIENT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { false } + + var enabled by remember { mutableStateOf(readEnabled()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + enabled = readEnabled() + } + } + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_GRADIENT), + false, observer, UserHandle.USER_ALL + ) + onDispose { contentResolver.unregisterContentObserver(observer) } + } + + return enabled +} + +@Composable +private fun rememberGradientColorMode(): Int { + val contentResolver = LocalContext.current.contentResolver + + fun readMode(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_COLOR_MODE, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { 0 } + + var mode by remember { mutableIntStateOf(readMode()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { mode = readMode() } + } + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_COLOR_MODE), + false, observer, UserHandle.USER_ALL + ) + onDispose { contentResolver.unregisterContentObserver(observer) } + } + + return mode +} + +@Composable +private fun rememberGradientCustomColors(): Pair { + val contentResolver = LocalContext.current.contentResolver + + fun readStart(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_START_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { 0 } + + fun readEnd(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_END_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { 0 } + + var startInt by remember { mutableIntStateOf(readStart()) } + var endInt by remember { mutableIntStateOf(readEnd()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + startInt = readStart() + endInt = readEnd() + } + } + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_START_COLOR), + false, observer, UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_END_COLOR), + false, observer, UserHandle.USER_ALL + ) + onDispose { contentResolver.unregisterContentObserver(observer) } + } + + val start = if (startInt != 0) Color(startInt) else MaterialTheme.colorScheme.primary + val end = if (endInt != 0) Color(endInt) else MaterialTheme.colorScheme.secondary + return start to end +} + +@Composable +private fun brightnessSliderGradient(): BrightnessGradient? { + if (!rememberSliderGradient()) return null + + val mode = rememberGradientColorMode() + val colors = if (mode == 1) { + val (start, end) = rememberGradientCustomColors() + listOf(start, end) + } else { + listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.secondary, + ) + } + + return BrightnessGradient(brush = Brush.horizontalGradient(colors)) +} + private fun Modifier.sliderBackground(color: Color, corner: Dp) = drawWithCache { val offsetAround = SliderBackgroundFrameSize.toSize() val newSize = Size(size.width + 2 * offsetAround.width, size.height + 2 * offsetAround.height) @@ -499,10 +755,21 @@ private fun readEnableHaptics(cr: ContentResolver): Boolean = false } +private fun readUseAxStyle(cr: ContentResolver): Boolean = + try { + Settings.System.getIntForUser( + cr, Settings.System.QS_BRIGHTNESS_SLIDER_STYLE, + 0, UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + @Composable private fun drawAutoBrightnessButton( autoMode: Boolean, hapticsEnabled: Boolean, + brightnessGradient: BrightnessGradient?, onIconClick: suspend () -> Unit, ) { val view = LocalView.current @@ -521,9 +788,10 @@ private fun drawAutoBrightnessButton( 3 -> RoundedCornerShape(0.dp) else -> RoundedCornerShape(animatedCornerRadius) } + val autoIconBrush: Brush? = if (autoMode) brightnessGradient?.brush else null val backgroundColor by animateColorAsState( targetValue = if (autoMode) { - MaterialTheme.colorScheme.primary + if (autoIconBrush == null) MaterialTheme.colorScheme.primary else Color.Unspecified } else { CustomColorScheme.current.qsTileColor } @@ -550,7 +818,10 @@ private fun drawAutoBrightnessButton( modifier = Modifier .size(TrackHeight) .clip(autoIconShape) - .background(backgroundColor) + .then( + if (autoIconBrush != null) Modifier.background(autoIconBrush) + else Modifier.background(backgroundColor) + ) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, // Disable ripple effect @@ -724,9 +995,10 @@ object BrightnessSliderMotionTestKeys { } @Composable -private fun colors(): SliderColors { +private fun colors(brightnessGradient: BrightnessGradient?): SliderColors { return SliderDefaults.colors() .copy( + activeTrackColor = if (brightnessGradient != null) Color.Transparent else SliderDefaults.colors().activeTrackColor, inactiveTrackColor = CustomColorScheme.current.qsTileColor, activeTickColor = MaterialTheme.colorScheme.onPrimary, inactiveTickColor = MaterialTheme.colorScheme.onSurface, diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt index 36efeaa1bca9b..e3026ba28a917 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt @@ -21,6 +21,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.FloatRange import androidx.annotation.StringRes import androidx.compose.runtime.getValue +import androidx.core.content.res.ResourcesCompat import com.android.systemui.brightness.domain.interactor.BrightnessPolicyEnforcementInteractor import com.android.systemui.brightness.domain.interactor.ScreenBrightnessInteractor import com.android.systemui.brightness.shared.model.GammaBrightness @@ -28,7 +29,6 @@ import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.asIcon -import com.android.systemui.graphics.ImageLoader import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator @@ -62,7 +62,6 @@ constructor( private val falsingInteractor: FalsingInteractor, @Assisted private val supportsMirroring: Boolean, private val brightnessWarningToast: BrightnessWarningToast, - private val imageLoader: ImageLoader, ) : ExclusiveActivatable() { init { @@ -110,13 +109,13 @@ constructor( suspend fun loadImage(@DrawableRes resId: Int, context: Context): Icon.Loaded? { return withTimeoutOrNull(500L) { - imageLoader - .loadDrawable( - android.graphics.drawable.Icon.createWithResource(context, resId), - maxHeight = 200, - maxWidth = 200, - ) - ?.asIcon(null, resId) + val drawable = ResourcesCompat.getDrawable( + context.resources, + resId, + context.theme + ) ?: return@withTimeoutOrNull null + + drawable.asIcon(null, resId) } } diff --git a/packages/SystemUI/src/com/android/systemui/clocks/AODStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/AODStyle.java new file mode 100644 index 0000000000000..275073c604d2c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/AODStyle.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2023-2024 the risingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.clocks; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import com.android.settingslib.drawable.CircleFramedDrawable; + +import com.android.systemui.res.R; +import com.android.systemui.Dependency; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.tuner.TunerService; + +public class AODStyle extends RelativeLayout implements TunerService.Tunable { + + private static final String CUSTOM_AOD_IMAGE_URI_KEY = "system:custom_aod_image_uri"; + private static final String CUSTOM_AOD_IMAGE_ENABLED_KEY = "system:custom_aod_image_enabled"; + + private final Context mContext; + private final TunerService mTunerService; + + private final StatusBarStateController mStatusBarStateController; + + private boolean mDozing; + + private ImageView mAodImageView; + private String mImagePath; + private String mCurrImagePath; + private boolean mAodImageEnabled; + private boolean mImageLoaded = false; + private boolean mCustomClockEnabled; + + // Burn-in protection + private static final int BURN_IN_PROTECTION_INTERVAL = 10000; // 10 seconds + private static final int BURN_IN_PROTECTION_MAX_SHIFT = 4; // 4 pixels + private final Handler mBurnInProtectionHandler = new Handler(); + private int mCurrentShiftX = 0; + private int mCurrentShiftY = 0; + + private final Runnable mBurnInProtectionRunnable = new Runnable() { + @Override + public void run() { + if (mDozing) { + mCurrentShiftX = (int) (Math.random() * BURN_IN_PROTECTION_MAX_SHIFT * 2) - BURN_IN_PROTECTION_MAX_SHIFT; + mCurrentShiftY = (int) (Math.random() * BURN_IN_PROTECTION_MAX_SHIFT * 2) - BURN_IN_PROTECTION_MAX_SHIFT; + if (mAodImageView != null) { + mAodImageView.setTranslationX(mCurrentShiftX); + mAodImageView.setTranslationY(mCurrentShiftY); + } + invalidate(); + mBurnInProtectionHandler.postDelayed(this, BURN_IN_PROTECTION_INTERVAL); + } + } + }; + + private final StatusBarStateController.StateListener mStatusBarStateListener = + new StatusBarStateController.StateListener() { + @Override + public void onStateChanged(int newState) {} + + @Override + public void onDozingChanged(boolean dozing) { + if (mDozing == dozing) { + return; + } + mDozing = dozing; + updateAodImageView(); + if (mDozing) { + startBurnInProtection(); + } else { + stopBurnInProtection(); + } + } + }; + + public AODStyle(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mTunerService = Dependency.get(TunerService.class); + mTunerService.addTunable(this, ClockStyle.CLOCK_STYLE_KEY, CUSTOM_AOD_IMAGE_URI_KEY, CUSTOM_AOD_IMAGE_ENABLED_KEY); + mStatusBarStateController = Dependency.get(StatusBarStateController.class); + mStatusBarStateController.addCallback(mStatusBarStateListener); + mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mAodImageView = findViewById(R.id.custom_aod_image_view); + loadAodImage(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mStatusBarStateController.removeCallback(mStatusBarStateListener); + mTunerService.removeTunable(this); + mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); + if (mAodImageView != null) { + mAodImageView.animate().cancel(); + mAodImageView.setImageDrawable(null); + } + } + + private void startBurnInProtection() { + mBurnInProtectionHandler.post(mBurnInProtectionRunnable); + } + + private void stopBurnInProtection() { + mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); + if (mAodImageView != null) { + mAodImageView.setTranslationX(0); + mAodImageView.setTranslationY(0); + } + } + + @Override + public void onTuningChanged(String key, String newValue) { + switch (key) { + case ClockStyle.CLOCK_STYLE_KEY: + int clockStyle = TunerService.parseInteger(newValue, 0); + mCustomClockEnabled = clockStyle != 0; + break; + case CUSTOM_AOD_IMAGE_URI_KEY: + mImagePath = newValue; + if (mImagePath != null && !mImagePath.isEmpty() + && !mImagePath.equals(mCurrImagePath)) { + mCurrImagePath = mImagePath; + mImageLoaded = false; + loadAodImage(); + } + break; + case CUSTOM_AOD_IMAGE_ENABLED_KEY: + mAodImageEnabled = TunerService.parseIntegerSwitch( + newValue, false) && mCustomClockEnabled; + break; + } + } + + private void updateAodImageView() { + if (mAodImageView == null || !mAodImageEnabled) { + if (mAodImageView != null) mAodImageView.setVisibility(View.GONE); + return; + } + loadAodImage(); + if (mDozing) { + mAodImageView.setVisibility(View.VISIBLE); + mAodImageView.setScaleX(0f); + mAodImageView.setScaleY(0f); + mAodImageView.animate() + .scaleX(1f) + .scaleY(1f) + .setDuration(500) + .withEndAction(this::startBurnInProtection) + .start(); + } else { + mAodImageView.animate() + .scaleX(0f) + .scaleY(0f) + .setDuration(250) + .withEndAction(() -> { + mAodImageView.setVisibility(View.GONE); + stopBurnInProtection(); + }) + .start(); + } + } + + private void loadAodImage() { + if (mAodImageView == null || mCurrImagePath == null || mCurrImagePath.isEmpty() || mImageLoaded) return; + Bitmap bitmap = null; + try { + bitmap = BitmapFactory.decodeFile(mCurrImagePath); + if (bitmap != null) { + int targetSize = (int) mContext.getResources().getDimension(R.dimen.custom_aod_image_size); + Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, targetSize, targetSize, true); + try (java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream()) { + scaledBitmap.compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 90, stream); + byte[] byteArray = stream.toByteArray(); + Bitmap compressedBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); + Drawable roundedImg = new CircleFramedDrawable(compressedBitmap, targetSize); + mAodImageView.setImageDrawable(roundedImg); + scaledBitmap.recycle(); + compressedBitmap.recycle(); + mImageLoaded = true; + } + } else { + mImageLoaded = false; + mAodImageView.setVisibility(View.GONE); + } + } catch (Exception e) { + mImageLoaded = false; + mAodImageView.setVisibility(View.GONE); + } finally { + if (bitmap != null) { + bitmap.recycle(); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clocks/AnalogClockView.java b/packages/SystemUI/src/com/android/systemui/clocks/AnalogClockView.java new file mode 100644 index 0000000000000..274701adafa80 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/AnalogClockView.java @@ -0,0 +1,100 @@ +package com.android.systemui.clocks; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.view.View; + +import java.util.Calendar; + +public class AnalogClockView extends View { + private Paint mPaint; + private float centerX, centerY, radius; + private final Handler handler = new Handler(Looper.getMainLooper()); + + public AnalogClockView(Context context) { + super(context); + init(); + } + + public AnalogClockView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public AnalogClockView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); // Smooth edges + handler.postDelayed(new Runnable() { + @Override + public void run() { + invalidate(); // Redraw the view + handler.postDelayed(this, 1000); // Update every second + } + }, 1000); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + centerX = width / 2; + centerY = height / 2; + radius = Math.min(centerX, centerY) - 20; // Leave margin + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Keep the wallpaper background (no transparent background here) + + // Draw clock face with hour markers + mPaint.setColor(0xFFFFFFFF); // White color for hour markers + mPaint.setStyle(Paint.Style.FILL); + mPaint.setStrokeWidth(5); + for (int i = 0; i < 12; i++) { + float angle = (float) (Math.PI / 6 * i); // 30 degrees per hour + float startX = (float) (centerX + radius * 0.85 * Math.cos(angle)); + float startY = (float) (centerY + radius * 0.85 * Math.sin(angle)); + float endX = (float) (centerX + radius * 0.95 * Math.cos(angle)); + float endY = (float) (centerY + radius * 0.95 * Math.sin(angle)); + canvas.drawRect(startX - 5, startY - 5, endX + 5, endY + 5, mPaint); + } + + // Get current time + Calendar calendar = Calendar.getInstance(); + int hour = calendar.get(Calendar.HOUR); + int minute = calendar.get(Calendar.MINUTE); + + // Draw hour hand + mPaint.setColor(0xFFFFFFFF); // White color for clock hands + mPaint.setStrokeWidth(12); + float hourAngle = (float) (Math.PI / 6 * (hour + minute / 60.0)); // Convert time to angle + canvas.save(); + canvas.rotate((float) Math.toDegrees(hourAngle), centerX, centerY); + canvas.drawLine(centerX, centerY, centerX, centerY - radius * 0.5f, mPaint); + canvas.restore(); + + // Draw minute hand + mPaint.setStrokeWidth(8); + float minuteAngle = (float) (Math.PI / 30 * minute); // Convert time to angle + canvas.save(); + canvas.rotate((float) Math.toDegrees(minuteAngle), centerX, centerY); + canvas.drawLine(centerX, centerY, centerX, centerY - radius * 0.7f, mPaint); + canvas.restore(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + handler.removeCallbacksAndMessages(null); // Stop updates when detached + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java new file mode 100644 index 0000000000000..e55951a5c35b0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2023-2024 the risingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.clocks; + +import android.app.KeyguardManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Color; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.ViewStub; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextClock; + +import com.android.systemui.res.R; +import com.android.systemui.Dependency; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.tuner.TunerService; + +public class ClockStyle extends RelativeLayout implements TunerService.Tunable { + + private static final int[] CLOCK_LAYOUTS = { + 0, + R.layout.keyguard_clock_oos, + R.layout.keyguard_clock_center, + R.layout.keyguard_clock_simple, + R.layout.keyguard_clock_miui, + R.layout.keyguard_clock_ide, + R.layout.keyguard_clock_moto, + R.layout.keyguard_clock_label, + R.layout.keyguard_clock_ios, + R.layout.keyguard_clock_num, + R.layout.keyguard_clock_taden, + R.layout.keyguard_clock_mont, + R.layout.keyguard_clock_accent, + R.layout.keyguard_clock_nos1, + R.layout.keyguard_clock_nos2, + R.layout.keyguard_clock_life, + R.layout.keyguard_clock_word, + R.layout.keyguard_clock_encode, + R.layout.keyguard_clock_nos3, + R.layout.keyguard_clock_analog, + R.layout.keyguard_clock_a9 + }; + + private static final int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}; + + public static final String CLOCK_STYLE_KEY = "clock_style"; + public static final String CLOCK_COLOR_MODE_KEY = "clock_color_mode"; + public static final String CLOCK_CUSTOM_COLOR_KEY = "clock_custom_color"; + public static final String CLOCK_TEXT_OPACITY_KEY = "clock_text_opacity"; + public static final String CLOCK_FRAME_MARGIN_TOP_KEY = "custom_clock_frame_margin_top"; + + public static final String COLOR_MODE_DEFAULT = "default"; + public static final String COLOR_MODE_ACCENT = "accent"; + public static final String COLOR_MODE_CUSTOM = "custom"; + + private static final int DEFAULT_STYLE = 0; // Disabled + private static final int DEFAULT_OPACITY = 100; + private static final int DEFAULT_MARGIN_TOP = 15; + private static final int DEFAULT_CUSTOM_COLOR = Color.WHITE; + private static final int AOD_OPACITY_CAP = 70; + + private static final long AOD_UPDATE_INTERVAL_MILLIS = 60_000L; + + private static final long UPDATE_INTERVAL_MILLIS = 15_000L; + + private final Context mContext; + private final KeyguardManager mKeyguardManager; + private final TunerService mTunerService; + + private View currentClockView; + private ViewStub mClockStub; + private ViewGroup mClockContainer; + + private int mClockStyle; + private String mColorMode = COLOR_MODE_DEFAULT; + private int mCustomColor = DEFAULT_CUSTOM_COLOR; + private int mClockOpacity = DEFAULT_OPACITY; + private int mClockFrameMarginTop = DEFAULT_MARGIN_TOP; + + private long lastUpdateTimeMillis = 0; + + private final StatusBarStateController mStatusBarStateController; + private boolean mDozing; + + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + // Burn-in protection + private static final int BURN_IN_PROTECTION_INTERVAL = 10_000; + private static final int BURN_IN_PROTECTION_MAX_SHIFT = 4; + private int mCurrentShiftX = 0; + private int mCurrentShiftY = 0; + + private boolean mCallbacksRegistered = false; + + private final BroadcastReceiver mScreenReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_SCREEN_ON.equals(action)) { + forceTimeUpdate(); + } else if (Intent.ACTION_TIME_TICK.equals(action) + || Intent.ACTION_TIME_CHANGED.equals(action)) { + onTimeChanged(); + } else if ("com.android.systemui.doze.pulse".equals(action)) { + onTimeChanged(); + } + } + }; + + private final Runnable mAodTickRunnable = new Runnable() { + @Override + public void run() { + if (!mDozing || mClockStyle == 0) return; + + forceTimeUpdate(); + + long now = System.currentTimeMillis(); + long nextMinute = ((now / AOD_UPDATE_INTERVAL_MILLIS) + 1) * AOD_UPDATE_INTERVAL_MILLIS; + mHandler.postDelayed(this, nextMinute - now); + } + }; + + private final Runnable mBurnInProtectionRunnable = new Runnable() { + @Override + public void run() { + if (!mDozing) return; + mCurrentShiftX = (int) (Math.random() * BURN_IN_PROTECTION_MAX_SHIFT * 2) + - BURN_IN_PROTECTION_MAX_SHIFT; + mCurrentShiftY = (int) (Math.random() * BURN_IN_PROTECTION_MAX_SHIFT * 2) + - BURN_IN_PROTECTION_MAX_SHIFT; + if (currentClockView != null) { + currentClockView.setTranslationX(mCurrentShiftX); + currentClockView.setTranslationY(mCurrentShiftY); + } + invalidate(); + mHandler.postDelayed(this, BURN_IN_PROTECTION_INTERVAL); + } + }; + + private final StatusBarStateController.StateListener mStatusBarStateListener = + new StatusBarStateController.StateListener() { + @Override + public void onStateChanged(int newState) {} + + @Override + public void onDozingChanged(boolean dozing) { + if (mDozing == dozing) return; + mDozing = dozing; + applyClockAlpha(); + if (mDozing) { + startBurnInProtection(); + startAodTick(); + } else { + stopBurnInProtection(); + stopAodTick(); + forceTimeUpdate(); + } + } + }; + + public ClockStyle(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + mTunerService = Dependency.get(TunerService.class); + mStatusBarStateController = Dependency.get(StatusBarStateController.class); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mClockStub = findViewById(R.id.clock_view_stub); + updateClockView(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!mCallbacksRegistered) { + mTunerService.addTunable(this, + CLOCK_STYLE_KEY, + CLOCK_COLOR_MODE_KEY, + CLOCK_CUSTOM_COLOR_KEY, + CLOCK_TEXT_OPACITY_KEY, + CLOCK_FRAME_MARGIN_TOP_KEY); + mStatusBarStateController.addCallback(mStatusBarStateListener); + mDozing = mStatusBarStateController.isDozing(); + mStatusBarStateListener.onDozingChanged(mDozing); + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction("com.android.systemui.doze.pulse"); + mContext.registerReceiver(mScreenReceiver, filter, Context.RECEIVER_EXPORTED); + mCallbacksRegistered = true; + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mCallbacksRegistered) { + mStatusBarStateController.removeCallback(mStatusBarStateListener); + mTunerService.removeTunable(this); + mHandler.removeCallbacks(mBurnInProtectionRunnable); + mHandler.removeCallbacks(mAodTickRunnable); + mContext.unregisterReceiver(mScreenReceiver); + mCallbacksRegistered = false; + } + } + + private void startAodTick() { + if (mClockStyle == 0) return; + mHandler.removeCallbacks(mAodTickRunnable); + long now = System.currentTimeMillis(); + long nextMinute = ((now / AOD_UPDATE_INTERVAL_MILLIS) + 1) * AOD_UPDATE_INTERVAL_MILLIS; + mHandler.postDelayed(mAodTickRunnable, nextMinute - now); + } + + private void stopAodTick() { + mHandler.removeCallbacks(mAodTickRunnable); + } + + private void startBurnInProtection() { + if (mClockStyle == 0) return; + mHandler.removeCallbacks(mBurnInProtectionRunnable); + mHandler.postDelayed(mBurnInProtectionRunnable, BURN_IN_PROTECTION_INTERVAL); + } + + private void stopBurnInProtection() { + if (mClockStyle == 0) return; + mHandler.removeCallbacks(mBurnInProtectionRunnable); + if (currentClockView != null) { + currentClockView.setTranslationX(0); + currentClockView.setTranslationY(0); + } + } + + private void updateTextClockViews(View view) { + if (view instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) view; + for (int i = 0; i < vg.getChildCount(); i++) { + updateTextClockViews(vg.getChildAt(i)); + } + } + if (view instanceof TextClock) { + ((TextClock) view).refreshTime(); + } + } + + public void onTimeChanged() { + long now = System.currentTimeMillis(); + if (now - lastUpdateTimeMillis >= UPDATE_INTERVAL_MILLIS) { + forceTimeUpdate(); + } + } + + private void forceTimeUpdate() { + if (currentClockView != null) { + updateTextClockViews(currentClockView); + lastUpdateTimeMillis = System.currentTimeMillis(); + } + } + + private int resolveClockColor() { + switch (mColorMode) { + case COLOR_MODE_ACCENT: + return mContext.getColor( + mContext.getResources().getIdentifier( + "system_accent1_100", "color", "android")); + case COLOR_MODE_CUSTOM: + return mCustomColor; + case COLOR_MODE_DEFAULT: + default: + return mContext.getColor(android.R.color.white); + } + } + + private void applyClockAlpha() { + if (currentClockView == null) return; + int effective = (mDozing && mClockOpacity > AOD_OPACITY_CAP) ? AOD_OPACITY_CAP : mClockOpacity; + currentClockView.setAlpha(effective / 100f); + } + + private void updateClockAppearance() { + if (currentClockView == null) return; + applyClockAlpha(); + applyTextClockColor(currentClockView); + } + + private void updateClockTextColor() { + if (currentClockView == null) return; + applyTextClockColor(currentClockView); + } + + private void applyTextClockColor(View view) { + if (view instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) view; + for (int i = 0; i < vg.getChildCount(); i++) { + applyTextClockColor(vg.getChildAt(i)); + } + } + if (!(view instanceof TextClock)) return; + TextClock tc = (TextClock) view; + if (tc.getTag(R.id.original_text_color) == null) { + tc.setTag(R.id.original_text_color, tc.getCurrentTextColor()); + } + int originalColor = (Integer) tc.getTag(R.id.original_text_color); + int whiteColor = mContext.getColor(android.R.color.white); + if ((originalColor & 0x00FFFFFF) != (whiteColor & 0x00FFFFFF)) return; + tc.setTextColor(resolveClockColor()); + } + + private void updateClockFrameMargin() { + View clockFrame = findViewById(R.id.clock_frame); + if (clockFrame != null) { + ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) clockFrame.getLayoutParams(); + int marginPx = (int) (mClockFrameMarginTop + * mContext.getResources().getDisplayMetrics().density); + params.topMargin = marginPx; + clockFrame.setLayoutParams(params); + } + } + + private void updateClockView() { + if (currentClockView != null) { + ViewParent parent = currentClockView.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(currentClockView); + } + currentClockView = null; + } + if (mClockStyle > 0 && mClockStyle < CLOCK_LAYOUTS.length) { + if (mClockStub != null) { + mClockStub.setLayoutResource(CLOCK_LAYOUTS[mClockStyle]); + currentClockView = mClockStub.inflate(); + mClockContainer = (ViewGroup) currentClockView.getParent(); + mClockStub = null; + } else if (mClockContainer != null) { + currentClockView = LayoutInflater.from(mContext) + .inflate(CLOCK_LAYOUTS[mClockStyle], mClockContainer, false); + mClockContainer.addView(currentClockView); + } + if (currentClockView != null) { + int gravity = isCenterClock(mClockStyle) ? Gravity.CENTER : Gravity.START; + if (currentClockView instanceof LinearLayout) { + ((LinearLayout) currentClockView).setGravity(gravity); + } + updateClockAppearance(); + updateClockFrameMargin(); + } + } + forceTimeUpdate(); + setVisibility(mClockStyle != 0 ? View.VISIBLE : View.GONE); + + if (mDozing && mClockStyle != 0) { + startAodTick(); + } + } + + @Override + public void onTuningChanged(String key, String newValue) { + switch (key) { + case CLOCK_STYLE_KEY: + mClockStyle = TunerService.parseInteger(newValue, DEFAULT_STYLE); + if (mClockStyle != 0) { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 0, + UserHandle.USER_CURRENT); + } + updateClockView(); + break; + case CLOCK_COLOR_MODE_KEY: + mColorMode = (newValue != null) ? newValue : COLOR_MODE_DEFAULT; + updateClockTextColor(); + break; + case CLOCK_CUSTOM_COLOR_KEY: + mCustomColor = TunerService.parseInteger(newValue, DEFAULT_CUSTOM_COLOR); + if (COLOR_MODE_CUSTOM.equals(mColorMode)) { + updateClockTextColor(); + } + break; + case CLOCK_TEXT_OPACITY_KEY: + mClockOpacity = TunerService.parseInteger(newValue, DEFAULT_OPACITY); + mClockOpacity = Math.max(0, Math.min(100, mClockOpacity)); + applyClockAlpha(); + break; + case CLOCK_FRAME_MARGIN_TOP_KEY: + mClockFrameMarginTop = TunerService.parseInteger(newValue, DEFAULT_MARGIN_TOP); + mClockFrameMarginTop = Math.max(0, Math.min(100, mClockFrameMarginTop)); + updateClockFrameMargin(); + break; + } + } + + private boolean isCenterClock(int clockStyle) { + for (int centerClock : mCenterClocks) { + if (centerClock == clockStyle) { + return true; + } + } + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clocks/NothingAnalogClockView.java b/packages/SystemUI/src/com/android/systemui/clocks/NothingAnalogClockView.java new file mode 100644 index 0000000000000..8faeb31fe5655 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/NothingAnalogClockView.java @@ -0,0 +1,141 @@ +package com.android.systemui.clocks; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import java.util.Calendar; + +public class NothingAnalogClockView extends View { + + private Paint mPaint; + private float centerX, centerY, width, height; + + public NothingAnalogClockView(Context context) { + super(context); + init(); + } + + public NothingAnalogClockView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public NothingAnalogClockView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); // Smooth edges for drawing + mPaint.setStrokeCap(Paint.Cap.ROUND); // Rounded ends for clock hands + + // Refresh the clock every second + postInvalidateOnAnimation(); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + + this.width = width; + this.height = height; + + centerX = width / 2f; + centerY = height / 2f; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Get current time + Calendar calendar = Calendar.getInstance(); + int hour = calendar.get(Calendar.HOUR); + int minute = calendar.get(Calendar.MINUTE); + int second = calendar.get(Calendar.SECOND); + + // Draw clock ticks in a rectangular shape + drawRectangularTicks(canvas); + + // Draw hour hand + drawHourHand(canvas, hour, minute); + + // Draw minute hand + drawMinuteHand(canvas, minute); + + // Draw second hand + drawSecondHand(canvas, second); + + // Trigger continuous updates + postInvalidateDelayed(16); // Smooth 60 FPS animation + } + + private void drawRectangularTicks(Canvas canvas) { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(0xFFFFFFFF); // White color for ticks + + // Rectangular bounds (inset from edges) + float horizontalInset = width * 0.1f; + float verticalInset = height * 0.2f; + + for (int i = 0; i < 60; i++) { + float angle = (float) (Math.PI / 30 * i); + float startX, startY, endX, endY; + + if (i % 5 == 0) { + // Major ticks (longer and thicker) + mPaint.setStrokeWidth(6); + startX = (float) (centerX + (width / 2 - horizontalInset) * Math.cos(angle)); + startY = (float) (centerY + (height / 2 - verticalInset) * Math.sin(angle)); + endX = (float) (centerX + (width / 2 - horizontalInset * 1.5) * Math.cos(angle)); + endY = (float) (centerY + (height / 2 - verticalInset * 1.5) * Math.sin(angle)); + } else { + // Minor ticks (shorter and thinner) + mPaint.setStrokeWidth(2); + startX = (float) (centerX + (width / 2 - horizontalInset) * Math.cos(angle)); + startY = (float) (centerY + (height / 2 - verticalInset) * Math.sin(angle)); + endX = (float) (centerX + (width / 2 - horizontalInset * 1.2) * Math.cos(angle)); + endY = (float) (centerY + (height / 2 - verticalInset * 1.2) * Math.sin(angle)); + } + + canvas.drawLine(startX, startY, endX, endY, mPaint); + } + } + + private void drawHourHand(Canvas canvas, int hour, int minute) { + float hourAngle = (float) (Math.PI / 6 * (hour + minute / 60.0)); + mPaint.setColor(0xFFFFFFFF); // White color for hour hand + mPaint.setStrokeWidth(12); + + float endX = (float) (centerX + (width * 0.25f) * Math.cos(hourAngle - Math.PI / 2)); + float endY = (float) (centerY + (height * 0.25f) * Math.sin(hourAngle - Math.PI / 2)); + + canvas.drawLine(centerX, centerY, endX, endY, mPaint); + } + + private void drawMinuteHand(Canvas canvas, int minute) { + float minuteAngle = (float) (Math.PI / 30 * minute); + mPaint.setColor(0xFFFFFFFF); // White color for minute hand + mPaint.setStrokeWidth(8); + + float endX = (float) (centerX + (width * 0.4f) * Math.cos(minuteAngle - Math.PI / 2)); + float endY = (float) (centerY + (height * 0.4f) * Math.sin(minuteAngle - Math.PI / 2)); + + canvas.drawLine(centerX, centerY, endX, endY, mPaint); + } + + private void drawSecondHand(Canvas canvas, int second) { + float secondAngle = (float) (Math.PI / 30 * second); + mPaint.setColor(0xFFFF0000); // Red color for second hand + mPaint.setStrokeWidth(4); + + float endX = (float) (centerX + (width * 0.45f) * Math.cos(secondAngle - Math.PI / 2)); + float endY = (float) (centerY + (height * 0.45f) * Math.sin(secondAngle - Math.PI / 2)); + + canvas.drawLine(centerX, centerY, endX, endY, mPaint); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clocks/WordClockView.java b/packages/SystemUI/src/com/android/systemui/clocks/WordClockView.java new file mode 100644 index 0000000000000..62211f7b34a3c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/WordClockView.java @@ -0,0 +1,78 @@ +package com.android.systemui.clocks; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.widget.TextView; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class WordClockView extends TextView { + private final Handler handler = new Handler(Looper.getMainLooper()); + private final Runnable updateTimeRunnable = new Runnable() { + @Override + public void run() { + updateClock(); + handler.postDelayed(this, 1000); + } + }; + + public WordClockView(Context context) { + super(context); + init(); + } + + public WordClockView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public WordClockView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + updateClock(); + handler.post(updateTimeRunnable); + } + + private void updateClock() { + String currentTime = new SimpleDateFormat("hh:mm a", Locale.getDefault()).format(new Date()); + String[] timeParts = currentTime.split(":"); + String hour = timeParts[0]; + String minute = timeParts[1].split(" ")[0]; + + String hourInWords = convertToWords(Integer.parseInt(hour)); + String minuteInWords = convertToWords(Integer.parseInt(minute)); + + // Set only the hour and minute in words + setText(hourInWords + "\n" + minuteInWords); + } + + private String convertToWords(int num) { + String[] words = { + "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", + "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", + "Seventeen", "Eighteen", "Nineteen", "Twenty", "Twenty-One", "Twenty-Two", + "Twenty-Three", "Twenty-Four", "Twenty-Five", "Twenty-Six", "Twenty-Seven", + "Twenty-Eight", "Twenty-Nine", "Thirty", "Thirty-One", "Thirty-Two", + "Thirty-Three", "Thirty-Four", "Thirty-Five", "Thirty-Six", "Thirty-Seven", + "Thirty-Eight", "Thirty-Nine", "Forty", "Forty-One", "Forty-Two", + "Forty-Three", "Forty-Four", "Forty-Five", "Forty-Six", "Forty-Seven", + "Forty-Eight", "Forty-Nine", "Fifty", "Fifty-One", "Fifty-Two", "Fifty-Three", + "Fifty-Four", "Fifty-Five", "Fifty-Six", "Fifty-Seven", "Fifty-Eight", + "Fifty-Nine" + }; + return num < words.length ? words[num] : ""; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + handler.removeCallbacks(updateTimeRunnable); // Stop updates when view is detached + } +} diff --git a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt index ab25fa25115e0..83c2c10295a0d 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt @@ -16,6 +16,7 @@ package com.android.systemui.common.ringer import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp @@ -39,6 +40,12 @@ interface RingerSliderTheme { val dndIcon: Color val dozeStroke: Dp + + val activeBgBrush: Brush? + @Composable get() = null + + val dndBgBrush: Brush? + @Composable get() = null } interface RingerSliderDimens { diff --git a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt index 109d040fdc763..3555efe68a960 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt @@ -50,7 +50,10 @@ fun RingerSliderWidget( ) { val mode by interactor.ringerMode.collectAsState(initial = interactor.getCurrentMode()) val isDndEnabled by interactor.dndMode.collectAsState(initial = interactor.isDndEnabled()) - + + val activeBrush = theme.activeBgBrush + val dndBrush = theme.dndBgBrush + val targetPosition = when (mode) { AudioManager.RINGER_MODE_NORMAL -> 0f AudioManager.RINGER_MODE_VIBRATE -> 1f @@ -189,13 +192,18 @@ fun RingerSliderWidget( .offset(x = thumbOffset) .size(dimens.thumbSize) .padding(dimens.thumbPadding) - .background( + .then( when { - isDozing -> Color.Transparent - isDndEnabled -> theme.dndBg - else -> theme.activeBg - }, - thumbShape + isDozing -> Modifier.background(Color.Transparent, thumbShape) + isDndEnabled -> if (dndBrush != null) + Modifier.background(dndBrush, thumbShape) + else + Modifier.background(theme.dndBg, thumbShape) + else -> if (activeBrush != null) + Modifier.background(activeBrush, thumbShape) + else + Modifier.background(theme.activeBg, thumbShape) + } ) .then( when { diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressController.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressController.java new file mode 100644 index 0000000000000..16ca95a150d5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressController.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cutoutprogress; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.PixelFormat; +import android.os.BatteryManager; +import android.os.Handler; +import android.view.WindowManager; + +import com.android.systemui.CoreStartable; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.cutoutprogress.ring.CutoutRingView; +import com.android.systemui.statusbar.notification.collection.NotifPipeline; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; + +import javax.inject.Inject; + +@SysUISingleton +public class CutoutProgressController implements CoreStartable { + + private static final int WINDOW_TYPE = 2024; + + private final Context mContext; + private final NotifPipeline mPipeline; + private final Handler mMainHandler; + + private final CutoutProgressSettings mSettings; + private final DownloadStateTracker mTracker; + private CutoutRingView mRingView; + + private boolean mOverlayAttached = false; + private boolean mListenerRegistered = false; + private boolean mBatteryReceiverRegistered = false; + + private final BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!mSettings.isEnabled() || !mSettings.isChargingRingEnabled()) { + mMainHandler.post(() -> mRingView.setChargingState(false, 0)); + return; + } + int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN); + boolean charging = status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL; + int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); + int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100); + int pct = scale > 0 ? level * 100 / scale : 0; + boolean pulseEnabled = mSettings.isChargingPulseEnabled(); + mMainHandler.post(() -> { + mRingView.setChargingPulseEnabled(pulseEnabled); + mRingView.setChargingState(charging, pct); + }); + } + }; + + @Inject + public CutoutProgressController( + Context context, + NotifPipeline notifPipeline, + @Main Handler mainHandler) { + mContext = context; + mPipeline = notifPipeline; + mMainHandler = mainHandler; + mSettings = new CutoutProgressSettings( + context.getContentResolver(), mainHandler); + mTracker = new DownloadStateTracker(); + } + + @Override + public void start() { + mRingView = new CutoutRingView(mContext); + mRingView.applySettings(mSettings); + bindTrackerToView(); + mSettings.observe(this::onSettingsChanged); + onSettingsChanged(); + } + + private void onSettingsChanged() { + mRingView.applySettings(mSettings); + + if (mSettings.isEnabled()) { + enableFeature(); + } else { + disableFeature(); + } + } + + private void enableFeature() { + attachOverlay(); + registerPipelineListener(); + registerBatteryReceiver(); + } + + private void disableFeature() { + mTracker.reset(); + detachOverlay(); + unregisterBatteryReceiver(); + } + + private void attachOverlay() { + if (mOverlayAttached) return; + + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT, + WINDOW_TYPE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, + PixelFormat.TRANSLUCENT); + + params.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + params.setTitle("CutoutProgressOverlay"); + + WindowManager wm = mContext.getSystemService(WindowManager.class); + if (wm != null) { + wm.addView(mRingView, params); + mOverlayAttached = true; + } + } + + private void detachOverlay() { + if (!mOverlayAttached) return; + WindowManager wm = mContext.getSystemService(WindowManager.class); + if (wm != null) { + wm.removeView(mRingView); + mOverlayAttached = false; + } + } + + private void registerPipelineListener() { + if (mListenerRegistered) return; + mListenerRegistered = true; + + mPipeline.addCollectionListener(new NotifCollectionListener() { + + @Override + public void onEntryAdded(NotificationEntry entry) { + if (!mSettings.isEnabled()) return; + mTracker.onNotificationChanged(entry); + } + + @Override + public void onEntryUpdated(NotificationEntry entry) { + if (!mSettings.isEnabled()) return; + mTracker.onNotificationChanged(entry); + } + + @Override + public void onEntryRemoved(NotificationEntry entry, int reason) { + if (!mSettings.isEnabled()) return; + mTracker.onNotificationRemoved(entry, reason); + } + }); + } + + private void registerBatteryReceiver() { + if (mBatteryReceiverRegistered) return; + IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + mContext.registerReceiver(mBatteryReceiver, filter); + mBatteryReceiverRegistered = true; + } + + private void unregisterBatteryReceiver() { + if (!mBatteryReceiverRegistered) return; + mContext.unregisterReceiver(mBatteryReceiver); + mBatteryReceiverRegistered = false; + mMainHandler.post(() -> mRingView.setChargingState(false, 0)); + } + + private void bindTrackerToView() { + mTracker.setOnProgress(progress -> + mMainHandler.post(() -> mRingView.setProgress(progress))); + + mTracker.setOnComplete(() -> + mMainHandler.post(() -> mRingView.setProgress(100))); + + mTracker.setOnError(() -> + mMainHandler.post(() -> mRingView.showError())); + + mTracker.setOnCountChanged(count -> + mMainHandler.post(() -> mRingView.setDownloadCount(count))); + + mTracker.setOnLabelChanged(label -> + mMainHandler.post(() -> mRingView.setFilenameHint(label))); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java new file mode 100644 index 0000000000000..5c0f9e0d6ed92 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cutoutprogress; + +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.graphics.Color; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; + +public final class CutoutProgressSettings { + + public static final String KEY_ENABLED = "cutout_progress_enabled"; + + public static final String KEY_RING_COLOR_MODE = "cutout_progress_ring_color_mode"; + + public static final String KEY_RING_COLOR = "cutout_progress_ring_color"; + + public static final String KEY_ERROR_COLOR = "cutout_progress_error_color"; + + public static final String KEY_FINISH_FLASH_COLOR = "cutout_progress_finish_flash_color"; + + public static final String KEY_STROKE_WIDTH_DP10 = "cutout_progress_stroke_width_dp10"; + + public static final String KEY_RING_GAP_X1000 = "cutout_progress_ring_gap_x1000"; + + public static final String KEY_OPACITY = "cutout_progress_opacity"; + + public static final String KEY_CLOCKWISE = "cutout_progress_clockwise"; + + public static final String KEY_FINISH_STYLE = "cutout_progress_finish_style"; + + public static final String KEY_FINISH_HOLD_MS = "cutout_progress_finish_hold_ms"; + + public static final String KEY_FINISH_EXIT_MS = "cutout_progress_finish_exit_ms"; + + public static final String KEY_FINISH_USE_FLASH = "cutout_progress_finish_use_flash"; + + public static final String KEY_COMPLETION_PULSE = "cutout_progress_completion_pulse"; + + public static final String KEY_PATH_MODE = "cutout_progress_path_mode"; + + public static final String KEY_RING_SCALE_X_X1000 = "cutout_progress_ring_scale_x_x1000"; + + public static final String KEY_RING_SCALE_Y_X1000 = "cutout_progress_ring_scale_y_x1000"; + + public static final String KEY_RING_OFFSET_X_DP10 = "cutout_progress_ring_offset_x_dp10"; + + public static final String KEY_RING_OFFSET_Y_DP10 = "cutout_progress_ring_offset_y_dp10"; + + public static final String KEY_BG_RING_ENABLED = "cutout_progress_bg_ring_enabled"; + + public static final String KEY_BG_RING_COLOR = "cutout_progress_bg_ring_color"; + + public static final String KEY_BG_RING_OPACITY = "cutout_progress_bg_ring_opacity"; + + public static final String KEY_MIN_VIS_ENABLED = "cutout_progress_min_vis_enabled"; + + public static final String KEY_MIN_VIS_MS = "cutout_progress_min_vis_ms"; + + public static final String KEY_SHOW_COUNT_BADGE = "cutout_progress_show_count_badge"; + + public static final String KEY_BADGE_OFFSET_X_DP10 = "cutout_progress_badge_offset_x_dp10"; + + public static final String KEY_BADGE_OFFSET_Y_DP10 = "cutout_progress_badge_offset_y_dp10"; + + public static final String KEY_BADGE_TEXT_SIZE_SP10 = "cutout_progress_badge_text_size_sp10"; + + public static final String KEY_PERCENT_ENABLED = "cutout_progress_percent_enabled"; + + public static final String KEY_PERCENT_SIZE_SP10 = "cutout_progress_percent_size_sp10"; + + public static final String KEY_PERCENT_BOLD = "cutout_progress_percent_bold"; + + public static final String KEY_PERCENT_POSITION = "cutout_progress_percent_position"; + + public static final String KEY_PERCENT_OFFSET_X = "cutout_progress_percent_offset_x"; + + public static final String KEY_PERCENT_OFFSET_Y = "cutout_progress_percent_offset_y"; + + public static final String KEY_FILENAME_ENABLED = "cutout_progress_filename_enabled"; + + public static final String KEY_FILENAME_SIZE_SP10 = "cutout_progress_filename_size_sp10"; + + public static final String KEY_FILENAME_BOLD = "cutout_progress_filename_bold"; + + public static final String KEY_FILENAME_POSITION = "cutout_progress_filename_position"; + + public static final String KEY_FILENAME_OFFSET_X = "cutout_progress_filename_offset_x"; + + public static final String KEY_FILENAME_OFFSET_Y = "cutout_progress_filename_offset_y"; + + public static final String KEY_FILENAME_MAX_CHARS = "cutout_progress_filename_max_chars"; + + public static final String KEY_FILENAME_TRUNCATE = "cutout_progress_filename_truncate"; + + public static final String KEY_PROGRESS_EASING = "cutout_progress_easing"; + + public static final String KEY_CHARGING_RING_ENABLED = "cutout_progress_charging_ring_enabled"; + + public static final String KEY_CHARGING_PULSE_ENABLED = "cutout_progress_charging_pulse_enabled"; + + public static final int RING_COLOR_MODE_ACCENT = 0; + public static final int RING_COLOR_MODE_RAINBOW = 1; + public static final int RING_COLOR_MODE_CUSTOM = 2; + private static final boolean DEF_ENABLED = false; + private static final int DEF_RING_COLOR_MODE = RING_COLOR_MODE_ACCENT; + private static final int DEF_RING_COLOR = 0xFF2196F3; + private static final int DEF_ERROR_COLOR = 0xFFF44336; + private static final int DEF_FINISH_FLASH_COLOR = Color.WHITE; + private static final float DEF_STROKE_DP = 2.0f; + private static final float DEF_RING_GAP = 1.155f; + private static final int DEF_OPACITY = 90; + private static final boolean DEF_CLOCKWISE = true; + private static final int DEF_FINISH_STYLE = 0; + private static final int DEF_FINISH_HOLD_MS = 500; + private static final int DEF_FINISH_EXIT_MS = 500; + private static final boolean DEF_FINISH_USE_FLASH = true; + private static final boolean DEF_COMPLETION_PULSE = true; + private static final boolean DEF_PATH_MODE = false; + private static final float DEF_RING_SCALE = 1.0f; + private static final float DEF_RING_OFFSET = 0.0f; + private static final boolean DEF_BG_RING_ENABLED = true; + private static final int DEF_BG_RING_COLOR = 0xFF808080; + private static final int DEF_BG_RING_OPACITY = 30; + private static final boolean DEF_MIN_VIS_ENABLED = true; + private static final int DEF_MIN_VIS_MS = 500; + private static final boolean DEF_SHOW_COUNT_BADGE = false; + private static final float DEF_BADGE_OFFSET = 0.0f; + private static final float DEF_BADGE_TEXT_SP = 10.0f; + private static final boolean DEF_PERCENT_ENABLED = false; + private static final float DEF_PERCENT_SP = 8.0f; + private static final boolean DEF_PERCENT_BOLD = true; + private static final int DEF_PERCENT_POSITION = 0; + private static final boolean DEF_FILENAME_ENABLED = false; + private static final float DEF_FILENAME_SP = 7.0f; + private static final boolean DEF_FILENAME_BOLD = false; + private static final int DEF_FILENAME_POSITION = 4; + private static final int DEF_FILENAME_MAX_CHARS = 20; + private static final int DEF_FILENAME_TRUNCATE = 0; + private static final int DEF_EASING = 0; + private static final boolean DEF_CHARGING_RING_ENABLED = true; + private static final boolean DEF_CHARGING_PULSE_ENABLED = true; + + static final String[] POSITION_NAMES = { + "right", "left", "top", "bottom", + "top_right", "top_left", "bottom_right", "bottom_left" + }; + + static final String[] FINISH_STYLE_NAMES = { "pop", "segmented", "snap" }; + + static final String[] EASING_NAMES = { + "linear", "accelerate", "decelerate", "ease_in_out" + }; + + static final String[] TRUNCATE_MODE_NAMES = { "middle", "start", "end" }; + + private final ContentResolver mCr; + private final Handler mHandler; + private ContentObserver mObserver; + private Runnable mCallback; + + public CutoutProgressSettings(ContentResolver cr, Handler handler) { + mCr = cr; + mHandler = handler; + } + + public void observe(Runnable onChange) { + mCallback = onChange; + mObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + if (mCallback != null) mCallback.run(); + } + }; + Uri base = Settings.Secure.getUriFor("cutout_progress_enabled").buildUpon() + .path("").build(); + mCr.registerContentObserver( + Settings.Secure.CONTENT_URI, true, mObserver); + } + + public void stopObserving() { + if (mObserver != null) { + mCr.unregisterContentObserver(mObserver); + mObserver = null; + } + } + + public boolean isEnabled() { + return getInt(KEY_ENABLED, DEF_ENABLED ? 1 : 0) != 0; + } + + public int getRingColorMode() { + return clamp(getInt(KEY_RING_COLOR_MODE, DEF_RING_COLOR_MODE), + RING_COLOR_MODE_ACCENT, RING_COLOR_MODE_CUSTOM); + } + + public int getRingColor() { + return getInt(KEY_RING_COLOR, DEF_RING_COLOR); + } + + public int getErrorColor() { + return getInt(KEY_ERROR_COLOR, DEF_ERROR_COLOR); + } + + public int getFinishFlashColor() { + return getInt(KEY_FINISH_FLASH_COLOR, DEF_FINISH_FLASH_COLOR); + } + + public float getStrokeWidthDp() { + return getInt(KEY_STROKE_WIDTH_DP10, (int)(DEF_STROKE_DP * 10)) / 10f; + } + + public float getRingGap() { + return getInt(KEY_RING_GAP_X1000, (int)(DEF_RING_GAP * 1000)) / 1000f; + } + + public int getOpacity() { + return clamp(getInt(KEY_OPACITY, DEF_OPACITY), 0, 100); + } + + public boolean isClockwise() { + return getInt(KEY_CLOCKWISE, DEF_CLOCKWISE ? 1 : 0) != 0; + } + + public String getFinishStyle() { + int idx = clamp(getInt(KEY_FINISH_STYLE, DEF_FINISH_STYLE), 0, + FINISH_STYLE_NAMES.length - 1); + return FINISH_STYLE_NAMES[idx]; + } + + public int getFinishHoldMs() { + return getInt(KEY_FINISH_HOLD_MS, DEF_FINISH_HOLD_MS); + } + + public int getFinishExitMs() { + return getInt(KEY_FINISH_EXIT_MS, DEF_FINISH_EXIT_MS); + } + + public boolean isFinishUseFlash() { + return getInt(KEY_FINISH_USE_FLASH, DEF_FINISH_USE_FLASH ? 1 : 0) != 0; + } + + public boolean isCompletionPulse() { + return getInt(KEY_COMPLETION_PULSE, DEF_COMPLETION_PULSE ? 1 : 0) != 0; + } + + public boolean isPathMode() { + return getInt(KEY_PATH_MODE, DEF_PATH_MODE ? 1 : 0) != 0; + } + + public float getRingScaleX() { + return getInt(KEY_RING_SCALE_X_X1000, (int)(DEF_RING_SCALE * 1000)) / 1000f; + } + + public float getRingScaleY() { + return getInt(KEY_RING_SCALE_Y_X1000, (int)(DEF_RING_SCALE * 1000)) / 1000f; + } + + public float getRingOffsetXDp() { + return getInt(KEY_RING_OFFSET_X_DP10, (int)(DEF_RING_OFFSET * 10)) / 10f; + } + + public float getRingOffsetYDp() { + return getInt(KEY_RING_OFFSET_Y_DP10, (int)(DEF_RING_OFFSET * 10)) / 10f; + } + + public boolean isBgRingEnabled() { + return getInt(KEY_BG_RING_ENABLED, DEF_BG_RING_ENABLED ? 1 : 0) != 0; + } + + public int getBgRingColor() { + return getInt(KEY_BG_RING_COLOR, DEF_BG_RING_COLOR); + } + + public int getBgRingOpacity() { + return clamp(getInt(KEY_BG_RING_OPACITY, DEF_BG_RING_OPACITY), 0, 100); + } + + public boolean isMinVisEnabled() { + return getInt(KEY_MIN_VIS_ENABLED, DEF_MIN_VIS_ENABLED ? 1 : 0) != 0; + } + + public int getMinVisMs() { + return getInt(KEY_MIN_VIS_MS, DEF_MIN_VIS_MS); + } + + public boolean isShowCountBadge() { + return getInt(KEY_SHOW_COUNT_BADGE, DEF_SHOW_COUNT_BADGE ? 1 : 0) != 0; + } + + public float getBadgeOffsetXDp() { + return getInt(KEY_BADGE_OFFSET_X_DP10, (int)(DEF_BADGE_OFFSET * 10)) / 10f; + } + + public float getBadgeOffsetYDp() { + return getInt(KEY_BADGE_OFFSET_Y_DP10, (int)(DEF_BADGE_OFFSET * 10)) / 10f; + } + + public float getBadgeTextSizeSp() { + return getInt(KEY_BADGE_TEXT_SIZE_SP10, (int)(DEF_BADGE_TEXT_SP * 10)) / 10f; + } + + public boolean isPercentEnabled() { + return getInt(KEY_PERCENT_ENABLED, DEF_PERCENT_ENABLED ? 1 : 0) != 0; + } + + public float getPercentTextSizeSp() { + return getInt(KEY_PERCENT_SIZE_SP10, (int)(DEF_PERCENT_SP * 10)) / 10f; + } + + public boolean isPercentBold() { + return getInt(KEY_PERCENT_BOLD, DEF_PERCENT_BOLD ? 1 : 0) != 0; + } + + public String getPercentPosition() { + int idx = clamp(getInt(KEY_PERCENT_POSITION, DEF_PERCENT_POSITION), 0, + POSITION_NAMES.length - 1); + return POSITION_NAMES[idx]; + } + + public float getPercentOffsetXDp() { + return getInt(KEY_PERCENT_OFFSET_X, 0) / 10f; + } + + public float getPercentOffsetYDp() { + return getInt(KEY_PERCENT_OFFSET_Y, 0) / 10f; + } + + public boolean isFilenameEnabled() { + return getInt(KEY_FILENAME_ENABLED, DEF_FILENAME_ENABLED ? 1 : 0) != 0; + } + + public float getFilenameTextSizeSp() { + return getInt(KEY_FILENAME_SIZE_SP10, (int)(DEF_FILENAME_SP * 10)) / 10f; + } + + public boolean isFilenameBold() { + return getInt(KEY_FILENAME_BOLD, DEF_FILENAME_BOLD ? 1 : 0) != 0; + } + + public String getFilenamePosition() { + int idx = clamp(getInt(KEY_FILENAME_POSITION, DEF_FILENAME_POSITION), 0, + POSITION_NAMES.length - 1); + return POSITION_NAMES[idx]; + } + + public float getFilenameOffsetXDp() { + return getInt(KEY_FILENAME_OFFSET_X, 0) / 10f; + } + + public float getFilenameOffsetYDp() { + return getInt(KEY_FILENAME_OFFSET_Y, 0) / 10f; + } + + public int getFilenameMaxChars() { + return getInt(KEY_FILENAME_MAX_CHARS, DEF_FILENAME_MAX_CHARS); + } + + public String getFilenameTruncateMode() { + int idx = clamp(getInt(KEY_FILENAME_TRUNCATE, DEF_FILENAME_TRUNCATE), 0, + TRUNCATE_MODE_NAMES.length - 1); + return TRUNCATE_MODE_NAMES[idx]; + } + + public String getProgressEasing() { + int idx = clamp(getInt(KEY_PROGRESS_EASING, DEF_EASING), 0, + EASING_NAMES.length - 1); + return EASING_NAMES[idx]; + } + + public boolean isChargingRingEnabled() { + return getInt(KEY_CHARGING_RING_ENABLED, DEF_CHARGING_RING_ENABLED ? 1 : 0) != 0; + } + + public boolean isChargingPulseEnabled() { + return getInt(KEY_CHARGING_PULSE_ENABLED, DEF_CHARGING_PULSE_ENABLED ? 1 : 0) != 0; + } + + public void setEnabled(boolean value) { + putInt(KEY_ENABLED, value ? 1 : 0); + } + + public void setRingColorMode(int mode) { + putInt(KEY_RING_COLOR_MODE, clamp(mode, RING_COLOR_MODE_ACCENT, RING_COLOR_MODE_CUSTOM)); + } + + public void setRingColor(int argb) { + putInt(KEY_RING_COLOR, argb); + } + + public void setOpacity(int opacity) { + putInt(KEY_OPACITY, clamp(opacity, 0, 100)); + } + + public void setClockwise(boolean cw) { + putInt(KEY_CLOCKWISE, cw ? 1 : 0); + } + + public void setFinishStyle(int styleIndex) { + putInt(KEY_FINISH_STYLE, clamp(styleIndex, 0, FINISH_STYLE_NAMES.length - 1)); + } + + private int getInt(String key, int def) { + return Settings.Secure.getInt(mCr, key, def); + } + + private void putInt(String key, int value) { + Settings.Secure.putInt(mCr, key, value); + } + + private static int clamp(int v, int lo, int hi) { + return Math.max(lo, Math.min(hi, v)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/DownloadStateTracker.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/DownloadStateTracker.java new file mode 100644 index 0000000000000..be2e4af909329 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/DownloadStateTracker.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cutoutprogress; + +import android.os.Bundle; + +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +import java.util.concurrent.ConcurrentHashMap; + +public final class DownloadStateTracker { + + private static final String EXTRA_PROGRESS = "android.progress"; + private static final String EXTRA_PROGRESS_MAX = "android.progressMax"; + private static final String EXTRA_TITLE = "android.title"; + + private static final long STALE_TIMEOUT_MS = 5 * 60 * 1000L; + + private static final int ERROR_THRESHOLD_PCT = 5; + + private static final int REASON_APP_CANCEL = 8; + private static final int REASON_APP_CANCEL_ALL = 9; + + private static final int RESET_DROP_PCT = 25; + + private static final class DownloadSnapshot { + final String pkg; + String label; + int progress; + long updatedAt; + + DownloadSnapshot(String pkg, String label, int progress) { + this.pkg = pkg; + this.label = label; + this.progress = progress; + this.updatedAt = System.currentTimeMillis(); + } + } + + private final ConcurrentHashMap mActive = + new ConcurrentHashMap<>(); + + public interface IntCallback { void onValue(int value); } + public interface StringCallback { void onValue(String value); } + + private IntCallback mOnProgress; + private Runnable mOnComplete; + private Runnable mOnError; + private IntCallback mOnCountChanged; + private StringCallback mOnLabelChanged; + + public void setOnProgress(IntCallback cb) { mOnProgress = cb; } + public void setOnComplete(Runnable cb) { mOnComplete = cb; } + public void setOnError(Runnable cb) { mOnError = cb; } + public void setOnCountChanged(IntCallback cb) { mOnCountChanged = cb; } + public void setOnLabelChanged(StringCallback cb) { mOnLabelChanged = cb; } + + public void onNotificationChanged(NotificationEntry entry) { + Bundle extras = entry.getSbn().getNotification().extras; + if (extras == null) return; + + int rawProgress = extras.getInt(EXTRA_PROGRESS, -1); + int rawMax = extras.getInt(EXTRA_PROGRESS_MAX, -1); + + String id = entryKey(entry); + String pkg = entry.getSbn().getPackageName(); + + if (rawProgress < 0 || rawMax <= 0) { + if (mActive.remove(id) != null) { + notifyCountChanged(); + publishAggregated(); + fireComplete(); + } + return; + } + + int pct = clamp(rawProgress * 100 / rawMax, 0, 100); + String label = charSeqStr(extras.getCharSequence(EXTRA_TITLE)); + + DownloadSnapshot existing = mActive.get(id); + boolean isNew = existing == null; + boolean isReset = existing != null && (existing.progress - pct) >= RESET_DROP_PCT; + + if (isReset) mActive.remove(id); + pruneStale(pkg, id); + + if (isNew || isReset || existing.progress != pct) { + String usedLabel = (label != null) ? label + : (existing != null ? existing.label : null); + mActive.put(id, new DownloadSnapshot(pkg, usedLabel, pct)); + publishAggregated(); + if (isNew || isReset) notifyCountChanged(); + } + + if (pct >= 100) { + mActive.remove(id); + notifyCountChanged(); + publishAggregated(); + fireComplete(); + } + } + + public void onNotificationRemoved(NotificationEntry entry, int reason) { + DownloadSnapshot snap = mActive.remove(entryKey(entry)); + if (snap == null) return; + + notifyCountChanged(); + publishAggregated(); + + if (snap.progress >= 100) { + fireComplete(); + } else if (reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL) { + fireComplete(); + } else if (snap.progress >= ERROR_THRESHOLD_PCT) { + fireError(); + } + } + + public void reset() { + mActive.clear(); + notifyCountChanged(); + fire(mOnProgress, 0); + fire(mOnLabelChanged, null); + } + + public int getActiveCount() { + return mActive.size(); + } + + private String entryKey(NotificationEntry e) { + return e.getSbn().getPackageName() + ":" + e.getSbn().getId(); + } + + private void pruneStale(String pkg, String currentId) { + long now = System.currentTimeMillis(); + for (java.util.Map.Entry e : mActive.entrySet()) { + if (!e.getKey().equals(currentId) + && e.getValue().pkg.equals(pkg) + && now - e.getValue().updatedAt > STALE_TIMEOUT_MS) { + mActive.remove(e.getKey()); + } + } + } + + private void publishAggregated() { + int avg = 0; + if (!mActive.isEmpty()) { + int sum = 0; + for (DownloadSnapshot s : mActive.values()) sum += s.progress; + avg = sum / mActive.size(); + } + fire(mOnProgress, avg); + publishBestLabel(); + } + + private void publishBestLabel() { + DownloadSnapshot best = null; + for (DownloadSnapshot s : mActive.values()) { + if (best == null || s.progress > best.progress) best = s; + } + String lbl = null; + if (best != null && best.label != null + && !best.label.toLowerCase().contains("untitled")) { + lbl = best.label; + } + fire(mOnLabelChanged, lbl); + } + + private void notifyCountChanged() { fire(mOnCountChanged, mActive.size()); } + private void fireComplete() { if (mOnComplete != null) mOnComplete.run(); } + private void fireError() { if (mOnError != null) mOnError.run(); } + + private void fire(IntCallback cb, int v) { if (cb != null) cb.onValue(v); } + private void fire(StringCallback cb, String v) { if (cb != null) cb.onValue(v); } + + private static String charSeqStr(CharSequence cs) { + return cs != null ? cs.toString() : null; + } + + private static int clamp(int v, int lo, int hi) { + return Math.max(lo, Math.min(hi, v)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/dagger/CutoutProgressModule.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/dagger/CutoutProgressModule.java new file mode 100644 index 0000000000000..3d5d4c8b37fce --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/dagger/CutoutProgressModule.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cutoutprogress.dagger; + +import com.android.systemui.CoreStartable; +import com.android.systemui.cutoutprogress.CutoutProgressController; + +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; + +@Module +public abstract class CutoutProgressModule { + + @Binds + @IntoMap + @ClassKey(CutoutProgressController.class) + abstract CoreStartable bindCutoutProgressController(CutoutProgressController controller); +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CapsuleRingRenderer.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CapsuleRingRenderer.java new file mode 100644 index 0000000000000..e100dde1ba48a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CapsuleRingRenderer.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cutoutprogress.ring; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.RectF; + +public final class CapsuleRingRenderer implements RingViewRenderer { + + private final Path mOutline = new Path(); + private final PathMeasure mMeasure = new PathMeasure(); + private final Path mWorkPath = new Path(); + private float mTotalLength = 0f; + + @Override + public void updateBounds(RectF bounds) { + mOutline.reset(); + buildCapsule(bounds); + mMeasure.setPath(mOutline, false); + mTotalLength = mMeasure.getLength(); + } + + private void buildCapsule(RectF b) { + float r = Math.min(b.width(), b.height()) / 2f; + float cx = b.centerX(); + + mOutline.moveTo(cx, b.top); + + if (b.width() >= b.height()) { + mOutline.lineTo(b.right - r, b.top); + mOutline.arcTo(b.right - 2*r, b.top, b.right, b.bottom, -90f, 180f, false); + mOutline.lineTo(b.left + r, b.bottom); + mOutline.arcTo(b.left, b.top, b.left + 2*r, b.bottom, 90f, 180f, false); + } else { + mOutline.arcTo(b.left, b.top, b.right, b.top + 2*r, -90f, 90f, false); + mOutline.lineTo(b.right, b.bottom - r); + mOutline.arcTo(b.left, b.bottom - 2*r, b.right, b.bottom, 0f, 180f, false); + mOutline.lineTo(b.left, b.top + r); + mOutline.arcTo(b.left, b.top, b.right, b.top + 2*r, 180f, 90f, false); + } + mOutline.close(); + } + + @Override + public void drawFullRing(Canvas canvas, Paint paint) { + if (mTotalLength == 0f) return; + canvas.drawPath(mOutline, paint); + } + + @Override + public void drawProgress(Canvas canvas, float sweepFraction, + boolean clockwise, Paint paint) { + if (mTotalLength == 0f) return; + if (sweepFraction >= 1f) { drawFullRing(canvas, paint); return; } + float len = Math.max(0f, Math.min(1f, sweepFraction)) * mTotalLength; + mWorkPath.reset(); + if (clockwise) { + mMeasure.getSegment(0f, len, mWorkPath, true); + } else { + mMeasure.getSegment(mTotalLength - len, mTotalLength, mWorkPath, true); + } + canvas.drawPath(mWorkPath, paint); + } + + @Override + public void drawSegmented(Canvas canvas, + int segments, float gapDeg, float arcDeg, + int highlight, + Paint basePaint, Paint shinePaint, float alpha) { + if (mTotalLength == 0f) return; + float totalDeg = segments * (arcDeg + gapDeg); + float segLen = mTotalLength * (arcDeg / totalDeg); + float gapLen = mTotalLength * (gapDeg / totalDeg); + + for (int i = 0; i < segments; i++) { + float start = i * (segLen + gapLen); + mWorkPath.reset(); + mMeasure.getSegment(start, start + segLen, mWorkPath, true); + + if (i == highlight || i == highlight - 1) { + Paint tmp = new Paint(shinePaint); + tmp.setAlpha((int)(255 * alpha)); + canvas.drawPath(mWorkPath, tmp); + } else { + canvas.drawPath(mWorkPath, basePaint); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CircleRingRenderer.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CircleRingRenderer.java new file mode 100644 index 0000000000000..1cda0ec70db67 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CircleRingRenderer.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cutoutprogress.ring; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +public final class CircleRingRenderer implements RingViewRenderer { + + private final RectF mBounds = new RectF(); + + @Override + public void updateBounds(RectF bounds) { + mBounds.set(bounds); + } + + @Override + public void drawFullRing(Canvas canvas, Paint paint) { + canvas.drawArc(mBounds, 0f, 360f, false, paint); + } + + @Override + public void drawProgress(Canvas canvas, float sweepFraction, + boolean clockwise, Paint paint) { + float sweep = 360f * Math.max(0f, Math.min(1f, sweepFraction)); + float actual = clockwise ? sweep : -sweep; + canvas.drawArc(mBounds, -90f, actual, false, paint); + } + + @Override + public void drawSegmented(Canvas canvas, + int segments, float gapDeg, float arcDeg, + int highlight, + Paint basePaint, Paint shinePaint, float alpha) { + for (int i = 0; i < segments; i++) { + float startAngle = -90f + i * (arcDeg + gapDeg); + if (i == highlight || i == highlight - 1) { + Paint tmp = new Paint(shinePaint); + tmp.setAlpha((int)(255 * alpha)); + canvas.drawArc(mBounds, startAngle, arcDeg, false, tmp); + } else { + canvas.drawArc(mBounds, startAngle, arcDeg, false, basePaint); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CountBadgePainter.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CountBadgePainter.java new file mode 100644 index 0000000000000..b1726b0fea4e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CountBadgePainter.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cutoutprogress.ring; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Typeface; + +import androidx.core.graphics.ColorUtils; + +public final class CountBadgePainter { + + private static final float H_PAD_DP = 6f; + private static final float V_PAD_DP = 3f; + private static final float MIN_WIDTH_DP = 20f; + private static final float DARKEN_AMOUNT = 0.75f; + private static final float BRIGHTEN_AMOUNT = 0.80f; + private static final float LIGHT_THRESHOLD = 0.40f; + private static final int BG_ALPHA = 230; + + private final float mDp; + private final Paint mBgPaint; + private final Paint mTextPaint; + private final RectF mRect = new RectF(); + + private int mLastCount = Integer.MIN_VALUE; + private String mLastText = ""; + + public CountBadgePainter(float density) { + mDp = density; + + mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mBgPaint.setStyle(Paint.Style.FILL); + + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setTextAlign(Paint.Align.CENTER); + mTextPaint.setSubpixelText(true); + mTextPaint.setTypeface(Typeface.DEFAULT_BOLD); + } + + public void applyConfig(int baseColor, float textSizeSp, float density) { + double lum = ColorUtils.calculateLuminance(baseColor); + mBgPaint.setColor(darken(baseColor, DARKEN_AMOUNT)); + int textColor = lum > LIGHT_THRESHOLD + ? Color.WHITE : brighten(baseColor, BRIGHTEN_AMOUNT); + mTextPaint.setColor(textColor); + mTextPaint.setTextSize(textSizeSp * density); + } + + public void draw(Canvas canvas, float cx, float topY, int count, int opacityPercent) { + if (count != mLastCount) { + mLastCount = count; + mLastText = String.valueOf(count); + } + + float textW = mTextPaint.measureText(mLastText); + float hPad = H_PAD_DP * mDp; + float vPad = V_PAD_DP * mDp; + float height = mTextPaint.getTextSize() + vPad * 2f; + float width = Math.max(textW + hPad * 2f, MIN_WIDTH_DP * mDp); + + mRect.set(cx - width / 2f, topY, cx + width / 2f, topY + height); + + int baseAlpha = clamp(opacityPercent, 0, 100) * 255 / 100; + mBgPaint.setAlpha(baseAlpha * BG_ALPHA / 255); + mTextPaint.setAlpha(baseAlpha); + + canvas.drawRoundRect(mRect, height / 2f, height / 2f, mBgPaint); + + Paint.FontMetrics fm = mTextPaint.getFontMetrics(); + float textY = mRect.centerY() - (fm.ascent + fm.descent) / 2f; + canvas.drawText(mLastText, cx, textY, mTextPaint); + } + + private static int darken(int color, float fraction) { + float[] hsl = new float[3]; + ColorUtils.colorToHSL(color, hsl); + hsl[2] *= (1f - fraction); + return ColorUtils.HSLToColor(hsl); + } + + private static int brighten(int color, float fraction) { + float[] hsl = new float[3]; + ColorUtils.colorToHSL(color, hsl); + hsl[2] += (1f - hsl[2]) * fraction; + return ColorUtils.HSLToColor(hsl); + } + + private static int clamp(int v, int lo, int hi) { + return Math.max(lo, Math.min(hi, v)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java new file mode 100644 index 0000000000000..7910bc2867752 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java @@ -0,0 +1,837 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cutoutprogress.ring; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.SweepGradient; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.util.TypedValue; +import android.view.DisplayCutout; +import android.view.Surface; +import android.view.View; +import android.view.WindowInsets; +import android.view.animation.LinearInterpolator; + +import com.android.systemui.cutoutprogress.CutoutProgressSettings; + +import java.util.Objects; + +public final class CutoutRingView extends View { + + private static final long BURN_IN_HIDE_MS = 10_000L; + + private static final long CHARGING_PULSE_INTERVAL_MS = 1500L; + private static final long CHARGING_PULSE_DURATION_MS = 900L; + + private static final int[] RAINBOW_COLORS = { + 0xFFFF0000, + 0xFFFF7F00, + 0xFFFFFF00, + 0xFF00FF00, + 0xFF00FFFF, + 0xFF0000FF, + 0xFF8B00FF, + 0xFFFF0000, + }; + + private final float mDp; + + private final Path mCutoutPath = new Path(); + private final Path mScaledPath = new Path(); + private final Matrix mScaleMatrix = new Matrix(); + private final RectF mPathBounds = new RectF(); + private final RectF mArcBounds = new RectF(); + private boolean mHasCutout = false; + + private final OverlayAnimationHelper mAnim; + private RingViewRenderer mRenderer; + private final CountBadgePainter mBadge; + + private final Paint mRingPaint = makePaint(); + private final Paint mShinePaint = makePaint(); + private final Paint mErrorPaint = makePaint(); + private final Paint mAnimPaint = makePaint(); + private final Paint mBgPaint = makePaint(); + private final Paint mChargingPaint = makePaint(); + private final TextPaint mPercentPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + private final TextPaint mFilenamePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + + private final Paint mRainbowPaint = makePaint(); + private SweepGradient mRainbowShader = null; + private float mRainbowCx = Float.NaN; + private float mRainbowCy = Float.NaN; + + private int mProgress = 0; + private int mDownloadCount = 0; + private String mFilenameHint = null; + + private long mDownloadStartMs = 0L; + private long mLastProgressMs = 0L; + private Runnable mPendingFinish = null; + + private boolean mIsCharging = false; + private int mBatteryPct = 0; + private boolean mChargingPulseEnabled = true; + private float mChargingPulsePhase = 0f; + private ValueAnimator mChargingPulseAnim = null; + private float mChargingDisplayPct = 0f; + private ValueAnimator mChargingLevelAnim = null; + + private int sCfgRingColorMode; + private int sCfgRingColor; + private int sCfgErrorColor; + private int sCfgFlashColor; + private float sCfgStrokeDp; + private float sCfgRingGap; + private int sCfgOpacity; + private boolean sCfgClockwise; + private String sCfgFinishStyle; + private int sCfgFinishHoldMs; + private int sCfgFinishExitMs; + private boolean sCfgFinishFlash; + private boolean sCfgPulse; + private boolean sCfgPathMode; + private float sCfgScaleX; + private float sCfgScaleY; + private float sCfgOffsetXDp; + private float sCfgOffsetYDp; + private boolean sCfgBgRing; + private int sCfgBgColor; + private int sCfgBgOpacity; + private boolean sCfgMinVis; + private int sCfgMinVisMs; + private boolean sCfgShowBadge; + private float sCfgBadgeOffXDp; + private float sCfgBadgeOffYDp; + private float sCfgBadgeSp; + private boolean sCfgPct; + private float sCfgPctSp; + private boolean sCfgPctBold; + private String sCfgPctPos; + private float sCfgPctOffXDp; + private float sCfgPctOffYDp; + private boolean sCfgFname; + private float sCfgFnameSp; + private boolean sCfgFnameBold; + private String sCfgFnamePos; + private float sCfgFnameOffXDp; + private float sCfgFnameOffYDp; + private int sCfgFnameMaxChars; + private String sCfgFnameTruncate; + private String sCfgEasing; + private boolean sCfgChargingRing; + private boolean sCfgChargingPulse; + + public CutoutRingView(Context ctx) { + super(ctx); + mDp = ctx.getResources().getDisplayMetrics().density; + mAnim = new OverlayAnimationHelper(this); + mRenderer = new CircleRingRenderer(); + mBadge = new CountBadgePainter(mDp); + initPaints(); + } + + public void applySettings(CutoutProgressSettings s) { + sCfgRingColorMode = s.getRingColorMode(); + sCfgRingColor = s.getRingColor(); + sCfgErrorColor = s.getErrorColor(); + sCfgFlashColor = s.getFinishFlashColor(); + sCfgStrokeDp = s.getStrokeWidthDp(); + sCfgRingGap = s.getRingGap(); + sCfgOpacity = s.getOpacity(); + sCfgClockwise = s.isClockwise(); + sCfgFinishStyle = s.getFinishStyle(); + sCfgFinishHoldMs = s.getFinishHoldMs(); + sCfgFinishExitMs = s.getFinishExitMs(); + sCfgFinishFlash = s.isFinishUseFlash(); + sCfgPulse = s.isCompletionPulse(); + sCfgPathMode = s.isPathMode(); + sCfgScaleX = s.getRingScaleX(); + sCfgScaleY = s.getRingScaleY(); + sCfgOffsetXDp = s.getRingOffsetXDp(); + sCfgOffsetYDp = s.getRingOffsetYDp(); + sCfgBgRing = s.isBgRingEnabled(); + sCfgBgColor = s.getBgRingColor(); + sCfgBgOpacity = s.getBgRingOpacity(); + sCfgMinVis = s.isMinVisEnabled(); + sCfgMinVisMs = s.getMinVisMs(); + sCfgShowBadge = s.isShowCountBadge(); + sCfgBadgeOffXDp = s.getBadgeOffsetXDp(); + sCfgBadgeOffYDp = s.getBadgeOffsetYDp(); + sCfgBadgeSp = s.getBadgeTextSizeSp(); + sCfgPct = s.isPercentEnabled(); + sCfgPctSp = s.getPercentTextSizeSp(); + sCfgPctBold = s.isPercentBold(); + sCfgPctPos = s.getPercentPosition(); + sCfgPctOffXDp = s.getPercentOffsetXDp(); + sCfgPctOffYDp = s.getPercentOffsetYDp(); + sCfgFname = s.isFilenameEnabled(); + sCfgFnameSp = s.getFilenameTextSizeSp(); + sCfgFnameBold = s.isFilenameBold(); + sCfgFnamePos = s.getFilenamePosition(); + sCfgFnameOffXDp = s.getFilenameOffsetXDp(); + sCfgFnameOffYDp = s.getFilenameOffsetYDp(); + sCfgFnameMaxChars= s.getFilenameMaxChars(); + sCfgFnameTruncate= s.getFilenameTruncateMode(); + sCfgEasing = s.getProgressEasing(); + sCfgChargingRing = s.isChargingRingEnabled(); + sCfgChargingPulse = s.isChargingPulseEnabled(); + + boolean needPath = sCfgPathMode; + if (needPath && !(mRenderer instanceof CapsuleRingRenderer)) { + mRenderer = new CapsuleRingRenderer(); + } else if (!needPath && !(mRenderer instanceof CircleRingRenderer)) { + mRenderer = new CircleRingRenderer(); + } + + if (!sCfgChargingRing && mIsCharging) { + stopChargingAnimations(); + } + mChargingPulseEnabled = sCfgChargingPulse; + + if (sCfgRingColorMode != CutoutProgressSettings.RING_COLOR_MODE_RAINBOW) { + mRainbowShader = null; + mRainbowCx = Float.NaN; + } + + refreshPaints(); + recalcScaledPath(); + invalidate(); + } + + private int resolveRingColor() { + switch (sCfgRingColorMode) { + case CutoutProgressSettings.RING_COLOR_MODE_ACCENT: + return resolveAccentColor(); + case CutoutProgressSettings.RING_COLOR_MODE_RAINBOW: + return RAINBOW_COLORS[0]; + default: + return sCfgRingColor; + } + } + + private int resolveAccentColor() { + TypedValue tv = new TypedValue(); + boolean resolved = getContext().getTheme() + .resolveAttribute(android.R.attr.colorAccent, tv, true); + int base = resolved ? tv.data : 0xFF2196F3; + + boolean isDark = (getContext().getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + + if (isDark) { + return lightenColor(base, 0.50f); + } else { + return base; + } + } + + private static int lightenColor(int color, float fraction) { + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + r = (int)(r + (255 - r) * fraction); + g = (int)(g + (255 - g) * fraction); + b = (int)(b + (255 - b) * fraction); + return Color.argb(Color.alpha(color), + Math.min(255, r), Math.min(255, g), Math.min(255, b)); + } + + private SweepGradient requireRainbowShader(float cx, float cy) { + if (mRainbowShader == null + || Math.abs(cx - mRainbowCx) > 0.5f + || Math.abs(cy - mRainbowCy) > 0.5f) { + mRainbowShader = new SweepGradient(cx, cy, RAINBOW_COLORS, null); + mRainbowCx = cx; + mRainbowCy = cy; + } + return mRainbowShader; + } + + private void applyRainbowShader(Paint paint, float cx, float cy) { + SweepGradient shader = requireRainbowShader(cx, cy); + Matrix m = new Matrix(); + m.setRotate(-90f, cx, cy); + shader.setLocalMatrix(m); + paint.setShader(shader); + } + + private static void clearShader(Paint paint) { + paint.setShader(null); + } + + public void setChargingState(boolean charging, int batteryPct) { + boolean wasCharging = mIsCharging; + mIsCharging = charging; + mBatteryPct = batteryPct; + + if (!charging) { + stopChargingAnimations(); + invalidate(); + return; + } + + if (!wasCharging) { + mChargingDisplayPct = 0f; + } + + animateChargingLevelTo(batteryPct); + + if (mChargingPulseEnabled && sCfgChargingPulse && mChargingPulseAnim == null) { + startChargingPulse(); + } + invalidate(); + } + + public void setChargingPulseEnabled(boolean enabled) { + mChargingPulseEnabled = enabled; + sCfgChargingPulse = enabled; + if (!enabled) { + stopChargingPulse(); + } else if (mIsCharging && mChargingPulseAnim == null) { + startChargingPulse(); + } + } + + private void animateChargingLevelTo(int targetPct) { + if (mChargingLevelAnim != null) mChargingLevelAnim.cancel(); + float start = mChargingDisplayPct; + float end = Math.max(0f, Math.min(100f, targetPct)); + if (Math.abs(end - start) < 0.5f) { + mChargingDisplayPct = end; + invalidate(); + return; + } + long dur = (long)(Math.abs(end - start) * 12f); + dur = Math.max(200L, Math.min(dur, 1200L)); + mChargingLevelAnim = ValueAnimator.ofFloat(start, end); + mChargingLevelAnim.setDuration(dur); + mChargingLevelAnim.setInterpolator(new LinearInterpolator()); + mChargingLevelAnim.addUpdateListener(a -> { + mChargingDisplayPct = (float) a.getAnimatedValue(); + invalidate(); + }); + mChargingLevelAnim.start(); + } + + private void startChargingPulse() { + stopChargingPulse(); + mChargingPulseAnim = ValueAnimator.ofFloat(0f, 1f); + mChargingPulseAnim.setDuration(CHARGING_PULSE_DURATION_MS); + mChargingPulseAnim.setRepeatCount(ValueAnimator.INFINITE); + mChargingPulseAnim.setRepeatMode(ValueAnimator.REVERSE); + mChargingPulseAnim.setInterpolator(new LinearInterpolator()); + mChargingPulseAnim.setStartDelay(0); + mChargingPulseAnim.addUpdateListener(a -> { + mChargingPulsePhase = (float) a.getAnimatedValue(); + invalidate(); + }); + mChargingPulseAnim.start(); + } + + private void stopChargingPulse() { + if (mChargingPulseAnim != null) { + mChargingPulseAnim.cancel(); + mChargingPulseAnim = null; + } + mChargingPulsePhase = 0f; + } + + private void stopChargingAnimations() { + stopChargingPulse(); + if (mChargingLevelAnim != null) { + mChargingLevelAnim.cancel(); + mChargingLevelAnim = null; + } + mChargingDisplayPct = 0f; + } + + public void setProgress(int value) { + int pct = Math.max(0, Math.min(100, value)); + if (mProgress == pct) return; + + int prev = mProgress; + mProgress = pct; + mLastProgressMs = System.currentTimeMillis(); + + removeCallbacks(mBurnInHide); + if (pct > 0 && pct < 100) { + postDelayed(mBurnInHide, BURN_IN_HIDE_MS); + } + + if (prev == 0 && pct > 0) { + mDownloadStartMs = System.currentTimeMillis(); + cancelPendingFinish(); + } + + if (pct == 100 && !mAnim.isFinishAnimating) { + long elapsed = System.currentTimeMillis() - mDownloadStartMs; + long remaining = (sCfgMinVis ? sCfgMinVisMs : 0) - elapsed; + if (remaining > 0 && mDownloadStartMs > 0) { + mPendingFinish = () -> { mPendingFinish = null; beginFinishAnim(); }; + postDelayed(mPendingFinish, remaining); + } else { + beginFinishAnim(); + } + } else if (pct > 0 && pct < 100 && mAnim.isFinishAnimating) { + mAnim.cancelFinish(); + } else if (pct == 0) { + mDownloadStartMs = 0L; + cancelPendingFinish(); + } + + invalidate(); + } + + public void setDownloadCount(int count) { + if (mDownloadCount != count) { mDownloadCount = count; invalidate(); } + } + + public void setFilenameHint(String hint) { + if (!Objects.equals(mFilenameHint, hint)) { mFilenameHint = hint; invalidate(); } + } + + public void showError() { + mAnim.startError(() -> setProgress(0)); + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + mCutoutPath.reset(); + mHasCutout = false; + + DisplayCutout cutout = insets.getDisplayCutout(); + if (cutout != null) { + Path native31 = null; + try { native31 = cutout.getCutoutPath(); } catch (NoSuchMethodError ignored) {} + + if (native31 != null && !native31.isEmpty()) { + mCutoutPath.set(native31); + mHasCutout = true; + } else if (!cutout.getBoundingRects().isEmpty()) { + android.graphics.Rect r = cutout.getBoundingRects().get(0); + float cx = r.exactCenterX(); + float cy = r.exactCenterY(); + float radius = Math.min(r.width(), r.height()) / 2f; + mCutoutPath.addCircle(cx, cy, radius, Path.Direction.CW); + mHasCutout = true; + } + } + + if (!mHasCutout) { + float cx = getResources().getDisplayMetrics().widthPixels / 2f; + float radius = 15f * mDp; + mCutoutPath.addCircle(cx, radius * 2f, radius, Path.Direction.CW); + mHasCutout = true; + } + + mRainbowShader = null; + mRainbowCx = Float.NaN; + + recalcScaledPath(); + invalidate(); + return super.onApplyWindowInsets(insets); + } + + private void recalcScaledPath() { + mCutoutPath.computeBounds(mPathBounds, true); + mScaleMatrix.setScale(sCfgRingGap, sCfgRingGap, + mPathBounds.centerX(), mPathBounds.centerY()); + mScaledPath.reset(); + mCutoutPath.transform(mScaleMatrix, mScaledPath); + } + + private final Runnable mBurnInHide = this::invalidate; + + @Override + protected void onDraw(Canvas canvas) { + if (!mHasCutout) return; + + if (mAnim.isErrorAnimating) { + computeArcBounds(); + mRenderer.updateBounds(mArcBounds); + mErrorPaint.setAlpha((int)(mAnim.errorAlpha * 255)); + mRenderer.drawFullRing(canvas, mErrorPaint); + return; + } + + int effectivePct = mAnim.isGeometryPreviewActive() ? 100 + : mAnim.isDynamicPreviewActive() ? mAnim.previewProgress + : mProgress; + + boolean burnedOut = effectivePct > 0 && effectivePct < 100 + && mLastProgressMs > 0 + && System.currentTimeMillis() - mLastProgressMs >= BURN_IN_HIDE_MS; + + boolean shouldDraw = mAnim.isFinishAnimating + || mAnim.isGeometryPreviewActive() + || mAnim.isDynamicPreviewActive() + || (effectivePct > 0 && effectivePct < 100 && !burnedOut) + || mPendingFinish != null; + + boolean showCharging = mIsCharging && sCfgChargingRing && !mAnim.isFinishAnimating + && !mAnim.isErrorAnimating && effectivePct == 0 && mProgress == 0; + + if (!shouldDraw && !showCharging) return; + + if (showCharging) { + drawChargingRing(canvas); + return; + } + + if (mAnim.displayScale != 1f) { + mScaledPath.computeBounds(mArcBounds, true); + canvas.save(); + canvas.scale(mAnim.displayScale, mAnim.displayScale, + mArcBounds.centerX(), mArcBounds.centerY()); + } + + int activeRingColor = resolveRingColor(); + + mAnimPaint.set(mRingPaint); + mAnimPaint.setColor(activeRingColor); + mAnimPaint.setStrokeWidth(sCfgStrokeDp * mDp); + int alpha = (int)(sCfgOpacity * 255f / 100f + * mAnim.displayAlpha * mAnim.completionPulseAlpha); + mAnimPaint.setAlpha(alpha); + + computeArcBounds(); + if (sCfgRingColorMode == CutoutProgressSettings.RING_COLOR_MODE_RAINBOW) { + applyRainbowShader(mAnimPaint, mArcBounds.centerX(), mArcBounds.centerY()); + } else { + clearShader(mAnimPaint); + } + + if (mAnim.successColorBlend > 0f) { + int flashColor = sCfgFinishFlash ? sCfgFlashColor + : brighten(activeRingColor, mAnim.successColorBlend); + mAnimPaint.setColor(blendColors(activeRingColor, flashColor, + mAnim.successColorBlend)); + clearShader(mAnimPaint); + } + + boolean isActive = effectivePct > 0 && effectivePct < 100 + || mAnim.isGeometryPreviewActive() + || mAnim.isDynamicPreviewActive(); + + if (sCfgBgRing && !mAnim.isFinishAnimating && isActive) { + mRenderer.updateBounds(mArcBounds); + mBgPaint.setAlpha((int)(sCfgBgOpacity * 255 / 100 * mAnim.displayAlpha)); + mRenderer.drawFullRing(canvas, mBgPaint); + } + + mRenderer.updateBounds(mArcBounds); + if (mAnim.isFinishAnimating) { + drawFinish(canvas, mAnimPaint); + } else { + float sweep = eased(effectivePct, sCfgEasing); + mRenderer.drawProgress(canvas, sweep, sCfgClockwise, mAnimPaint); + + if (isActive) drawLabels(canvas, effectivePct, activeRingColor); + } + + boolean showBadge = !mAnim.isDynamicPreviewActive() && sCfgShowBadge + && (mDownloadCount > 1 || mAnim.isGeometryPreviewActive()); + if (showBadge) { + mScaledPath.computeBounds(mArcBounds, true); + float badgeCx = mArcBounds.centerX() + sCfgBadgeOffXDp * mDp; + float badgeTop = mArcBounds.bottom + 4f * mDp + sCfgBadgeOffYDp * mDp; + int badgeN = mAnim.isGeometryPreviewActive() ? 3 : mDownloadCount; + mBadge.draw(canvas, badgeCx, badgeTop, badgeN, sCfgOpacity); + } + + if (mAnim.displayScale != 1f) canvas.restore(); + } + + private void drawChargingRing(Canvas canvas) { + computeArcBounds(); + mRenderer.updateBounds(mArcBounds); + + int baseAlpha = sCfgOpacity * 255 / 100; + + if (sCfgBgRing) { + mBgPaint.setAlpha(sCfgBgOpacity * 255 / 100); + mRenderer.drawFullRing(canvas, mBgPaint); + } + + int levelColor = chargingColor(mChargingDisplayPct); + applyStroke(mChargingPaint, levelColor, sCfgStrokeDp * mDp, baseAlpha); + + if (mChargingPulseEnabled && sCfgChargingPulse && mChargingPulseAnim != null) { + float drawFraction = mChargingPulsePhase * (mChargingDisplayPct / 100f); + drawSymmetricArc(canvas, drawFraction, mChargingPaint); + } else { + drawSymmetricArc(canvas, mChargingDisplayPct / 100f, mChargingPaint); + } + } + + private void drawSymmetricArc(Canvas canvas, float fraction, Paint paint) { + if (fraction <= 0f) return; + fraction = Math.min(fraction, 1f); + float sweep = fraction * 180f; + + canvas.drawArc(mArcBounds, 90f - sweep, sweep, false, paint); + canvas.drawArc(mArcBounds, 90f, sweep, false, paint); + } + + private static int chargingColor(float pct) { + if (pct < 30f) return 0xFFF44336; + if (pct < 60f) return 0xFFFF9800; + return 0xFF4CAF50; + } + + private void drawFinish(Canvas canvas, Paint paint) { + if ("segmented".equals(sCfgFinishStyle)) { + mRenderer.drawSegmented(canvas, + OverlayAnimationHelper.SEGMENT_COUNT, + OverlayAnimationHelper.SEGMENT_GAP_DEG, + OverlayAnimationHelper.SEGMENT_ARC_DEG, + mAnim.segmentHighlight, + paint, mShinePaint, mAnim.displayAlpha); + } else { + mRenderer.drawFullRing(canvas, paint); + } + } + + private void drawLabels(Canvas canvas, int pct, int ringColor) { + float pad = 4f * mDp; + int alpha = sCfgOpacity * 255 / 100; + + if (sCfgPct) { + String text = pct + "%"; + float tw = mPercentPaint.measureText(text); + float[] pos = labelXY(sCfgPctPos, pad, mPercentPaint.getTextSize(), tw); + mPercentPaint.setColor(ringColor); + mPercentPaint.setAlpha(alpha); + canvas.drawText(text, pos[0] + sCfgPctOffXDp * mDp, + pos[1] + sCfgPctOffYDp * mDp, mPercentPaint); + } + + boolean geoPreview = mAnim.isGeometryPreviewActive(); + String fname = mFilenameHint != null ? mFilenameHint + : geoPreview ? "EvolutionX-16.0-arm64.zip" : null; + + if (sCfgFname && fname != null && (mDownloadCount <= 1 || geoPreview)) { + String display = truncate(fname, sCfgFnameMaxChars, sCfgFnameTruncate); + float[] pos = labelXY(sCfgFnamePos, pad, mFilenamePaint.getTextSize(), null); + mFilenamePaint.setColor(ringColor); + mFilenamePaint.setAlpha(alpha); + canvas.drawText(display, pos[0] + sCfgFnameOffXDp * mDp, + pos[1] + sCfgFnameOffYDp * mDp, mFilenamePaint); + } + } + + private float[] labelXY(String position, float pad, float textHeight, Float textW) { + switch (position) { + case "left": + return new float[]{ + textW != null ? mArcBounds.left - textW / 2f - pad + : mArcBounds.left - pad, + mArcBounds.centerY() + textHeight / 3f}; + case "top": + return new float[]{mArcBounds.centerX(), mArcBounds.top - pad}; + case "bottom": + return new float[]{mArcBounds.centerX(), + mArcBounds.bottom + textHeight + pad}; + case "top_left": + return new float[]{mArcBounds.left - pad, mArcBounds.top - pad}; + case "top_right": + return new float[]{mArcBounds.right + pad, mArcBounds.top - pad}; + case "bottom_left": + return new float[]{mArcBounds.left - pad, + mArcBounds.bottom + textHeight + pad}; + case "bottom_right": + return new float[]{mArcBounds.right + pad, + mArcBounds.bottom + textHeight + pad}; + default: + return new float[]{ + textW != null ? mArcBounds.right + textW / 2f + pad + : mArcBounds.right + pad, + mArcBounds.centerY() + textHeight / 3f}; + } + } + + private void computeArcBounds() { + mScaledPath.computeBounds(mArcBounds, true); + float[] offRotated = rotateOffset(sCfgOffsetXDp, sCfgOffsetYDp); + float cx = mArcBounds.centerX() + offRotated[0]; + float cy = mArcBounds.centerY() + offRotated[1]; + + float halfW, halfH; + if (sCfgPathMode) { + halfW = mArcBounds.width() / 2f * sCfgScaleX; + halfH = mArcBounds.height() / 2f * sCfgScaleY; + } else { + float halfBase = Math.max(mArcBounds.width(), mArcBounds.height()) / 2f; + halfW = halfBase * sCfgScaleX; + halfH = halfBase * sCfgScaleY; + } + mArcBounds.set(cx - halfW, cy - halfH, cx + halfW, cy + halfH); + } + + private float[] rotateOffset(float dx, float dy) { + int rot = getDisplay() != null ? getDisplay().getRotation() : Surface.ROTATION_0; + switch (rot) { + case Surface.ROTATION_90: return new float[]{ dy * mDp, -dx * mDp}; + case Surface.ROTATION_180: return new float[]{-dx * mDp, -dy * mDp}; + case Surface.ROTATION_270: return new float[]{-dy * mDp, dx * mDp}; + default: return new float[]{ dx * mDp, dy * mDp}; + } + } + + private void initPaints() { + sCfgRingColorMode = CutoutProgressSettings.RING_COLOR_MODE_ACCENT; + sCfgRingColor = 0xFF2196F3; + sCfgErrorColor = 0xFFF44336; + sCfgFlashColor = Color.WHITE; + sCfgStrokeDp = 2f; + sCfgRingGap = 1.155f; + sCfgOpacity = 90; + sCfgBgColor = 0xFF808080; + sCfgBgOpacity = 30; + sCfgPctSp = 8f; + sCfgPctBold = true; + sCfgFnameSp = 7f; + sCfgBadgeSp = 10f; + sCfgPctPos = "right"; + sCfgFnamePos = "top_right"; + sCfgFnameTruncate = "middle"; + sCfgFnameMaxChars = 20; + sCfgEasing = "linear"; + sCfgClockwise = true; + sCfgFinishStyle= "pop"; + sCfgScaleX = sCfgScaleY = 1f; + sCfgBgRing = sCfgMinVis = true; + sCfgMinVisMs = 500; + sCfgChargingRing = true; + sCfgChargingPulse = true; + refreshPaints(); + } + + private void refreshPaints() { + float stroke = sCfgStrokeDp * mDp; + int baseColor = (sCfgRingColorMode == CutoutProgressSettings.RING_COLOR_MODE_CUSTOM) + ? sCfgRingColor : resolveRingColor(); + applyStroke(mRingPaint, baseColor, stroke, sCfgOpacity * 255 / 100); + applyStroke(mShinePaint, sCfgFlashColor, stroke * 1.2f, 255); + applyStroke(mErrorPaint, sCfgErrorColor, stroke * 1.5f, 255); + applyStroke(mBgPaint, sCfgBgColor, stroke, sCfgBgOpacity * 255 / 100); + applyStroke(mChargingPaint, baseColor, stroke, sCfgOpacity * 255 / 100); + + mRainbowPaint.setStyle(Paint.Style.STROKE); + mRainbowPaint.setAntiAlias(true); + mRainbowPaint.setStrokeWidth(stroke); + mRainbowPaint.setStrokeCap(Paint.Cap.BUTT); + mRainbowPaint.setAlpha(sCfgOpacity * 255 / 100); + + mPercentPaint.setTypeface(sCfgPctBold + ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT); + mPercentPaint.setTextSize(spToPx(sCfgPctSp)); + mPercentPaint.setTextAlign(Paint.Align.CENTER); + + mFilenamePaint.setTypeface(sCfgFnameBold + ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT); + mFilenamePaint.setTextSize(spToPx(sCfgFnameSp)); + mFilenamePaint.setTextAlign(Paint.Align.LEFT); + + mBadge.applyConfig(baseColor, sCfgBadgeSp, mDp); + } + + private static void applyStroke(Paint p, int color, float width, int alpha) { + p.setStyle(Paint.Style.STROKE); + p.setAntiAlias(true); + p.setColor(color); + p.setAlpha(alpha); + p.setStrokeWidth(width); + p.setStrokeCap(Paint.Cap.BUTT); + } + + private void beginFinishAnim() { + mAnim.startFinish(sCfgFinishStyle, sCfgFinishHoldMs, sCfgFinishExitMs, + sCfgPulse, () -> setProgress(0)); + } + + private void cancelPendingFinish() { + if (mPendingFinish != null) { + removeCallbacks(mPendingFinish); + mPendingFinish = null; + } + } + + private static float eased(int pct, String mode) { + float v = pct / 100f; + switch (mode) { + case "accelerate": return v * v; + case "decelerate": return 1f - (1f - v) * (1f - v); + case "ease_in_out": return v < .5f ? 2*v*v : 1f - (float)Math.pow(-2*v+2,2)/2f; + default: return v; + } + } + + private static int brighten(int c, float f) { + return Color.argb(Color.alpha(c), + Math.min(255, (int)(Color.red(c) + (255 - Color.red(c)) * f)), + Math.min(255, (int)(Color.green(c) + (255 - Color.green(c)) * f)), + Math.min(255, (int)(Color.blue(c) + (255 - Color.blue(c)) * f))); + } + + private static int blendColors(int c1, int c2, float ratio) { + float inv = 1f - ratio; + return Color.argb(Color.alpha(c1), + (int)(Color.red(c1)*inv + Color.red(c2)*ratio), + (int)(Color.green(c1)*inv + Color.green(c2)*ratio), + (int)(Color.blue(c1)*inv + Color.blue(c2)*ratio)); + } + + private static String truncate(String s, int max, String mode) { + if (s.length() <= max) return s; + String e = "\u2026"; + int avail = max - 1; + if (avail <= 0) return e; + switch (mode) { + case "start": return e + s.substring(s.length() - avail); + case "end": return s.substring(0, avail) + e; + default: { + int head = (avail + 1) / 2; + int tail = avail - head; + return s.substring(0, head) + e + s.substring(s.length() - tail); + } + } + } + + private float spToPx(float sp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, + getResources().getDisplayMetrics()); + } + + private static Paint makePaint() { + Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); + p.setStyle(Paint.Style.STROKE); + return p; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/OverlayAnimationHelper.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/OverlayAnimationHelper.java new file mode 100644 index 0000000000000..f20b562ba6f4e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/OverlayAnimationHelper.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cutoutprogress.ring; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.LinearInterpolator; +import android.view.animation.OvershootInterpolator; + +public final class OverlayAnimationHelper { + + public static final int SEGMENT_COUNT = 12; + public static final float SEGMENT_GAP_DEG = 6f; + public static final float SEGMENT_ARC_DEG = + (360f - SEGMENT_COUNT * SEGMENT_GAP_DEG) / SEGMENT_COUNT; + + public boolean isFinishAnimating = false; + public boolean isErrorAnimating = false; + + public float displayAlpha = 1f; + public float displayScale = 1f; + public int segmentHighlight = -1; + public float successColorBlend = 0f; + public float completionPulseAlpha = 1f; + public float errorAlpha = 0f; + + public enum PreviewMode { NONE, DYNAMIC, GEOMETRY } + public PreviewMode previewMode = PreviewMode.NONE; + public int previewProgress = 0; + + public boolean isDynamicPreviewActive() { return previewMode == PreviewMode.DYNAMIC; } + public boolean isGeometryPreviewActive() { return previewMode == PreviewMode.GEOMETRY; } + + private static final int MAX_ANIM_MS = 800; + private static final float INTENSITY_POP = 1.5f; + private static final float POP_SCALE_FACTOR = 0.08f; + private static final float SEGMENT_SHINE_BLEND = 0.4f; + private static final float PULSE_MIN_ALPHA = 0.7f; + private static final long PULSE_DURATION_MS = 400L; + private static final long PREVIEW_DEBOUNCE_MS = 300L; + private static final long GEOMETRY_HOLD_MS = 3000L; + + private final View mHost; + + private ValueAnimator mFinishAnim; + private ValueAnimator mPulseAnim; + private ValueAnimator mErrorAnim; + private ValueAnimator mPreviewAnim; + private Runnable mPreviewDebounce; + private Runnable mGeometryHideTask; + + public OverlayAnimationHelper(View host) { + mHost = host; + } + + public void startFinish(String style, int holdMs, int exitMs, + boolean pulse, Runnable onComplete) { + cancelFinish(); + isFinishAnimating = true; + displayAlpha = 1f; + displayScale = 1f; + segmentHighlight = -1; + successColorBlend = 1f; + completionPulseAlpha = 1f; + + Runnable runStyle = () -> { + switch (style) { + case "snap": + isFinishAnimating = false; + resetFinishState(); + onComplete.run(); + break; + case "segmented": + animateSegmented(holdMs, exitMs, INTENSITY_POP, onComplete); + break; + default: + animatePop(holdMs, exitMs, INTENSITY_POP, onComplete); + } + }; + + if (pulse && !"snap".equals(style)) { + animatePulse(runStyle); + } else { + runStyle.run(); + } + } + + private void animatePop(int holdMs, int exitMs, float intensity, Runnable onComplete) { + int total = Math.min(holdMs + exitMs, MAX_ANIM_MS); + long scaleDuration = (long)(total * 0.4f); + long fadeDuration = total - scaleDuration; + + mFinishAnim = animate(0f, 1f, scaleDuration, new OvershootInterpolator(2f * intensity), + f -> displayScale = 1f + POP_SCALE_FACTOR * intensity * f, + () -> mFinishAnim = animate(0f, 1f, fadeDuration, + new AccelerateDecelerateInterpolator(), + f -> { + displayScale = 1f + POP_SCALE_FACTOR * intensity * (1f - f * 0.5f); + displayAlpha = 1f - f; + }, + () -> endFinish(onComplete))); + } + + private void animateSegmented(int holdMs, int exitMs, float intensity, Runnable onComplete) { + int total = Math.min(holdMs + exitMs, MAX_ANIM_MS); + long cascadeDuration = (long)(total * 0.6f); + long fadeDuration = total - cascadeDuration; + + mFinishAnim = animateInt(0, SEGMENT_COUNT + 2, cascadeDuration, + new LinearInterpolator(), + seg -> { + segmentHighlight = seg; + successColorBlend = intensity * SEGMENT_SHINE_BLEND; + }, + () -> { + segmentHighlight = -1; + mFinishAnim = animate(1f, 0f, fadeDuration, + new AccelerateDecelerateInterpolator(), + f -> displayAlpha = f, + () -> endFinish(onComplete)); + }); + } + + private void animatePulse(Runnable then) { + if (mPulseAnim != null) mPulseAnim.cancel(); + mPulseAnim = animate(1f, PULSE_MIN_ALPHA, PULSE_DURATION_MS, + new AccelerateDecelerateInterpolator(), + f -> completionPulseAlpha = f, null); + ValueAnimator second = animate(PULSE_MIN_ALPHA, 1f, PULSE_DURATION_MS, + new AccelerateDecelerateInterpolator(), + f -> completionPulseAlpha = f, then); + mPulseAnim.addListener(new AnimatorListenerAdapter() { + @Override public void onAnimationEnd(Animator a) { second.start(); } + }); + mPulseAnim.start(); + } + + private void endFinish(Runnable onComplete) { + isFinishAnimating = false; + resetFinishState(); + onComplete.run(); + mHost.invalidate(); + } + + public void cancelFinish() { + cancel(mFinishAnim); mFinishAnim = null; + cancel(mPulseAnim); mPulseAnim = null; + isFinishAnimating = false; + resetFinishState(); + } + + private void resetFinishState() { + displayAlpha = 1f; + displayScale = 1f; + segmentHighlight = -1; + successColorBlend = 0f; + completionPulseAlpha = 1f; + } + + public void startError(Runnable onComplete) { + if (isFinishAnimating || isErrorAnimating) return; + isErrorAnimating = true; + cancel(mErrorAnim); + + mErrorAnim = ValueAnimator.ofFloat(0f, 1f, 0f, 1f, 0f); + mErrorAnim.setDuration(600); + mErrorAnim.setInterpolator(new LinearInterpolator()); + mErrorAnim.addUpdateListener(a -> { + errorAlpha = (float) a.getAnimatedValue(); + mHost.invalidate(); + }); + mErrorAnim.addListener(new AnimatorListenerAdapter() { + @Override public void onAnimationEnd(Animator a) { + isErrorAnimating = false; + errorAlpha = 0f; + if (onComplete != null) onComplete.run(); + mHost.invalidate(); + } + }); + mErrorAnim.start(); + } + + public void cancelError() { + cancel(mErrorAnim); mErrorAnim = null; + isErrorAnimating = false; + errorAlpha = 0f; + } + + public void startDynamicPreview(String finishStyle, int holdMs, + int exitMs, boolean pulse) { + mHost.removeCallbacks(mPreviewDebounce); + mPreviewDebounce = () -> runDynamicPreview(finishStyle, holdMs, exitMs, pulse); + mHost.postDelayed(mPreviewDebounce, PREVIEW_DEBOUNCE_MS); + } + + private void runDynamicPreview(String finishStyle, int holdMs, + int exitMs, boolean pulse) { + cancelDynamicPreview(); + previewMode = PreviewMode.DYNAMIC; + previewProgress = 0; + + mPreviewAnim = animateInt(0, 100, 800, + new AccelerateDecelerateInterpolator(), + p -> previewProgress = p, + () -> { + previewProgress = 100; + startFinish(finishStyle, holdMs, exitMs, pulse, () -> + mHost.postDelayed(() -> { + previewMode = PreviewMode.NONE; + previewProgress = 0; + mHost.invalidate(); + }, 200)); + }); + } + + public void cancelDynamicPreview() { + mHost.removeCallbacks(mPreviewDebounce); + mPreviewDebounce = null; + cancel(mPreviewAnim); mPreviewAnim = null; + previewMode = PreviewMode.NONE; + previewProgress = 0; + } + + public void showGeometryPreview(boolean autoHide) { + mHost.removeCallbacks(mGeometryHideTask); + mGeometryHideTask = null; + previewMode = PreviewMode.GEOMETRY; + mHost.invalidate(); + if (autoHide) { + mGeometryHideTask = () -> { + previewMode = PreviewMode.NONE; + mGeometryHideTask = null; + mHost.invalidate(); + }; + mHost.postDelayed(mGeometryHideTask, GEOMETRY_HOLD_MS); + } + } + + public void cancelGeometryPreview() { + mHost.removeCallbacks(mGeometryHideTask); + mGeometryHideTask = null; + if (previewMode == PreviewMode.GEOMETRY) { + previewMode = PreviewMode.NONE; + mHost.invalidate(); + } + } + + public void cancelAll() { + cancelFinish(); + cancelError(); + cancelDynamicPreview(); + cancelGeometryPreview(); + } + + private interface FloatConsumer { void accept(float v); } + private interface IntConsumer { void accept(int v); } + + private ValueAnimator animate(float from, float to, long durationMs, + TimeInterpolator interp, + FloatConsumer onUpdate, Runnable onEnd) { + if (durationMs <= 0) { + onUpdate.accept(to); + mHost.invalidate(); + if (onEnd != null) onEnd.run(); + return new ValueAnimator(); + } + ValueAnimator va = ValueAnimator.ofFloat(from, to); + va.setDuration(durationMs); + va.setInterpolator(interp); + va.addUpdateListener(a -> { + onUpdate.accept((float) a.getAnimatedValue()); + mHost.invalidate(); + }); + if (onEnd != null) va.addListener(new AnimatorListenerAdapter() { + @Override public void onAnimationEnd(Animator a) { onEnd.run(); } + }); + va.start(); + return va; + } + + private ValueAnimator animateInt(int from, int to, long durationMs, + TimeInterpolator interp, + IntConsumer onUpdate, Runnable onEnd) { + if (durationMs <= 0) { + onUpdate.accept(to); + mHost.invalidate(); + if (onEnd != null) onEnd.run(); + return new ValueAnimator(); + } + ValueAnimator va = ValueAnimator.ofInt(from, to); + va.setDuration(durationMs); + va.setInterpolator(interp); + va.addUpdateListener(a -> { + onUpdate.accept((int) a.getAnimatedValue()); + mHost.invalidate(); + }); + if (onEnd != null) va.addListener(new AnimatorListenerAdapter() { + @Override public void onAnimationEnd(Animator a) { onEnd.run(); } + }); + va.start(); + return va; + } + + private static void cancel(ValueAnimator va) { + if (va != null) va.cancel(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/RingViewRenderer.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/RingViewRenderer.java new file mode 100644 index 0000000000000..26ef233598b08 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/RingViewRenderer.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.cutoutprogress.ring; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +public interface RingViewRenderer { + + void updateBounds(RectF bounds); + + void drawFullRing(Canvas canvas, Paint paint); + + void drawProgress(Canvas canvas, float sweepFraction, boolean clockwise, Paint paint); + + void drawSegmented(Canvas canvas, + int segments, float gapDeg, float arcDeg, + int highlight, + Paint basePaint, Paint shinePaint, float alpha); +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java index c2e1e33f53182..001f62e89d2aa 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java @@ -34,6 +34,7 @@ import com.android.systemui.usb.UsbConfirmActivity; import com.android.systemui.usb.UsbDebuggingActivity; import com.android.systemui.usb.UsbDebuggingSecondaryUserActivity; +import com.android.systemui.usb.UsbFunctionActivity; import com.android.systemui.usb.UsbPermissionActivity; import com.android.systemui.user.CreateUserActivity; @@ -102,6 +103,12 @@ public abstract Activity bindUsbDebuggingSecondaryUserActivity( @ClassKey(UsbAccessoryUriActivity.class) public abstract Activity bindUsbAccessoryUriActivity(UsbAccessoryUriActivity activity); + /** Inject into UsbFunctionActivity. */ + @Binds + @IntoMap + @ClassKey(UsbFunctionActivity.class) + public abstract Activity bindUsbFunctionActivity(UsbFunctionActivity activity); + /** Inject into CreateUserActivity. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 46d706dc7c2d6..db21ba592045d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -107,6 +107,7 @@ import com.android.systemui.navigationbar.gestural.dagger.GestureModule; import com.android.systemui.notetask.NoteTaskModule; import com.android.systemui.people.PeopleModule; +import com.android.systemui.cutoutprogress.dagger.CutoutProgressModule; import com.android.systemui.plugins.BcSmartspaceConfigPlugin; import com.android.systemui.plugins.BcSmartspaceDataPlugin; import com.android.systemui.privacy.PrivacyModule; @@ -343,6 +344,7 @@ LowLightClockModule.class, PerDisplayRepositoriesModule.class, InputDeviceModule.class, + CutoutProgressModule.class, }, subcomponents = { ComplicationComponent.class, diff --git a/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java b/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java index 78b742892aac3..a7ea719fdb404 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java @@ -44,7 +44,7 @@ public class AlwaysOnDisplayPolicy { public static final String TAG = "AlwaysOnDisplayPolicy"; - private static final long DEFAULT_PROX_SCREEN_OFF_DELAY_MS = 10 * DateUtils.SECOND_IN_MILLIS; + private static final long DEFAULT_PROX_SCREEN_OFF_DELAY_MS = 3 * DateUtils.SECOND_IN_MILLIS; private static final long DEFAULT_PROX_COOLDOWN_TRIGGER_MS = 2 * DateUtils.SECOND_IN_MILLIS; private static final long DEFAULT_PROX_COOLDOWN_PERIOD_MS = 5 * DateUtils.SECOND_IN_MILLIS; private static final long DEFAULT_WALLPAPER_VISIBILITY_MS = 60 * DateUtils.SECOND_IN_MILLIS; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index a7ad24715b209..0d5a22ada6432 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -16,6 +16,7 @@ package com.android.systemui.keyguard; +import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK; import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.provider.Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT; @@ -48,6 +49,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.SuppressLint; +import android.app.ActivityTaskManager; import android.app.AlarmManager; import android.app.BroadcastOptions; import android.app.IActivityTaskManager; @@ -4038,6 +4040,17 @@ public void onBootCompleted() { if (mBootSendUserPresent) { sendUserPresentBroadcast(); } + + mHandler.post(() -> { + try { + if (mActivityTaskManagerService.getLockTaskModeState() + == LOCK_TASK_MODE_PINNED) { + mActivityTaskManagerService.rebuildSystemLockTaskPinnedMode(); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to rebuild lock task pinned mode", e); + } + }); } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt index 683c11a88b892..bfcc47c1dbb63 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt @@ -116,6 +116,8 @@ constructor( TAG, ) } + + override fun onFlashlightStrengthChanged(level: Int) {} } flashlightController.addCallback(flashlightCallback) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt index afeb7f45e5551..816b003cd53dd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt @@ -36,6 +36,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.shared.clocks.ClockRegistry import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SystemSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -84,6 +85,7 @@ class KeyguardClockRepositoryImpl @Inject constructor( private val secureSettings: SecureSettings, + private val systemSettings: SystemSettings, private val clockRegistry: ClockRegistry, override val clockEventController: ClockEventController, @Background private val backgroundDispatcher: CoroutineDispatcher, @@ -98,7 +100,19 @@ constructor( override val forcedClockSize: Flow = if (featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) { configurationRepository.onAnyConfigurationChange.map { - if (context.resources.getBoolean(R.bool.force_small_clock_on_lockscreen)) { + if ( + context.resources.getBoolean(R.bool.force_small_clock_on_lockscreen) || + secureSettings.getIntForUser( + "clock_style", + 0, // Default value + UserHandle.USER_CURRENT + ) != 0 || + systemSettings.getIntForUser( + "lockscreen_widgets_enabled", + 0, // Default value + UserHandle.USER_CURRENT + ) != 0 + ) { ClockSize.SMALL } else { null @@ -170,5 +184,26 @@ constructor( UserHandle.USER_CURRENT, ) ) + val isDoubleLineClock = secureSettings.getIntForUser( + Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, + 1, // Default value + UserHandle.USER_CURRENT + ) + val clockStyleEnabled = secureSettings.getIntForUser( + "clock_style", + 0, // Default value + UserHandle.USER_CURRENT + ) != 0 + val lockscreenWidgetsEnabled = systemSettings.getIntForUser( + "lockscreen_widgets_enabled", + 0, // Default value + UserHandle.USER_CURRENT + ) != 0 + val clockSettingValue = if (clockStyleEnabled || lockscreenWidgetsEnabled) { + 0 + } else { + isDoubleLineClock + } + return ClockSizeSetting.fromSettingValue(clockSettingValue) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt index 8e2fe3d7ba590..d4213cdae88c0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt @@ -35,6 +35,10 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSec import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule.Companion.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection +import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWidgetViewSection +import com.android.systemui.keyguard.ui.view.layout.sections.InfoWidgetsSection +import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardClockStyleSection +import com.android.systemui.keyguard.ui.view.layout.sections.AODStyleSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWeatherViewSection import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import java.util.Optional @@ -69,6 +73,10 @@ constructor( smartspaceSection: SmartspaceSection, keyguardWeatherViewSection: KeyguardWeatherViewSection, keyguardSliceViewSection: KeyguardSliceViewSection, + keyguardWidgetViewSection: KeyguardWidgetViewSection, + infoWidgetsSection: InfoWidgetsSection, + keyguardClockStyleSection: KeyguardClockStyleSection, + aodStyleSection: AODStyleSection, udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection, ) : KeyguardBlueprint { override val id: String = DEFAULT @@ -90,6 +98,11 @@ constructor( clockSection, keyguardWeatherViewSection, keyguardSliceViewSection, + keyguardWidgetViewSection, + infoWidgetsSection, + keyguardClockStyleSection, + aodStyleSection, + keyguardWeatherViewSection, defaultDeviceEntrySection, udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt index ba3b1f0afdd50..fe73165df7ae7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt @@ -31,6 +31,9 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopu import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule +import com.android.systemui.keyguard.ui.view.layout.sections.InfoWidgetsSection +import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardClockStyleSection +import com.android.systemui.keyguard.ui.view.layout.sections.AODStyleSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWeatherViewSection import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection @@ -67,6 +70,9 @@ constructor( aodBurnInSection: AodBurnInSection, clockSection: ClockSection, smartspaceSection: SmartspaceSection, + infoWidgetsSection: InfoWidgetsSection, + keyguardClockStyleSection: KeyguardClockStyleSection, + aodStyleSection: AODStyleSection, mediaSection: SplitShadeMediaSection, keyguardWeatherViewSection: KeyguardWeatherViewSection, keyguardSliceViewSection: KeyguardSliceViewSection, @@ -91,6 +97,9 @@ constructor( clockSection, keyguardWeatherViewSection, keyguardSliceViewSection, + infoWidgetsSection, + keyguardClockStyleSection, + aodStyleSection, mediaSection, defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views. ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt new file mode 100644 index 0000000000000..c44fd2beaee0a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2025 the RisingOS Revived Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.ui.view.layout.sections + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.Barrier +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R +import javax.inject.Inject + +class AODStyleSection +@Inject +constructor( + private val context: Context, +) : KeyguardSection() { + + private var aodStyleView: View? = null + + override fun addViews(constraintLayout: ConstraintLayout) { + // Remove existing view with the same ID if it exists + constraintLayout.findViewById(R.id.aod_ls)?.let { existingView -> + (existingView.parent as? ViewGroup)?.removeView(existingView) + } + + // Inflate the AOD style layout + aodStyleView = LayoutInflater.from(context).inflate( + R.layout.keyguard_aod_style, + constraintLayout, + false + ).apply { + id = R.id.aod_ls + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + } + + constraintLayout.addView(aodStyleView) + } + + override fun bindData(constraintLayout: ConstraintLayout) { + // The AODStyle component handles its own data binding + // through its TunerService integration and StatusBarStateController callbacks + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + // Position AOD style within the keyguard_status_area + connect( + R.id.aod_ls, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START + ) + connect( + R.id.aod_ls, + ConstraintSet.END, + ConstraintSet.PARENT_ID, + ConstraintSet.END + ) + + // Position at the top of status area with proper margin to avoid status bar overlap + val topMargin = (context.resources.getDimensionPixelSize(R.dimen.status_bar_height) * 1.25f).toInt() + connect( + R.id.aod_ls, + ConstraintSet.TOP, + ConstraintSet.PARENT_ID, + ConstraintSet.TOP, + topMargin + ) + + // Set dimensions + constrainHeight(R.id.aod_ls, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.aod_ls, ConstraintSet.MATCH_CONSTRAINT) + + // Set appropriate margins matching the original XML structure + setMargin(R.id.aod_ls, ConstraintSet.START, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start)) + setMargin(R.id.aod_ls, ConstraintSet.END, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start)) + + // Ensure proper layering within the status area + setElevation(R.id.aod_ls, 2f) // Higher elevation than info widgets + + // Update other elements to position below AOD style when it's visible + if (constraintSet.getConstraint(R.id.lockscreen_clock_view) != null) { + connect( + R.id.lockscreen_clock_view, + ConstraintSet.TOP, + R.id.aod_ls, + ConstraintSet.BOTTOM, + 8 + ) + } + + if (constraintSet.getConstraint(R.id.clock_ls) != null) { + connect( + R.id.clock_ls, + ConstraintSet.TOP, + R.id.aod_ls, + ConstraintSet.BOTTOM, + 8 + ) + } + + // Update the barrier to include AOD style for proper notification positioning + // This ensures notifications appear below all status area content including AOD + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf( + R.id.aod_ls, + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.clock_ls, + R.id.keyguard_info_widgets + ) + ) + + // Ensure notification icons are positioned below the barrier + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + aodStyleView?.let { view -> + (view.parent as? ViewGroup)?.removeView(view) + } + aodStyleView = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index 6b278212c50a3..53aff4c6232e5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -18,6 +18,8 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context +import android.os.UserHandle +import android.provider.Settings import android.view.View import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout @@ -121,6 +123,19 @@ constructor( setAlpha(getTargetClockFace(clock).views, 1F) setAlpha(getNonTargetClockFace(clock).views, 0F) + // Hide small clock when custom clock is enabled by setting alpha to 0 + val isCustomClockEnabled = Settings.Secure.getIntForUser( + context.contentResolver, + "clock_style", + 0, + UserHandle.USER_CURRENT + ) != 0 + + if (isCustomClockEnabled) { + setAlpha(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, 0F) + setAlpha(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, 0F) + } + if (!keyguardClockViewModel.isLargeClockVisible.value) { if (keyguardClockViewModel.shouldDateWeatherBeBelowSmallClock.value) { connect( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/InfoWidgetsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/InfoWidgetsSection.kt new file mode 100644 index 0000000000000..ef20ce8897a9a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/InfoWidgetsSection.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2025 the RisingOS Revived Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.ui.view.layout.sections + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.Barrier +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R +import javax.inject.Inject + +class InfoWidgetsSection +@Inject +constructor( + private val context: Context, +) : KeyguardSection() { + + private var infoWidgetsView: View? = null + + override fun addViews(constraintLayout: ConstraintLayout) { + + constraintLayout.findViewById(R.id.keyguard_info_widgets)?.let { existingView -> + (existingView.parent as? ViewGroup)?.removeView(existingView) + } + + infoWidgetsView = LayoutInflater.from(context).inflate( + R.layout.keyguard_info_widgets, + constraintLayout, + false + ).apply { + id = R.id.keyguard_info_widgets + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + } + + constraintLayout.addView(infoWidgetsView) + } + + override fun bindData(constraintLayout: ConstraintLayout) { + // ProgressImageView components handle their own data binding + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + + constraintSet.apply { + // Info widgets positioning - below WEATHER (3rd in hierarchy) + connect(R.id.keyguard_info_widgets, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) + connect(R.id.keyguard_info_widgets, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + + // Chain to weather (primary) or fallback hierarchy + when { + constraintSet.getConstraint(R.id.keyguard_weather) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.keyguard_weather, ConstraintSet.BOTTOM, 12) + } + constraintSet.getConstraint(R.id.default_weather_image) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.default_weather_image, ConstraintSet.BOTTOM, 12) + } + constraintSet.getConstraint(R.id.clock_ls) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.clock_ls, ConstraintSet.BOTTOM, 12) + } + constraintSet.getConstraint(R.id.keyguard_slice_view) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.keyguard_slice_view, ConstraintSet.BOTTOM, 12) + } + else -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.lockscreen_clock_view, ConstraintSet.BOTTOM, 12) + } + } + + constrainHeight(R.id.keyguard_info_widgets, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.keyguard_info_widgets, ConstraintSet.MATCH_CONSTRAINT) + setMargin(R.id.keyguard_info_widgets, ConstraintSet.START, 0) + setMargin(R.id.keyguard_info_widgets, ConstraintSet.END, 0) + // Add small bottom margin for AOD to prevent notification overlap + setMargin(R.id.keyguard_info_widgets, ConstraintSet.BOTTOM, 6) // 6dp bottom margin for AOD + setElevation(R.id.keyguard_info_widgets, 1f) + + // UNIFIED BARRIER - Create barrier in every section that could be last + createUnifiedBarrierAndNotificationConstraints(constraintSet) + } + } + + private fun createUnifiedBarrierAndNotificationConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + // UNIFIED BARRIER - Include ALL status area elements + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf( + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.default_weather_image, + R.id.default_weather_text, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets, + R.id.lockscreen_clock_view // Include fallback clock + ) + ) + + // Position notifications below ALL status area content + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + infoWidgetsView?.let { view -> + (view.parent as? ViewGroup)?.removeView(view) + } + infoWidgetsView = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt new file mode 100644 index 0000000000000..bb22a03672962 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2025 the RisingOS Revived Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.ui.view.layout.sections + +import android.content.Context +import android.os.UserHandle +import android.util.Log +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.Barrier +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.clocks.ClockStyle +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject + +class KeyguardClockStyleSection +@Inject +constructor( + private val context: Context, + private val secureSettings: SecureSettings, +) : KeyguardSection() { + + private var clockStyleView: ClockStyle? = null + private var isCustomClockEnabled: Boolean = false + + private val TAG = "KeyguardClockStyleSection" + + override fun addViews(constraintLayout: ConstraintLayout) { + isCustomClockEnabled = try { + val clockStyle = secureSettings.getIntForUser( + ClockStyle.CLOCK_STYLE_KEY, 0, UserHandle.USER_CURRENT + ) + clockStyle != 0 + } catch (e: Exception) { + false + } + + if (!isCustomClockEnabled) return + + constraintLayout.findViewById(R.id.clock_ls)?.let { existingView -> + (existingView.parent as? ViewGroup)?.removeView(existingView) + } + + val inflater = android.view.LayoutInflater.from(context) + clockStyleView = inflater.inflate(R.layout.keyguard_clock_style, null) as ClockStyle + clockStyleView?.apply { + id = R.id.clock_ls + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.VISIBLE + } + + clockStyleView?.let { constraintLayout.addView(it) } + } + + override fun bindData(constraintLayout: ConstraintLayout) { + clockStyleView?.let { clockView -> + clockView.onTimeChanged() + clockView.requestLayout() + } + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + val currentlyEnabled = try { + secureSettings.getIntForUser( + ClockStyle.CLOCK_STYLE_KEY, 0, UserHandle.USER_CURRENT + ) != 0 + } catch (e: Exception) { + isCustomClockEnabled + } + + if (!currentlyEnabled) return + + constraintSet.apply { + // Clock positioning - TOP of hierarchy + connect(R.id.clock_ls, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) + connect(R.id.clock_ls, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + + val topMargin = (context.resources.getDimensionPixelSize(R.dimen.status_bar_height) * 1.25f).toInt() + connect(R.id.clock_ls, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, topMargin) + + constrainHeight(R.id.clock_ls, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.clock_ls, ConstraintSet.MATCH_CONSTRAINT) + setMargin(R.id.clock_ls, ConstraintSet.START, 0) + setMargin(R.id.clock_ls, ConstraintSet.END, 0) + setElevation(R.id.clock_ls, 1f) + + // UNIFIED BARRIER - Create barrier in every section that could be last + createUnifiedBarrierAndNotificationConstraints(constraintSet) + } + } + + private fun createUnifiedBarrierAndNotificationConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + // UNIFIED BARRIER - Include ALL status area elements + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf( + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.default_weather_image, + R.id.default_weather_text, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets, + R.id.lockscreen_clock_view // Include fallback clock + ) + ) + + // Position notifications below ALL status area content + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + clockStyleView?.let { clockView -> + (clockView.parent as? ViewGroup)?.removeView(clockView) + } + clockStyleView = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt index f0817a1396804..bd855c3009217 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024-2026 crDroid Android Project + * Copyright (C) 2025 the RisingOS Revived Android Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,91 +12,190 @@ * WITHOUT WARRANTIES OR CONDITIONS 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.ui.view.layout.sections import android.content.Context -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.customization.clocks.R as clocksR +import com.android.systemui.customization.clocks.R as custR import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.plugins.keyguard.ui.clocks.ClockViewIds import com.android.systemui.res.R -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController +import com.android.systemui.shared.R as sharedR +import com.android.systemui.weather.WeatherImageView +import com.android.systemui.weather.WeatherTextView import javax.inject.Inject -import com.android.systemui.weather.WeatherInfoView - -class KeyguardWeatherViewSection -@Inject -constructor( +class KeyguardWeatherViewSection @Inject constructor( private val context: Context, - val layoutInflater: LayoutInflater, - val smartspaceController: LockscreenSmartspaceController, ) : KeyguardSection() { - private lateinit var weatherView: WeatherInfoView + + private var weatherImageView: WeatherImageView? = null + private var weatherTextView: WeatherTextView? = null override fun addViews(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - weatherView = - layoutInflater.inflate(R.layout.keyguard_weather_area, null, false) as WeatherInfoView - constraintLayout.addView(weatherView) + val weatherContainer = constraintLayout.findViewById(R.id.keyguard_weather) + + if (weatherContainer != null) { + weatherImageView = weatherContainer.findViewById(R.id.default_weather_image) + weatherTextView = weatherContainer.findViewById(R.id.default_weather_text) + + if (weatherContainer.parent !== constraintLayout) { + (weatherContainer.parent as? ViewGroup)?.removeView(weatherContainer) + constraintLayout.addView(weatherContainer) + } + } else { + createWeatherViews(constraintLayout) + } + + initializeWeatherViews() } - override fun bindData(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return + private fun createWeatherViews(constraintLayout: ConstraintLayout) { + weatherImageView = WeatherImageView(context, isCustomClock = false).apply { + id = R.id.default_weather_image + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.WRAP_CONTENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.GONE + } + + weatherTextView = WeatherTextView(context, isCustomClock = false).apply { + id = R.id.default_weather_text + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.WRAP_CONTENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + setTextColor(context.getColor(android.R.color.white)) + textSize = 16f + visibility = View.GONE + } + + weatherImageView?.let { constraintLayout.addView(it) } + weatherTextView?.let { constraintLayout.addView(it) } + } - weatherView.init() + private fun initializeWeatherViews() { + // Weather views initialize automatically when attached to window + } + + override fun bindData(constraintLayout: ConstraintLayout) { + // Weather data binding handled by individual weather views } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return constraintSet.apply { - connect( - R.id.keyguard_weather_area, - ConstraintSet.START, - ConstraintSet.PARENT_ID, - ConstraintSet.START, - context.resources.getDimensionPixelSize(clocksR.dimen.clock_padding_start) + - context.resources.getDimensionPixelSize(clocksR.dimen.status_view_margin_horizontal), - ) - connect( - R.id.keyguard_weather_area, - ConstraintSet.END, - ConstraintSet.PARENT_ID, - ConstraintSet.END - ) - constrainHeight(R.id.keyguard_weather_area, ConstraintSet.WRAP_CONTENT) + val startMargin = context.resources.getDimensionPixelSize(custR.dimen.clock_padding_start) + + context.resources.getDimensionPixelSize(custR.dimen.status_view_margin_horizontal) - connect( - R.id.keyguard_weather_area, - ConstraintSet.TOP, - R.id.keyguard_slice_view, - ConstraintSet.BOTTOM - ) + // Weather positioning - below CLOCK (2nd in hierarchy) + if (constraintSet.getConstraint(R.id.keyguard_weather) != null) { + connect(R.id.keyguard_weather, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, startMargin) + connect(R.id.keyguard_weather, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + + // Chain to clock (primary) or fallback to slice_view + if (constraintSet.getConstraint(R.id.clock_ls) != null) { + connect(R.id.keyguard_weather, ConstraintSet.TOP, R.id.clock_ls, ConstraintSet.BOTTOM, 8) + } else if (constraintSet.getConstraint(R.id.keyguard_slice_view) != null) { + connect(R.id.keyguard_weather, ConstraintSet.TOP, R.id.keyguard_slice_view, ConstraintSet.BOTTOM, 8) + } else { + connect(R.id.keyguard_weather, ConstraintSet.TOP, R.id.lockscreen_clock_view, ConstraintSet.BOTTOM, 8) + } + + constrainHeight(R.id.keyguard_weather, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.keyguard_weather, ConstraintSet.MATCH_CONSTRAINT) + } else { + applyWeatherImageConstraints(constraintSet, startMargin) + applyWeatherTextConstraints(constraintSet) + } + + // UNIFIED BARRIER - Create barrier in every section that could be last + createUnifiedBarrierAndNotificationConstraints(constraintSet) + } + } + private fun applyWeatherImageConstraints(constraintSet: ConstraintSet, startMargin: Int) { + if (constraintSet.getConstraint(R.id.default_weather_image) != null) { + constraintSet.apply { + connect(R.id.default_weather_image, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, startMargin) + + if (constraintSet.getConstraint(R.id.clock_ls) != null) { + connect(R.id.default_weather_image, ConstraintSet.TOP, R.id.clock_ls, ConstraintSet.BOTTOM, 8) + } else if (constraintSet.getConstraint(R.id.keyguard_slice_view) != null) { + connect(R.id.default_weather_image, ConstraintSet.TOP, R.id.keyguard_slice_view, ConstraintSet.BOTTOM, 8) + } else { + connect(R.id.default_weather_image, ConstraintSet.TOP, R.id.lockscreen_clock_view, ConstraintSet.BOTTOM, 8) + } + + constrainHeight(R.id.default_weather_image, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.default_weather_image, ConstraintSet.WRAP_CONTENT) + } + } + } + + private fun applyWeatherTextConstraints(constraintSet: ConstraintSet) { + if (constraintSet.getConstraint(R.id.default_weather_text) != null) { + constraintSet.apply { + connect(R.id.default_weather_text, ConstraintSet.START, R.id.default_weather_image, ConstraintSet.END, 12) + connect(R.id.default_weather_text, ConstraintSet.TOP, R.id.default_weather_image, ConstraintSet.TOP) + connect(R.id.default_weather_text, ConstraintSet.BOTTOM, R.id.default_weather_image, ConstraintSet.BOTTOM) + constrainHeight(R.id.default_weather_text, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.default_weather_text, ConstraintSet.WRAP_CONTENT) + } + } + } + + private fun createUnifiedBarrierAndNotificationConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + // UNIFIED BARRIER - Include ALL status area elements createBarrier( R.id.smart_space_barrier_bottom, Barrier.BOTTOM, 0, - *intArrayOf(R.id.keyguard_weather_area) + *intArrayOf( + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.default_weather_image, + R.id.default_weather_text, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets, + R.id.lockscreen_clock_view, + ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, + sharedR.id.bc_smartspace_view, + sharedR.id.date_smartspace_view, + ) ) + + // Position notifications below ALL status area content + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } } } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - - constraintLayout.findViewById(R.id.keyguard_weather_area)?.let { weatherArea -> - weatherArea.cleanup() - constraintLayout.removeView(weatherArea) + constraintLayout.findViewById(R.id.keyguard_weather)?.let { weatherContainer -> + constraintLayout.removeView(weatherContainer) } + + weatherImageView?.let { constraintLayout.removeView(it) } + weatherTextView?.let { constraintLayout.removeView(it) } + + weatherImageView = null + weatherTextView = null } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt new file mode 100644 index 0000000000000..4a46d9e1e65b9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2025 the RisingOS Revived Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.ui.view.layout.sections + +import android.content.Context +import android.util.Log +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.Barrier +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R +import javax.inject.Inject +import com.android.systemui.lockscreen.LockScreenWidgets + +class KeyguardWidgetViewSection +@Inject +constructor( + private val context: Context, +) : KeyguardSection() { + + private var widgetView: LockScreenWidgets? = null + private val TAG = "KeyguardWidgetViewSection" + + private fun createWidgetView(): LockScreenWidgets? { + Log.d(TAG, "Creating LockScreenWidgets view") + return try { + val layoutInflater = android.view.LayoutInflater.from(context) + // Try to inflate from your actual layout file + val view = layoutInflater.inflate(R.layout.keyguard_clock_widgets, null) as LockScreenWidgets + + view.apply { + // Override the ID to match what the keyguard system expects + id = R.id.keyguard_widgets + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.VISIBLE + } + + Log.d(TAG, "Successfully inflated LockScreenWidgets from keyguard_clock_widgets layout") + view + } catch (e: Exception) { + Log.e(TAG, "Failed to inflate from keyguard_clock_widgets, trying direct instantiation", e) + try { + // Create a dummy AttributeSet for direct instantiation + val parser = context.resources.getLayout(android.R.layout.simple_list_item_1) + val attrs = android.util.Xml.asAttributeSet(parser) + + LockScreenWidgets(context, attrs).apply { + id = R.id.keyguard_widgets + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.VISIBLE + } + } catch (e2: Exception) { + Log.e(TAG, "Failed to create LockScreenWidgets directly", e2) + null + } + } + } + + override fun addViews(constraintLayout: ConstraintLayout) { + Log.d(TAG, "addViews called") + + // Check if the widget view already exists in the layout + val existingView = constraintLayout.findViewById(R.id.keyguard_widgets) + + if (existingView != null) { + Log.d(TAG, "Found existing widget view") + widgetView = existingView as? LockScreenWidgets + return + } + + // Check if we already have a widget view instance + if (widgetView != null) { + Log.d(TAG, "Reusing existing widget view instance") + // Remove from any previous parent + (widgetView?.parent as? ViewGroup)?.removeView(widgetView) + } else { + Log.d(TAG, "Creating new widget view") + widgetView = createWidgetView() + } + + widgetView?.let { view -> + try { + constraintLayout.addView(view) + Log.d(TAG, "Successfully added widget view to constraint layout") + } catch (e: Exception) { + Log.e(TAG, "Failed to add widget view", e) + } + } + } + + override fun bindData(constraintLayout: ConstraintLayout) { + Log.d(TAG, "bindData called") + // Ensure the widget view is properly initialized and visible + widgetView?.let { view -> + if (view.visibility != View.VISIBLE) { + view.visibility = View.VISIBLE + Log.d(TAG, "Set widget view visibility to VISIBLE") + } + + // Force a layout pass to ensure the view is measured and laid out + view.requestLayout() + } + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + Log.d(TAG, "applyConstraints called") + + // Only apply constraints if the widget view exists + widgetView ?: run { + Log.w(TAG, "Widget view is null, skipping constraints") + return + } + + try { + constraintSet.apply { + // Position widgets within the keyguard layout + connect( + R.id.keyguard_widgets, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START + ) + connect( + R.id.keyguard_widgets, + ConstraintSet.END, + ConstraintSet.PARENT_ID, + ConstraintSet.END + ) + + // Try to position below other status content with priority order + val anchorViews = listOf( + R.id.keyguard_info_widgets, + R.id.clock_ls, + R.id.keyguard_weather, + R.id.keyguard_slice_view, + R.id.lockscreen_clock_view + ) + + var positioned = false + for (anchorId in anchorViews) { + if (constraintSet.getConstraint(anchorId) != null) { + connect( + R.id.keyguard_widgets, + ConstraintSet.TOP, + anchorId, + ConstraintSet.BOTTOM, + 8 // 8dp margin + ) + positioned = true + Log.d(TAG, "Positioned widgets below anchor: ${context.resources.getResourceEntryName(anchorId)}") + break + } + } + + // If no anchor found, position at top with margin + if (!positioned) { + connect( + R.id.keyguard_widgets, + ConstraintSet.TOP, + ConstraintSet.PARENT_ID, + ConstraintSet.TOP, + 16 // 16dp margin from top + ) + Log.d(TAG, "No anchor found, positioned at top") + } + + // Set dimensions + constrainHeight(R.id.keyguard_widgets, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.keyguard_widgets, ConstraintSet.MATCH_CONSTRAINT) + + // Set margins + setMargin(R.id.keyguard_widgets, ConstraintSet.START, 0) + setMargin(R.id.keyguard_widgets, ConstraintSet.END, 0) + + // Set elevation for proper layering + setElevation(R.id.keyguard_widgets, 2f) + + // Update barrier to include widgets + try { + val potentialBarrierViews = listOf( + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets + ) + val barrierViews = potentialBarrierViews + .filter { constraintSet.getConstraint(it) != null } + .toIntArray() + + if (barrierViews.isNotEmpty()) { + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *barrierViews + ) + Log.d(TAG, "Created barrier with ${barrierViews.size} views") + } + + // Position notification icons below barrier + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + try { + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + } catch (e: Exception) { + 8 // fallback margin + } + ) + } + } catch (e: Exception) { + Log.w(TAG, "Failed to create barrier", e) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to apply constraints", e) + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + Log.d(TAG, "removeViews called") + widgetView?.let { view -> + try { + constraintLayout.removeView(view) + Log.d(TAG, "Successfully removed widget view") + } catch (e: Exception) { + Log.w(TAG, "Failed to remove widget view", e) + } + } + widgetView = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt index 937f79d15e055..82c2980dd66cf 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt @@ -93,7 +93,7 @@ constructor( } val maxBlurRadius: Float - get() = secureSettings.getFloat("system_blur_radius", 70f) + get() = secureSettings.getFloat("system_blur_radius", 34f) override val notificationBlurRadius: Flow = if (Flags.bouncerUiRevamp()) { diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 6a58ed9a8d142..90db8c8f07395 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -42,6 +42,7 @@ import com.android.systemui.qs.tiles.SoundTile import com.android.systemui.qs.tiles.SyncTile import com.android.systemui.qs.tiles.UsbTetherTile import com.android.systemui.qs.tiles.VolumeQSTile +import com.android.systemui.qs.tiles.VPNTetheringTile import com.android.systemui.qs.tiles.VolumeTile import com.android.systemui.qs.tiles.VpnTile import com.android.systemui.qs.tiles.WeatherTile @@ -188,6 +189,12 @@ interface LineageModule { @IntoMap @StringKey(VolumeQSTile.TILE_SPEC) fun bindVolumeQSTile(volumeQSTile: VolumeQSTile): QSTileImpl<*> + + /** Inject VPNTetheringTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(VPNTetheringTile.TILE_SPEC) + fun bindVPNTetheringTile(vpnTetheringTile: VPNTetheringTile): QSTileImpl<*> /** Inject VolumeTile into tileMap in QSModule */ @Binds @@ -594,5 +601,20 @@ interface LineageModule { instanceId = uiEventLogger.getNewInstanceId(), category = TileCategory.UTILITIES ) + + @Provides + @IntoMap + @StringKey(VPNTetheringTile.TILE_SPEC) + fun provideVPNTetheringTileConfig(uiEventLogger: QsEventLogger): QSTileConfig { + return QSTileConfig( + tileSpec = TileSpec.create(VPNTetheringTile.TILE_SPEC), + uiConfig = QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_vpn_tethering, + labelRes = R.string.vpn_tethering_label + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.CONNECTIVITY + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.kt new file mode 100644 index 0000000000000..1866196925ffa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.kt @@ -0,0 +1,155 @@ +/* + Copyright (C) 2023-2025 the risingOS Android Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS 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.lockscreen + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.media.AudioManager +import android.os.DeviceIdleManager +import android.provider.AlarmClock +import android.provider.MediaStore +import android.speech.RecognizerIntent +import android.widget.Toast +import androidx.annotation.StringRes +import com.android.systemui.Dependency +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R + +class ActivityLauncherUtils(private val context: Context) { + + companion object { + private const val PERSONALIZATIONS_ACTIVITY = "com.android.settings.Settings\$mistifySettingsLayoutActivity" + private const val SERVICE_PACKAGE = "org.omnirom.omnijaws" + } + + private val activityStarter: ActivityStarter? = Dependency.get(ActivityStarter::class.java) + private val packageManager: PackageManager = context.packageManager + + fun getInstalledMusicApp(): String { + val intent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_APP_MUSIC) + } + val musicApps = packageManager.queryIntentActivities(intent, 0) + return musicApps.firstOrNull()?.activityInfo?.packageName.orEmpty() + } + + fun launchAppIfAvailable(launchIntent: Intent, @StringRes appTypeResId: Int) { + val apps = packageManager.queryIntentActivities(launchIntent, PackageManager.MATCH_DEFAULT_ONLY) + if (apps.isNotEmpty()) { + activityStarter?.startActivity(launchIntent, true) + } else { + showNoDefaultAppFoundToast(appTypeResId) + } + } + + fun launchVoiceAssistant() { + val dim = context.getSystemService(DeviceIdleManager::class.java) + dim?.endIdle("voice-search") + val voiceIntent = Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE).apply { + putExtra(RecognizerIntent.EXTRA_SECURE, true) + } + activityStarter?.startActivity(voiceIntent, true) + } + + fun launchCamera() { + val launchIntent = Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) + launchAppIfAvailable(launchIntent, R.string.camera) + } + + fun launchTimer() { + val launchIntent = Intent(AlarmClock.ACTION_SET_TIMER) + launchAppIfAvailable(launchIntent, R.string.clock_timer) + } + + fun launchCalculator() { + val launchIntent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_APP_CALCULATOR) + } + launchAppIfAvailable(launchIntent, R.string.calculator) + } + + fun launchSettingsComponent(className: String) { + val intent = if (className == PERSONALIZATIONS_ACTIVITY) { + Intent(Intent.ACTION_MAIN) + } else { + Intent().setComponent(ComponentName("com.android.settings", className)) + } + activityStarter?.startActivity(intent, true) + } + + fun launchWeatherApp() { + val launchIntent = Intent(Intent.ACTION_MAIN).apply { + setClassName(SERVICE_PACKAGE, "$SERVICE_PACKAGE.WeatherActivity") + } + launchAppIfAvailable(launchIntent, R.string.omnijaws_weather) + } + + fun launchWalletApp() { + val launchIntent = context.packageManager.getLaunchIntentForPackage("com.google.android.apps.walletnfcrel")?.apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + launchIntent?.let { + launchAppIfAvailable(it, R.string.google_wallet) + } + } + + fun launchMediaPlayerApp(packageName: String) { + if (packageName.isNotEmpty()) { + val launchIntent = packageManager.getLaunchIntentForPackage(packageName) + launchIntent?.let { + activityStarter?.startActivity(it, true) + } + } + } + + fun launchQrScanner() { + try { + val qrScannerComponent = context.resources.getString( + com.android.internal.R.string.config_defaultQrCodeComponent + ) + val intent = if (qrScannerComponent.isNotEmpty()) { + Intent().apply { + component = ComponentName.unflattenFromString(qrScannerComponent) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } else { + Intent().apply { + component = ComponentName( + "com.google.android.googlequicksearchbox", + "com.google.android.apps.search.lens.LensActivity" + ) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } + startIntent(intent) + } catch (e: Exception) { + Toast.makeText(context, "Unable to launch QR Scanner", Toast.LENGTH_SHORT).show() + } + } + + fun startSettingsActivity() { + val settingsIntent = Intent(android.provider.Settings.ACTION_SETTINGS) + activityStarter?.startActivity(settingsIntent, true) + } + + fun startIntent(intent: Intent) { + activityStarter?.startActivity(intent, true) + } + + private fun showNoDefaultAppFoundToast(@StringRes appTypeResId: Int) { + Toast.makeText(context, context.getString(appTypeResId) + " not found", Toast.LENGTH_SHORT).show() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgets.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgets.kt new file mode 100644 index 0000000000000..45f6b02fc47ac --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgets.kt @@ -0,0 +1,67 @@ +/* + Copyright (C) 2024 the risingOS Android Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS 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.lockscreen + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout + +import com.android.internal.jank.InteractionJankMonitor + +import com.android.systemui.Dependency +import com.android.systemui.animation.Expandable +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.media.dialog.MediaOutputDialogManager + +class LockScreenWidgets(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { + + private val mViewController: LockScreenWidgetsController? = LockScreenWidgetsController(this) + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + mViewController?.registerCallbacks() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + mViewController?.unregisterCallbacks() + } + + override fun onFinishInflate() { + super.onFinishInflate() + mViewController?.initViews() + } + + fun showMediaDialog(view: View, lastMediaPackage: String) { + val packageName = lastMediaPackage.takeIf { it.isNotEmpty() } ?: return + Dependency.get(MediaOutputDialogManager::class.java) + .createAndShowWithController( + packageName, + true, + Expandable.fromView(view).dialogController() + ) + } + + private fun Expandable.dialogController(): DialogTransitionAnimator.Controller? { + return dialogTransitionController( + cuj = + DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + MediaOutputDialogManager.INTERACTION_JANK_TAG + ) + ) + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java new file mode 100644 index 0000000000000..f98297217c29b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java @@ -0,0 +1,1082 @@ +/* + Copyright (C) 2024 the risingOS Android Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS 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.lockscreen; + +import android.annotation.NonNull; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +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.ColorStateList; +import android.database.ContentObserver; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.hardware.camera2.CameraManager; +import android.media.AudioManager; +import android.media.MediaMetadata; +import android.media.session.MediaSessionLegacyHelper; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.KeyEvent; +import android.provider.MediaStore; +import android.provider.Settings; +import android.util.AttributeSet; +import android.os.UserHandle; +import android.text.TextUtils; +import android.widget.LinearLayout; +import android.widget.Toast; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.android.settingslib.net.DataUsageController; +import com.android.settingslib.Utils; + +import com.android.systemui.res.R; +import com.android.systemui.Dependency; +import com.android.systemui.animation.Expandable; +import com.android.systemui.animation.view.LaunchableImageView; +import com.android.systemui.animation.view.LaunchableFAB; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.bluetooth.ui.viewModel.BluetoothDetailsContentViewModel; +import com.android.systemui.qs.tiles.dialog.InternetDialogManager; +import com.android.systemui.statusbar.policy.BluetoothController; +import com.android.systemui.statusbar.policy.BluetoothController.Callback; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; +import com.android.systemui.statusbar.policy.FlashlightController; +import com.android.systemui.statusbar.policy.HotspotController; +import com.android.systemui.statusbar.connectivity.AccessPointController; +import com.android.systemui.statusbar.connectivity.IconState; +import com.android.systemui.statusbar.connectivity.NetworkController; +import com.android.systemui.statusbar.connectivity.SignalCallback; +import com.android.systemui.statusbar.connectivity.MobileDataIndicators; +import com.android.systemui.statusbar.connectivity.WifiIndicators; +import com.android.systemui.util.MediaSessionManagerHelper; +import com.android.internal.util.lunaris.VibrationUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.android.internal.util.lunaris.OmniJawsClient; + +public class LockScreenWidgetsController implements OmniJawsClient.OmniJawsObserver, MediaSessionManagerHelper.MediaMetadataListener { + + private static final String LOCKSCREEN_WIDGETS_ENABLED = + "lockscreen_widgets_enabled"; + + private static final String LOCKSCREEN_WIDGETS = + "lockscreen_widgets"; + + private static final String LOCKSCREEN_WIDGETS_EXTRAS = + "lockscreen_widgets_extras"; + + private static final String LOCKSCREEN_WIDGETS_STYLE = + "lockscreen_widgets_style"; + + private static final String LOCKSCREEN_WIDGETS_TRANSPARENCY = + "lockscreen_widgets_transparency"; + + private static final int[] MAIN_WIDGETS_VIEW_IDS = { + R.id.main_kg_item_placeholder1, + R.id.main_kg_item_placeholder2 + }; + + private static final int[] WIDGETS_VIEW_IDS = { + R.id.kg_item_placeholder1, + R.id.kg_item_placeholder2, + R.id.kg_item_placeholder3, + R.id.kg_item_placeholder4 + }; + + public static final int BT_ACTIVE = R.drawable.qs_bluetooth_icon_on; + public static final int BT_INACTIVE = R.drawable.qs_bluetooth_icon_off; + public static final int DATA_ACTIVE = R.drawable.ic_signal_cellular_alt_24; + public static final int DATA_INACTIVE = R.drawable.ic_mobiledata_off_24; + public static final int RINGER_ACTIVE = R.drawable.ic_vibration_24; + public static final int RINGER_INACTIVE = R.drawable.ic_ring_volume_24; + public static final int TORCH_RES_ACTIVE = R.drawable.ic_flashlight_on; + public static final int TORCH_RES_INACTIVE = R.drawable.ic_flashlight_off; + public static final int WIFI_ACTIVE = R.drawable.ic_wifi_24; + public static final int WIFI_INACTIVE = R.drawable.ic_wifi_off_24; + public static final int HOTSPOT_ACTIVE = R.drawable.qs_hotspot_icon_on; + public static final int HOTSPOT_INACTIVE = R.drawable.qs_hotspot_icon_off; + + public static final int BT_LABEL_INACTIVE = R.string.quick_settings_bluetooth_label; + public static final int DATA_LABEL_INACTIVE = R.string.quick_settings_data_label; + public static final int RINGER_LABEL_INACTIVE = R.string.quick_settings_ringer_label; + public static final int TORCH_LABEL_ACTIVE = R.string.torch_active; + public static final int TORCH_LABEL_INACTIVE = R.string.quick_settings_flashlight_label; + public static final int WIFI_LABEL_INACTIVE = R.string.quick_settings_wifi_label; + public static final int HOTSPOT_LABEL = R.string.accessibility_status_bar_hotspot; + + private OmniJawsClient mWeatherClient; + private OmniJawsClient.WeatherInfo mWeatherInfo; + + private final AccessPointController mAccessPointController; + private final BluetoothController mBluetoothController; + private final BluetoothDetailsContentViewModel mBluetoothDetailsContentViewModel; + private final ConfigurationController mConfigurationController; + private final DataUsageController mDataController; + private final FlashlightController mFlashlightController; + private final InternetDialogManager mInternetDialogManager; + private final NetworkController mNetworkController; + private final StatusBarStateController mStatusBarStateController; + private final MediaSessionManagerHelper mMediaSessionManagerHelper; + private final LockscreenWidgetsObserver mLockscreenWidgetsObserver; + private final ActivityLauncherUtils mActivityLauncherUtils; + private final HotspotController mHotspotController; + + protected final CellSignalCallback mCellSignalCallback = new CellSignalCallback(); + protected final WifiSignalCallback mWifiSignalCallback = new WifiSignalCallback(); + private final HotspotCallback mHotspotCallback = new HotspotCallback(); + + private Context mContext; + private LaunchableImageView mWidget1, mWidget2, mWidget3, mWidget4, mediaButton, torchButton, weatherButton; + private LaunchableFAB mediaButtonFab, torchButtonFab, weatherButtonFab, hotspotButtonFab; + private LaunchableFAB wifiButtonFab, dataButtonFab, ringerButtonFab, btButtonFab; + private LaunchableImageView wifiButton, dataButton, ringerButton, btButton, hotspotButton; + private int mDarkColor, mDarkColorActive, mLightColor, mLightColorActive; + + private CameraManager mCameraManager; + private String mCameraId; + private boolean isFlashOn = false; + + private String mMainLockscreenWidgetsList; + private String mSecondaryLockscreenWidgetsList; + private LaunchableFAB[] mMainWidgetViews; + private LaunchableImageView[] mSecondaryWidgetViews; + private List mMainWidgetsList = new ArrayList<>(); + private List mSecondaryWidgetsList = new ArrayList<>(); + private String mWidgetImagePath; + + private AudioManager mAudioManager; + private String mLastTrackTitle = null; + + private boolean mDozing; + + private boolean mIsInflated = false; + private GestureDetector mGestureDetector; + private boolean mIsLongPress = false; + + private boolean mLockscreenWidgetsEnabled; + private int mThemeStyle = 0; + private float mTransparency = 0.3f; + + final ConfigurationListener mConfigurationListener = new ConfigurationListener() { + @Override + public void onUiModeChanged() { + updateWidgetViews(); + } + @Override + public void onThemeChanged() { + updateWidgetViews(); + } + }; + + private final View mView; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + public LockScreenWidgetsController(View view) { + mView = view; + mContext = mView.getContext(); + mAccessPointController = Dependency.get(AccessPointController.class); + mBluetoothDetailsContentViewModel = Dependency.get(BluetoothDetailsContentViewModel.class); + mConfigurationController = Dependency.get(ConfigurationController.class); + mFlashlightController = Dependency.get(FlashlightController.class); + mInternetDialogManager = Dependency.get(InternetDialogManager.class); + mStatusBarStateController = Dependency.get(StatusBarStateController.class); + mBluetoothController = Dependency.get(BluetoothController.class); + mNetworkController = Dependency.get(NetworkController.class); + mDataController = mNetworkController.getMobileDataController(); + mHotspotController = Dependency.get(HotspotController.class); + mMediaSessionManagerHelper = MediaSessionManagerHelper.Companion.getInstance(mContext); + + mActivityLauncherUtils = new ActivityLauncherUtils(mContext); + + mLockscreenWidgetsObserver = new LockscreenWidgetsObserver(); + + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); + + initResources(); + + // FIX 1: OmniJawsClient no longer takes Context in constructor; use get() + if (mWeatherClient == null) { + mWeatherClient = OmniJawsClient.get(); + } + + try { + String[] cameraIds = mCameraManager.getCameraIdList(); + if (cameraIds != null && cameraIds.length > 0) { + mCameraId = cameraIds[0]; + } + } catch (Exception e) {} + + IntentFilter ringerFilter = new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); + mContext.registerReceiver(mRingerModeReceiver, ringerFilter); + } + + private final StatusBarStateController.StateListener mStatusBarStateListener = + new StatusBarStateController.StateListener() { + @Override + public void onStateChanged(int newState) { + if (newState == StatusBarState.KEYGUARD) { + syncAllTileStatesOnKeyguard(); + } + } + @Override + public void onDozingChanged(boolean dozing) { + if (mDozing == dozing) return; + mDozing = dozing; + updateWidgetViews(); + updateContainerVisibility(); + } + }; + + private void syncAllTileStatesOnKeyguard() { + if (isWidgetEnabled("wifi")) { + mNetworkController.removeCallback(mWifiSignalCallback); + mNetworkController.addCallback(mWifiSignalCallback); + } + if (isWidgetEnabled("data")) { + mNetworkController.removeCallback(mCellSignalCallback); + mNetworkController.addCallback(mCellSignalCallback); + updateMobileDataState(isMobileDataEnabled()); + } + if (isWidgetEnabled("bt")) { + updateBtState(); + } + if (isWidgetEnabled("torch")) { + isFlashOn = mFlashlightController.isEnabled(); + updateTorchButtonState(); + } + if (isWidgetEnabled("ringer")) { + updateRingerButtonState(); + } + if (isWidgetEnabled("hotspot")) { + updateHotspotState(); + } + } + + private final FlashlightController.FlashlightListener mFlashlightCallback = + new FlashlightController.FlashlightListener() { + @Override + public void onFlashlightChanged(boolean enabled) { + isFlashOn = enabled; + updateTorchButtonState(); + } + @Override + public void onFlashlightError() { + } + @Override + public void onFlashlightAvailabilityChanged(boolean available) { + isFlashOn = mFlashlightController.isEnabled() && available; + updateTorchButtonState(); + } + // Add the missing method + @Override + public void onFlashlightStrengthChanged(int level) { + // Handle flashlight strength changes if needed + updateTorchButtonState(); + } + }; + + private void initResources() { + mDarkColor = mContext.getResources().getColor(R.color.lockscreen_widget_background_color_dark); + mLightColor = mContext.getResources().getColor(R.color.lockscreen_widget_background_color_light); + mDarkColorActive = mContext.getResources().getColor(R.color.lockscreen_widget_active_color_dark); + mLightColorActive = mContext.getResources().getColor(R.color.lockscreen_widget_active_color_light); + } + + public void registerCallbacks() { + if (isWidgetEnabled("hotspot")) { + mHotspotController.addCallback(mHotspotCallback); + } + if (isWidgetEnabled("wifi")) { + mNetworkController.addCallback(mWifiSignalCallback); + } + if (isWidgetEnabled("data")) { + mNetworkController.addCallback(mCellSignalCallback); + } + if (isWidgetEnabled("bt")) { + mBluetoothController.addCallback(mBtCallback); + } + if (isWidgetEnabled("torch")) { + mFlashlightController.addCallback(mFlashlightCallback); + isFlashOn = mFlashlightController.isEnabled(); + updateTorchButtonState(); + } + mConfigurationController.addCallback(mConfigurationListener); + mStatusBarStateController.addCallback(mStatusBarStateListener); + mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); + mMediaSessionManagerHelper.addMediaMetadataListener(this); + mLockscreenWidgetsObserver.observe(); + updateWidgetViews(); + updateMediaPlaybackState(); + } + + public void unregisterCallbacks() { + if (isWidgetEnabled("weather")) { + disableWeatherUpdates(); + } + if (isWidgetEnabled("wifi")) { + mNetworkController.removeCallback(mWifiSignalCallback); + } + if (isWidgetEnabled("data")) { + mNetworkController.removeCallback(mCellSignalCallback); + } + if (isWidgetEnabled("bt")) { + mBluetoothController.removeCallback(mBtCallback); + } + if (isWidgetEnabled("torch")) { + mFlashlightController.removeCallback(mFlashlightCallback); + } + if (isWidgetEnabled("hotspot")) { + mHotspotController.removeCallback(mHotspotCallback); + } + mConfigurationController.removeCallback(mConfigurationListener); + mStatusBarStateController.removeCallback(mStatusBarStateListener); + try { + mContext.unregisterReceiver(mRingerModeReceiver); + } catch (IllegalArgumentException e) { + } + mLockscreenWidgetsObserver.unobserve(); + mHandler.removeCallbacksAndMessages(null); + mMediaSessionManagerHelper.removeMediaMetadataListener(this); + } + + public void initViews() { + mMainWidgetViews = new LaunchableFAB[MAIN_WIDGETS_VIEW_IDS.length]; + for (int i = 0; i < mMainWidgetViews.length; i++) { + mMainWidgetViews[i] = mView.findViewById(MAIN_WIDGETS_VIEW_IDS[i]); + } + mSecondaryWidgetViews = new LaunchableImageView[WIDGETS_VIEW_IDS.length]; + for (int i = 0; i < mSecondaryWidgetViews.length; i++) { + mSecondaryWidgetViews[i] = mView.findViewById(WIDGETS_VIEW_IDS[i]); + } + mIsInflated = true; + updateWidgetViews(); + } + + public void updateWidgetViews() { + if (!mIsInflated) return; + if (!mLockscreenWidgetsEnabled) { + if (mMainWidgetViews != null) { + for (LaunchableFAB v : mMainWidgetViews) { if (v != null) v.setVisibility(View.GONE); } + } + if (mSecondaryWidgetViews != null) { + for (LaunchableImageView v : mSecondaryWidgetViews) { if (v != null) v.setVisibility(View.GONE); } + } + final View mc = mView.findViewById(R.id.main_widgets_container); + if (mc != null) mc.setVisibility(View.GONE); + final View sc = mView.findViewById(R.id.secondary_widgets_container); + if (sc != null) sc.setVisibility(View.GONE); + mView.setVisibility(View.GONE); + return; + } + if (mMainWidgetViews != null && mMainWidgetsList != null) { + for (int i = 0; i < mMainWidgetViews.length; i++) { + if (mMainWidgetViews[i] != null) { + mMainWidgetViews[i].setVisibility(i < mMainWidgetsList.size() ? View.VISIBLE : View.GONE); + } + } + for (int i = 0; i < Math.min(mMainWidgetsList.size(), mMainWidgetViews.length); i++) { + String widgetType = mMainWidgetsList.get(i); + if (widgetType != null && mMainWidgetViews[i] != null) { + setUpWidgetWiews(null, mMainWidgetViews[i], widgetType); + updateMainWidgetResources(mMainWidgetViews[i], false); + } + } + } + if (mSecondaryWidgetViews != null && mSecondaryWidgetsList != null) { + for (int i = 0; i < mSecondaryWidgetViews.length; i++) { + if (mSecondaryWidgetViews[i] != null) { + mSecondaryWidgetViews[i].setVisibility(i < mSecondaryWidgetsList.size() ? View.VISIBLE : View.GONE); + } + } + for (int i = 0; i < Math.min(mSecondaryWidgetsList.size(), mSecondaryWidgetViews.length); i++) { + String widgetType = mSecondaryWidgetsList.get(i); + if (widgetType != null && mSecondaryWidgetViews[i] != null) { + setUpWidgetWiews(mSecondaryWidgetViews[i], null, widgetType); + updateWidgetsResources(mSecondaryWidgetViews[i]); + } + } + } + updateContainerVisibility(); + } + + private void updateMainWidgetResources(LaunchableFAB efab, boolean active) { + if (efab == null) return; + efab.setElevation(0); + if (mDozing) { + int bgRes = (mThemeStyle == 1 || mThemeStyle == 2) + ? R.drawable.lockscreen_widget_background_square_aod + : R.drawable.lockscreen_widget_background_circle_aod; + efab.setBackgroundTintList(null); + efab.setBackgroundDrawable(mContext.getDrawable(bgRes)); + } else { + int bgRes = (mThemeStyle == 1 || mThemeStyle == 2) + ? R.drawable.lockscreen_widget_background_square + : R.drawable.lockscreen_widget_background_circle; + efab.setBackgroundDrawable(mContext.getDrawable(bgRes)); + } + setButtonActiveState(null, efab, false); + long visibleWidgetCount = mMainWidgetsList.stream().filter(w -> !"none".equals(w)).count(); + ViewGroup.LayoutParams params = efab.getLayoutParams(); + if (params instanceof LinearLayout.LayoutParams) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) params; + if (efab.getVisibility() == View.VISIBLE && visibleWidgetCount == 1) { + lp.width = mContext.getResources().getDimensionPixelSize(R.dimen.kg_widget_main_width); + lp.height = mContext.getResources().getDimensionPixelSize(R.dimen.kg_widget_main_height); + } else { + lp.width = 0; + lp.weight = 1; + } + efab.setLayoutParams(lp); + } + } + + private void updateContainerVisibility() { + if (!mLockscreenWidgetsEnabled) { + final View mc = mView.findViewById(R.id.main_widgets_container); + if (mc != null) mc.setVisibility(View.GONE); + final View sc = mView.findViewById(R.id.secondary_widgets_container); + if (sc != null) sc.setVisibility(View.GONE); + mView.setVisibility(View.GONE); + return; + } + final boolean isMainEmpty = mMainLockscreenWidgetsList == null || TextUtils.isEmpty(mMainLockscreenWidgetsList); + final boolean isSecondaryEmpty = mSecondaryLockscreenWidgetsList == null || TextUtils.isEmpty(mSecondaryLockscreenWidgetsList); + final View mc = mView.findViewById(R.id.main_widgets_container); + if (mc != null) mc.setVisibility(isMainEmpty ? View.GONE : View.VISIBLE); + final View sc = mView.findViewById(R.id.secondary_widgets_container); + if (sc != null) sc.setVisibility(isSecondaryEmpty ? View.GONE : View.VISIBLE); + mView.setVisibility((isMainEmpty && isSecondaryEmpty) ? View.GONE : View.VISIBLE); + } + + private void updateWidgetsResources(LaunchableImageView iv) { + if (iv == null) return; + int bgRes; + if (mDozing) { + bgRes = (mThemeStyle == 1 || mThemeStyle == 2) + ? R.drawable.lockscreen_widget_background_square_aod + : R.drawable.lockscreen_widget_background_circle_aod; + } else { + bgRes = (mThemeStyle == 1 || mThemeStyle == 2) + ? R.drawable.lockscreen_widget_background_square + : R.drawable.lockscreen_widget_background_circle; + } + iv.setBackgroundResource(bgRes); + setButtonActiveState(iv, null, false); + } + + private boolean isNightMode() { + final Configuration config = mContext.getResources().getConfiguration(); + return (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + } + + private void setUpWidgetWiews(LaunchableImageView iv, LaunchableFAB efab, String type) { + View.OnClickListener clickListener = null; + View.OnLongClickListener longClickListener = null; + int drawableRes = 0; + int stringRes = 0; + + switch (type) { + case "none": + if (iv != null) iv.setVisibility(View.GONE); + if (efab != null) efab.setVisibility(View.GONE); + return; + case "wifi": + clickListener = v -> toggleWiFi(); + longClickListener = v -> { showInternetDialog(v); return true; }; + drawableRes = WIFI_INACTIVE; + stringRes = R.string.quick_settings_wifi_label; + if (iv != null) wifiButton = iv; + if (efab != null) wifiButtonFab = efab; + break; + case "data": + clickListener = v -> toggleMobileData(); + longClickListener = v -> { showInternetDialog(v); return true; }; + drawableRes = DATA_INACTIVE; + stringRes = DATA_LABEL_INACTIVE; + if (iv != null) dataButton = iv; + if (efab != null) dataButtonFab = efab; + break; + case "ringer": + clickListener = v -> toggleRingerMode(); + drawableRes = RINGER_INACTIVE; + stringRes = RINGER_LABEL_INACTIVE; + if (iv != null) ringerButton = iv; + if (efab != null) ringerButtonFab = efab; + break; + case "bt": + clickListener = v -> toggleBluetoothState(); + longClickListener = v -> { showBluetoothDialog(v); return true; }; + drawableRes = BT_INACTIVE; + stringRes = BT_LABEL_INACTIVE; + if (iv != null) btButton = iv; + if (efab != null) btButtonFab = efab; + break; + case "torch": + clickListener = v -> toggleFlashlight(); + drawableRes = TORCH_RES_INACTIVE; + stringRes = TORCH_LABEL_INACTIVE; + if (iv != null) torchButton = iv; + if (efab != null) torchButtonFab = efab; + break; + case "timer": + clickListener = v -> mActivityLauncherUtils.launchTimer(); + drawableRes = R.drawable.ic_alarm; + stringRes = R.string.clock_timer; + break; + case "calculator": + clickListener = v -> mActivityLauncherUtils.launchCalculator(); + drawableRes = R.drawable.ic_calculator; + stringRes = R.string.calculator; + break; + case "media": + clickListener = v -> toggleMediaPlaybackState(); + longClickListener = v -> { showMediaDialog(v); return true; }; + drawableRes = R.drawable.ic_media_play; + stringRes = R.string.controls_media_button_play; + if (iv != null) mediaButton = iv; + if (efab != null) mediaButtonFab = efab; + break; + case "weather": + clickListener = v -> mActivityLauncherUtils.launchWeatherApp(); + drawableRes = R.drawable.ic_weather; + stringRes = R.string.weather_data_unavailable; + if (iv != null) weatherButton = iv; + if (efab != null) weatherButtonFab = efab; + enableWeatherUpdates(); + break; + case "hotspot": + clickListener = v -> toggleHotspot(); + longClickListener = v -> { showInternetDialog(v); return true; }; + drawableRes = HOTSPOT_INACTIVE; + stringRes = HOTSPOT_LABEL; + if (iv != null) hotspotButton = iv; + if (efab != null) hotspotButtonFab = efab; + break; + case "wallet": + clickListener = v -> mActivityLauncherUtils.launchWalletApp(); + drawableRes = R.drawable.ic_wallet_lockscreen; + stringRes = R.string.google_wallet; + break; + case "qrscanner": + clickListener = v -> mActivityLauncherUtils.launchQrScanner(); + drawableRes = R.drawable.ic_qr_code_scanner; + stringRes = R.string.qr_code_scanner_title; + break; + default: + return; + } + + if (efab != null) { + efab.setOnClickListener(clickListener); + efab.setIcon(mContext.getDrawable(drawableRes)); + efab.setText(mContext.getResources().getString(stringRes)); + if (longClickListener != null) efab.setOnLongClickListener(longClickListener); + if (mediaButtonFab == efab) attachSwipeGesture(efab); + } + if (iv != null) { + iv.setOnClickListener(clickListener); + if (longClickListener != null) iv.setOnLongClickListener(longClickListener); + iv.setImageResource(drawableRes); + } + } + + private void attachSwipeGesture(LaunchableFAB efab) { + final GestureDetector gestureDetector = new GestureDetector(mContext, + new GestureDetector.SimpleOnGestureListener() { + private static final int SWIPE_THRESHOLD = 100; + private static final int SWIPE_VELOCITY_THRESHOLD = 100; + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + float diffX = e2.getX() - e1.getX(); + if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { + if (diffX > 0) { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PREVIOUS); + } else { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_NEXT); + } + VibrationUtils.triggerVibration(mContext, 2); + return true; + } + return false; + } + @Override + public void onLongPress(MotionEvent e) { + super.onLongPress(e); + mIsLongPress = true; + showMediaDialog(efab); + mHandler.postDelayed(() -> mIsLongPress = false, 2500); + } + }); + efab.setOnTouchListener((v, event) -> { + boolean isClick = gestureDetector.onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP && !isClick && !mIsLongPress) { + v.performClick(); + } + return true; + }); + } + + private void setButtonActiveState(LaunchableImageView iv, LaunchableFAB efab, boolean active) { + if (mDozing) { + if (iv != null) { + iv.setBackgroundTintList(null); + iv.setImageTintList(ColorStateList.valueOf(Color.WHITE)); + } + if (efab != null) { + efab.setBackgroundTintList(null); + efab.setIconTint(efab == weatherButtonFab ? null : ColorStateList.valueOf(Color.WHITE)); + efab.setTextColor(Color.WHITE); + } + return; + } + int bgTint, tintColor; + if (mThemeStyle == 2 || mThemeStyle == 3) { + bgTint = active ? Utils.applyAlpha(mTransparency, mDarkColorActive) : Utils.applyAlpha(mTransparency, Color.WHITE); + tintColor = active ? mDarkColorActive : Color.WHITE; + } else { + bgTint = active ? (isNightMode() ? mDarkColorActive : mLightColorActive) : (isNightMode() ? mDarkColor : mLightColor); + tintColor = active ? (isNightMode() ? mDarkColor : mLightColor) : (isNightMode() ? mLightColor : mDarkColor); + } + if (iv != null) { + iv.setBackgroundTintList(ColorStateList.valueOf(bgTint)); + iv.setImageTintList(iv == weatherButton ? null : ColorStateList.valueOf(tintColor)); + } + if (efab != null) { + efab.setBackgroundTintList(ColorStateList.valueOf(bgTint)); + efab.setIconTint(efab == weatherButtonFab ? null : ColorStateList.valueOf(tintColor)); + efab.setTextColor(tintColor); + } + } + + private void toggleMediaPlaybackState() { + if (mMediaSessionManagerHelper.isMediaPlaying()) { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PAUSE); + } else { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PLAY); + } + } + + private void showMediaDialog(View view) { + String lastMediaPkg = getLastUsedMedia(); + if (TextUtils.isEmpty(lastMediaPkg)) return; + if (!(mView instanceof LockScreenWidgets)) return; + mHandler.post(() -> { + ((LockScreenWidgets) mView).showMediaDialog(view, lastMediaPkg); + VibrationUtils.triggerVibration(mContext, 2); + }); + } + + private String getLastUsedMedia() { + return Settings.System.getString(mContext.getContentResolver(), "media_session_last_package_name"); + } + + private void dispatchMediaKeyWithWakeLockToMediaSession(final int keycode) { + final MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); + if (helper == null) return; + KeyEvent event = new KeyEvent(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), + KeyEvent.ACTION_DOWN, keycode, 0); + helper.sendMediaButtonEvent(event, true); + event = KeyEvent.changeAction(event, KeyEvent.ACTION_UP); + helper.sendMediaButtonEvent(event, true); + mHandler.postDelayed(this::updateMediaPlaybackState, 250); + } + + private void updateMediaPlaybackState() { + boolean isPlaying = mMediaSessionManagerHelper.isMediaPlaying(); + int stateIcon = isPlaying ? R.drawable.ic_media_pause : R.drawable.ic_media_play; + if (mediaButton != null) { + mediaButton.setImageResource(stateIcon); + setButtonActiveState(mediaButton, null, isPlaying); + } + if (mediaButtonFab != null) { + MediaMetadata meta = mMediaSessionManagerHelper.getCurrentMediaMetadata(); + String trackTitle = meta != null ? meta.getString(MediaMetadata.METADATA_KEY_TITLE) : ""; + if (!TextUtils.isEmpty(trackTitle) && !trackTitle.equals(mLastTrackTitle)) { + mLastTrackTitle = trackTitle; + } + final boolean canShowTitle = isPlaying || !TextUtils.isEmpty(mLastTrackTitle); + mediaButtonFab.setIcon(mContext.getDrawable(stateIcon)); + mediaButtonFab.setText(canShowTitle ? mLastTrackTitle : mContext.getString(R.string.controls_media_button_play)); + setButtonActiveState(null, mediaButtonFab, isPlaying); + } + } + + private void toggleFlashlight() { + if (torchButton == null && torchButtonFab == null) return; + try { + final boolean newState = !isFlashOn; + isFlashOn = newState; + updateTorchButtonState(); + mFlashlightController.setFlashlight(newState); + } catch (Exception e) { + isFlashOn = mFlashlightController.isEnabled(); + updateTorchButtonState(); + } + } + + private void toggleWiFi() { + final WifiCallbackInfo cbi = mWifiSignalCallback.mInfo; + final boolean newEnabled = !cbi.enabled; + cbi.enabled = newEnabled; + mNetworkController.setWifiEnabled(newEnabled); + updateWiFiButtonState(newEnabled); + } + + private boolean isMobileDataEnabled() { + return mDataController.isMobileDataEnabled(); + } + + private void toggleMobileData() { + final boolean newEnabled = !isMobileDataEnabled(); + mDataController.setMobileDataEnabled(newEnabled); + updateMobileDataState(newEnabled); + mHandler.postDelayed(() -> updateMobileDataState(isMobileDataEnabled()), 250); + } + + private void showInternetDialog(View view) { + mHandler.post(() -> mInternetDialogManager.create(true, + mAccessPointController.canConfigMobileData(), + mAccessPointController.canConfigWifi(), Expandable.fromView(view))); + VibrationUtils.triggerVibration(mContext, 2); + } + + private void toggleRingerMode() { + if (mAudioManager == null) return; + int mode = mAudioManager.getRingerMode(); + mAudioManager.setRingerMode(mode == AudioManager.RINGER_MODE_NORMAL + ? AudioManager.RINGER_MODE_VIBRATE : AudioManager.RINGER_MODE_NORMAL); + updateRingerButtonState(); + } + + private void updateTileButtonState(LaunchableImageView iv, LaunchableFAB efab, + boolean active, int activeResource, int inactiveResource, + String activeString, String inactiveString) { + mHandler.post(() -> { + if (iv != null) { + iv.setImageResource(active ? activeResource : inactiveResource); + setButtonActiveState(iv, null, active); + } + if (efab != null) { + efab.setIcon(mContext.getDrawable(active ? activeResource : inactiveResource)); + efab.setText(active ? activeString : inactiveString); + setButtonActiveState(null, efab, active); + } + }); + } + + public void updateTorchButtonState() { + if (!isWidgetEnabled("torch")) return; + String activeString = mContext.getResources().getString(TORCH_LABEL_ACTIVE); + String inactiveString = mContext.getResources().getString(TORCH_LABEL_INACTIVE); + updateTileButtonState(torchButton, torchButtonFab, isFlashOn, + TORCH_RES_ACTIVE, TORCH_RES_INACTIVE, activeString, inactiveString); + } + + private final BroadcastReceiver mRingerModeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateRingerButtonState(); + } + }; + + private final BluetoothController.Callback mBtCallback = new BluetoothController.Callback() { + @Override + public void onBluetoothStateChange(boolean enabled) { + updateBtState(); + } + @Override + public void onBluetoothDevicesChanged() { + updateBtState(); + } + }; + + private void updateWiFiButtonState(boolean enabled) { + if (!isWidgetEnabled("wifi")) return; + if (wifiButton == null && wifiButtonFab == null) return; + final WifiCallbackInfo cbi = mWifiSignalCallback.mInfo; + String inactiveString = mContext.getResources().getString(WIFI_LABEL_INACTIVE); + updateTileButtonState(wifiButton, wifiButtonFab, enabled, + WIFI_ACTIVE, WIFI_INACTIVE, + cbi.ssid != null ? removeDoubleQuotes(cbi.ssid) : inactiveString, + inactiveString); + } + + private void updateRingerButtonState() { + if (!isWidgetEnabled("ringer")) return; + if (ringerButton == null && ringerButtonFab == null) return; + if (mAudioManager == null) return; + boolean isVibrateActive = mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE; + String inactiveString = mContext.getResources().getString(RINGER_LABEL_INACTIVE); + updateTileButtonState(ringerButton, ringerButtonFab, isVibrateActive, + RINGER_ACTIVE, RINGER_INACTIVE, inactiveString, inactiveString); + } + + private void updateMobileDataState(boolean enabled) { + if (!isWidgetEnabled("data")) return; + if (dataButton == null && dataButtonFab == null) return; + String networkName = mNetworkController == null ? "" : mNetworkController.getMobileDataNetworkName(); + boolean hasNetwork = !TextUtils.isEmpty(networkName) && mNetworkController != null + && mNetworkController.hasMobileDataFeature(); + String inactiveString = mContext.getResources().getString(DATA_LABEL_INACTIVE); + updateTileButtonState(dataButton, dataButtonFab, enabled, + DATA_ACTIVE, DATA_INACTIVE, + hasNetwork && enabled ? networkName : inactiveString, + inactiveString); + } + + private void toggleBluetoothState() { + final boolean newEnabled = !mBluetoothController.isBluetoothEnabled(); + mBluetoothController.setBluetoothEnabled(newEnabled); + updateBtState(); + } + + private void showBluetoothDialog(View view) { + mHandler.post(() -> mBluetoothDetailsContentViewModel.showDialog(Expandable.fromView(view))); + VibrationUtils.triggerVibration(mContext, 2); + } + + private void updateBtState() { + if (!isWidgetEnabled("bt")) return; + if (btButton == null && btButtonFab == null) return; + final boolean btEnabled = mBluetoothController.isBluetoothEnabled(); + String deviceName = btEnabled ? mBluetoothController.getConnectedDeviceName() : ""; + boolean isConnected = !TextUtils.isEmpty(deviceName); + String inactiveString = mContext.getResources().getString(BT_LABEL_INACTIVE); + updateTileButtonState(btButton, btButtonFab, btEnabled, + BT_ACTIVE, BT_INACTIVE, + isConnected ? deviceName : inactiveString, + inactiveString); + } + + @Nullable + private static String removeDoubleQuotes(String string) { + if (string == null) return null; + final int length = string.length(); + if (length > 1 && string.charAt(0) == '"' && string.charAt(length - 1) == '"') { + return string.substring(1, length - 1); + } + return string; + } + + protected static final class WifiCallbackInfo { + boolean enabled; + @Nullable String ssid; + } + + protected final class WifiSignalCallback implements SignalCallback { + final WifiCallbackInfo mInfo = new WifiCallbackInfo(); + @Override + public void setWifiIndicators(@NonNull WifiIndicators indicators) { + if (indicators.qsIcon == null) { + mInfo.enabled = false; + mInfo.ssid = null; + updateWiFiButtonState(false); + return; + } + mInfo.enabled = indicators.enabled; + mInfo.ssid = indicators.description; + updateWiFiButtonState(mInfo.enabled); + } + } + + private final class CellSignalCallback implements SignalCallback { + @Override + public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) { + if (indicators.qsIcon == null) { + updateMobileDataState(false); + return; + } + updateMobileDataState(isMobileDataEnabled()); + } + @Override + public void setNoSims(boolean show, boolean simDetected) { + updateMobileDataState(simDetected && isMobileDataEnabled()); + } + @Override + public void setIsAirplaneMode(@NonNull IconState icon) { + updateMobileDataState(!icon.visible && isMobileDataEnabled()); + } + } + + // FIX 2: addObserver now requires Context as first argument + public void enableWeatherUpdates() { + if (mWeatherClient != null) { + mWeatherClient.addObserver(mContext, this); + queryAndUpdateWeather(); + } + } + + // FIX 3: removeObserver now requires Context as first argument + public void disableWeatherUpdates() { + if (mWeatherClient != null) { + mWeatherClient.removeObserver(mContext, this); + } + } + + @Override + public void weatherError(int errorReason) { + if (errorReason == OmniJawsClient.EXTRA_ERROR_DISABLED) { + mWeatherInfo = null; + } + } + + @Override + public void weatherUpdated() { + queryAndUpdateWeather(); + } + + @Override + public void updateSettings() { + queryAndUpdateWeather(); + } + + private void queryAndUpdateWeather() { + try { + // FIX 4: isOmniJawsEnabled and queryWeather now require Context argument + // FIX 5: getWeatherConditionImage now requires Context as first argument + if (mWeatherClient == null || !mWeatherClient.isOmniJawsEnabled(mContext)) return; + mWeatherClient.queryWeather(mContext); + mWeatherInfo = mWeatherClient.getWeatherInfo(); + if (mWeatherInfo != null) { + String formattedCondition = mWeatherInfo.condition; + if (formattedCondition.toLowerCase().contains("clouds")) { + formattedCondition = mContext.getString(R.string.weather_condition_clouds); + } else if (formattedCondition.toLowerCase().contains("rain")) { + formattedCondition = mContext.getString(R.string.weather_condition_rain); + } else if (formattedCondition.toLowerCase().contains("clear")) { + formattedCondition = mContext.getString(R.string.weather_condition_clear); + } else if (formattedCondition.toLowerCase().contains("storm")) { + formattedCondition = mContext.getString(R.string.weather_condition_storm); + } else if (formattedCondition.toLowerCase().contains("snow")) { + formattedCondition = mContext.getString(R.string.weather_condition_snow); + } else if (formattedCondition.toLowerCase().contains("wind")) { + formattedCondition = mContext.getString(R.string.weather_condition_wind); + } else if (formattedCondition.toLowerCase().contains("mist")) { + formattedCondition = mContext.getString(R.string.weather_condition_mist); + } + if (formattedCondition.contains("_")) { + String[] words = formattedCondition.split("_"); + StringBuilder sb = new StringBuilder(); + for (String word : words) { + sb.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1)).append(" "); + } + formattedCondition = sb.toString().trim(); + } + final Drawable d = mWeatherClient.getWeatherConditionImage(mContext, mWeatherInfo.conditionCode); + if (weatherButtonFab != null) { + weatherButtonFab.setIcon(d); + weatherButtonFab.setText(mWeatherInfo.temp + mWeatherInfo.tempUnits + " \u2022 " + formattedCondition); + weatherButtonFab.setIconTint(null); + } + if (weatherButton != null) { + weatherButton.setImageDrawable(d); + weatherButton.setImageTintList(null); + } + } + } catch (Exception e) {} + } + + private boolean isWidgetEnabled(String widget) { + return (mMainLockscreenWidgetsList != null && mMainLockscreenWidgetsList.contains(widget)) + || (mSecondaryLockscreenWidgetsList != null && mSecondaryLockscreenWidgetsList.contains(widget)); + } + + @Override + public void onMediaMetadataChanged() { updateMediaPlaybackState(); } + + @Override + public void onPlaybackStateChanged() { updateMediaPlaybackState(); } + + private class LockscreenWidgetsObserver extends ContentObserver { + public LockscreenWidgetsObserver() { + super(new Handler(Looper.getMainLooper())); + } + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + updateSettings(); + } + void observe() { + ContentResolver cr = mContext.getContentResolver(); + cr.registerContentObserver(Settings.System.getUriFor(LOCKSCREEN_WIDGETS_ENABLED), false, this); + cr.registerContentObserver(Settings.System.getUriFor(LOCKSCREEN_WIDGETS), false, this); + cr.registerContentObserver(Settings.System.getUriFor(LOCKSCREEN_WIDGETS_EXTRAS), false, this); + cr.registerContentObserver(Settings.System.getUriFor(LOCKSCREEN_WIDGETS_STYLE), false, this); + cr.registerContentObserver(Settings.System.getUriFor(LOCKSCREEN_WIDGETS_TRANSPARENCY), false, this); + updateSettings(); + } + void unobserve() { + mContext.getContentResolver().unregisterContentObserver(this); + } + void updateSettings() { + ContentResolver cr = mContext.getContentResolver(); + mLockscreenWidgetsEnabled = Settings.System.getInt(cr, LOCKSCREEN_WIDGETS_ENABLED, 0) == 1; + mMainLockscreenWidgetsList = Settings.System.getString(cr, LOCKSCREEN_WIDGETS); + mSecondaryLockscreenWidgetsList = Settings.System.getString(cr, LOCKSCREEN_WIDGETS_EXTRAS); + mThemeStyle = Settings.System.getInt(cr, LOCKSCREEN_WIDGETS_STYLE, 0); + mTransparency = Settings.System.getInt(cr, LOCKSCREEN_WIDGETS_TRANSPARENCY, 30) / 100f; + if (mMainLockscreenWidgetsList != null) { + mMainWidgetsList = Arrays.asList(mMainLockscreenWidgetsList.split(",")); + } + if (mSecondaryLockscreenWidgetsList != null) { + mSecondaryWidgetsList = Arrays.asList(mSecondaryLockscreenWidgetsList.split(",")); + } + updateWidgetViews(); + } + } + + private void updateHotspotState() { + if (!isWidgetEnabled("hotspot")) return; + if (hotspotButton == null && hotspotButtonFab == null) return; + String hotspotString = mContext.getResources().getString(HOTSPOT_LABEL); + updateTileButtonState(hotspotButton, hotspotButtonFab, mHotspotController.isHotspotEnabled(), + HOTSPOT_ACTIVE, HOTSPOT_INACTIVE, hotspotString, hotspotString); + } + + private void toggleHotspot() { + mHotspotController.setHotspotEnabled(!mHotspotController.isHotspotEnabled()); + updateHotspotState(); + mHandler.postDelayed(this::updateHotspotState, 250); + } + + private final class HotspotCallback implements HotspotController.Callback { + @Override + public void onHotspotChanged(boolean enabled, int numDevices) { + updateHotspotState(); + } + @Override + public void onHotspotAvailabilityChanged(boolean available) {} + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt index b2e7fe0012d99..da65471afae4c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt @@ -94,7 +94,7 @@ constructor( context.resources.getDimensionPixelSize( com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize ), - 500 + 460 ) private val artworkHeight: Int = context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) 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 05d48d651f1f5..b92c812a19d8a 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -458,6 +458,9 @@ public void onInputDeviceRemoved(int deviceId) { mTrackpadsConnected.remove(deviceId); if (mTrackpadsConnected.isEmpty()) { update(); + if (mStateChangeCallback != null) { + mStateChangeCallback.run(); + } } }); } @@ -548,8 +551,9 @@ public interface Factory { mDesktopState = desktopState; mTunerService = tunerService; - ComponentName recentsComponentName = ComponentName.unflattenFromString( - context.getString(com.android.internal.R.string.config_recentsComponentName)); + int defaultLauncher = SystemProperties.getInt("persist.sys.default_launcher", 0); + String[] launcherComponents = context.getResources().getStringArray(com.android.internal.R.array.config_launcherComponents); + ComponentName recentsComponentName = ComponentName.unflattenFromString(launcherComponents[defaultLauncher]); if (recentsComponentName != null) { String recentsPackageName = recentsComponentName.getPackageName(); PackageManager manager = context.getPackageManager(); diff --git a/packages/SystemUI/src/com/android/systemui/nowplaying/NowPlayingViewController.kt b/packages/SystemUI/src/com/android/systemui/nowplaying/NowPlayingViewController.kt index 7aa8e8e5ac43e..e0d7abd76e3ee 100644 --- a/packages/SystemUI/src/com/android/systemui/nowplaying/NowPlayingViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/nowplaying/NowPlayingViewController.kt @@ -45,6 +45,7 @@ constructor( private val mediaSessionManager = context.getSystemService(MediaSessionManager::class.java)!! private var activeController: MediaController? = null + private var bouncerShowingOrKeyguardDismissing = false private var currentTrackTitle: String = "" private var currentArtist: String = "" private var currentPackageName: String = "" @@ -175,6 +176,7 @@ constructor( val shouldShow = when { !isPlaying || currentTrackTitle.isEmpty() -> false + bouncerShowingOrKeyguardDismissing -> false !isPanelCollapsed -> false isDozing -> currentSettings.showOnAod isKeyguardShowing -> currentSettings.showOnLockscreen @@ -193,6 +195,33 @@ constructor( updateVisibility() } + override fun onPrimaryBouncerShowingChanged(showing: Boolean) { + bouncerShowingOrKeyguardDismissing = showing + if (showing) { + nowPlayingView.hide() + } else { + updateVisibility() + } + } + + override fun onKeyguardGoingAwayChanged(goingAway: Boolean) { + bouncerShowingOrKeyguardDismissing = goingAway + if (goingAway) { + nowPlayingView.hide() + } else { + updateVisibility() + } + } + + override fun onKeyguardFadingAwayChanged(fadingAway: Boolean) { + bouncerShowingOrKeyguardDismissing = fadingAway + if (fadingAway) { + nowPlayingView.hide() + } else { + updateVisibility() + } + } + override fun onDozingChanged(dozing: Boolean) { try { isDozing = ScrimUtils.get()?.isDozing() ?: false diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt index 633fecee67bb3..a43de64a220a4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.ui.compose +import android.provider.Settings import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -30,6 +31,7 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -46,6 +48,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.development.ui.compose.BuildNumber import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.qs.footer.ui.compose.rememberSystemSettingEnabled import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType import com.android.systemui.qs.panels.ui.compose.Dimensions.FooterHeight import com.android.systemui.qs.panels.ui.compose.Dimensions.InterPageSpacing @@ -172,6 +175,8 @@ private fun FooterBar( editButtonViewModelFactory: EditModeButtonViewModel.Factory, isVisible: () -> Boolean = { true }, ) { + val showEdit by rememberSystemSettingEnabled(Settings.System.QS_FOOTER_SHOW_EDIT) + val editButtonViewModel = rememberViewModel(traceName = "PaginatedGridLayout-editButtonViewModel") { editButtonViewModelFactory.create() @@ -203,7 +208,9 @@ private fun FooterBar( ) Row(Modifier.weight(1f)) { Spacer(modifier = Modifier.weight(1f)) - EditModeButton(viewModel = editButtonViewModel, isVisible = isVisible()) + if (showEdit) { + EditModeButton(viewModel = editButtonViewModel, isVisible = isVisible()) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt index 84869c71eac51..ab97fe28dc512 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt @@ -16,7 +16,9 @@ package com.android.systemui.qs.panels.ui.compose +import android.provider.Settings import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -24,6 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope @@ -31,6 +34,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.grid.ui.compose.VerticalSpannedGrid import com.android.systemui.qs.composefragment.ui.GridAnchor import com.android.systemui.qs.flags.QSMaterialExpressiveTiles +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel @@ -48,6 +52,11 @@ fun ContentScope.QuickQuickSettings( val tiles = sizedTiles.fastMap { it.tile } val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() + + val context = androidx.compose.ui.platform.LocalContext.current + val useModifiedSpacing = remember { + Settings.System.getInt(context.contentResolver, Settings.System.QS_USE_MODIFIED_TILE_SPACING, 0) == 1 + } Box(modifier = modifier) { GridAnchor() @@ -80,12 +89,22 @@ fun ContentScope.QuickQuickSettings( val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } + VerticalSpannedGrid( columns = columns, - columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), - rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical), + columnSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileColumnSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_horizontal) + }, + rowSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileRowSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_vertical) + }, spans = spans, - modifier = Modifier.sysuiResTag("qqs_tile_layout"), + modifier = Modifier.sysuiResTag("qqs_tile_layout") + .then(if (useModifiedSpacing) Modifier.padding(horizontal = 10.dp) else Modifier), keys = { sizedTiles[it].tile.spec }, ) { spanIndex, column, isFirstInColumn, isLastInColumn -> val it = sizedTiles[spanIndex] diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/AxTileStyle.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/AxTileStyle.kt new file mode 100644 index 0000000000000..60b000d369fd9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/AxTileStyle.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.panels.ui.compose.infinitegrid + +import android.content.Context +import android.graphics.drawable.Drawable +import android.service.quicksettings.Tile.STATE_ACTIVE +import android.service.quicksettings.Tile.STATE_INACTIVE +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.android.compose.modifiers.thenIf +import com.android.systemui.Flags +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabelSettings +import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState +import com.android.systemui.qs.ui.compose.borderOnFocus + +object AxTileDefaults { + val TileCornerRadius = 50.dp + val ActiveTileCornerRadius = 26.dp + + val LargeIconSize = 24.dp + val DividerWidth = 1.dp + val DividerHeight = 16.dp + val IconDividerSpacing = 12.dp + val DividerLabelSpacing = 16.dp + val LargeTileStartPadding = 24.dp + val LargeTileEndPadding = 16.dp + + @Composable + fun dividerColor(state: Int): Color { + return when (state) { + STATE_ACTIVE -> MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.2f) + STATE_INACTIVE -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.15f) + else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f) + } + } + + @Composable + fun animateTileShapeAsState( + state: Int, + invertRadius: Boolean = false, + ): androidx.compose.runtime.State = animateAxShape( + state = state, + activeCornerRadius = if (invertRadius) TileCornerRadius else ActiveTileCornerRadius, + inactiveCornerRadius = if (invertRadius) ActiveTileCornerRadius else TileCornerRadius, + label = "AxTileCornerRadius", + ) + + @Composable + fun animateIconShapeAsState( + state: Int, + invertRadius: Boolean = false, + ): androidx.compose.runtime.State = animateAxShape( + state = state, + activeCornerRadius = TileCornerRadius, + inactiveCornerRadius = TileCornerRadius, + label = "AxIconCornerRadius", + ) + + @Composable + private fun animateAxShape( + state: Int, + activeCornerRadius: Dp, + inactiveCornerRadius: Dp, + label: String, + ): State { + val radius by animateDpAsState( + targetValue = if (state == STATE_ACTIVE) activeCornerRadius else inactiveCornerRadius, + label = label, + ) + return remember { + mutableStateOf( + RoundedCornerShape( + object : CornerSize { + override fun toPx(shapeSize: Size, density: Density): Float = + with(density) { radius.toPx() } + } + ) + ) + } + } +} + +@Composable +fun AxLargeTileContent( + label: String, + secondaryLabel: String?, + iconProvider: Context.() -> Icon, + sideDrawable: Drawable?, + colors: TileColors, + squishiness: () -> Float, + tileState: Int, + modifier: Modifier = Modifier, + isVisible: () -> Boolean = { true }, + accessibilityUiState: AccessibilityUiState? = null, + iconShape: RoundedCornerShape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius), + textScale: () -> Float = { 1f }, + toggleClick: (() -> Unit)? = null, + onLongClick: (() -> Unit)? = null, +) { + val isDualTarget = toggleClick != null + val dividerColor = AxTileDefaults.dividerColor(tileState) + val focusBorderColor = MaterialTheme.colorScheme.secondary + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding( + start = AxTileDefaults.LargeTileStartPadding, + end = AxTileDefaults.LargeTileEndPadding, + ), + ) { + val longPressLabel = longPressLabelSettings().takeIf { onLongClick != null } + + Box( + modifier = Modifier + .fillMaxHeight() + .thenIf(isDualTarget) { + Modifier + .borderOnFocus(color = focusBorderColor, iconShape.topEnd) + .combinedClickable( + onClick = toggleClick!!, + onLongClick = onLongClick, + onLongClickLabel = longPressLabel, + hapticFeedbackEnabled = !Flags.msdlFeedback(), + ) + }, + contentAlignment = Alignment.Center, + ) { + SmallTileContent( + iconProvider = iconProvider, + color = colors.icon, + size = { AxTileDefaults.LargeIconSize }, + modifier = Modifier, + ) + } + + if (isDualTarget) { + Spacer(modifier = Modifier.width(AxTileDefaults.IconDividerSpacing)) + Box( + modifier = Modifier + .width(AxTileDefaults.DividerWidth) + .height(AxTileDefaults.DividerHeight) + .background(dividerColor) + ) + Spacer(modifier = Modifier.width(AxTileDefaults.DividerLabelSpacing)) + } else { + Spacer(modifier = Modifier.width(AxTileDefaults.DividerLabelSpacing)) + } + + LargeTileLabels( + label = label, + secondaryLabel = secondaryLabel, + colors = colors, + accessibilityUiState = accessibilityUiState, + isVisible = isVisible, + modifier = Modifier + .weight(1f) + .bounceScale(TransformOrigin(0f, .5f), textScale), + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index c58093da862ac..196b6627120e5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -148,7 +148,14 @@ fun LargeTileContent( Modifier.borderOnFocus(color = focusBorderColor, iconShape.topEnd) .clip(iconShape) .verticalSquish(squishiness) - .drawBehind { drawRect(animatedBackgroundColor) } + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = animatedBackgroundColor) + } + } .combinedClickable( onClick = toggleClick!!, onLongClick = onLongClick, @@ -411,19 +418,27 @@ object TileBounceMotionTestKeys { } object CommonTileDefaults { - val IconSize = 32.dp - val LargeTileIconSize = 28.dp + val IconSize = 24.dp + val LargeTileIconSize = 24.dp + val SideIconWidth = 32.dp val SideIconHeight = 20.dp val ChevronSize = 14.dp val ToggleTargetSize = 56.dp + val TileHeight = 72.dp + val TileStartPadding = 8.dp val TileEndPadding = 12.dp val TileDualTargetEndPadding = 8.dp + val TileArrangementPadding = 6.dp + val TileColumnSpacing = 20.dp + val TileRowSpacing = 20.dp + + val InactiveCornerRadius = 36.dp val ActiveTileCornerRadius = 24.dp - val InactiveCornerRadius = 50.dp + val TileLabelBlurWidth = 32.dp const val TILE_MARQUEE_ITERATIONS = 1 const val TILE_INITIAL_DELAY_MILLIS = 2000 diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt index 8e8f34d880e97..e469783fabb87 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt @@ -46,7 +46,7 @@ class CustomColorScheme(private val context: Context) { else com.android.internal.R.color.surface_effect_1 } else { - com.android.internal.R.color.surface_effect_2 + com.android.internal.R.color.materialColorSurfaceBright } val tileColor = context.resources.getColor(colorRes, context.theme) return Color(tileColor) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 734377e8d2169..52ad368b4118c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -16,6 +16,8 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid +import android.provider.Settings +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -28,6 +30,7 @@ import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope @@ -83,11 +86,14 @@ constructor( rememberViewModel(traceName = "InfiniteGridLayout.TileGrid", key = context) { textFeedbackContentViewModelFactory.create(context) } + + val useModifiedSpacing = remember { + Settings.System.getInt(context.contentResolver, Settings.System.QS_USE_MODIFIED_TILE_SPACING, 0) == 1 + } val columns = viewModel.columnsWithMediaViewModel.columns val largeTilesSpan = viewModel.columnsWithMediaViewModel.largeSpan val largeTiles by viewModel.iconTilesViewModel.largeTilesState - // Tiles or largeTiles may be updated while this is composed, so listen to any changes val sizedTiles = remember(tiles, largeTiles, largeTilesSpan) { tiles.map { @@ -124,13 +130,24 @@ constructor( val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } + VerticalSpannedGrid( columns = columns, - columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), - rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical), + columnSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileColumnSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_horizontal) + }, + rowSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileRowSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_vertical) + }, spans = spans, keys = { sizedTiles[it].tile.spec }, - modifier = modifier, + modifier = modifier.then( + if (useModifiedSpacing) Modifier.padding(horizontal = 10.dp) else Modifier + ), ) { spanIndex, column, isFirstInColumn, isLastInColumn -> val it = sizedTiles[spanIndex] diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index d30e2a3e22833..9631fea47720b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -68,9 +68,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -205,14 +209,16 @@ fun ContentScope.Tile( tile.state.collect { value = it.toIconProvider() } } - val colors = TileDefaults.getColorForState(uiState, iconOnly) + val useMinimalStyle = rememberAxTileStyle() + + val colors = TileDefaults.getColorForState(uiState, iconOnly, forceMonochrome = useMinimalStyle) val hapticsViewModel: TileHapticsViewModel? = if (rememberTileHaptic()) { rememberViewModel(traceName = "TileHapticsViewModel") { tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile) } } else { - null + null } if (tile.spec.spec == "sound" && !iconOnly) { @@ -220,11 +226,18 @@ fun ContentScope.Tile( return@trace } - val shapeMode = rememberTileShapeMode() - val wantCircle = shapeMode == 4 && iconOnly + val shapeMode = if (useMinimalStyle) 0 else rememberTileShapeMode() + val wantCircle = !useMinimalStyle && shapeMode == 4 && iconOnly val tileShape = - if (wantCircle) CircleShape - else TileDefaults.animateTileShapeAsState(uiState.state, shapeMode).value + if (wantCircle) { + CircleShape + } else if (useMinimalStyle) { + val useMinimalInvert = rememberAxMinimalInvert() + AxTileDefaults.animateTileShapeAsState(uiState.state, useMinimalInvert).value + } else { + TileDefaults.animateTileShapeAsState(uiState.state, shapeMode).value + } + val animatedColor by animateColorAsState(colors.background, label = "QSTileBackgroundColor") val isDualTarget = uiState.handlesSecondaryClick @@ -254,8 +267,8 @@ fun ContentScope.Tile( modifier = modifier .then(surfaceRevealModifier) - .thenIf(!wantCircle) { - modifier.borderOnFocus(color = focusBorderColor, outerShape.topEnd) + .thenIf(!wantCircle) { + Modifier.borderOnFocus(color = focusBorderColor, outerShape.topEnd) } .fillMaxWidth() .height(CommonTileDefaults.TileHeight) @@ -331,6 +344,7 @@ fun ContentScope.Tile( requestToggleTextFeedback(tile.spec) } } + if (wantCircle) { val interaction = remember { MutableInteractionSource() } @@ -340,7 +354,14 @@ fun ContentScope.Tile( .size(CommonTileDefaults.TileHeight) .align(Alignment.Center) .clip(CircleShape) - .background(animatedColor) + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = animatedColor) + } + } .indication(interaction, LocalIndication.current) .tileCombinedClickable( onClick = { click?.invoke() ?: Unit }, @@ -372,6 +393,7 @@ fun ContentScope.Tile( iconOnly = iconOnly, isDualTarget = isDualTarget, modifier = contentRevealModifier, + colors = colors, ) { val iconProvider: Context.() -> Icon = { getTileIcon(icon = icon) } if (iconOnly) { @@ -384,7 +406,6 @@ fun ContentScope.Tile( }, ) } else { - val iconShape by TileDefaults.animateIconShapeAsState(uiState.state, shapeMode) val secondaryClick: (() -> Unit)? = { hapticsViewModel?.setTileInteractionState( @@ -393,22 +414,43 @@ fun ContentScope.Tile( tile.toggleClick() } .takeIf { isDualTarget } - LargeTileContent( - label = uiState.label, - secondaryLabel = uiState.secondaryLabel, - iconProvider = iconProvider, - sideDrawable = uiState.sideDrawable, - colors = colors, - iconShape = iconShape, - toggleClick = secondaryClick, - onLongClick = longClick, - accessibilityUiState = uiState.accessibilityUiState, - squishiness = squishiness, - isVisible = isVisible, - textScale = { contentBounceable.textBounceScale }, - modifier = - Modifier.largeTilePadding(isDualTarget = uiState.handlesLongClick), - ) + if (useMinimalStyle) { + val useMinimalInvert = rememberAxMinimalInvert() + val iconShape by AxTileDefaults.animateIconShapeAsState(uiState.state, useMinimalInvert) + AxLargeTileContent( + label = uiState.label, + secondaryLabel = uiState.secondaryLabel, + iconProvider = iconProvider, + sideDrawable = uiState.sideDrawable, + colors = colors, + iconShape = iconShape, + tileState = uiState.state, + toggleClick = secondaryClick, + onLongClick = longClick, + accessibilityUiState = uiState.accessibilityUiState, + squishiness = squishiness, + isVisible = isVisible, + textScale = { contentBounceable.textBounceScale }, + ) + } else { + val iconShape by TileDefaults.animateIconShapeAsState(uiState.state, shapeMode) + LargeTileContent( + label = uiState.label, + secondaryLabel = uiState.secondaryLabel, + iconProvider = iconProvider, + sideDrawable = uiState.sideDrawable, + colors = colors, + iconShape = iconShape, + toggleClick = secondaryClick, + onLongClick = longClick, + accessibilityUiState = uiState.accessibilityUiState, + squishiness = squishiness, + isVisible = isVisible, + textScale = { contentBounceable.textBounceScale }, + modifier = + Modifier.largeTilePadding(isDualTarget = uiState.handlesLongClick), + ) + } } } } @@ -443,6 +485,7 @@ fun TileContainer( isDualTarget: Boolean, interactionSource: MutableInteractionSource?, modifier: Modifier = Modifier, + colors: TileColors, content: @Composable BoxScope.() -> Unit, ) { Box( @@ -458,7 +501,16 @@ fun TileContainer( isDualTarget = isDualTarget, interactionSource = interactionSource, ) - .tileTestTag(iconOnly), + .tileTestTag(iconOnly) + .thenIf(!isDualTarget || iconOnly || (colors.iconBackgroundGradient != null && colors.background == MaterialTheme.colorScheme.primary)) { + Modifier + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } + } + }, content = content, ) } @@ -476,7 +528,14 @@ fun LargeStaticTile( Box( modifier .clip(TileDefaults.animateTileShapeAsState(state = uiState.state, shapeMode = shapeMode).value) - .background(colors.background) + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = colors.background) + } + } .height(TileHeight) .largeTilePadding() ) { @@ -548,8 +607,49 @@ data class TileColors( val label: Color, val secondaryLabel: Color, val icon: Color, + val iconBackgroundGradient: Brush? = null, ) +@Composable +fun rememberAxTileStyle(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readAxStyle(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_TILE_STYLE_MINIMAL, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var axStyle by remember { mutableStateOf(readAxStyle()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + axStyle = readAxStyle() + } + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_TILE_STYLE_MINIMAL), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return axStyle +} + @Composable fun rememberTileShapeMode(): Int { val context = LocalContext.current @@ -590,6 +690,46 @@ fun rememberTileShapeMode(): Int { return shapeMode } +@Composable +fun rememberAxMinimalInvert(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readMinimalInvert(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_TILE_STYLE_MINIMAL_INVERT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var minimalInvert by remember { mutableStateOf(readMinimalInvert()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + minimalInvert = readMinimalInvert() + } + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_TILE_STYLE_MINIMAL_INVERT), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return minimalInvert +} + @Composable fun rememberTileHaptic(): Boolean { val context = LocalContext.current @@ -630,28 +770,176 @@ fun rememberTileHaptic(): Boolean { return hapticEnabled } +@Composable +fun rememberQsGradient(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readEnabled(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_TILE_GRADIENT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var enabled by remember { mutableStateOf(readEnabled()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + enabled = readEnabled() + } + } + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_TILE_GRADIENT), + false, observer, UserHandle.USER_ALL + ) + onDispose { contentResolver.unregisterContentObserver(observer) } + } + + return enabled +} + +@Composable +fun rememberQsTileBackgroundBrush(): Brush? { + val enabled = rememberQsGradient() + val mode = rememberGradientColorMode() + val (start, end) = rememberGradientCustomColors() + + if (!enabled) return null + + val colors = if (mode == 1) { + listOf(start, end) + } else { + listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.secondary + ) + } + + return Brush.linearGradient( + colors = colors, + start = Offset(0f, 0f), + end = Offset.Infinite + ) +} + +@Composable +internal fun rememberGradientColorMode(): Int { + val contentResolver = LocalContext.current.contentResolver + + fun readMode(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_COLOR_MODE, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var mode by remember { mutableIntStateOf(readMode()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + mode = readMode() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_COLOR_MODE), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return mode +} + +@Composable +internal fun rememberGradientCustomColors(): Pair { + val contentResolver = LocalContext.current.contentResolver + + fun readStart(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_START_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + fun readEnd(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_END_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var startInt by remember { mutableIntStateOf(readStart()) } + var endInt by remember { mutableIntStateOf(readEnd()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + startInt = readStart() + endInt = readEnd() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_START_COLOR), + false, observer, UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_END_COLOR), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + val start = if (startInt != 0) Color(startInt) else MaterialTheme.colorScheme.primary + val end = if (endInt != 0) Color(endInt) else MaterialTheme.colorScheme.secondary + return start to end +} + private object TileDefaults { val ActiveIconCornerRadius = 16.dp /** An active tile uses the active color as background */ @Composable - @ReadOnlyComposable - fun activeTileColors(): TileColors = - TileColors( + fun activeTileColors(): TileColors { + val gradient = rememberQsTileBackgroundBrush() + + return TileColors( background = MaterialTheme.colorScheme.primary, iconBackground = MaterialTheme.colorScheme.primary, label = MaterialTheme.colorScheme.onPrimary, secondaryLabel = MaterialTheme.colorScheme.onPrimary, icon = MaterialTheme.colorScheme.onPrimary, + iconBackgroundGradient = gradient, ) + } /** An active tile with dual target only show the active color on the icon */ @Composable - @ReadOnlyComposable fun activeDualTargetTileColors(): TileColors { val context = LocalContext.current val isSingleToneStyle = DualTargetTileStyleProvider.isSingleToneStyle(context) - + val gradient = rememberQsTileBackgroundBrush() + return if (isSingleToneStyle) { TileColors( background = MaterialTheme.colorScheme.primary, @@ -659,6 +947,7 @@ private object TileDefaults { label = MaterialTheme.colorScheme.onPrimary, secondaryLabel = MaterialTheme.colorScheme.onPrimary, icon = MaterialTheme.colorScheme.onPrimary, + iconBackgroundGradient = gradient, ) } else { TileColors( @@ -667,6 +956,7 @@ private object TileDefaults { label = MaterialTheme.colorScheme.onSurface, secondaryLabel = MaterialTheme.colorScheme.onSurface, icon = MaterialTheme.colorScheme.onPrimary, + iconBackgroundGradient = gradient, ) } } @@ -676,7 +966,7 @@ private object TileDefaults { fun inactiveDualTargetTileColors(): TileColors { val context = LocalContext.current val isSingleToneStyle = DualTargetTileStyleProvider.isSingleToneStyle(context) - + return if (isSingleToneStyle) { TileColors( background = CustomColorScheme.current.qsTileColor, @@ -721,13 +1011,41 @@ private object TileDefaults { ) } + @Composable + fun activeDualTargetMonochromeTileColors(): TileColors { + val gradient = rememberQsTileBackgroundBrush() + return TileColors( + background = MaterialTheme.colorScheme.primary, + iconBackground = Color.Transparent, + label = MaterialTheme.colorScheme.onPrimary, + secondaryLabel = MaterialTheme.colorScheme.onPrimary, + icon = MaterialTheme.colorScheme.onPrimary, + iconBackgroundGradient = gradient, + ) + } + @Composable @ReadOnlyComposable - fun getColorForState(uiState: TileUiState, iconOnly: Boolean): TileColors { + fun inactiveDualTargetMonochromeTileColors(): TileColors = + TileColors( + background = CustomColorScheme.current.qsTileColor, + iconBackground = Color.Transparent, + label = MaterialTheme.colorScheme.onSurface, + secondaryLabel = MaterialTheme.colorScheme.onSurface, + icon = MaterialTheme.colorScheme.onSurface, + ) + + @Composable + fun getColorForState( + uiState: TileUiState, + iconOnly: Boolean, + forceMonochrome: Boolean = false, + ): TileColors { return when (uiState.state) { STATE_ACTIVE -> { if (uiState.handlesSecondaryClick && !iconOnly) { - activeDualTargetTileColors() + if (forceMonochrome) activeDualTargetMonochromeTileColors() + else activeDualTargetTileColors() } else { activeTileColors() } @@ -735,12 +1053,15 @@ private object TileDefaults { STATE_INACTIVE -> { if (uiState.handlesSecondaryClick && !iconOnly) { - inactiveDualTargetTileColors() + if (forceMonochrome) inactiveDualTargetMonochromeTileColors() + else inactiveDualTargetTileColors() } else { inactiveTileColors() } } + STATE_UNAVAILABLE -> unavailableTileColors() + else -> unavailableTileColors() } } @@ -772,25 +1093,23 @@ private object TileDefaults { label: String, shapeMode: Int, ): State { - val animatedCornerRadius by - animateDpAsState( - targetValue = when (shapeMode) { - 1 -> InactiveCornerRadius // Circle-ish - 2 -> activeCornerRadius // Rounded Square - 3 -> 0.dp // Square - 4 -> InactiveCornerRadius // Circle - else -> if (state == STATE_ACTIVE) activeCornerRadius else InactiveCornerRadius - }, - label = label, - ) + val animatedCornerRadius by animateDpAsState( + targetValue = when (shapeMode) { + 1 -> InactiveCornerRadius // Circle-ish + 2 -> activeCornerRadius // Rounded Square + 3 -> 0.dp // Square + 4 -> InactiveCornerRadius // Circle + else -> if (state == STATE_ACTIVE) activeCornerRadius else InactiveCornerRadius + }, + label = label, + ) return remember { - val corner = - object : CornerSize { - override fun toPx(shapeSize: Size, density: Density): Float { - return with(density) { animatedCornerRadius.toPx() } - } + val corner = object : CornerSize { + override fun toPx(shapeSize: Size, density: Density): Float { + return with(density) { animatedCornerRadius.toPx() } } + } mutableStateOf(RoundedCornerShape(corner)) } } @@ -819,25 +1138,23 @@ enum class DualTargetTileStyle { } object DualTargetTileStyleProvider { - + fun getStyle(context: android.content.Context): DualTargetTileStyle { val value = Settings.System.getInt( context.contentResolver, Settings.System.DUAL_TARGET_TILE_STYLE, 0 ) - + return when (value) { 1 -> DualTargetTileStyle.SINGLE else -> DualTargetTileStyle.DUAL } } - - fun isSingleToneStyle(context: android.content.Context): Boolean { - return getStyle(context) == DualTargetTileStyle.SINGLE - } - - fun isDualToneStyle(context: android.content.Context): Boolean { - return getStyle(context) == DualTargetTileStyle.DUAL - } + + fun isSingleToneStyle(context: android.content.Context): Boolean = + getStyle(context) == DualTargetTileStyle.SINGLE + + fun isDualToneStyle(context: android.content.Context): Boolean = + getStyle(context) == DualTargetTileStyle.DUAL } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt index 5111ff7d7b46d..c5eaa6a5898fa 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.ui.compose.toolbar +import android.provider.Settings import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedVisibility @@ -35,6 +36,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -50,6 +52,7 @@ import com.android.systemui.common.ui.compose.load import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel +import com.android.systemui.qs.footer.ui.compose.rememberSystemSettingEnabled import com.android.systemui.qs.panels.ui.compose.toolbar.Toolbar.TransitionKeys.SecurityInfoKey import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackContentViewModel import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackViewModel @@ -92,10 +95,14 @@ fun Toolbar( } } - IconButton( - viewModel.powerButtonViewModel, - Modifier.sysuiResTag("pm_lite").minimumInteractiveComponentSize(), - ) + val showPowerMenu by rememberSystemSettingEnabled(Settings.System.QS_FOOTER_SHOW_POWER_MENU) + + if (showPowerMenu) { + IconButton( + viewModel.powerButtonViewModel, + Modifier.sysuiResTag("pm_lite").minimumInteractiveComponentSize(), + ) + } } } @@ -107,6 +114,9 @@ private fun SharedTransitionScope.StandardToolbarLayout( isFullyVisible: () -> Boolean, modifier: Modifier = Modifier, ) { + val showEdit by rememberSystemSettingEnabled(Settings.System.QS_FOOTER_SHOW_EDIT) + val showSettings by rememberSystemSettingEnabled(Settings.System.QS_FOOTER_SHOW_SETTINGS) + Row(modifier) { // User switcher button IconButton( @@ -119,13 +129,17 @@ private fun SharedTransitionScope.StandardToolbarLayout( // Edit mode button val editModeButtonViewModel = rememberViewModel("Toolbar") { viewModel.editModeButtonViewModelFactory.create() } - EditModeButton(editModeButtonViewModel, isVisible = isFullyVisible()) + if (showEdit) { + EditModeButton(editModeButtonViewModel, isVisible = isFullyVisible()) + } // Settings button - IconButton( - model = viewModel.settingsButtonViewModel, - Modifier.sysuiResTag("settings_button_container").minimumInteractiveComponentSize(), - ) + if (showSettings) { + IconButton( + model = viewModel.settingsButtonViewModel, + Modifier.sysuiResTag("settings_button_container").minimumInteractiveComponentSize(), + ) + } // Security info button SecurityInfo( diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java index 2b127d60b2be4..454c6768f8a83 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java @@ -26,8 +26,11 @@ import androidx.annotation.Nullable; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.animation.DialogCuj; +import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -38,11 +41,14 @@ import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tiles.dialog.FlashlightDialogDelegate; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.res.R; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.FlashlightController; import javax.inject.Inject; +import javax.inject.Provider; /** * Quick settings tile: Control flashlight @@ -51,8 +57,15 @@ public class FlashlightTile extends QSTileImpl implements FlashlightController.FlashlightListener { public static final String TILE_SPEC = "flashlight"; + private static final String FLASHLIGHT_BRIGHTNESS_SETTING = "flashlight_brightness"; + private static final String INTERACTION_JANK_TAG = "flashlight_strength"; + private final FlashlightController mFlashlightController; + private final Handler mHandler; + private final Provider mFlashlightDialogProvider; + private final DialogTransitionAnimator mDialogTransitionAnimator; + @Inject public FlashlightTile( QSHost host, @@ -64,12 +77,17 @@ public FlashlightTile( StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, - FlashlightController flashlightController + FlashlightController flashlightController, + Provider flashlightDialogDelegateProvider, + DialogTransitionAnimator dialogTransitionAnimator ) { super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); + mHandler = mainHandler; mFlashlightController = flashlightController; mFlashlightController.observe(getLifecycle(), this); + mFlashlightDialogProvider = flashlightDialogDelegateProvider; + mDialogTransitionAnimator = dialogTransitionAnimator; } @Override @@ -80,7 +98,7 @@ protected void handleDestroy() { @Override public BooleanState newTileState() { BooleanState state = new BooleanState(); - state.handlesLongClick = false; + state.handlesLongClick = true; return state; } @@ -103,6 +121,40 @@ protected void handleClick(@Nullable Expandable expandable) { if (ActivityManager.isUserAMonkey()) { return; } + + if (mFlashlightController.isStrengthControlSupported()) { + Runnable runnable = new Runnable() { + @Override + public void run() { + SystemUIDialog dialog = mFlashlightDialogProvider.get().createDialog(); + if (expandable != null) { + DialogTransitionAnimator.Controller controller = + expandable.dialogTransitionController( + new DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG)); + if (controller != null) { + mDialogTransitionAnimator.show(dialog, controller); + } else { + dialog.show(); + } + } else { + dialog.show(); + } + } + }; + + mHandler.post(runnable); + } else { + handleSecondaryClick(expandable); + } + } + + @Override + protected void handleSecondaryClick(@Nullable Expandable expandable) { + if (ActivityManager.isUserAMonkey()) { + return; + } boolean newState = !mState.value; refreshState(newState); mFlashlightController.setFlashlight(newState); @@ -123,6 +175,7 @@ protected void handleUpdateState(BooleanState state, Object arg) { state.label = mHost.getContext().getString(R.string.quick_settings_flashlight_label); state.secondaryLabel = ""; state.stateDescription = ""; + state.handlesSecondaryClick = mFlashlightController.isStrengthControlSupported(); if (!mFlashlightController.isAvailable()) { state.secondaryLabel = mContext.getString( R.string.quick_settings_flashlight_camera_in_use); @@ -131,6 +184,15 @@ protected void handleUpdateState(BooleanState state, Object arg) { state.icon = maybeLoadResourceIcon(R.drawable.qs_flashlight_icon_off); return; } + if (mFlashlightController.isStrengthControlSupported()) { + boolean enabled = mFlashlightController.isEnabled(); + float percent = mFlashlightController.getCurrentPercent(); + + if (enabled) { + state.secondaryLabel = Math.round(percent * 100f) + "%"; + state.stateDescription = state.secondaryLabel; + } + } if (arg instanceof Boolean) { boolean value = (Boolean) arg; if (value == state.value) { @@ -166,4 +228,9 @@ public void onFlashlightError() { public void onFlashlightAvailabilityChanged(boolean available) { refreshState(); } + + @Override + public void onFlashlightStrengthChanged(int level) { + refreshState(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt index b9cf3eb8c5e8e..fb39db11c5d7f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt @@ -153,11 +153,12 @@ constructor( } override fun handleClick(expandable: Expandable?) { - lifecycle.coroutineScope.launch { userActionInteractor.handleClick(expandable) } + userActionInteractor.handleSecondaryClick(expandable) } override fun handleSecondaryClick(expandable: Expandable?) { - userActionInteractor.handleSecondaryClick(expandable) + // Disabled + handleClick(expandable) } override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent @@ -200,8 +201,7 @@ constructor( contentDescription = tileState.contentDescription expandedAccessibilityClassName = tileState.expandedAccessibilityClassName - handlesSecondaryClick = - tileState.supportedActions.contains(QSTileState.UserAction.TOGGLE_CLICK) + handlesSecondaryClick = false handlesLongClick = tileState.supportedActions.contains(QSTileState.UserAction.LONG_CLICK) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/VPNTetheringTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/VPNTetheringTile.java new file mode 100644 index 0000000000000..7977129dcd74a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/VPNTetheringTile.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * Copyright (C) 2017-2021 The LineageOS Project + * Copyright (C) 2022 The LibreMobileOS Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles; + +import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN; + +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Intent; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemProperties; +import android.provider.Settings; +import android.provider.Settings.Secure; +import android.service.quicksettings.Tile; +import android.text.TextUtils; +import com.android.systemui.animation.Expandable; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.res.R; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.qs.UserSettingObserver; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; + +import javax.inject.Inject; + +/** Quick settings tile: VPN Tethering **/ +public class VPNTetheringTile extends QSTileImpl { + + public static final String TILE_SPEC = "vpn_tethering"; + + private final Icon mIcon = ResourceIcon.get(R.drawable.ic_qs_vpn_tethering); + private static final Intent TETHER_SETTINGS = new Intent().setComponent(new ComponentName( + "com.android.settings", "com.android.settings.TetherSettings")); + + private final UserSettingObserver mSetting; + + @Inject + public VPNTetheringTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger, + UserTracker userTracker, + SecureSettings secureSettings + ) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + + mSetting = new UserSettingObserver(secureSettings, mHandler, Settings.Secure.TETHERING_ALLOW_VPN_UPSTREAMS, + userTracker.getUserId(), 0) { + @Override + protected void handleValueChanged(int value, boolean observedChange) { + handleRefreshState(value); + } + }; + } + + @Override + protected void handleDestroy() { + super.handleDestroy(); + mSetting.setListening(false); + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleSetListening(boolean listening) { + super.handleSetListening(listening); + mSetting.setListening(listening); + } + + @Override + protected void handleUserSwitch(int newUserId) { + mSetting.setUserId(newUserId); + handleRefreshState(mSetting.getValue()); + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + mSetting.setValue(mState.value ? 0 : 1); + } + + @Override + public Intent getLongClickIntent() { + return new Intent(TETHER_SETTINGS); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + final int value = arg instanceof Integer ? (Integer) arg : mSetting.getValue(); + final boolean enable = value != 0; + state.value = enable; + state.label = mContext.getString(R.string.vpn_tethering_label); + state.icon = mIcon; + if (enable) { + state.contentDescription = mContext.getString( + R.string.vpn_tethering_changed_on); + state.state = Tile.STATE_ACTIVE; + } else { + state.contentDescription = mContext.getString( + R.string.vpn_tethering_changed_off); + state.state = Tile.STATE_INACTIVE; + } + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.vpn_tethering_label); + } + + @Override + public int getMetricsCategory() { + return VIEW_UNKNOWN; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt index 6236dc90b790f..7e944fe239c51 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt @@ -149,11 +149,12 @@ constructor( } override fun handleClick(expandable: Expandable?) { - lifecycle.coroutineScope.launch { userActionInteractor.handleClick(expandable) } + userActionInteractor.handleSecondaryClick(expandable) } override fun handleSecondaryClick(expandable: Expandable?) { - userActionInteractor.handleSecondaryClick(expandable) + // Disabled + handleClick(expandable) } override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent @@ -168,27 +169,24 @@ constructor( (tileState.icon as? Icon.Loaded)?.resId?.let { resId -> maybeLoadResourceIcon(resId) } ?: SignalIcon(SignalDrawable.getState(0, 4, false)) - label = tileState.label - - secondaryLabel = if (this.state == Tile.STATE_ACTIVE) { - if (showDataUsage) { - val dataUsage = getFormattedWifiDataUsage() - if (!TextUtils.isEmpty(dataUsage)) { - dataUsage - } else { - tileState.secondaryLabel - } + + if (this.state == Tile.STATE_ACTIVE && showDataUsage) { + val dataUsage = getFormattedWifiDataUsage() + if (!TextUtils.isEmpty(dataUsage)) { + label = tileState.secondaryLabel + secondaryLabel = dataUsage } else { - tileState.secondaryLabel + label = tileState.label + secondaryLabel = tileState.secondaryLabel } } else { - null + label = tileState.label + secondaryLabel = if (this.state == Tile.STATE_ACTIVE) tileState.secondaryLabel else null } - + contentDescription = tileState.contentDescription expandedAccessibilityClassName = tileState.expandedAccessibilityClassName - handlesSecondaryClick = - tileState.supportedActions.contains(QSTileState.UserAction.TOGGLE_CLICK) + handlesSecondaryClick = false handlesLongClick = tileState.supportedActions.contains(QSTileState.UserAction.LONG_CLICK) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt new file mode 100644 index 0000000000000..c5c02cb3c663b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2025 Neoteric OS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.qs.tiles.dialog + +import android.content.Context +import android.os.Bundle +import android.os.VibrationEffect +import android.os.Vibrator +import android.widget.FrameLayout +import android.view.ContextThemeWrapper +import android.view.Gravity +import androidx.annotation.MainThread +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.FlashlightController +import com.android.systemui.util.concurrency.DelayableExecutor +import com.google.android.material.slider.Slider +import javax.inject.Inject + +class FlashlightDialogDelegate @Inject constructor( + private val context: Context, + private val systemUIDialogFactory: SystemUIDialog.Factory, + @Main private val mainExecutor: DelayableExecutor, + @Background private val backgroundExecutor: DelayableExecutor, + private val flashlightController: FlashlightController +) : SystemUIDialog.Delegate { + + private lateinit var slider: Slider + + private val vibrator = context.getSystemService(Vibrator::class.java) + private val flashlightMoveHaptic: VibrationEffect = + VibrationEffect.get(VibrationEffect.EFFECT_TICK) + + override fun createDialog(): SystemUIDialog = systemUIDialogFactory.create(this) + + override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + dialog.setTitle(R.string.flashlight_strength_title) + + val container = FrameLayout(dialog.context) + + val themedContext = ContextThemeWrapper(dialog.context, + com.google.android.material.R.style.Theme_Material3_DynamicColors_DayNight) + slider = Slider(themedContext).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER + ) + } + + container.addView(slider) + dialog.setView(container) + dialog.setPositiveButton(R.string.quick_settings_done, null, true) + dialog.setNeutralButton( + if (flashlightController.isEnabled()) + R.string.flashlight_strength_turn_off + else + R.string.flashlight_strength_turn_on, + { _, _ -> + val newState = !flashlightController.isEnabled() + flashlightController.setFlashlight(newState) + dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL)?.text = + if (newState) + dialog.context.getString(R.string.flashlight_strength_turn_off) + else + dialog.context.getString(R.string.flashlight_strength_turn_on) + }, + false + ) + } + + override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + val maxLevel = flashlightController.getMaxLevel() + val currentPercent = flashlightController.getCurrentPercent() + + slider.isEnabled = true + slider.valueFrom = 1f + slider.valueTo = 100f + slider.value = (currentPercent * 100f).coerceAtLeast(1f) + slider.setLabelFormatter { value -> value.toInt().toString() } + + var last = -1 + slider.addOnChangeListener { _, value, fromUser -> + if (fromUser) { + val percent = value / 100f + val p = Math.round(percent * 100) + if (p != last) { + vibrator?.vibrate(flashlightMoveHaptic) + last = p + } + + updateFlashlightStrength(percent, maxLevel) + } + } + } + + @MainThread + private fun updateFlashlightStrength(percent: Float, maxLevel: Int) { + val level = (percent * maxLevel).toInt().coerceAtLeast(1) + flashlightController.setFlashlightStrengthLevel(level) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt index 5091623390113..bc2bfd7c5de6a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.tiles.impl.cell.domain.interactor import android.content.Context import android.os.UserHandle +import android.telephony.TelephonyManager import com.android.settingslib.graph.SignalDrawable import com.android.systemui.Flags as AconfigFlags import com.android.systemui.common.shared.model.ContentDescription @@ -126,7 +127,12 @@ constructor( override fun availability(user: UserHandle): Flow = flowOf(isAvailable()) + fun isVoiceCapable(): Boolean { + val telephony = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager + return telephony?.isVoiceCapable == true + } + fun isAvailable(): Boolean { - return AconfigFlags.qsSplitInternetTile() + return isVoiceCapable() && AconfigFlags.qsSplitInternetTile() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt index 2fd2486a47c33..b907efafbaa31 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt @@ -70,6 +70,7 @@ constructor( else FlashlightModel.Unavailable.Temporarily.CameraInUse ) } + override fun onFlashlightStrengthChanged(level: Int) {} } flashlightController.addCallback(callback) awaitClose { flashlightController.removeCallback(callback) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt index 15f1baa7a8c80..7a3ff849d6f72 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.impl.ringer import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.material3.MaterialTheme import androidx.compose.ui.unit.Dp @@ -24,6 +25,7 @@ import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.common.ringer.RingerSliderDimens import com.android.systemui.common.ringer.RingerSliderTheme import com.android.systemui.qs.panels.ui.compose.infinitegrid.CustomColorScheme +import com.android.systemui.qs.panels.ui.compose.infinitegrid.rememberQsTileBackgroundBrush class QSTileRingerTheme( ) : RingerSliderTheme { @@ -46,6 +48,12 @@ class QSTileRingerTheme( @Composable get() = MaterialTheme.colorScheme.onPrimary override val dozeStroke: Dp = 2.dp + + override val activeBgBrush: Brush? + @Composable get() = rememberQsTileBackgroundBrush() + + override val dndBgBrush: Brush? + @Composable get() = rememberQsTileBackgroundBrush() } class QSTileRingerDimens( diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java index 97ef9fc90b411..0e350359d491e 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java @@ -407,7 +407,7 @@ public void setBlurRadius(float blurRadius) { setRenderEffect(RenderEffect.createBlurEffect( blurRadius, blurRadius, - Shader.TileMode.CLAMP)); + Shader.TileMode.MIRROR)); } else { debugLog("Resetting blur RenderEffect to ScrimView " + mScrimName); setRenderEffect(null); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 9dc323228a7bc..e7d4befbbc913 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -62,6 +62,7 @@ import android.graphics.Rect; import android.graphics.Region; import android.graphics.RenderEffect; +import android.graphics.drawable.Animatable; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; @@ -70,6 +71,7 @@ import android.os.Handler; import android.os.PowerManager; import android.os.Trace; +import android.os.UserHandle; import android.provider.Settings; import android.util.IndentingPrintWriter; import android.util.Log; @@ -1071,7 +1073,7 @@ private void handleBouncerShowingChanged(Boolean isBouncerShowing) { mBlurRenderEffect = RenderEffect.createBlurEffect( mBlurConfig.getMaxBlurRadiusPx(), mBlurConfig.getMaxBlurRadiusPx(), - Shader.TileMode.CLAMP); + Shader.TileMode.MIRROR); } debugLog("Applying blur RenderEffect to shade."); Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, "ShadeBlurRenderEffect", "active", @@ -1270,6 +1272,16 @@ private ClockSize computeDesiredClockSizeForSplitShade() { return ClockSize.LARGE; } + private boolean shouldForceSmallClock() { + return !isOnAod() + // True on small landscape screens + && mResources.getBoolean(R.bool.force_small_clock_on_lockscreen) || + (Settings.Secure.getIntForUser( + mContentResolver, "clock_style", 0, UserHandle.USER_CURRENT) != 0 || + Settings.System.getIntForUser( + mContentResolver, "lockscreen_widgets_enabled", 0, UserHandle.USER_CURRENT) != 0); + } + private void updateKeyguardStatusViewAlignment() { boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered(); mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered)); @@ -4535,15 +4547,51 @@ private void updateHeaderImage() { mShadeHeaderExpansion = shadeHeaderExpansion; mQsHeaderImageView.setImageAlpha( (int) (mShadeHeaderExpansion * (255 - mHeaderImageShadow))); + startHeaderAnimIfPossible(); } } else { mQsHeaderLayout.setVisibility(View.GONE); + stopHeaderAnimIfRunning(); + } + } + + private void startHeaderAnimIfPossible() { + Drawable drawable = mQsHeaderImageView.getDrawable(); + if (drawable == null) return; + + // Only animate when we are actually showing it + if (mQsHeaderLayout.getVisibility() != View.VISIBLE || mQsHeaderImageView.getVisibility() != View.VISIBLE) { + return; + } + + drawable.setVisible(true, true); + if (drawable instanceof Animatable anim && !anim.isRunning()) { + anim.start(); + } + } + + private void stopHeaderAnimIfRunning() { + Drawable drawable = mQsHeaderImageView.getDrawable(); + if (drawable instanceof Animatable anim && anim.isRunning()) { + anim.stop(); + } + if (drawable != null) { + drawable.setVisible(false, false); } } private void setNotificationPanelHeaderBackground(Drawable dw, boolean force) { - if (mQsHeaderImageView.getDrawable() != null && !force) { - Drawable[] layers = new Drawable[]{mQsHeaderImageView.getDrawable(), dw}; + // Stop previous anim, if any, before swapping + stopHeaderAnimIfRunning(); + + if (dw instanceof Animatable) { + mQsHeaderImageView.setImageDrawable(dw); + return; + } + + Drawable current = mQsHeaderImageView.getDrawable(); + if (current != null && !force && !(current instanceof Animatable)) { + Drawable[] layers = new Drawable[]{current, dw}; TransitionDrawable transitionDrawable = new TransitionDrawable(layers); transitionDrawable.setCrossFadeEnabled(true); mQsHeaderImageView.setImageDrawable(transitionDrawable); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt index 2bde29cb21d59..fe16970fe9e14 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt @@ -60,7 +60,7 @@ constructor( ) : Dumpable { val minBlurRadius = resources.getDimensionPixelSize(R.dimen.min_window_blur_radius).toFloat() val maxBlurRadius: Float - get() = secureSettings.getFloatForUser("system_blur_radius", 70f, UserHandle.USER_CURRENT) + get() = secureSettings.getFloatForUser("system_blur_radius", 34f, UserHandle.USER_CURRENT) val maxBlurRadiusFlow: Flow = secureSettings .observerFlow("system_blur_radius") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 63420902eb658..048822a52c196 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -62,6 +62,7 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.hardware.biometrics.BiometricSourceType; import android.os.BatteryManager; import android.os.Handler; @@ -71,8 +72,10 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.format.Formatter; +import android.text.style.ImageSpan; import android.util.Pair; import android.view.View; import android.view.ViewGroup; @@ -1181,7 +1184,7 @@ protected final void updateDeviceEntryIndication(boolean animate) { boolean useMisalignmentColor = false; mLockScreenIndicationView.setVisibility(View.GONE); mTopIndicationView.setVisibility(VISIBLE); - CharSequence newIndication; + CharSequence newIndication = ""; if (!TextUtils.isEmpty(mBiometricMessage)) { newIndication = mBiometricMessage; // note: doesn't show mBiometricMessageFollowUp } else if (!TextUtils.isEmpty(mTransientIndication)) { @@ -1200,25 +1203,93 @@ protected final void updateDeviceEntryIndication(boolean animate) { } else if (mPowerPluggedIn || mEnableBatteryDefender) { newIndication = computePowerIndication(); } else { - newIndication = NumberFormat.getPercentInstance() - .format(mBatteryLevel / 100f); + String batteryLevel = NumberFormat.getPercentInstance().format(mBatteryLevel / 100f); + String batteryTemp = com.android.internal.util.lunaris.ThemeUtils.batteryTemperature(mContext, false); + String cpuTemp = com.android.internal.util.lunaris.ThemeUtils.getCPUTemp(mContext); + + Drawable batteryIcon = mContext.getDrawable(R.drawable.ic_ambient_battery); + Drawable cpuIcon = mContext.getDrawable(R.drawable.ic_ambient_cpu); + Drawable temperatureIcon = mContext.getDrawable(R.drawable.ic_ambient_temperature); + + if (batteryIcon != null) { + batteryIcon.setBounds(0, 0, batteryIcon.getIntrinsicWidth(), batteryIcon.getIntrinsicHeight()); + } + + if (cpuIcon != null) { + cpuIcon.setBounds(0, 0, cpuIcon.getIntrinsicWidth(), cpuIcon.getIntrinsicHeight()); + } + + if (temperatureIcon != null) { + temperatureIcon.setBounds(0, 0, temperatureIcon.getIntrinsicWidth(), temperatureIcon.getIntrinsicHeight()); + } + + UserManager userManager = mContext.getSystemService(UserManager.class); + String userName = null; + if (userManager != null) { + UserHandle currentUser = android.os.Process.myUserHandle(); + UserInfo userInfo = userManager.getUserInfo(currentUser.getIdentifier()); + if (userInfo != null) { + userName = userInfo.name; + } + } + if (TextUtils.isEmpty(userName)) { + userName = mContext.getString(R.string.default_user_name); + } + + SpannableStringBuilder indicationBuilder = new SpannableStringBuilder(); + + switch (getAmbientShowSettings()) { + case 2: // Battery level & battery temperature + appendIcons(indicationBuilder, batteryLevel, batteryIcon, ambientShowSettingsIcon()); + appendWithSeparator(indicationBuilder, " | "); + appendIcons(indicationBuilder, batteryTemp, temperatureIcon, ambientShowSettingsIcon()); + newIndication = indicationBuilder; + break; + + case 3: // Battery level, battery temperature & CPU temperature + appendIcons(indicationBuilder, batteryLevel, batteryIcon, ambientShowSettingsIcon()); + appendWithSeparator(indicationBuilder, " | "); + appendIcons(indicationBuilder, batteryTemp, temperatureIcon, ambientShowSettingsIcon()); + appendWithSeparator(indicationBuilder, " | "); + appendIcons(indicationBuilder, cpuTemp, cpuIcon, ambientShowSettingsIcon()); + newIndication = indicationBuilder; + break; + + case 4: // Hidden → show current user name + newIndication = userName; + break; + + case 0: // Hidden (safe) + SpannableStringBuilder hiddenIndication = new SpannableStringBuilder(""); + newIndication = hiddenIndication; + break; + + case 1: // Show battery level + default: + appendIcons(indicationBuilder, batteryLevel, batteryIcon, ambientShowSettingsIcon()); + newIndication = indicationBuilder; + break; + } } if (!TextUtils.equals(mTopIndicationView.getText(), newIndication)) { mWakeLock.setAcquired(true); - final KeyguardIndication.Builder builder = new KeyguardIndication.Builder() - .setMessage(newIndication) - .setTextColor(ColorStateList.valueOf( - useMisalignmentColor - ? mContext.getColor(R.color.misalignment_text_color) - : Color.WHITE)); - if (mBiometricMessage != null && newIndication == mBiometricMessage) { - builder.setForceAccessibilityLiveRegionAssertive(); + KeyguardIndication indication = null; + if (!TextUtils.isEmpty(newIndication)) { + final KeyguardIndication.Builder builder = new KeyguardIndication.Builder() + .setMessage(newIndication) + .setTextColor(ColorStateList.valueOf( + useMisalignmentColor + ? mContext.getColor(R.color.misalignment_text_color) + : Color.WHITE)); + if (mBiometricMessage != null && newIndication == mBiometricMessage) { + builder.setForceAccessibilityLiveRegionAssertive(); + } + indication = builder.build(); } - mTopIndicationView.switchIndication(newIndication, - builder.build(), - animate, () -> mWakeLock.setAcquired(false)); + mTopIndicationView.switchIndication(newIndication, indication, animate, + () -> mWakeLock.setAcquired(false)); } return; } @@ -1230,6 +1301,28 @@ protected final void updateDeviceEntryIndication(boolean animate) { updateLockScreenIndications(animate, getCurrentUser()); } + private int getAmbientShowSettings() { + return Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.AMBIENT_SHOW_SETTINGS, 0, UserHandle.USER_CURRENT); + } + + private boolean ambientShowSettingsIcon() { + return Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.AMBIENT_SHOW_SETTINGS_ICONS, 0, UserHandle.USER_CURRENT) != 0; + } + + private void appendIcons(SpannableStringBuilder builder, String text, Drawable icon, boolean showIcon) { + if (showIcon && icon != null) { + builder.append(" "); + builder.setSpan(new ImageSpan(icon), builder.length() - 1, builder.length(), builder.SPAN_INCLUSIVE_EXCLUSIVE); + } + builder.append(text); + } + + private void appendWithSeparator(SpannableStringBuilder builder, String separator) { + builder.append(separator); + } + /** * Assumption: device is charging */ @@ -1257,7 +1350,9 @@ protected String computePowerChargingStringIndication() { return mContext.getResources().getString(R.string.keyguard_plugged_in, percentage); } - final boolean hasChargingTime = mChargingTimeRemaining > 0; + final boolean chargingTimeEnabled = Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.LOCKSCREEN_CHARGING_TIME, 1, UserHandle.USER_CURRENT) == 1; + final boolean hasChargingTime = mChargingTimeRemaining > 0 && chargingTimeEnabled; int chargingId; if (mPowerPluggedInWired) { switch (mChargingSpeed) { @@ -1498,7 +1593,7 @@ public void onRefreshBatteryInfo(BatteryStatus status) { mBatteryLevel = status.level; mBatteryPresent = status.present; mTemperature = status.temperature; - mBatteryDefender = isBatteryDefender(status); + mBatteryDefender = isBatteryDefender(status) && mBatteryLevel >= 80; mBatteryDead = status.isDead(); // when the battery is overheated, device doesn't charge so only guard on pluggedIn: mEnableBatteryDefender = mBatteryDefender && status.isPluggedIn(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.kt new file mode 100644 index 0000000000000..7fb9f00ab954f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.kt @@ -0,0 +1,959 @@ +/* + * SPDX-FileCopyrightText: VoltageOS + * SPDX-FileCopyrightText: crDroid Android Project + * SPDX-FileCopyrightText: Lunaris AOSP + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.statusbar + +import android.app.Notification +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.database.ContentObserver +import android.graphics.Bitmap +import android.media.MediaMetadata +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.os.VibrationEffect +import android.provider.Settings +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import android.util.Log +import androidx.annotation.VisibleForTesting +import android.graphics.drawable.Drawable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.graphics.drawable.toBitmap +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager +import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.util.MediaSessionManagerHelper +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +class OnGoingActionProgressController( + private val context: Context, + private val notificationListener: NotificationListener, + private val keyguardStateController: KeyguardStateController, + private val headsUpManager: HeadsUpManager, + private val vibrator: VibratorHelper, + @VisibleForTesting + private val bgDispatcher: CoroutineDispatcher = Dispatchers.Default, +) : NotificationListener.NotificationHandler, + KeyguardStateController.Callback, + OnHeadsUpChangedListener { + + private val contentResolver: ContentResolver = context.contentResolver + private val mainScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + + private val mediaSessionHelper = MediaSessionManagerHelper.getInstance(context) + + private val iconCache = HashMap() + private val inFlightIconLoads = ConcurrentHashMap() + + private var showMediaProgress = true + private var isTrackingProgress = false + private var isForceHidden = false + private var headsUpPinned = false + private var isEnabled = false + private var isCompactModeEnabled = false + + private var currentProgress = 0 + private var currentProgressMax = 0 + private var currentIcon: Drawable? = null + + private var currentTrackTitle: String? = null + private var currentArtistName: String? = null + private var currentAppLabel: String? = null + private var currentAlbumArt: Bitmap? = null + + private val trackChangeCounter = AtomicLong(0L) + private var currentTrackChangeId: Long = 0L + + private var lastObservedTitle: String? = null + + private var lastActiveQueueItemId: Long = Long.MIN_VALUE + private var lastPlaybackPosition: Long = 0L + private var lastPlaybackState: Int = -1 + + private var isMenuVisible = false + private var isSystemChipVisible = false + + private var trackedNotificationKey: String? = null + private var trackedPackageName: String? = null + + private var needsFullUiUpdate = true + private var isViewAttached = false + private var isExpanded = false + + private var pauseStale = false + private var pausedStaleJob: Job? = null + + private var lastUpdateTime = 0L + private var uiUpdateJob: Job? = null + + private var mediaProgressJob: Job? = null + private var finishedProgressTimeoutJob: Job? = null + private var compactCollapseJob: Job? = null + private var menuCollapseJob: Job? = null + private var albumArtRetryJob: Job? = null + + private val _state = MutableStateFlow(ProgressState()) + val state: StateFlow = _state.asStateFlow() + + private val settingsObserver = + object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + if (uri == null) return + if (uri == Settings.System.getUriFor(ONGOING_ACTION_CHIP_ENABLED) || + uri == Settings.System.getUriFor(ONGOING_MEDIA_PROGRESS) || + uri == Settings.System.getUriFor(ONGOING_COMPACT_MODE_ENABLED)) { + updateSettings() + } + } + + fun register() { + contentResolver.registerContentObserver( + Settings.System.getUriFor(ONGOING_ACTION_CHIP_ENABLED), + false, + this, + UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(ONGOING_MEDIA_PROGRESS), + false, + this, + UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(ONGOING_COMPACT_MODE_ENABLED), + false, + this, + UserHandle.USER_ALL + ) + updateSettings() + } + + fun unregister() { + contentResolver.unregisterContentObserver(this) + } + } + + private val mediaMetadataListener = object : MediaSessionManagerHelper.MediaMetadataListener { + override fun onMediaMetadataChanged() { + needsFullUiUpdate = true + pauseStale = false + + val metadata = mediaSessionHelper.mediaMetadata.value + + val newTitle = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) + val isTitleChange = newTitle != lastObservedTitle + if (isTitleChange) { + lastObservedTitle = newTitle + onTrackChanged() + } + + currentTrackTitle = newTitle?.takeIf { it.isNotBlank() } + currentArtistName = (metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) + ?: metadata?.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST)) + ?.takeIf { it.isNotBlank() } + + val freshArt = + metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + ?: metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) + ?: metadata?.getBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON) + + if (freshArt != null) { + currentAlbumArt = freshArt + albumArtRetryJob?.cancel() + } else if (isTitleChange) { + scheduleAlbumArtRetry(currentTrackChangeId) + } + + val appIcon = mediaSessionHelper.getMediaAppIcon() + if (appIcon != null) currentIcon = appIcon + + val pkg = mediaSessionHelper.getMediaControllerPlaybackState() + ?.extras?.getString("package") ?: trackedPackageName + if (!pkg.isNullOrEmpty()) { + currentAppLabel = try { + val pm = context.packageManager + pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString() + } catch (_: Exception) { pkg.substringAfterLast('.') } + } + + requestUiUpdate() + } + + override fun onPlaybackStateChanged() { + needsFullUiUpdate = true + pauseStale = false + pausedStaleJob?.cancel() + + val ps = mediaSessionHelper.playbackState.value + val currentQueueItemId = ps?.activeQueueItemId ?: Long.MIN_VALUE + val currentPosition = ps?.position ?: 0L + val currentState = ps?.state ?: -1 + + val queueItemChanged = currentQueueItemId != Long.MIN_VALUE && + lastActiveQueueItemId != Long.MIN_VALUE && + currentQueueItemId != lastActiveQueueItemId + + val positionReset = lastPlaybackState == android.media.session.PlaybackState.STATE_PLAYING && + currentState == android.media.session.PlaybackState.STATE_PLAYING && + lastPlaybackPosition > POSITION_RESET_THRESHOLD_MS && + currentPosition < POSITION_RESET_THRESHOLD_MS + + lastActiveQueueItemId = currentQueueItemId + lastPlaybackPosition = currentPosition + lastPlaybackState = currentState + + if (queueItemChanged || positionReset) { + currentAlbumArt = null + onTrackChanged() + scheduleAlbumArtRetry(currentTrackChangeId) + requestUiUpdate() + } + + if (showMediaProgress && + mediaSessionHelper.isMediaSessionActive() && + !mediaSessionHelper.isMediaPlaying()) { + pausedStaleJob = mainScope.launch { + delay(PAUSED_STALE_GRACE_MS) + pauseStale = true + requestUiUpdate() + } + } + + requestUiUpdate() + } + } + + + private fun onTrackChanged() { + currentTrackChangeId = trackChangeCounter.incrementAndGet() + needsFullUiUpdate = true + currentAlbumArt = null + } + + private fun scheduleAlbumArtRetry(capturedTrackId: Long) { + albumArtRetryJob?.cancel() + albumArtRetryJob = mainScope.launch { + repeat(ALBUM_ART_RETRY_COUNT) { + delay(ALBUM_ART_RETRY_INTERVAL_MS) + if (currentTrackChangeId != capturedTrackId) return@launch + + val metadata = mediaSessionHelper.mediaMetadata.value + val art = + metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + ?: metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) + ?: metadata?.getBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON) + + if (art != null) { + currentAlbumArt = art + requestUiUpdate() + return@launch + } + } + } + } + + init { + requireNotNull(notificationListener) { "notificationListener cannot be null" } + + keyguardStateController.addCallback(this) + headsUpManager.addListener(this) + notificationListener.addNotificationHandler(this) + + settingsObserver.register() + mediaSessionHelper.addMediaMetadataListener(mediaMetadataListener) + + isViewAttached = true + updateSettings() + } + + private fun updateTrackMetadata() { + val metadata = mediaSessionHelper.getCurrentMediaMetadata() + currentTrackTitle = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)?.takeIf { it.isNotBlank() } + currentArtistName = (metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) + ?: metadata?.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST))?.takeIf { it.isNotBlank() } + + currentAlbumArt = mediaSessionHelper.getMediaBitmap() + + val appIcon = mediaSessionHelper.getMediaAppIcon() + if (appIcon != null) currentIcon = appIcon + val pkg = mediaSessionHelper.getMediaControllerPlaybackState()?.extras?.getString("package") + ?: trackedPackageName + if (!pkg.isNullOrEmpty()) { + currentAppLabel = try { + val pm = context.packageManager + pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString() + } catch (_: Exception) { pkg.substringAfterLast('.') } + } + } + + private fun publish(state: ProgressState) { + _state.value = state + } + + fun expandCompactView() { + val wasExpanded = isExpanded + isExpanded = true + compactCollapseJob?.cancel() + compactCollapseJob = mainScope.launch { + delay(COMPACT_COLLAPSE_TIMEOUT_MS) + if (isCompactModeEnabled && isExpanded) { + isExpanded = false + requestUiUpdate() + } + } + if (!wasExpanded) requestUiUpdate() + } + + private fun requestUiUpdate() { + val now = System.currentTimeMillis() + uiUpdateJob?.cancel() + uiUpdateJob = mainScope.launch { + val elapsed = now - lastUpdateTime + if (elapsed <= DEBOUNCE_DELAY_MS) delay(DEBOUNCE_DELAY_MS) + lastUpdateTime = System.currentTimeMillis() + updateViews() + } + } + + private fun isMediaSessionActiveForChip(): Boolean { + if (!showMediaProgress) return false + if (!mediaSessionHelper.isMediaSessionActive()) return false + if (mediaSessionHelper.isMediaPlaying()) return true + if (isMenuVisible) return true + return !pauseStale + } + + private fun updateProgressState() { + var isVisible = !isForceHidden && !headsUpPinned && !isSystemChipVisible + val hasMediaSession = isMediaSessionActiveForChip() + val hasNotificationProgress = isEnabled && isTrackingProgress + val isCompact = isCompactModeEnabled && !isExpanded + + isVisible = isVisible && (hasMediaSession || hasNotificationProgress) + + if (!isVisible) { + publish( + ProgressState( + isVisible = false, + progress = 0, + maxProgress = 0, + iconBitmap = null, + albumArtBitmap = null, + packageName = null, + isCompactMode = isCompact, + showMediaControls = false, + isMediaPlaying = false, + trackTitle = null, + artistName = null, + appLabel = null, + trackChangeId = currentTrackChangeId, + ) + ) + return + } + + val density = context.resources.displayMetrics.density + + val iconSizePx = if (isCompact) (14f * density).toInt() * 2 + else (16f * density).toInt() * 2 + + val currentIconBitmap = try { + currentIcon?.let { drawable -> + drawable.toBitmap( + width = iconSizePx, + height = iconSizePx, + config = Bitmap.Config.ARGB_8888 + ).asImageBitmap() + } + } catch (e: Exception) { Log.e(TAG, "Failed to convert icon to bitmap", e); null } + + val albumArtSnapshot: Bitmap? = if (!isCompact && hasMediaSession) currentAlbumArt else null + val albumArtBitmap: ImageBitmap? = albumArtSnapshot?.let { + try { + val size = (56f * density).toInt() + Bitmap.createScaledBitmap(it, size, size, true).asImageBitmap() + } catch (e: Exception) { null } + } + + val isMediaPlaying = showMediaProgress && mediaSessionHelper.isMediaPlaying() + + publish( + ProgressState( + isVisible = true, + progress = currentProgress, + maxProgress = currentProgressMax, + iconBitmap = currentIconBitmap, + albumArtBitmap = albumArtBitmap, + packageName = trackedPackageName, + isCompactMode = isCompact, + showMediaControls = isMenuVisible, + isMediaPlaying = isMediaPlaying, + trackTitle = if (!isCompact && hasMediaSession) currentTrackTitle else null, + artistName = if (!isCompact && hasMediaSession) currentArtistName else null, + appLabel = if (!isCompact && hasMediaSession) currentAppLabel else null, + trackChangeId = currentTrackChangeId, + ) + ) + } + + private fun updateViews() { + if (!isViewAttached) { + updateProgressState() + return + } + + if (isForceHidden || headsUpPinned) { + updateProgressState() + return + } + + val hasMediaSession = isMediaSessionActiveForChip() + + if (isCompactModeEnabled && !isExpanded) { + if (!isEnabled && !hasMediaSession) { + stopMediaLoop() + updateProgressState() + return + } + if (hasMediaSession) { + updateMediaProgressCompact() + } else { + updateNotificationProgressCompact() + } + } else { + val isMediaPlaying = showMediaProgress && mediaSessionHelper.isMediaPlaying() + if (isTrackingProgress && !isMediaPlaying && !hasMediaSession) { + stopMediaLoop() + updateNotificationProgress() + } else if (hasMediaSession) { + if (needsFullUiUpdate) { + updateMediaProgressFull() + needsFullUiUpdate = false + } else { + updateMediaProgressOnly() + } + + if (isMediaPlaying) { + ensureMediaLoopRunning() + } else { + stopMediaLoop() + } + } else { + stopMediaLoop() + updateNotificationProgress() + } + } + + updateProgressState() + } + + private fun ensureMediaLoopRunning() { + if (mediaProgressJob?.isActive == true) return + mediaProgressJob = mainScope.launch { + while (isActive && showMediaProgress && mediaSessionHelper.isMediaPlaying()) { + updateMediaProgressOnly() + delay(MEDIA_UPDATE_INTERVAL_MS) + } + } + } + + private fun stopMediaLoop() { + mediaProgressJob?.cancel() + mediaProgressJob = null + } + + private fun updateMediaProgressOnly() { + val totalDuration = mediaSessionHelper.getTotalDuration() + val playbackState = mediaSessionHelper.getMediaControllerPlaybackState() + val pos = playbackState?.position ?: 0L + currentProgress = pos.toInt() + currentProgressMax = totalDuration.toInt().takeIf { it > 0 } ?: 100 + updateProgressState() + } + + private fun updateMediaProgressFull() { + if (mediaSessionHelper.isMediaPlaying()) ensureMediaLoopRunning() else stopMediaLoop() + updateTrackMetadata() + if (currentIcon == null) setDefaultMediaIcon() + updateMediaProgressOnly() + } + + private fun updateMediaProgressCompact() { + if (mediaSessionHelper.isMediaPlaying()) ensureMediaLoopRunning() else stopMediaLoop() + + val totalDuration = mediaSessionHelper.getTotalDuration() + val playbackState = mediaSessionHelper.getMediaControllerPlaybackState() + val pos = playbackState?.position ?: 0L + currentProgress = pos.toInt() + currentProgressMax = totalDuration.toInt().takeIf { it > 0 } ?: 100 + + val mediaAppIcon = mediaSessionHelper.getMediaAppIcon() + if (mediaAppIcon != null) { + currentIcon = mediaAppIcon + return + } + + val pkg = playbackState?.extras?.getString("package") + if (pkg.isNullOrEmpty()) { + setDefaultMediaIconCompact() + return + } + + loadIcon(pkg) { drawable -> + currentIcon = if (drawable != null) { + drawable + } else { + setDefaultMediaIconCompact() + null + } + updateProgressState() + } + } + + private fun setDefaultMediaIcon() { + currentIcon = context.resources.getDrawable(R.drawable.ic_default_music_icon, context.theme) + } + + private fun setDefaultMediaIconCompact() = setDefaultMediaIcon() + + private fun updateNotificationProgress() { + if (!isEnabled || !isTrackingProgress) { + stopMediaLoop() + return + } + + if (currentProgressMax <= 0) { + Log.w(TAG, "Invalid max progress $currentProgressMax, using 100") + currentProgressMax = 100 + } + + val pkg = trackedPackageName ?: return + loadIcon(pkg) { drawable -> + currentIcon = drawable + updateProgressState() + } + } + + private fun updateNotificationProgressCompact() { + updateNotificationProgress() + } + + private fun fetchPackageIcon(packageName: String): Drawable { + val pm = context.packageManager + return try { + pm.getApplicationIcon(packageName) + } catch (t: Throwable) { + Log.w(TAG, "Failed to load icon for $packageName", t) + pm.defaultActivityIcon + } + } + + private fun loadIcon(packageName: String, onLoaded: (Drawable?) -> Unit) { + iconCache[packageName]?.let { + onLoaded(it) + return + } + if (inFlightIconLoads.containsKey(packageName)) return + + val job = mainScope.launch { + val drawable = withContext(bgDispatcher) { + fetchPackageIcon(packageName) + } + val sizePx = (24f * context.resources.displayMetrics.density).toInt() + drawable.setBounds(0, 0, sizePx, sizePx) + + iconCache[packageName] = drawable + onLoaded(drawable) + } + + inFlightIconLoads[packageName] = job + job.invokeOnCompletion { inFlightIconLoads.remove(packageName) } + } + + private fun extractProgress(notification: Notification) { + val extras = notification.extras + currentProgressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 100) + currentProgress = extras.getInt(Notification.EXTRA_PROGRESS, 0) + } + + private fun cancelFinishedProgressTimeout() { + finishedProgressTimeoutJob?.cancel() + finishedProgressTimeoutJob = null + } + + private fun scheduleFinishedProgressTimeoutIfNeeded() { + if (!isTrackingProgress) { + cancelFinishedProgressTimeout() + return + } + val keyAtSchedule = trackedNotificationKey ?: run { + cancelFinishedProgressTimeout() + return + } + + val finished = currentProgressMax > 0 && currentProgress >= currentProgressMax + if (!finished) { + cancelFinishedProgressTimeout() + return + } + + cancelFinishedProgressTimeout() + finishedProgressTimeoutJob = mainScope.launch { + delay(PROGRESS_TIMEOUT_MS) + + if (!isTrackingProgress) return@launch + if (trackedNotificationKey != keyAtSchedule) return@launch + + val stillFinished = currentProgressMax > 0 && currentProgress >= currentProgressMax + if (!stillFinished) return@launch + + val sbn = findNotificationByKey(keyAtSchedule) + if (sbn == null || !hasProgress(sbn.notification)) { + clearProgressTracking() + return@launch + } + + clearProgressTracking() + } + } + + private fun trackProgress(sbn: StatusBarNotification) { + isTrackingProgress = true + trackedNotificationKey = sbn.key + trackedPackageName = sbn.packageName + extractProgress(sbn.notification) + requestUiUpdate() + scheduleFinishedProgressTimeoutIfNeeded() + } + + private fun clearProgressTracking() { + isTrackingProgress = false + trackedNotificationKey = null + trackedPackageName = null + currentProgress = 0 + currentProgressMax = 0 + cancelFinishedProgressTimeout() + requestUiUpdate() + } + + private fun updateProgressIfNeeded(sbn: StatusBarNotification) { + if (!isTrackingProgress) return + if (sbn.key != trackedNotificationKey) return + if (!hasProgress(sbn.notification)) { + clearProgressTracking() + return + } + extractProgress(sbn.notification) + requestUiUpdate() + scheduleFinishedProgressTimeoutIfNeeded() + } + + private fun findNotificationByKey(key: String): StatusBarNotification? { + val actives = notificationListener.activeNotifications ?: return null + for (n in actives) { + if (n.key == key) return n + } + return null + } + + private fun hasProgress(notification: Notification): Boolean { + val extras = notification.extras ?: return false + val indeterminate = extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE, false) + val maxValid = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0 + return extras.containsKey(Notification.EXTRA_PROGRESS) && + extras.containsKey(Notification.EXTRA_PROGRESS_MAX) && + !indeterminate && maxValid + } + + fun onInteraction() { + if (isCompactModeEnabled && !isExpanded) { + vibrator.vibrate(HAPTIC_EXPAND); expandCompactView(); return + } + vibrator.vibrate(HAPTIC_POPUP) + if (isMediaSessionActiveForChip()) { + isMenuVisible = !isMenuVisible + if (isMenuVisible) collapseMediaControlsWithDelay() + } else openTrackedApp() + updateProgressState() + } + + fun onLongPress() { + vibrator.vibrate(HAPTIC_LONG) + if (isMediaSessionActiveForChip()) openMediaApp() else openTrackedApp() + } + + fun onDoubleTap() { + if (isMediaSessionActiveForChip()) { + vibrator.vibrate(HAPTIC_PLAYPAUSE); toggleMediaPlaybackState() + } + } + + fun onSwipe(isNext: Boolean) { + if (isNext) skipToNextTrack() else skipToPreviousTrack() + } + + fun onMediaAction(action: Int) { + vibrator.vibrate(HAPTIC_CLICK) + + when (action) { + 0 -> skipToPreviousTrack() + 1 -> toggleMediaPlaybackState() + 2 -> skipToNextTrack() + } + collapseMediaControlsWithDelay() + } + + fun onSeek(fraction: Float) { + val duration = mediaSessionHelper.getTotalDuration() + if (duration <= 0) return + mediaSessionHelper.seekTo((fraction * duration).toLong().coerceIn(0L, duration)) + } + + fun collapseMediaControlsWithDelay() { + if (!isMenuVisible) return + menuCollapseJob?.cancel() + menuCollapseJob = mainScope.launch { + delay(MENU_COLLAPSE_TIMEOUT_MS) + onMediaMenuDismiss() + } + } + + fun onMediaMenuDismiss() { + isMenuVisible = false + updateProgressState() + } + + fun setSystemChipVisible(visible: Boolean) { + if (isSystemChipVisible == visible) return + isSystemChipVisible = visible + updateProgressState() + requestUiUpdate() + } + + private fun openTrackedApp() { + val pkg = trackedPackageName + if (pkg.isNullOrEmpty()) { + Log.w(TAG, "No tracked package available") + return + } + val launchIntent = context.packageManager.getLaunchIntentForPackage(pkg) + if (launchIntent == null) { + Log.w(TAG, "No launch intent for package: $pkg") + return + } + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(launchIntent) + } + + private fun toggleMediaPlaybackState() { + mediaSessionHelper.toggleMediaPlaybackState() + } + + private fun skipToNextTrack() { + mediaSessionHelper.nextSong() + } + + private fun skipToPreviousTrack() { + mediaSessionHelper.prevSong() + } + + private fun openMediaApp() { + mediaSessionHelper.launchMediaApp() + } + + override fun onNotificationPosted( + sbn: StatusBarNotification?, + rankingMap: NotificationListenerService.RankingMap? + ) { if (sbn == null) return; mainScope.launch { handleNotificationPosted(sbn) } } + + override fun onNotificationRemoved( + sbn: StatusBarNotification?, + rankingMap: NotificationListenerService.RankingMap? + ) { if (sbn == null) return; mainScope.launch { handleNotificationRemoved(sbn) } } + + override fun onNotificationRemoved( + sbn: StatusBarNotification?, + rankingMap: NotificationListenerService.RankingMap?, + reason: Int + ) { if (sbn == null) return; mainScope.launch { handleNotificationRemoved(sbn) } } + + override fun onNotificationRankingUpdate(rankingMap: NotificationListenerService.RankingMap?) = Unit + override fun onNotificationsInitialized() = Unit + + private fun handleNotificationPosted(sbn: StatusBarNotification) { + if (!isEnabled) return + val notification = sbn.notification ?: return + val hasValidProgress = hasProgress(notification) + val currentKey = trackedNotificationKey + if (!hasValidProgress) { + if (currentKey != null && currentKey == sbn.key) { + clearProgressTracking() + } + return + } + if (!isTrackingProgress) { + trackProgress(sbn) + } else if (sbn.key == currentKey) { + updateProgressIfNeeded(sbn) + } + } + + private fun handleNotificationRemoved(sbn: StatusBarNotification) { + if (!isTrackingProgress) return + if (sbn.key == trackedNotificationKey) { + clearProgressTracking() + return + } + if (sbn.packageName == trackedPackageName) { + val current = trackedNotificationKey?.let { findNotificationByKey(it) } + if (current == null || !hasProgress(current.notification)) { + clearProgressTracking() + } + } + } + + override fun onHeadsUpPinnedModeChanged(inPinnedMode: Boolean) { + headsUpPinned = inPinnedMode + updateProgressState() + requestUiUpdate() + } + + override fun onKeyguardShowingChanged() { + setForceHidden(keyguardStateController.isShowing) + } + + fun setForceHidden(forceHidden: Boolean) { + if (isForceHidden == forceHidden) return + Log.d(TAG, "setForceHidden $forceHidden") + isForceHidden = forceHidden + updateProgressState() + requestUiUpdate() + } + + private fun updateSettings() { + val wasEnabled = isEnabled + val wasShowingMedia = showMediaProgress + val wasCompactMode = isCompactModeEnabled + + isEnabled = Settings.System.getIntForUser( + contentResolver, + ONGOING_ACTION_CHIP_ENABLED, + 0, + UserHandle.USER_CURRENT + ) == 1 + + showMediaProgress = Settings.System.getIntForUser( + contentResolver, + ONGOING_MEDIA_PROGRESS, + 0, + UserHandle.USER_CURRENT + ) == 1 + + isCompactModeEnabled = Settings.System.getIntForUser( + contentResolver, + ONGOING_COMPACT_MODE_ENABLED, + 0, + UserHandle.USER_CURRENT + ) == 1 + + if (wasEnabled != isEnabled || wasShowingMedia != showMediaProgress || + wasCompactMode != isCompactModeEnabled) { + needsFullUiUpdate = true; isExpanded = false + } + requestUiUpdate() + } + + fun destroy() { + isViewAttached = false + settingsObserver.unregister() + keyguardStateController.removeCallback(this) + headsUpManager.removeListener(this) + mediaSessionHelper.removeMediaMetadataListener(mediaMetadataListener) + notificationListener.removeNotificationHandler(this) + + uiUpdateJob?.cancel() + mediaProgressJob?.cancel() + finishedProgressTimeoutJob?.cancel() + compactCollapseJob?.cancel() + menuCollapseJob?.cancel() + pausedStaleJob?.cancel() + albumArtRetryJob?.cancel() + + iconCache.clear() + inFlightIconLoads.values.forEach { it.cancel() } + inFlightIconLoads.clear() + + currentIcon = null; currentTrackTitle = null; currentArtistName = null + currentAppLabel = null; currentAlbumArt = null + mainScope.cancel() + } + + companion object { + private const val TAG = "OngoingActionProgressController" + + private const val ONGOING_ACTION_CHIP_ENABLED = Settings.System.ONGOING_ACTION_CHIP + private const val ONGOING_MEDIA_PROGRESS = Settings.System.ONGOING_MEDIA_PROGRESS + private const val ONGOING_COMPACT_MODE_ENABLED = Settings.System.ONGOING_COMPACT_MODE + + private const val MEDIA_UPDATE_INTERVAL_MS = 1000L + private const val DEBOUNCE_DELAY_MS = 150L + private const val PROGRESS_TIMEOUT_MS = 30000L + private const val COMPACT_COLLAPSE_TIMEOUT_MS = 10_000L + private const val MENU_COLLAPSE_TIMEOUT_MS = 5_000L + private const val PAUSED_STALE_GRACE_MS = 20_000L + + private const val ALBUM_ART_RETRY_COUNT = 5 + private const val ALBUM_ART_RETRY_INTERVAL_MS = 300L + + private const val POSITION_RESET_THRESHOLD_MS = 1_500L + + private val HAPTIC_EXPAND = VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK) + private val HAPTIC_POPUP = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) + private val HAPTIC_PLAYPAUSE = VibrationEffect.createPredefined(VibrationEffect.EFFECT_DOUBLE_CLICK) + private val HAPTIC_LONG = VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK) + private val HAPTIC_CLICK = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) + } +} + +@Immutable +data class ProgressState( + val isVisible: Boolean = false, + val progress: Int = 0, + val maxProgress: Int = 0, + val iconBitmap: ImageBitmap? = null, + val albumArtBitmap: ImageBitmap? = null, + val packageName: String? = null, + val isCompactMode: Boolean = false, + val showMediaControls: Boolean = false, + val isMediaPlaying: Boolean = false, + val trackTitle: String? = null, + val artistName: String? = null, + val appLabel: String? = null, + val trackChangeId: Long = 0L, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt b/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt new file mode 100644 index 0000000000000..3384e423d1ef7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt @@ -0,0 +1,590 @@ +/* + * SPDX-FileCopyrightText: VoltageOS + * SPDX-FileCopyrightText: crDroid Android Project + * SPDX-FileCopyrightText: Lunaris AOSP + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.statusbar + +import android.content.Context +import android.util.Log +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +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.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.asComposeRenderEffect +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties +import com.android.systemui.media.controls.ui.view.WaveformSeekBar +import com.android.systemui.res.R +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager +import com.android.systemui.statusbar.policy.KeyguardStateController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +private const val TAG = "OngoingActionProgressCompose" + +private const val EXPAND_DURATION_MS = 350 +private const val COLLAPSE_DURATION_MS = 250 + +/** + * Composable that displays an ongoing action progress indicator in the status bar. + * Shows app icon and progress bar for notifications with progress information. + */ +@Composable +fun OngoingActionProgress( + controller: OnGoingActionProgressComposeController, + modifier: Modifier = Modifier +) { + val state by controller.state.collectAsState() + + val accent = MaterialTheme.colorScheme.primary + val chipShape = RoundedCornerShape(24.dp) + + var showPlayer by remember { mutableStateOf(null) } + + LaunchedEffect(state.showMediaControls) { + if (state.showMediaControls) { + showPlayer = true + } else if (showPlayer == true) { + showPlayer = false + } + } + + if (!state.isVisible) return + + var dragOffset = 0f + val gestureModifier = Modifier + .pointerInput(Unit) { + detectHorizontalDragGestures( + onDragStart = { dragOffset = 0f }, + onDragEnd = { + if (dragOffset < -50) controller.onSwipe(true) + else if (dragOffset > 50) controller.onSwipe(false) + } + ) { _, delta -> dragOffset += delta } + } + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { controller.onDoubleTap() }, + onLongPress = { controller.onLongPress() }, + onTap = { controller.onInteraction() } + ) + } + + Box(modifier = modifier, contentAlignment = Alignment.Center) { + + when { + state.isCompactMode -> { + val pv = progressFraction(state) + Box( + modifier = Modifier.size(26.dp).then(gestureModifier), + contentAlignment = Alignment.Center + ) { + Canvas(Modifier.fillMaxSize()) { + val strokePx = 3.dp.toPx() + val diam = size.minDimension - strokePx + val r = diam / 2 + val tl = center - Offset(r, r) + val sz = Size(diam, diam) + drawArc(Color(0x33FFFFFF), 0f, 360f, false, tl, sz, + style = Stroke(strokePx)) + drawArc(accent, -90f, 360f * pv, false, tl, sz, + style = Stroke(strokePx, cap = StrokeCap.Round)) + } + state.iconBitmap?.let { bmp -> + Image(bmp, null, Modifier.size(14.dp).clip(RoundedCornerShape(14.dp))) + } + } + } + + state.trackTitle != null -> { + MusicChip(state = state, chipShape = chipShape, + gestureModifier = gestureModifier) + } + + else -> { + val pv = progressFraction(state) + Row( + modifier = Modifier + .width(86.dp).height(26.dp) + .padding(horizontal = 6.dp, vertical = 4.dp) + .then(gestureModifier), + verticalAlignment = Alignment.CenterVertically + ) { + state.iconBitmap?.let { bmp -> + Image(bmp, null, Modifier.size(16.dp) + .clip(RoundedCornerShape(16.dp)).padding(start = 1.dp)) + Spacer(Modifier.width(5.dp)) + } + Box( + Modifier.weight(1f).height(6.dp).padding(end = 3.dp) + .clip(RoundedCornerShape(3.dp)).background(Color(0x33FFFFFF)) + ) { + Box(Modifier.fillMaxHeight().fillMaxWidth(pv).background(accent)) + } + } + } + } + + if (showPlayer != null) { + Popup( + alignment = Alignment.BottomCenter, + onDismissRequest = { controller.onMediaMenuDismiss() }, + properties = PopupProperties(focusable = false) + ) { + AnimatedMiniMediaPlayer( + state = state, + isOpening = showPlayer == true, + onAnimationEnd = { showPlayer = null }, + onPrev = { controller.onMediaAction(0) }, + onPlayPause = { controller.onMediaAction(1) }, + onNext = { controller.onMediaAction(2) }, + onSeek = { controller.onSeek(it) }, + ) + } + } + } +} + +@Composable +private fun AnimatedMiniMediaPlayer( + state: ProgressState, + isOpening: Boolean, + onAnimationEnd: () -> Unit, + onPrev: () -> Unit, + onPlayPause: () -> Unit, + onNext: () -> Unit, + onSeek: (Float) -> Unit, +) { + val anim = remember { Animatable(0f) } + + LaunchedEffect(isOpening) { + if (isOpening) { + anim.animateTo(1f, tween(EXPAND_DURATION_MS, easing = FastOutSlowInEasing)) + } else { + anim.animateTo(0f, tween(COLLAPSE_DURATION_MS, easing = LinearOutSlowInEasing)) + onAnimationEnd() + } + } + + val p = anim.value + val scale = 0.88f + p * 0.12f + val alpha = p + + Box( + modifier = Modifier.graphicsLayer { + scaleX = scale + scaleY = scale + this.alpha = alpha + } + ) { + MiniMediaPlayer(state, onPrev, onPlayPause, onNext, onSeek) + } +} + +@Composable +private fun MiniMediaPlayer( + state: ProgressState, + onPrev: () -> Unit, + onPlayPause: () -> Unit, + onNext: () -> Unit, + onSeek: (Float) -> Unit, +) { + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val cardWidth = screenWidth - 24.dp + val cardShape = RoundedCornerShape(24.dp) + val accent = MaterialTheme.colorScheme.primary + + val progressMs = state.progress.toLong() + val durationMs = state.maxProgress.toLong() + + val artKey = state.trackChangeId + + val hasRealArt = state.albumArtBitmap != null + + val blurEffect = remember { + android.graphics.RenderEffect + .createBlurEffect(28f, 28f, android.graphics.Shader.TileMode.MIRROR) + .asComposeRenderEffect() + } + + Box( + modifier = Modifier + .padding(bottom = 12.dp, start = 12.dp, end = 12.dp) + .width(cardWidth) + .wrapContentHeight() + .shadow(20.dp, cardShape) + .clip(cardShape) + ) { + key(artKey) { + if (hasRealArt) { + Image(state.albumArtBitmap!!, null, + contentScale = ContentScale.Crop, + modifier = Modifier.matchParentSize().graphicsLayer { + renderEffect = blurEffect; scaleX = 1.15f; scaleY = 1.15f + }) + } else { + Box(Modifier.matchParentSize() + .background(MaterialTheme.colorScheme.surfaceVariant)) + } + } + + Box(Modifier.matchParentSize() + .background(Color.Black.copy(alpha = if (hasRealArt) 0.45f else 0f))) + Box(Modifier.matchParentSize().background(accent.copy(alpha = 0.07f))) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + key(artKey) { + Box( + modifier = Modifier + .size(58.dp) + .clip(RoundedCornerShape(10.dp)) + .background(Color.White.copy(alpha = 0.10f)), + contentAlignment = Alignment.Center + ) { + when { + state.albumArtBitmap != null -> Image( + state.albumArtBitmap, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(10.dp)) + ) + state.iconBitmap != null -> Image( + state.iconBitmap, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(36.dp).clip(RoundedCornerShape(8.dp)) + ) + else -> Image( + painterResource(R.drawable.ic_default_music_icon), + contentDescription = null, + modifier = Modifier.size(26.dp), + colorFilter = ColorFilter.tint(Color.White.copy(alpha = 0.55f)) + ) + } + } + } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = state.trackTitle ?: "", + style = TextStyle(color = Color.White, fontSize = 13.sp, + fontWeight = FontWeight.SemiBold), + maxLines = 1, overflow = TextOverflow.Ellipsis + ) + if (!state.artistName.isNullOrBlank()) { + Text( + text = state.artistName, + style = TextStyle(color = Color.White.copy(alpha = 0.72f), + fontSize = 11.sp), + maxLines = 1, overflow = TextOverflow.Ellipsis + ) + } + + Spacer(Modifier.height(6.dp)) + + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(formatMs(progressMs), + style = TextStyle(color = Color.White.copy(alpha = 0.55f), + fontSize = 9.sp, fontWeight = FontWeight.Medium)) + if (durationMs > 0) + Text("-${formatMs(durationMs - progressMs)}", + style = TextStyle(color = Color.White.copy(alpha = 0.55f), + fontSize = 9.sp, fontWeight = FontWeight.Medium)) + } + + WaveformSeekBarCompose( + progressFraction = progressFraction(state), + isPlaying = state.isMediaPlaying, + onSeek = onSeek, + modifier = Modifier.fillMaxWidth().height(28.dp) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + PlayerButton(R.drawable.ic_media_control_skip_previous, "Previous", + 36.dp, 20.dp, Color.White.copy(alpha = 0.88f), onPrev) + Box( + modifier = Modifier.size(44.dp).clip(CircleShape) + .background(Color.White.copy(alpha = 0.20f)) + .clickable(onClick = onPlayPause), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource( + if (state.isMediaPlaying) R.drawable.ic_media_control_pause + else R.drawable.ic_media_control_play), + contentDescription = if (state.isMediaPlaying) "Pause" else "Play", + modifier = Modifier.size(22.dp), + colorFilter = ColorFilter.tint(Color.White) + ) + } + PlayerButton(R.drawable.ic_media_control_skip_next, "Next", + 36.dp, 20.dp, Color.White.copy(alpha = 0.88f), onNext) + } + } + } +} + +@Composable +private fun WaveformSeekBarCompose( + progressFraction: Float, + isPlaying: Boolean, + onSeek: (Float) -> Unit, + modifier: Modifier = Modifier, +) { + var isScrubbing by remember { mutableStateOf(false) } + + AndroidView( + factory = { ctx -> + WaveformSeekBar(ctx).apply { + max = 10_000 + setWaveformColor(android.graphics.Color.WHITE) + setThumbColor(android.graphics.Color.WHITE) + setOnSeekBarChangeListener(object : android.widget.SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(sb: android.widget.SeekBar?, + v: Int, fromUser: Boolean) { if (fromUser) onSeek(v / 10_000f) } + override fun onStartTrackingTouch(sb: android.widget.SeekBar?) { isScrubbing = true } + override fun onStopTrackingTouch(sb: android.widget.SeekBar?) { isScrubbing = false } + }) + } + }, + update = { bar -> + if (!isScrubbing) { + val target = (progressFraction * 10_000f).toInt().coerceIn(0, 10_000) + if (bar.progress != target) bar.progress = target + } + when { + isPlaying && !bar.isPlaying -> bar.startWaveAnimation() + !isPlaying && bar.isPlaying -> bar.stopWaveAnimation() + } + }, + modifier = modifier + ) +} + +@Composable +private fun PlayerButton( + iconRes: Int, contentDescription: String, + size: Dp, iconSize: Dp, tint: Color, onClick: () -> Unit, +) { + Box( + modifier = Modifier.size(size).clip(CircleShape).clickable(onClick = onClick), + contentAlignment = Alignment.Center + ) { + Image(painterResource(iconRes), contentDescription, + modifier = Modifier.size(iconSize), colorFilter = ColorFilter.tint(tint)) + } +} + +@Composable +private fun MusicChip( + state: ProgressState, + chipShape: RoundedCornerShape, + gestureModifier: Modifier, +) { + val bg = colorResource(android.R.color.system_accent1_500) + val text = colorResource(android.R.color.system_accent1_100) + + Row( + modifier = Modifier + .animateContentSize(animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)) + .widthIn(min = 55.dp, max = 85.dp) + .padding(start = 4.dp) + .clip(chipShape) + .background(bg) + .padding(horizontal = 5.dp, vertical = 3.dp) + .then(gestureModifier), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + state.iconBitmap?.let { bmp -> + Image(bmp, null, Modifier.size(15.dp).clip(RoundedCornerShape(4.dp))) + Spacer(Modifier.width(4.dp)) + } + Box( + Modifier.fadingEdge( + Brush.horizontalGradient(0.85f to Color.White, 1f to Color.Transparent)) + ) { + Text( + text = state.trackTitle ?: "", + style = TextStyle(color = text, fontSize = 10.sp, + fontWeight = FontWeight.Normal), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .basicMarquee(initialDelayMillis = 15_000, repeatDelayMillis = 15_000) + .padding(start = 1.dp) + ) + } + } +} + +private fun Modifier.fadingEdge(brush: Brush) = this + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .drawWithContent { drawContent(); drawRect(brush = brush, blendMode = BlendMode.DstIn) } + +private fun progressFraction(state: ProgressState): Float = + if (state.maxProgress > 0) + (state.progress.toFloat() / state.maxProgress.toFloat()).coerceIn(0f, 1f) + else 0f + +private fun formatMs(ms: Long): String { + if (ms <= 0) return "0:00" + val s = ms / 1000 + return "${s / 60}:${(s % 60).toString().padStart(2, '0')}" +} + +/** + * Compose-facing controller that adapts OnGoingActionProgressController state + * into Compose-friendly ProgressState. + */ +class OnGoingActionProgressComposeController( + private val context: Context, + notificationListener: NotificationListener, + keyguardStateController: KeyguardStateController, + headsUpManager: HeadsUpManager, + vibrator: VibratorHelper +) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + + private val _state = MutableStateFlow(ProgressState()) + val state: StateFlow = _state + + private val controller: OnGoingActionProgressController + + init { + Log.d(TAG, "Initializing OnGoingActionProgressComposeController") + + controller = OnGoingActionProgressController( + context, + notificationListener, + keyguardStateController, + headsUpManager, + vibrator + ) + + scope.launch { + controller.state.collect { s -> + _state.value = ProgressState( + isVisible = s.isVisible, + progress = s.progress, + maxProgress = s.maxProgress, + iconBitmap = s.iconBitmap, + albumArtBitmap = s.albumArtBitmap, + packageName = s.packageName, + isCompactMode = s.isCompactMode, + showMediaControls = s.showMediaControls, + isMediaPlaying = s.isMediaPlaying, + trackTitle = s.trackTitle, + artistName = s.artistName, + appLabel = s.appLabel, + trackChangeId = s.trackChangeId, + ) + } + } + + Log.d(TAG, "OnGoingActionProgressComposeController initialized successfully") + } + + fun destroy() { + scope.cancel() + controller.destroy() + } + + fun onInteraction() = controller.onInteraction() + fun onMediaAction(action: Int) = controller.onMediaAction(action) + fun onMediaMenuDismiss() = controller.onMediaMenuDismiss() + fun onDoubleTap() = controller.onDoubleTap() + fun onSwipe(isNext: Boolean) = controller.onSwipe(isNext) + fun onLongPress() = controller.onLongPress() + fun onSeek(fraction: Float) = controller.onSeek(fraction) + fun setSystemChipVisible(visible: Boolean) = controller.setSystemChipVisible(visible) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java index 106ed94cdfe96..2d6a2e39dcb71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java @@ -82,8 +82,11 @@ public class MobileSignalController extends SignalController 0f); + + return hasExplicitTint ? mTintedRippleColor : mNormalRippleColor; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java index ade9655d551d7..2149cda8f79e1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java @@ -100,10 +100,10 @@ public NotificationBackgroundView(Context context, AttributeSet attrs) { /** Sets whether blur/translucency is supported for notification rows. */ public void setIsBlurSupported(boolean isBlurSupported) { - mIsBlurSupported = isBlurSupported; - // Re-apply PorterDuff on blur changes. - if (mBackground != null) { - setTint(mTintColor); + if (mIsBlurSupported != isBlurSupported) { + mIsBlurSupported = isBlurSupported; + setStatefulColors(); + invalidate(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 06cab55e31e06..019a3a8737d26 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -805,6 +805,10 @@ boolean openGutsInternal( } final ExpandableNotificationRow row = (ExpandableNotificationRow) view; + if (affectedByWorkProfileLock(row)) { + return false; + } + if (row.isNotificationRowLongClickable()) { view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } @@ -875,6 +879,18 @@ public void run() { return true; } + boolean affectedByWorkProfileLock(ExpandableNotificationRow row) { + StatusBarNotification sbn = NotificationBundleUi.isEnabled() + ? (row.getEntryAdapter() != null ? row.getEntryAdapter().getSbn() : null) + : (row.getEntryLegacy() != null ? row.getEntryLegacy().getSbn() : null); + + if (sbn == null) return false; + + int userId = sbn.getNormalizedUserId(); + return mUserManager.isManagedProfile(userId) + && mLockscreenUserManager.isLockscreenPublicMode(userId); + } + /** * @param gutsListener the listener for open and close guts events */ 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 9a3a7daead9d0..c8149990863a1 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 @@ -6539,7 +6539,7 @@ public void setBlurRadius(float blurRadius) { private void updateBlurEffect() { if (mBlurRadius > 0) { mBlurEffect = - RenderEffect.createBlurEffect(mBlurRadius, mBlurRadius, Shader.TileMode.CLAMP); + RenderEffect.createBlurEffect(mBlurRadius, mBlurRadius, Shader.TileMode.MIRROR); spewLog("Setting up blur RenderEffect for NotificationStackScrollLayout"); } else { spewLog("Clearing the blur RenderEffect setup for NotificationStackScrollLayout"); 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 072a3cab5c136..4fdf8eabd759c 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 @@ -1491,7 +1491,7 @@ public void setBlurRadius(float blurRadius) { mView.setRenderEffect(RenderEffect.createBlurEffect( blurRadius, blurRadius, - Shader.TileMode.CLAMP)); + Shader.TileMode.MIRROR)); } else { debugLog("Resetting blur RenderEffect for NotificationStackScrollLayoutController"); mView.setRenderEffect(null); 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 abc25b7a80bfd..927d68cbb7320 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -110,7 +110,6 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba private final int mDisplayId; private final UserTracker mUserTracker; private final boolean mVibrateOnOpening; - private final VibrationEffect mCameraLaunchGestureVibrationEffect; private final ActivityStarter mActivityStarter; private final Lazy mCameraLauncherLazy; private final QuickSettingsController mQsController; @@ -192,8 +191,6 @@ enum PowerButtonLaunchGestureTarget { mQSHost = qsHost; mKeyguardInteractor = keyguardInteractor; mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation); - mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect( - mVibratorOptional, resources); mActivityStarter = activityStarter; mEmergencyGestureIntentFactory = emergencyGestureIntentFactory; mWalletController = walletController; @@ -568,35 +565,7 @@ private boolean isWakingUpOrAwake() { private void vibrateForCameraGesture() { mVibratorOptional.ifPresent( - v -> v.vibrate(mCameraLaunchGestureVibrationEffect, - HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES)); - } - - private static VibrationEffect getCameraGestureVibrationEffect( - Optional vibratorOptional, Resources resources) { - if (vibratorOptional.isPresent() && vibratorOptional.get().areAllPrimitivesSupported( - VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, - VibrationEffect.Composition.PRIMITIVE_CLICK)) { - return VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 50) - .compose(); - } - if (vibratorOptional.isPresent() && vibratorOptional.get().hasAmplitudeControl()) { - // Make sure to pass -1 for repeat so VibratorManagerService doesn't stop us when going - // to sleep. - return VibrationEffect.createWaveform( - CentralSurfaces.CAMERA_LAUNCH_GESTURE_VIBRATION_TIMINGS, - CentralSurfaces.CAMERA_LAUNCH_GESTURE_VIBRATION_AMPLITUDES, - /* repeat= */ -1); - } - - int[] pattern = resources.getIntArray(R.array.config_cameraLaunchGestureVibePattern); - long[] timings = new long[pattern.length]; - for (int i = 0; i < pattern.length; i++) { - timings[i] = pattern[i]; - } - return VibrationEffect.createWaveform(timings, /* repeat= */ -1); + v -> v.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK))); } @VisibleForTesting 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 a1cb793751ea7..c5aeb3cc1c7d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -83,11 +83,13 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import android.widget.DateTimeView; +import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; @@ -1153,17 +1155,67 @@ public void onHoldStatusBarOpenChange() { (requestTopUi, componentTag) -> mMainExecutor.execute( () -> mTopUiController.setRequestTopUi(requestTopUi, componentTag) ))); - getNotifContainerParentView().addView(mMediaViewController.getMediaArtScrim(), 0); - getNotifContainerParentView().addView(mPulseViewController.getPulseView(), 1); - getNotifContainerParentView().addView(mEdgeLightViewController.getEdgeLightView(), 2); - getNotifContainerParentView().addView(mNowPlayingViewController.getNowPlayingView(), 3); - getNotifContainerParentView().addView(mChargingAnimationViewController.getChargingView(), 4); - } - - private ViewGroup getNotifContainerParentView() { - ViewGroup rootView = (ViewGroup) getNotificationShadeWindowView().findViewById(R.id.scrim_behind).getParent(); - ViewGroup targetView = rootView.findViewById(R.id.notification_container_parent); - return targetView; + attachCustomOverlays(); + } + + private ViewGroup getScrimOverlayContainer() { + ViewGroup root = (ViewGroup) getNotificationShadeWindowView(); + + FrameLayout container = root.findViewById(R.id.custom_overlay_container); + if (container != null) { + return container; + } + + container = new FrameLayout(mContext); + container.setId(R.id.custom_overlay_container); + container.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + View scrimInFront = root.findViewById(R.id.scrim_in_front); + int scrimIndex = Math.max(root.indexOfChild(scrimInFront) - 3, 0); + root.addView(container, scrimIndex); + + return container; + } + + private void attachCustomOverlays() { + ViewGroup overlay = getScrimOverlayContainer(); + + detachFromParent(mMediaViewController.getMediaArtScrim()); + detachFromParent(mPulseViewController.getPulseView()); + detachFromParent(mEdgeLightViewController.getEdgeLightView()); + detachFromParent(mNowPlayingViewController.getNowPlayingView()); + detachFromParent(mChargingAnimationViewController.getChargingView()); + + overlay.addView(mMediaViewController.getMediaArtScrim(), + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + overlay.addView(mPulseViewController.getPulseView(), + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + overlay.addView(mEdgeLightViewController.getEdgeLightView(), + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + overlay.addView(mNowPlayingViewController.getNowPlayingView(), + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + overlay.addView(mChargingAnimationViewController.getChargingView(), + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + } + + private static void detachFromParent(View v) { + if (v == null) return; + final ViewParent p = v.getParent(); + if (p instanceof ViewGroup) { + ((ViewGroup) p).removeView(v); + } } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt index a941b3a4d8b9b..e348f1f2590a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt @@ -61,24 +61,25 @@ constructor(@Assisted private val context: Context) : fontWeightAdjustment = currentConfig.fontWeightAdjustment } - override fun notifyThemeChanged() { + private inline fun forEachListener(block: (ConfigurationListener) -> Unit) { // Avoid concurrent modification exception - val listeners = synchronized(this.listeners) { ArrayList(this.listeners) } + val snapshot = synchronized(listeners) { listeners.toList() } + snapshot.filterNotNull().forEach(block) + } - listeners.filterForEach({ this.listeners.contains(it) }) { it.onThemeChanged() } + override fun notifyThemeChanged() { + forEachListener { it.onThemeChanged() } } override fun dispatchOnMovedToDisplay(newDisplayId: Int, newConfiguration: Configuration) { - val listeners = synchronized(this.listeners) { ArrayList(this.listeners) } - listeners.filterForEach({ this.listeners.contains(it) }) { + forEachListener { it.onMovedToDisplay(newDisplayId, newConfiguration) } } override fun onConfigurationChanged(newConfig: Configuration) { // Avoid concurrent modification exception - val listeners = synchronized(this.listeners) { ArrayList(this.listeners) } - listeners.filterForEach({ this.listeners.contains(it) }) { it.onConfigChanged(newConfig) } + forEachListener { it.onConfigChanged(newConfig) } val fontScale = newConfig.fontScale val density = newConfig.densityDpi val uiMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK @@ -86,7 +87,7 @@ constructor(@Assisted private val context: Context) : val uiModeChanged = uiMode != this.uiMode if (density != this.density || fontScale != this.fontScale || inCarMode && uiModeChanged || fontWeightAdjustment != this.fontWeightAdjustment) { - listeners.filterForEach({ this.listeners.contains(it) }) { + forEachListener { it.onDensityOrFontScaleChanged() } this.density = density @@ -97,7 +98,7 @@ constructor(@Assisted private val context: Context) : val smallestScreenWidth = newConfig.smallestScreenWidthDp if (smallestScreenWidth != this.smallestScreenWidth) { this.smallestScreenWidth = smallestScreenWidth - listeners.filterForEach({ this.listeners.contains(it) }) { + forEachListener { it.onSmallestScreenWidthChanged() } } @@ -109,13 +110,13 @@ constructor(@Assisted private val context: Context) : // would be a direct reference to windowConfiguration.maxBounds, so the if statement // above would always fail. See b/245799099 for more information. this.maxBounds.set(maxBounds) - listeners.filterForEach({ this.listeners.contains(it) }) { it.onMaxBoundsChanged() } + forEachListener { it.onMaxBoundsChanged() } } val localeList = newConfig.locales if (localeList != this.localeList) { this.localeList = localeList - listeners.filterForEach({ this.listeners.contains(it) }) { it.onLocaleListChanged() } + forEachListener { it.onLocaleListChanged() } } if (uiModeChanged) { @@ -124,35 +125,37 @@ constructor(@Assisted private val context: Context) : context.theme.applyStyle(context.themeResId, true) this.uiMode = uiMode - listeners.filterForEach({ this.listeners.contains(it) }) { it.onUiModeChanged() } + forEachListener { it.onUiModeChanged() } } if (layoutDirection != newConfig.layoutDirection) { layoutDirection = newConfig.layoutDirection - listeners.filterForEach({ this.listeners.contains(it) }) { + forEachListener { it.onLayoutDirectionChanged(layoutDirection == LAYOUT_DIRECTION_RTL) } } if (lastConfig.updateFrom(newConfig) and ActivityInfo.CONFIG_ASSETS_PATHS != 0) { - listeners.filterForEach({ this.listeners.contains(it) }) { it.onThemeChanged() } + forEachListener { it.onThemeChanged() } } val newOrientation = newConfig.orientation if (orientation != newOrientation) { orientation = newOrientation - listeners.filterForEach({ this.listeners.contains(it) }) { + forEachListener { it.onOrientationChanged(orientation) } } } override fun addCallback(listener: ConfigurationListener) { + if (listener == null) return synchronized(listeners) { listeners.add(listener) } listener.onDensityOrFontScaleChanged() } override fun removeCallback(listener: ConfigurationListener) { + if (listener == null) return synchronized(listeners) { listeners.remove(listener) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index 01227c43ea07c..caae7fe62d383 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -23,16 +23,21 @@ import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; +import android.inputmethodservice.InputMethodService; +import android.os.RemoteException; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; +import android.view.ContextThemeWrapper; import android.view.Display; import android.view.DisplayCutout; +import android.view.IWindowManager; import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; +import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityEvent; import android.widget.FrameLayout; import android.widget.LinearLayout; @@ -41,10 +46,15 @@ import androidx.annotation.NonNull; import com.android.internal.policy.SystemBarUtils; +import com.android.settingslib.Utils; import com.android.systemui.Gefingerpoken; import com.android.systemui.res.R; import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress; import com.android.systemui.shade.StatusBarLongPressGestureDetector; +import com.android.systemui.shared.rotation.FloatingRotationButton; +import com.android.systemui.shared.rotation.RotationButtonController; +import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.CommandQueue.Callbacks; import com.android.systemui.statusbar.core.StatusBarConnectedDisplays; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; import com.android.systemui.statusbar.policy.Offset; @@ -52,16 +62,20 @@ import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import com.android.systemui.util.leak.RotationUtils; +import com.android.systemui.Dependency; +import com.android.systemui.rotation.RotationPolicyWrapper; import java.util.Objects; import java.util.function.BooleanSupplier; -public class PhoneStatusBarView extends FrameLayout { +public class PhoneStatusBarView extends FrameLayout implements Callbacks { private static final String TAG = "PhoneStatusBarView"; + private final CommandQueue mCommandQueue; private StatusBarWindowControllerStore mStatusBarWindowControllerStore; private boolean mShouldUpdateStatusBarHeightWhenControllerSet = false; private int mRotationOrientation = -1; + private RotationButtonController mRotationButtonController; @Nullable private View mCutoutSpace; @Nullable @@ -96,6 +110,54 @@ public class PhoneStatusBarView extends FrameLayout { public PhoneStatusBarView(Context context, AttributeSet attrs) { super(context, attrs); + mCommandQueue = Dependency.get(CommandQueue.class); + + // Only create FRB here if there is no navbar + if (!hasNavigationBar()) { + final Context lightContext = new ContextThemeWrapper(context, + Utils.getThemeAttr(context, R.attr.lightIconTheme)); + final Context darkContext = new ContextThemeWrapper(context, + Utils.getThemeAttr(context, R.attr.darkIconTheme)); + final int lightIconColor = + Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor); + final int darkIconColor = + Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor); + final FloatingRotationButton floatingRotationButton = new FloatingRotationButton( + context, + R.string.accessibility_rotate_button, R.layout.rotate_suggestion, + R.id.rotate_suggestion, R.dimen.floating_rotation_button_min_margin, + R.dimen.rounded_corner_content_padding, + R.dimen.floating_rotation_button_taskbar_left_margin, + R.dimen.floating_rotation_button_taskbar_bottom_margin, + R.dimen.floating_rotation_button_diameter, R.dimen.key_button_ripple_max_width, + R.bool.floating_rotation_button_position_left); + + final RotationPolicyWrapper rotationPolicyWrapper = + Dependency.get(RotationPolicyWrapper.class); + mRotationButtonController = new RotationButtonController(rotationPolicyWrapper, + lightContext, lightIconColor, darkIconColor, + R.drawable.ic_sysbar_rotate_button_ccw_start_0, + R.drawable.ic_sysbar_rotate_button_ccw_start_90, + R.drawable.ic_sysbar_rotate_button_cw_start_0, + R.drawable.ic_sysbar_rotate_button_cw_start_90, + () -> getDisplay().getRotation()); + mRotationButtonController.setRotationButton(floatingRotationButton, null); + } + } + + @Override + public void onRotationProposal(final int rotation, boolean isValid) { + if (mRotationButtonController != null && !hasNavigationBar()) { + mRotationButtonController.onRotationProposal(rotation, isValid); + } + } + + private boolean hasNavigationBar() { + try { + IWindowManager windowManager = WindowManagerGlobal.getWindowManagerService(); + return windowManager.hasNavigationBar(Display.DEFAULT_DISPLAY); + } catch (RemoteException ex) { } + return false; } void setLongPressGestureDetector( @@ -159,12 +221,20 @@ protected void onAttachedToWindow() { updateLayoutForCutout(); updateWindowHeight(); } + + if (mRotationButtonController != null && !hasNavigationBar()) { + mCommandQueue.addCallback(this); + } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mDisplayCutout = null; + + if (mRotationButtonController != null) { + mCommandQueue.removeCallback(this); + } } // Per b/300629388, we let the PhoneStatusBarView detect onConfigurationChanged to @@ -330,6 +400,15 @@ public boolean onInterceptTouchEvent(MotionEvent event) { return mTouchEventHandler.onInterceptTouchEvent(event); } + @Override + public void setImeWindowStatus(int displayId, int vis, int backDisposition, + boolean showImeSwitcher) { + if (mRotationButtonController != null) { + final boolean imeShown = (vis & InputMethodService.IME_VISIBLE) != 0; + mRotationButtonController.getRotationButton().setCanShowRotationButton(!imeShown); + } + } + public void updateResources() { mCutoutSideNudge = getResources().getDimensionPixelSize( R.dimen.display_cutout_margin_consumption); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 75dc42ba4b4ee..aeab291699d15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -354,8 +354,8 @@ constructor( return false } - // If animations are disabled system-wide, don't play this one either. - if (globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f) { + // If animations are sped up, don't play this one either to avoid flickering. + if (globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) != 1f) { return false } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithPercent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithPercent.kt index 36d3ba1e63d63..6a50697119c57 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithPercent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithPercent.kt @@ -95,6 +95,15 @@ fun BatteryWithPercent( } } + if (viewModel.shouldShowBoltInTextMode) { + BasicText( + text = "\u26A1", + color = colorProducer, + style = textStyle, + maxLines = 1, + ) + } + if (showEstimate) { viewModel.batteryTimeRemainingEstimate?.let { BasicText( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt index d0e9109b92bba..3257eca3720b6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.battery.ui.composable import android.graphics.Rect import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -427,6 +428,7 @@ fun CircleBatteryBody( modifier: Modifier = Modifier, contentDescription: String = "", ) { + val colorError = MaterialTheme.colorScheme.error val textMeasurer = rememberTextMeasurer() Canvas(modifier = modifier, contentDescription = contentDescription) { @@ -445,7 +447,11 @@ fun CircleBatteryBody( // Draw colored arc representing charge level if (level != null && level > 0) { drawArc( - colors.attribution, + if (level <= 20 && attr !is BatteryGlyph.Bolt && attr !is BatteryGlyph.Plus) { + colorError + } else { + colors.attribution + }, 270f, 3.6f * level, useCenter = false, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt index 73a9a1d600aa5..07015abff30e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt @@ -94,6 +94,18 @@ sealed class BatteryViewModel( source = interactor.showPercentInsideIcon, ) + val shouldShowBoltInTextMode: Boolean by + hydrator.hydratedStateOf( + traceName = "shouldShowBoltInTextMode", + initialValue = false, + source = combine( + interactor.batteryIconStyle.map { it == BatteryRepository.ICON_STYLE_TEXT }, + interactor.isCharging + ) { isTextMode, charging -> + isTextMode && charging + } + ) + /** A [List] representation of the current [level] */ private val levelGlyphs: Flow> = interactor.level.map { it?.glyphRepresentation() ?: emptyList() } 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 index 0f2a0253ad7f8..4461e12de0a41 100644 --- 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 @@ -154,6 +154,8 @@ interface MobileIconInteractor { val isVoWifiForceHidden: Flow val shouldShowFourgIcon: StateFlow + + val disableStackedMobileIcons: Flow } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ @@ -214,6 +216,9 @@ class MobileIconInteractorImpl( private final val SHOW_FOURG_ICON: String = "system:" + Settings.System.SHOW_FOURG_ICON; + private final val DISABLE_STACKED_MOBILE_ICONS: String = + "system:" + Settings.System.DISABLE_STACKED_MOBILE_ICONS + override val shouldShowFourgIcon: StateFlow = conflatedCallbackFlow { val callback = @@ -235,6 +240,29 @@ class MobileIconInteractorImpl( true ) + private val _disableStackedMobileIcons: StateFlow = + conflatedCallbackFlow { + val callback = + object : TunerService.Tunable { + override fun onTuningChanged(key: String, newValue: String?) { + when (key) { + DISABLE_STACKED_MOBILE_ICONS -> + trySend(TunerService.parseIntegerSwitch(newValue, false)) + } + } + } + Dependency.get(TunerService::class.java).addTunable(callback, DISABLE_STACKED_MOBILE_ICONS) + + awaitClose { Dependency.get(TunerService::class.java).removeTunable(callback) } + } + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + false + ) + + override val disableStackedMobileIcons: Flow = _disableStackedMobileIcons + // True if there exists _any_ icon override for this carrierId. Note that overrides can include // any or none of the icon groups defined in MobileMappings, so we still need to check on a // per-network-type basis whether or not the given icon group is overridden diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt index bc23e67817329..99386e0151348 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt @@ -152,6 +152,9 @@ interface MobileIconInteractorKairos { /** Whether to show the 4G icon instead of LTE. */ val shouldShowFourgIcon: State + + /** True if stacked mobile icons should be disabled */ + val disableStackedMobileIcons: State } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ @@ -419,6 +422,9 @@ class MobileIconInteractorKairosImpl( private val SHOW_FOURG_ICON: String = "system:" + Settings.System.SHOW_FOURG_ICON + private final val DISABLE_STACKED_MOBILE_ICONS: String = + "system:" + Settings.System.DISABLE_STACKED_MOBILE_ICONS + override val shouldShowFourgIcon: State = buildState { callbackFlow { val callback = @@ -436,4 +442,24 @@ class MobileIconInteractorKairosImpl( } .toState(initialValue = false) } + + private val _disableStackedMobileIcons: State = buildState { + callbackFlow { + val callback = + object : TunerService.Tunable { + override fun onTuningChanged(key: String, newValue: String?) { + when (key) { + DISABLE_STACKED_MOBILE_ICONS -> + trySend(TunerService.parseIntegerSwitch(newValue, false)) + } + } + } + Dependency.get(TunerService::class.java).addTunable(callback, DISABLE_STACKED_MOBILE_ICONS) + + awaitClose { Dependency.get(TunerService::class.java).removeTunable(callback) } + } + .toState(initialValue = false) + } + + override val disableStackedMobileIcons: State = _disableStackedMobileIcons } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt index 1eef307d527b0..08616b2192787 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt @@ -178,6 +178,12 @@ fun BuildScope.MobileIconInteractorKairosAdapter( "MobileIconInteractorKairosAdapter(subId=$subscriptionId).shouldShowFourgIcon" } ), + disableStackedMobileIcons = + disableStackedMobileIcons.toStateFlow( + nameTag { + "MobileIconInteractorKairosAdapter(subId=$subscriptionId).disableStackedMobileIcons" + } + ), ) } private class MobileIconInteractorKairosAdapter( @@ -207,4 +213,5 @@ private class MobileIconInteractorKairosAdapter( override val isAllowedDuringAirplaneMode: StateFlow, override val carrierNetworkChangeActive: StateFlow, override val shouldShowFourgIcon: StateFlow, + override val disableStackedMobileIcons: StateFlow, ) : MobileIconInteractor 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 index 658ee5094f1dc..5fe7abcd758e3 100644 --- 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 @@ -318,26 +318,29 @@ constructor( } .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) - override val isStackable = - if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { - icons.flatMapLatest { icons -> - if (icons.isEmpty()) { - flowOf(false) - } else { - combine(icons.map { it.signalLevelIcon }) { signalLevelIcons -> - // These are only stackable if: - // - They are cellular - // - There's exactly two - // - They have the same number of levels - signalLevelIcons.filterIsInstance().let { - it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels - } + override val isStackable: StateFlow = + if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { + icons.flatMapLatest { iconsList -> + when { + iconsList.isEmpty() -> flowOf(false) + iconsList.size != 2 -> flowOf(false) + else -> { + combine( + iconsList[0].disableStackedMobileIcons, + iconsList[0].signalLevelIcon, + iconsList[1].signalLevelIcon + ) { disableStacking, icon0, icon1 -> + !disableStacking && + icon0 is SignalIconModel.Cellular && + icon1 is SignalIconModel.Cellular && + icon0.numberOfLevels == icon1.numberOfLevels } } } - } else { - flowOf(false) - } + }.stateIn(scope, SharingStarted.WhileSubscribed(), false) + } else { + flowOf(false).stateIn(scope, SharingStarted.WhileSubscribed(), false) + } /** * Copied from the old pipeline. We maintain a 2s period of time where we will keep the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt index c5d15609fd0f1..e0940a6eef994 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt @@ -306,7 +306,8 @@ constructor( } override val isStackable: State = - if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { + if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { + combine( icons.flatMap { iconsBySubId: Map -> iconsBySubId.values .map { it.signalLevelIcon } @@ -319,10 +320,16 @@ constructor( it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels } } + }, + icons.flatMap { iconsBySubId -> + iconsBySubId.values.firstOrNull()?.disableStackedMobileIcons ?: stateOf(false) } - } else { - stateOf(false) + ) { shouldStack, disableStacking -> + shouldStack && !disableStacking } + } else { + stateOf(false) + } override val activeDataIconInteractor: State = combine(mobileConnectionsRepo.activeMobileDataSubscriptionId, icons) { activeSubId, icons -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt index fedf421dbc081..9d4aafc77a225 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt @@ -263,6 +263,9 @@ constructor( override val shouldShowFourgIcon: StateFlow = latest { shouldShowFourgIcon } .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val disableStackedMobileIcons: StateFlow = latest { disableStackedMobileIcons } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + private fun latest(block: MobileIconInteractor.() -> Flow): Flow = interactorsBySubId.flatMapLatestConflated { it[subId]?.block() ?: emptyFlow() } } 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 index d834ddf58058e..ad6f86564e620 100644 --- 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 @@ -72,6 +72,7 @@ object MobileIconBinder { val mobileDrawable = SignalDrawable(view.context) val mobileHdView = view.requireViewById(R.id.mobile_hd) val mobileHdSpace = view.requireViewById(R.id.mobile_hd_space) + val endSideRoamingView = view.requireViewById(R.id.mobile_roaming_updated) val dotView = view.requireViewById(R.id.status_bar_dot) view.isVisible = viewModel.isVisible.value @@ -230,6 +231,13 @@ object MobileIconBinder { } } + // Set the roaming indicator (single SIM - end side) + launch { + viewModel.isRoamingVisible.distinctUntilChanged().collect { isRoaming -> + endSideRoamingView.isVisible = isRoaming + } + } + if (statusBarStaticInoutIndicators()) { // Set the opacity of the activity indicators launch { @@ -276,6 +284,7 @@ object MobileIconBinder { networkTypeView.imageTintList = tint } + endSideRoamingView.imageTintList = tint mobileHdView.imageTintList = tint activityIn.imageTintList = tint activityOut.imageTintList = tint 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 index 521bfd58f9e6f..3a5f5a10d2c14 100644 --- 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 @@ -51,6 +51,7 @@ interface MobileIconViewModelCommon { val icon: Flow val contentDescription: Flow val roaming: Flow + val isRoamingVisible: Flow /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ val networkTypeIcon: Flow /** The slice attribution. Drawn as a background layer */ @@ -127,6 +128,8 @@ class MobileIconViewModel( override val roaming: Flow = vmProvider.flatMapLatest { it.roaming } + override val isRoamingVisible: Flow = vmProvider.flatMapLatest { it.isRoamingVisible } + override val networkTypeIcon: Flow = vmProvider.flatMapLatest { it.networkTypeIcon } @@ -165,6 +168,7 @@ private class CarrierBasedSatelliteViewModelImpl( /** These fields are not used for satellite icons currently */ override val roaming: Flow = flowOf(false) + override val isRoamingVisible: Flow = flowOf(false) override val networkTypeIcon: Flow = flowOf(null) override val networkTypeBackground: StateFlow = MutableStateFlow(null) override val activityInVisible: Flow = flowOf(false) @@ -339,6 +343,16 @@ private class CellularIconViewModel( ) .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isRoamingVisible: StateFlow = + combine( + roaming, + iconInteractor.isRoamingForceHidden + ) { isRoaming, isHidden -> + isRoaming && !isHidden + } + .distinctUntilChanged() + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + private val activity: Flow = if (!constants.shouldShowActivityConfig) { flowOf(null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt index 968c6acc6f1ec..65b1645f3f1db 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt @@ -50,6 +50,7 @@ interface MobileIconViewModelKairosCommon { val icon: KairosState val contentDescription: KairosState val roaming: KairosState + val isRoamingVisible: KairosState /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ val networkTypeIcon: KairosState /** The slice attribution. Drawn as a background layer */ @@ -117,6 +118,8 @@ class MobileIconViewModelKairos( override val roaming: KairosState = vmProvider.flatMap { it.roaming } + override val isRoamingVisible: KairosState = vmProvider.flatMap { it.isRoamingVisible } + override val networkTypeIcon: KairosState = vmProvider.flatMap { it.networkTypeIcon } @@ -148,6 +151,7 @@ private class CarrierBasedSatelliteViewModelKairosImpl( /** These fields are not used for satellite icons currently */ override val roaming: KairosState = stateOf(false) + override val isRoamingVisible: KairosState = stateOf(false) override val networkTypeIcon: KairosState = stateOf(null) override val networkTypeBackground: KairosState = stateOf(null) override val activityInVisible: KairosState = stateOf(false) @@ -304,6 +308,23 @@ private class CellularIconViewModelKairos( } } + override val isRoamingVisible: KairosState = + combine( + iconInteractor.isRoaming, + iconInteractor.isRoamingForceHidden + ) { isRoaming, isHidden -> + isRoaming && !isHidden + }.also { + onActivated { + logDiffsForTable( + name = nameTag { "CellularIconViewModelKairos(subId=$subscriptionId).isRoamingVisible" }, + it, + iconInteractor.tableLogBuffer, + columnName = "roamingVisible", + ) + } + } + private val activity: KairosState = if (!constants.shouldShowActivityConfig) { stateOf(null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt index c17696aed69c7..fbef7850e3686 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt @@ -175,6 +175,7 @@ constructor( override val isVoWifi: State = latest(false) { isVoWifi } override val isVoWifiForceHidden: State = latest(true) { isVoWifiForceHidden } override val shouldShowFourgIcon: State = latest(false) { shouldShowFourgIcon } + override val disableStackedMobileIcons: State = latest(false) { disableStackedMobileIcons } } private fun trackedCommonViewModel(subId: Int) = @@ -193,6 +194,7 @@ constructor( override val contentDescription: State = latest(null) { contentDescription } override val roaming: State = latest(false) { roaming } + override val isRoamingVisible: KairosState = stateOf(false) override val networkTypeIcon: State = latest(null) { networkTypeIcon } override val networkTypeBackground: State = latest(null) { networkTypeBackground } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt index 532dafef8bd72..8a86a6f2ee3a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt @@ -48,7 +48,7 @@ interface StackedMobileIconViewModel { val activityContainerVisible: Boolean /** [Context] to use when loading the [networkTypeIcon] */ val mobileContext: Context? - val roaming: Boolean + val isRoamingVisible: Boolean val isIconVisible: Boolean } @@ -190,7 +190,7 @@ constructor( initialValue = null, ) - override val roaming: Boolean by + private val roaming: Boolean by hydrator.hydratedStateOf( traceName = "isRoaming", source = @@ -211,6 +211,21 @@ constructor( initialValue = false, ) + override val isRoamingVisible: Boolean by + hydrator.hydratedStateOf( + traceName = "isRoamingVisible", + source = + iconViewModelFlow.flatMapLatest { viewModels -> + viewModels.firstOrNull()?.isRoamingVisible ?: flowOf(false) + } + .logDiffsForTable( + tableLogBuffer = tableLogger, + columnName = COL_ROAMING_VISIBLE, + initialValue = false, + ), + initialValue = false, + ) + override val isIconVisible: Boolean by hydrator.hydratedStateOf( traceName = "isIconVisible", @@ -245,5 +260,6 @@ constructor( private companion object { const val COL_IS_ICON_VISIBLE = "isIconVisible" const val COL_ROAMING = "roam" + const val COL_ROAMING_VISIBLE = "roamVisible" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt index 0cac207beffaf..5b41b0106f23a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt @@ -132,13 +132,22 @@ constructor( initialValue = null, ) - override val roaming: Boolean by + private val roaming: Boolean by hydratedComposeStateOf( name = "roaming", source = iconList.flatMap { icons -> icons.firstOrNull()?.roaming ?: stateOf(false) }, initialValue = false, ) + override val isRoamingVisible: Boolean by + hydratedComposeStateOf( + name = "isRoamingVisible", + source = iconList.flatMap { icons -> + icons.firstOrNull()?.isRoamingVisible ?: stateOf(false) + }, + initialValue = false, + ) + override val isIconVisible: Boolean get() = isStackable && dualSim != null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 8dc5b2230b6d0..4c67aed7917ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -723,6 +723,7 @@ constructor( ) if (style < 1 || style > clockBackgrounds.size) { + clock.setStaticColor(false) return } @@ -742,7 +743,10 @@ constructor( // Set text color to white for visibility on filled chip backgrounds // Styles 2 and 8 are outline-only (transparent background), so use normal color if (style != 2 && style != 8) { + clock.setStaticColor(true) clock.setTextColor(Color.WHITE) + } else { + clock.setStaticColor(false) } // For outline styles (2, 8), let Clock's DarkIconDispatcher handle the color } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt index 1e93ee0d4f644..1f35ef14b21d3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt @@ -123,7 +123,7 @@ fun StackedMobileIcon(viewModel: StackedMobileIconViewModel, modifier: Modifier contentDescription = viewModel.contentDescription, ) - if (viewModel.roaming) { + if (viewModel.isRoamingVisible) { val height = with(LocalDensity.current) { RoamingIconHeightSp.toDp() } val paddingTop = with(LocalDensity.current) { RoamingIconPaddingTopSp.toDp() } Image( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index 805692bb5f3cb..9e8612983c5f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -76,11 +76,15 @@ import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ui.composable.VariableDayDate +import com.android.systemui.statusbar.NotificationListener +import com.android.systemui.statusbar.OngoingActionProgress import com.android.systemui.statusbar.StatusBarAlwaysUseRegionSampling +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.core.RudimentaryBattery import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.core.StatusBarForDesktop import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips @@ -96,6 +100,7 @@ import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.phone.ui.DarkIconManager import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager @@ -143,6 +148,10 @@ constructor( @DisplayAware private val homeStatusBarViewBinder: HomeStatusBarViewBinder, @DisplayAware private val homeStatusBarViewModelFactory: HomeStatusBarViewModelFactory, private val statusBarRegionSamplingViewModelFactory: StatusBarRegionSamplingViewModel.Factory, + private val notificationListener: NotificationListener, + private val keyguardStateController: KeyguardStateController, + private val headsUpManager: HeadsUpManager, + private val vibrator: VibratorHelper, ) { fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView { val composeView = ComposeView(root.context) @@ -168,6 +177,10 @@ constructor( statusBarRegionSamplingViewModelFactory = statusBarRegionSamplingViewModelFactory, onViewCreated = andThen, + notificationListener = notificationListener, + keyguardStateController = keyguardStateController, + headsUpManager = headsUpManager, + vibrator = vibrator, modifier = Modifier.sysUiResTagContainer(), ) } @@ -207,6 +220,10 @@ fun StatusBarRoot( mediaViewModelFactory: MediaViewModel.Factory, statusBarRegionSamplingViewModelFactory: StatusBarRegionSamplingViewModel.Factory, onViewCreated: (ViewGroup) -> Unit, + notificationListener: NotificationListener, + keyguardStateController: KeyguardStateController, + headsUpManager: HeadsUpManager, + vibrator: VibratorHelper, modifier: Modifier = Modifier, ) { val displayId = parent.context.displayId @@ -266,6 +283,10 @@ fun StatusBarRoot( statusBarViewModel = statusBarViewModel, iconViewStore = iconViewStore, appHandlesViewModel = appHandlesViewModel, + notificationListener = notificationListener, + keyguardStateController = keyguardStateController, + headsUpManager = headsUpManager, + vibrator = vibrator, context = context, ) } @@ -407,6 +428,10 @@ private fun addStartSideComposable( statusBarViewModel: HomeStatusBarViewModel, iconViewStore: NotificationIconContainerViewBinder.IconViewStore?, appHandlesViewModel: AppHandlesViewModel, + notificationListener: NotificationListener, + keyguardStateController: KeyguardStateController, + headsUpManager: HeadsUpManager, + vibrator: VibratorHelper, context: Context, ) { val startSideExceptHeadsUp = @@ -425,9 +450,7 @@ private fun addStartSideComposable( LinearLayout.LayoutParams.WRAP_CONTENT, ) .apply { - if (showDate) { - gravity = android.view.Gravity.CENTER_VERTICAL - } + gravity = android.view.Gravity.CENTER_VERTICAL } setContent { @@ -488,7 +511,22 @@ private fun addStartSideComposable( ) } + val progressController = remember { + com.android.systemui.statusbar.OnGoingActionProgressComposeController( + context, + notificationListener, + keyguardStateController, + headsUpManager, + vibrator + ) + } + val chipsVisibilityModel = statusBarViewModel.ongoingActivityChips + val hasSystemChips = chipsVisibilityModel.chips.active.isNotEmpty() + progressController.setSystemChipVisible(hasSystemChips) + + OngoingActionProgress(controller = progressController) + if (chipsVisibilityModel.areChipsAllowed) { OngoingActivityChips( chips = chipsVisibilityModel.chips, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index 848038f8553a7..1be9c20d70166 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -140,6 +140,8 @@ public class Clock extends TextView implements private String mClockDateFormat = null; private boolean mIsStatusBar; + private boolean useStaticColor = false; + private int lastDynamicColor = -1; // Tracks config changes that will make the clock change dimensions private final InterestingConfigChanges mInterestingConfigChanges; @@ -432,14 +434,25 @@ public void disable(int displayId, int state1, int state2, boolean animate) { @Override public void onDarkChanged(ArrayList areas, float darkIntensity, int tint) { mNonAdaptedColor = DarkIconDispatcher.getTint(areas, this, tint); - setTextColor(mNonAdaptedColor); + lastDynamicColor = mNonAdaptedColor; + if (useStaticColor) return; + setTextColor(lastDynamicColor); } // Update text color based when shade scrim changes color. public void onColorsChanged(boolean lightTheme) { final Context context = new ContextThemeWrapper(mContext, lightTheme ? R.style.Theme_SystemUI_LightWallpaper : R.style.Theme_SystemUI); - setTextColor(Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor)); + lastDynamicColor = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor); + if (useStaticColor) return; + setTextColor(lastDynamicColor); + } + + public void setStaticColor(boolean enable) { + useStaticColor = enable; + if (!useStaticColor && lastDynamicColor != -1) { + setTextColor(lastDynamicColor); + } } public void onDensityOrFontScaleChanged() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java index dc1b27cfb1f27..a3746c7cf8f5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java @@ -25,6 +25,13 @@ public interface FlashlightController extends CallbackController mCameraId; private final AtomicBoolean mInitted = new AtomicBoolean(false); + private static final String FLASHLIGHT_BRIGHTNESS_SETTING = "flashlight_brightness"; + + @GuardedBy("this") + private int mDefaultLevel; + @GuardedBy("this") + private int mMaxLevel; + @GuardedBy("this") + private int mCurrentLevel; + @GuardedBy("this") + private boolean mStrengthControlSupported; + @Inject public FlashlightControllerImpl( + Context context, DumpManager dumpManager, CameraManager cameraManager, + @Main Handler mainHandler, @Background Executor bgExecutor, SecureSettings secureSettings, BroadcastSender broadcastSender, PackageManager packageManager ) { + mContext = context; mCameraManager = cameraManager; + mHandler = mainHandler; mExecutor = bgExecutor; mCameraId = new AtomicReference<>(null); mSecureSettings = secureSettings; @@ -110,7 +132,29 @@ private void init() { private void tryInitCamera() { if (!mHasFlashlight || mCameraId.get() != null) return; try { - mCameraId.set(getCameraId()); + String id = getCameraId(); + if (id != null) { + CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id); + Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + if (flashAvailable != null && flashAvailable) { + mDefaultLevel = c.get(CameraCharacteristics.FLASH_INFO_STRENGTH_DEFAULT_LEVEL); + mMaxLevel = c.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL); + mStrengthControlSupported = (mMaxLevel > 1); + mCameraId.set(id); + + if (mStrengthControlSupported) { + float percent = Settings.System.getFloatForUser( + mContext.getContentResolver(), + FLASHLIGHT_BRIGHTNESS_SETTING, + (float) mDefaultLevel / mMaxLevel, + UserHandle.USER_CURRENT); + synchronized (this) { + mCurrentLevel = Math.max(1, Math.round(percent * mMaxLevel)); + } + if (DEBUG) Log.d(TAG, "Restored torch level: " + mCurrentLevel); + } + } + } } catch (Throwable e) { Log.e(TAG, "Couldn't initialize.", e); return; @@ -131,7 +175,20 @@ public void setFlashlight(boolean enabled) { synchronized (this) { if (mFlashlightEnabled != enabled) { try { - mCameraManager.setTorchMode(mCameraId.get(), enabled); + if (enabled) { + if (mStrengthControlSupported) { + int level = (mCurrentLevel > 0) ? mCurrentLevel : mDefaultLevel; + level = Math.max(1, Math.min(level, mMaxLevel)); + if (DEBUG) Log.d(TAG, "Turning on torch with level " + level); + mCameraManager.turnOnTorchWithStrengthLevel(mCameraId.get(), level); + } else { + if (DEBUG) Log.d(TAG, "Turning on torch (no strength support)"); + mCameraManager.setTorchMode(mCameraId.get(), true); + } + } else { + if (DEBUG) Log.d(TAG, "Turning off torch"); + mCameraManager.setTorchMode(mCameraId.get(), false); + } } catch (CameraAccessException e) { Log.e(TAG, "Couldn't set torch mode", e); dispatchError(); @@ -153,6 +210,80 @@ public synchronized boolean isAvailable() { return mTorchAvailable; } + public boolean isStrengthControlSupported() { + return mStrengthControlSupported; + } + + public int getMaxLevel() { + return mMaxLevel; + } + + public int getDefaultLevel() { + return mDefaultLevel; + } + + public synchronized int getCurrentLevel() { + return mCurrentLevel; + } + + public synchronized float getCurrentPercent() { + if (!mStrengthControlSupported || mMaxLevel <= 0) { + return 0f; + } + return Math.max(0.01f, (float) mCurrentLevel / (float) mMaxLevel); + } + + public void setFlashlightStrengthLevel(int level) { + if (!mStrengthControlSupported) { + setFlashlight(level > 0); + return; + } + if (mCameraId.get() == null) { + mExecutor.execute(this::tryInitCamera); + } + + final int requestedLevel = level; + + mHandler.post(() -> { + if (mCameraId.get() == null) return; + + int clampedLevel; + boolean wasEnabled; + synchronized (this) { + clampedLevel = Math.max(1, Math.min(requestedLevel, mMaxLevel)); + if (mCurrentLevel == clampedLevel && !mFlashlightEnabled) return; + mCurrentLevel = clampedLevel; + wasEnabled = mFlashlightEnabled; + } + + try { + if (DEBUG) Log.d(TAG, "Setting torch strength level: " + clampedLevel + + ", wasEnabled: " + wasEnabled); + + if (wasEnabled) { + mCameraManager.turnOnTorchWithStrengthLevel(mCameraId.get(), clampedLevel); + } + + dispatchStrengthChanged(clampedLevel); + + mExecutor.execute(() -> { + float percent = Math.max(0.01f, (float) clampedLevel / mMaxLevel); + Settings.System.putFloatForUser( + mContext.getContentResolver(), + FLASHLIGHT_BRIGHTNESS_SETTING, + percent, + UserHandle.USER_CURRENT + ); + }); + + } catch (CameraAccessException e) { + Log.e(TAG, "Couldn't set torch strength level", e); + dispatchError(); + } + }); + } + + @Override public void addCallback(@NonNull FlashlightListener l) { synchronized (mListeners) { @@ -163,6 +294,7 @@ public void addCallback(@NonNull FlashlightListener l) { mListeners.add(new WeakReference<>(l)); l.onFlashlightAvailabilityChanged(isAvailable()); l.onFlashlightChanged(isEnabled()); + l.onFlashlightStrengthChanged(getCurrentLevel()); } } @@ -204,6 +336,17 @@ private void dispatchAvailabilityChanged(boolean available) { dispatchListeners(DISPATCH_AVAILABILITY_CHANGED, available); } + private void dispatchStrengthChanged(int level) { + synchronized (mListeners) { + for (WeakReference ref : mListeners) { + FlashlightListener l = ref.get(); + if (l != null) { + l.onFlashlightStrengthChanged(level); + } + } + } + } + private void dispatchListeners(int message, boolean argument) { synchronized (mListeners) { final ArrayList> copy = @@ -263,6 +406,24 @@ public void onTorchModeChanged(String cameraId, boolean enabled) { } } + @Override + @WorkerThread + public void onTorchStrengthLevelChanged(@NonNull String cameraId, int newStrengthLevel) { + if (!TextUtils.equals(cameraId, mCameraId.get())) return; + synchronized (FlashlightControllerImpl.this) { + if (mCurrentLevel == newStrengthLevel) return; + mCurrentLevel = newStrengthLevel; + } + float percent = Math.max(0.01f, (float) newStrengthLevel / (float) mMaxLevel); + Settings.System.putFloatForUser( + mContext.getContentResolver(), + FLASHLIGHT_BRIGHTNESS_SETTING, + percent, + UserHandle.USER_CURRENT); + if (DEBUG) Log.d(TAG, "Strength level changed: " + newStrengthLevel + "/" + mMaxLevel); + dispatchStrengthChanged(newStrengthLevel); + } + private void setCameraAvailable(boolean available) { boolean changed; synchronized (FlashlightControllerImpl.this) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/util/MediaSessionManagerHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/util/MediaSessionManagerHelper.kt new file mode 100644 index 0000000000000..e8d06edae84b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/util/MediaSessionManagerHelper.kt @@ -0,0 +1,281 @@ +/* + * SPDX-FileCopyrightText: The risingOS Android Project + * SPDX-FileCopyrightText: The AxionAOSP Project + * SPDX-FileCopyrightText: crDroid Android Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.statusbar.util + +import android.content.Context +import android.content.res.Configuration +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSessionLegacyHelper +import android.media.session.MediaSessionManager +import android.media.session.PlaybackState +import android.os.SystemClock +import android.provider.Settings +import android.text.TextUtils +import android.view.KeyEvent +import android.view.View + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +class MediaSessionManagerHelper private constructor(private val context: Context) { + + interface MediaMetadataListener { + fun onMediaMetadataChanged() {} + fun onPlaybackStateChanged() {} + } + + private val _mediaMetadata = MutableStateFlow(null) + val mediaMetadata: StateFlow = _mediaMetadata + + private val _playbackState = MutableStateFlow(null) + val playbackState: StateFlow = _playbackState + + private val scope = CoroutineScope(Dispatchers.Main) + private var collectJob: Job? = null + + private var lastSavedPackageName: String? = null + private val mediaSessionManager: MediaSessionManager = context.getSystemService(MediaSessionManager::class.java)!! + private var activeController: MediaController? = null + private val listeners = mutableSetOf() + + private val mediaControllerCallback = object : MediaController.Callback() { + override fun onMetadataChanged(metadata: MediaMetadata?) { + _mediaMetadata.value = metadata + } + + override fun onPlaybackStateChanged(state: PlaybackState?) { + _playbackState.value = state + } + } + + private val tickerFlow = flow { + while (true) { + emit(Unit) + delay(1000) + } + }.flowOn(Dispatchers.Default) + + init { + lastSavedPackageName = Settings.System.getString( + context.contentResolver, + "media_session_last_package_name" + ) + + scope.launch { + tickerFlow + .map { fetchActiveController() } + .distinctUntilChanged { old, new -> sameSessions(old, new) } + .collect { controller -> + activeController?.unregisterCallback(mediaControllerCallback) + activeController = controller + controller?.registerCallback(mediaControllerCallback) + _mediaMetadata.value = controller?.metadata + _playbackState.value = controller?.playbackState + saveLastNonNullPackageName() + } + } + } + + private fun isEligibleState(state: Int?): Boolean { + return when (state) { + null, + PlaybackState.STATE_NONE, + PlaybackState.STATE_STOPPED, + PlaybackState.STATE_ERROR -> false + else -> true + } + } + + private suspend fun fetchActiveController(): MediaController? = withContext(Dispatchers.IO) { + var localController: MediaController? = null + val remoteSessions = mutableSetOf() + + mediaSessionManager.getActiveSessions(null) + .filter { controller -> + isEligibleState(controller.playbackState?.state) && + controller.playbackInfo != null + } + .sortedWith( + compareByDescending { it.playbackState?.state == PlaybackState.STATE_PLAYING } + .thenByDescending { it.playbackState?.state == PlaybackState.STATE_BUFFERING } + .thenByDescending { it.playbackState?.state == PlaybackState.STATE_PAUSED } + ) + .forEach { controller -> + when (controller.playbackInfo?.playbackType) { + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> { + remoteSessions.add(controller.packageName) + if (localController?.packageName == controller.packageName) { + localController = null + } + } + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { + if (!remoteSessions.contains(controller.packageName)) { + localController = localController ?: controller + } + } + } + } + + localController + } + + fun addMediaMetadataListener(listener: MediaMetadataListener) { + listeners.add(listener) + if (listeners.size == 1) { + startCollecting() + } + listener.onMediaMetadataChanged() + listener.onPlaybackStateChanged() + } + + fun removeMediaMetadataListener(listener: MediaMetadataListener) { + listeners.remove(listener) + if (listeners.isEmpty()) { + stopCollecting() + } + } + + private fun startCollecting() { + collectJob = scope.launch { + launch { mediaMetadata.collect { notifyListeners { onMediaMetadataChanged() } } } + launch { playbackState.collect { notifyListeners { onPlaybackStateChanged() } } } + } + } + + private fun stopCollecting() { + collectJob?.cancel() + collectJob = null + } + + private fun notifyListeners(action: MediaMetadataListener.() -> Unit) { + listeners.forEach { it.action() } + } + + fun seekTo(time: Long) { + activeController?.transportControls?.seekTo(time) + } + + fun getTotalDuration() = mediaMetadata.value?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L + + private fun saveLastNonNullPackageName() { + activeController?.packageName?.takeIf { it.isNotEmpty() }?.let { pkg -> + if (pkg != lastSavedPackageName) { + Settings.System.putString( + context.contentResolver, + "media_session_last_package_name", + pkg + ) + lastSavedPackageName = pkg + } + } + } + + fun getMediaBitmap(): Bitmap? = mediaMetadata.value?.let { + it.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) ?: + it.getBitmap(MediaMetadata.METADATA_KEY_ART) ?: + it.getBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON) + } + + fun getCurrentMediaMetadata(): MediaMetadata? { + return mediaMetadata.value + } + + fun getMediaAppIcon(): Drawable? { + val packageName = activeController?.packageName ?: return null + return try { + val pm = context.packageManager + pm.getApplicationIcon(packageName) + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + fun isMediaControllerAvailable() = activeController?.packageName?.isNotEmpty() ?: false + + fun isMediaSessionActive(): Boolean { + val controller = activeController ?: return false + val st = controller.playbackState?.state + return st != null && + st != PlaybackState.STATE_NONE && + st != PlaybackState.STATE_STOPPED && + st != PlaybackState.STATE_ERROR + } + + fun isMediaPaused(): Boolean = playbackState.value?.state == PlaybackState.STATE_PAUSED + + fun isMediaPlaying(): Boolean = playbackState.value?.state == PlaybackState.STATE_PLAYING + + fun getMediaControllerPlaybackState(): PlaybackState? { + return activeController?.playbackState ?: null + } + + private fun sameSessions(a: MediaController?, b: MediaController?): Boolean { + if (a == b) return true + if (a == null) return false + return a.controlsSameSession(b) + } + + private fun dispatchMediaKeyWithWakeLockToMediaSession(keycode: Int) { + val helper = MediaSessionLegacyHelper.getHelper(context) ?: return + var event = KeyEvent( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + KeyEvent.ACTION_DOWN, + keycode, + 0 + ) + helper.sendMediaButtonEvent(event, true) + event = KeyEvent.changeAction(event, KeyEvent.ACTION_UP) + helper.sendMediaButtonEvent(event, true) + } + + fun prevSong() { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PREVIOUS) + } + + fun nextSong() { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_NEXT) + } + + fun toggleMediaPlaybackState() { + if (isMediaPlaying()) { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PAUSE) + } else { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PLAY) + } + } + + fun launchMediaApp() { + lastSavedPackageName?.takeIf { it.isNotEmpty() }?.let { + launchMediaPlayerApp(it) + } + } + + fun launchMediaPlayerApp(packageName: String) { + if (packageName.isNotEmpty()) { + val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) + launchIntent?.let { intent -> + context.startActivity(intent) + } + } + } + + companion object { + @Volatile + private var instance: MediaSessionManagerHelper? = null + + fun getInstance(context: Context): MediaSessionManagerHelper = + instance ?: synchronized(this) { + instance ?: MediaSessionManagerHelper(context).also { instance = it } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java b/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java index 2f2b8f734d7b1..5df126e7b9f17 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java +++ b/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java @@ -26,6 +26,8 @@ public class RisingSettingsConstants { }; public static final String[] SECURE_SETTINGS_KEYS = { + "clock_text_accent_color", + "clock_text_opacity" }; public static final String[] SYSTEM_SETTINGS_NOTIFY_ONLY_KEYS = { diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java index caae2c4ce99a8..6302df21d8ba7 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java @@ -113,6 +113,12 @@ public class ThemeOverlayApplier implements Dumpable { @VisibleForTesting static final String OVERLAY_CATEGORY_ICON_THEME_PICKER = "android.theme.customization.icon_pack.themepicker"; + @VisibleForTesting + static final String OVERLAY_CATEGORY_ICON_SIGNAL = + "android.theme.customization.signal_icon"; + @VisibleForTesting + static final String OVERLAY_CATEGORY_ICON_WIFI = + "android.theme.customization.wifi_icon"; /* * All theme customization categories used by the system, in order that they should be applied, @@ -126,7 +132,9 @@ public class ThemeOverlayApplier implements Dumpable { OVERLAY_CATEGORY_DYNAMIC_COLOR, OVERLAY_CATEGORY_ICON_ANDROID, OVERLAY_CATEGORY_ICON_SYSUI, - OVERLAY_CATEGORY_ICON_SETTINGS); + OVERLAY_CATEGORY_ICON_SETTINGS, + OVERLAY_CATEGORY_ICON_SIGNAL, + OVERLAY_CATEGORY_ICON_WIFI); /* Categories that need to be applied to the current user as well as the system user. */ @VisibleForTesting @@ -137,7 +145,9 @@ public class ThemeOverlayApplier implements Dumpable { OVERLAY_CATEGORY_FONT, OVERLAY_CATEGORY_SHAPE, OVERLAY_CATEGORY_ICON_ANDROID, - OVERLAY_CATEGORY_ICON_SYSUI); + OVERLAY_CATEGORY_ICON_SYSUI, + OVERLAY_CATEGORY_ICON_SIGNAL, + OVERLAY_CATEGORY_ICON_WIFI); /* Allowed overlay categories for each target package. */ private final Map> mTargetPackageToCategories = new ArrayMap<>(); @@ -177,6 +187,8 @@ public ThemeOverlayApplier(OverlayManager overlayManager, mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_ANDROID, ANDROID_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SYSUI, SYSUI_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SETTINGS, SETTINGS_PACKAGE); + mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SIGNAL, SYSUI_PACKAGE); + mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_WIFI, SYSUI_PACKAGE); dumpManager.registerDumpable(TAG, this); } diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index b80bf940d3296..efe9629927913 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -68,6 +68,8 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import com.android.internal.graphics.cam.Cam; +import com.android.internal.graphics.ColorUtils; import com.android.systemui.CoreStartable; import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -92,6 +94,7 @@ import com.google.ux.material.libmonet.dynamiccolor.DynamicColor; import com.google.ux.material.libmonet.dynamiccolor.MaterialDynamicColors; +import com.google.ux.material.libmonet.hct.Cam16; import kotlinx.coroutines.flow.Flow; import kotlinx.coroutines.flow.StateFlow; @@ -156,6 +159,8 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { protected int mMainWallpaperColor = Color.TRANSPARENT; // UI contrast as reported by UiModeManager private double mContrast = 0.0; + private double mChromaBoost = 0.0; + private boolean mIsFidelityEnabled = true; // Theme variant: Vibrant, Tonal, Expressive, etc @VisibleForTesting @ThemeStyle.Type @@ -490,6 +495,7 @@ public void start() { mThemeController.observeSettings(() -> reevaluateSystemTheme(true)); mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter, mMainExecutor, UserHandle.ALL); + int userId = mUserTracker.getUserId(); mSecureSettings.registerContentObserverForUserSync( Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, false, @@ -515,19 +521,6 @@ public void onChange(boolean selfChange, Collection collection, int flags, } }, UserHandle.USER_ALL); - int userId = mUserTracker.getUserId(); - if (fixContrastAndForceInvertStateForMultiUser()) { - UiModeManager uiModeManager = mUiModeManagerProvider.forUser(UserHandle.of(userId)); - uiModeManager.addContrastChangeListener(mMainExecutor, mContrastChangeListener); - mContrast = uiModeManager.getContrast(); - } else { - mContrast = mUiModeManager.getContrast(); - mUiModeManager.addContrastChangeListener(mMainExecutor, contrast -> { - mContrast = contrast; - // Force reload so that we update even when the main color has not changed - reevaluateSystemTheme(true /* forceReload */); - }); - } mSecureSettings.registerContentObserverForUserSync( LineageSettings.Secure.getUriFor(LineageSettings.Secure.BERRY_BLACK_THEME), @@ -672,6 +665,7 @@ private void reevaluateSystemTheme(boolean forceReload) { mMainWallpaperColor = mainColor; if (mIsMonetEnabled) { + fetchCustomThemeSettings(); mThemeStyle = fetchThemeStyleFromSetting(); createOverlays(mMainWallpaperColor); mNeedsOverlayCreation = true; @@ -684,6 +678,28 @@ private void reevaluateSystemTheme(boolean forceReload) { updateThemeOverlays(); } + private void fetchCustomThemeSettings() { + final String overlayPackageJson = mSecureSettings.getStringForUser( + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, + mUserTracker.getUserId()); + if (!TextUtils.isEmpty(overlayPackageJson)) { + try { + JSONObject object = new JSONObject(overlayPackageJson); + mContrast = object.optDouble("_contrast_level", 0.0); + mChromaBoost = object.optDouble("_chroma_boost", 0.0); + mIsFidelityEnabled = object.optBoolean("_fidelity_enabled", true); + if (DEBUG) { + Log.d(TAG, "Custom theme settings: contrast=" + mContrast + + " chromaBoost=" + mChromaBoost + + " fidelity=" + mIsFidelityEnabled); + } + } catch (JSONException e) { + Log.w(TAG, "Failed to parse custom theme settings.", e); + } + } + } + + /** * Return the main theme color from a given {@link WallpaperColors} instance. */ @@ -713,8 +729,12 @@ protected boolean isPrivateProfile(UserHandle userHandle) { } private void createOverlays(int color) { - mDarkColorScheme = new ColorScheme(color, true /* isDark */, mThemeStyle, mContrast); - mLightColorScheme = new ColorScheme(color, false /* isDark */, mThemeStyle, mContrast); + int style = mThemeStyle; + if (mIsFidelityEnabled) { + style = ThemeStyle.CONTENT; + } + mDarkColorScheme = new ColorScheme(color, true /* isDark */, style, mContrast); + mLightColorScheme = new ColorScheme(color, false /* isDark */, style, mContrast); mColorScheme = isNightMode() ? mDarkColorScheme : mLightColorScheme; mAccentOverlay = newFabricatedOverlay("accent"); @@ -734,20 +754,46 @@ private void createOverlays(int color) { private void assignColorsToOverlay(FabricatedOverlay overlay, List> colors, Boolean isFixed) { - colors.forEach(p -> { + for (Pair p : colors) { + String prefix = "android:color/system_" + p.first; if (isFixed) { - overlay.setResourceValue(prefix, TYPE_INT_COLOR_ARGB8, - p.second.getArgb(mLightColorScheme.getMaterialScheme()), null); - return; + int original = p.second.getArgb(mLightColorScheme.getMaterialScheme()); + int boosted = boostChroma(original); + overlay.setResourceValue(prefix, TYPE_INT_COLOR_ARGB8, boosted, null); + continue; } - overlay.setResourceValue(prefix + "_light", TYPE_INT_COLOR_ARGB8, - p.second.getArgb(mLightColorScheme.getMaterialScheme()), null); - overlay.setResourceValue(prefix + "_dark", TYPE_INT_COLOR_ARGB8, - p.second.getArgb(mDarkColorScheme.getMaterialScheme()), null); - }); + int lightOriginal = p.second.getArgb(mLightColorScheme.getMaterialScheme()); + int darkOriginal = p.second.getArgb(mDarkColorScheme.getMaterialScheme()); + + int boostedLight = boostChroma(lightOriginal); + int boostedDark = boostChroma(darkOriginal); + + overlay.setResourceValue(prefix + "_light", + TYPE_INT_COLOR_ARGB8, boostedLight, null); + + overlay.setResourceValue(prefix + "_dark", + TYPE_INT_COLOR_ARGB8, boostedDark, null); + } + } + + private int boostChroma(int argb) { + Cam cam = Cam.fromInt(argb); + + final float chromaBoost = (float) mChromaBoost; + + float boostedChroma = cam.getChroma() * (1f + chromaBoost/ 100f); + boostedChroma = Math.min(boostedChroma, 150f); + + int boosted = ColorUtils.CAMToColor( + cam.getHue(), + boostedChroma, + cam.getJ() + ); + + return ColorUtils.setAlphaComponent(boosted, Color.alpha(argb)); } /** @@ -803,6 +849,10 @@ private void updateThemeOverlays() { if (!TextUtils.isEmpty(overlayPackageJson)) { try { JSONObject object = new JSONObject(overlayPackageJson); + + mContrast = object.optDouble("_contrast_level", 0.0); + mChromaBoost = object.optDouble("_chroma_boost", 0.0); + for (String category : ThemeOverlayApplier.THEME_CATEGORIES) { if (object.has(category)) { OverlayIdentifier identifier = @@ -921,7 +971,7 @@ private int fetchThemeStyleFromSetting() { try { JSONObject object = new JSONObject(overlayPackageJson); style = ThemeStyle.valueOf( - object.getString(OVERLAY_CATEGORY_THEME_STYLE)); + object.optString(OVERLAY_CATEGORY_THEME_STYLE, ThemeStyle.name(ThemeStyle.TONAL_SPOT))); if (!validStyles.contains(style)) { style = ThemeStyle.TONAL_SPOT; } diff --git a/packages/SystemUI/src/com/android/systemui/usb/UsbFunctionActivity.kt b/packages/SystemUI/src/com/android/systemui/usb/UsbFunctionActivity.kt new file mode 100644 index 0000000000000..f297710f5f6e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/usb/UsbFunctionActivity.kt @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2024 Paranoid Android + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.usb + +import android.app.AlertDialog +import android.content.BroadcastReceiver +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.hardware.usb.UsbManager +import android.net.TetheringManager +import android.os.Bundle +import android.os.Handler +import android.os.HandlerExecutor +import android.os.Looper +import android.os.UserHandle +import android.os.UserManager +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS +import android.widget.ArrayAdapter +import android.widget.CheckedTextView +import com.android.internal.app.AlertActivity +import com.android.internal.app.AlertController.AlertParams +import com.android.systemui.res.R +import com.android.systemui.broadcast.BroadcastDispatcher +import javax.inject.Inject + +class UsbFunctionActivity @Inject constructor( + private val broadcastDispatcher: BroadcastDispatcher, +): AlertActivity(), DialogInterface.OnClickListener { + + private lateinit var usbManager: UsbManager + private lateinit var userManager: UserManager + private lateinit var tetheringManager: TetheringManager + + private var tetheringSupported = false + private var midiSupported = false + private val uvcEnabled = UsbManager.isUvcSupportEnabled() + + private val handler = Handler(Looper.getMainLooper()) + private val executor = HandlerExecutor(handler) + + private var previousFunctions: Long = 0L + private val tetheringCallback = object : TetheringManager.StartTetheringCallback { + override fun onTetheringFailed(error: Int) { + Log.w(TAG, "onTetheringFailed() error : $error") + usbManager.setCurrentFunctions(previousFunctions) + } + } + + private val usbReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != UsbManager.ACTION_USB_STATE) { + return + } + + val connected = intent.getBooleanExtra(UsbManager.USB_CONNECTED, false) + if (!connected) { + dlog("usb disconnected, goodbye") + finish() + } + } + } + + private lateinit var adapter: UsbFunctionAdapter + private lateinit var supportedFunctions: List + + override fun onCreate(savedInstanceState: Bundle?) { + dlog("onCreate()") + super.onCreate(savedInstanceState) + + window.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS) + + usbManager = getSystemService(UsbManager::class.java) ?: return + userManager = getSystemService(UserManager::class.java) ?: return + tetheringManager = getSystemService(TetheringManager::class.java) ?: return + + tetheringSupported = tetheringManager.isTetheringSupported() + midiSupported = packageManager.hasSystemFeature(PackageManager.FEATURE_MIDI) + + supportedFunctions = getSupportedFunctions() + adapter = UsbFunctionAdapter(this, supportedFunctions) + + mAlertParams.apply { + mAdapter = adapter + mOnClickListener = this@UsbFunctionActivity + mTitle = getString(R.string.usb_use) + mIsSingleChoice = true + mCheckedItem = supportedFunctions.indexOf(getCurrentFunction()) + mPositiveButtonText = getString(com.android.internal.R.string.done_label) + mPositiveButtonListener = this@UsbFunctionActivity + mNeutralButtonText = getString(com.android.internal.R.string.more_item_label) + mNeutralButtonListener = this@UsbFunctionActivity + } + dlog("mCheckedItem=${mAlertParams.mCheckedItem}") + + setupAlert() + mAlert.listView?.requestFocus() + + broadcastDispatcher.registerReceiver( + usbReceiver, + IntentFilter(UsbManager.ACTION_USB_STATE) + ) + } + + override fun onDestroy() { + broadcastDispatcher.unregisterReceiver(usbReceiver) + super.onDestroy() + } + + override fun onClick(dialog: DialogInterface, which: Int) { + dlog("onClick: which = $which") + when (which) { + AlertDialog.BUTTON_POSITIVE -> finish() + AlertDialog.BUTTON_NEUTRAL -> { + val intent = Intent() + .setClassName( + "com.android.settings", + "com.android.settings.Settings\$UsbDetailsActivity" + ) + runCatching { + startActivityAsUser(intent, UserHandle.CURRENT) + finish() + }.onFailure { e -> + Log.e(TAG, "unable to start activity $intent" , e); + } + } + else -> { + adapter.getItem(which)?.let { setCurrentFunction(it) } + // adapter.notifyDataSetChanged() + finish() + } + } + } + + private fun getSupportedFunctions() = ALL_FUNCTIONS + .filter { function -> areFunctionsSupported(function.mask) } + + private fun getCurrentFunction(): UsbFunction { + var currentFunctions = usbManager.getCurrentFunctions().also { + Log.d(TAG, "current usb functions: $it (${UsbManager.usbFunctionsToString(it)})") + } + + if ((currentFunctions and UsbManager.FUNCTION_ACCESSORY) != 0L) { + currentFunctions = UsbManager.FUNCTION_MTP + } else if (currentFunctions == UsbManager.FUNCTION_NCM) { + currentFunctions = UsbManager.FUNCTION_RNDIS + } + + return supportedFunctions + .find { it -> it.mask == currentFunctions } + ?: NONE_FUNCTION + } + + private fun setCurrentFunction(function: UsbFunction) { + if (isClickEventIgnored(function.mask)) { + dlog("setCurrentFunction ignored for $function") + return + } + + dlog("setCurrentFunction: $function") + when (function.mask) { + UsbManager.FUNCTION_RNDIS -> { + previousFunctions = usbManager.getCurrentFunctions() + tetheringManager.startTethering( + TetheringManager.TETHERING_USB, + executor, + tetheringCallback + ) + } + else -> usbManager.setCurrentFunctions(function.mask) + } + } + + // Below functions are replicated from com.android.settings.connecteddevice.usb.UsbBackend + + private fun isClickEventIgnored(function: Long): Boolean { + val currentFunctions = usbManager.getCurrentFunctions() + return (currentFunctions and UsbManager.FUNCTION_ACCESSORY) != 0L + && function == UsbManager.FUNCTION_MTP + } + + private fun areFunctionsSupported(functions: Long): Boolean { + if ((!midiSupported && (functions and UsbManager.FUNCTION_MIDI) != 0L) + || (!tetheringSupported && (functions and UsbManager.FUNCTION_RNDIS) != 0L)) { + return false + } + return !(areFunctionDisallowed(functions) || areFunctionsDisallowedBySystem(functions) + || areFunctionsDisallowedByNonAdminUser(functions)) + } + + private fun isUsbFileTransferRestricted(): Boolean { + return userManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER) + } + + private fun isUsbTetheringRestricted(): Boolean { + return userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING) + } + + private fun isUsbFileTransferRestrictedBySystem(): Boolean { + return userManager.hasBaseUserRestriction( + UserManager.DISALLOW_USB_FILE_TRANSFER, + UserHandle.of(UserHandle.myUserId()) + ) + } + + private fun isUsbTetheringRestrictedBySystem(): Boolean { + return userManager.hasBaseUserRestriction( + UserManager.DISALLOW_CONFIG_TETHERING, + UserHandle.of(UserHandle.myUserId()) + ) + } + + private fun areFunctionDisallowed(functions: Long): Boolean { + return (isUsbFileTransferRestricted() && ((functions and UsbManager.FUNCTION_MTP) != 0L + || (functions and UsbManager.FUNCTION_PTP) != 0L)) + || (isUsbTetheringRestricted() && ((functions and UsbManager.FUNCTION_RNDIS) != 0L)) + } + + private fun areFunctionsDisallowedBySystem(functions: Long): Boolean { + return (isUsbFileTransferRestrictedBySystem() && ((functions and UsbManager.FUNCTION_MTP) != 0L + || (functions and UsbManager.FUNCTION_PTP) != 0L)) + || (isUsbTetheringRestrictedBySystem() && ((functions and UsbManager.FUNCTION_RNDIS) != 0L)) + || (!uvcEnabled && ((functions and UsbManager.FUNCTION_UVC) != 0L)) + } + + private fun areFunctionsDisallowedByNonAdminUser(functions: Long): Boolean { + return !userManager.isAdminUser() && (functions and UsbManager.FUNCTION_RNDIS) != 0L + } + + private companion object { + const val TAG = "UsbFunctionActivity" + + val NONE_FUNCTION = UsbFunction( + UsbManager.FUNCTION_NONE, + "", + R.string.usb_use_charging_only + ) + + val ALL_FUNCTIONS = listOf( + UsbFunction( + UsbManager.FUNCTION_MTP, + UsbManager.USB_FUNCTION_MTP, + R.string.usb_use_file_transfers + ), + UsbFunction( + UsbManager.FUNCTION_RNDIS, + UsbManager.USB_FUNCTION_RNDIS, + R.string.usb_use_tethering + ), + UsbFunction( + UsbManager.FUNCTION_MIDI, + UsbManager.USB_FUNCTION_MIDI, + R.string.usb_use_MIDI + ), + UsbFunction( + UsbManager.FUNCTION_PTP, + UsbManager.USB_FUNCTION_PTP, + R.string.usb_use_photo_transfers + ), + UsbFunction( + UsbManager.FUNCTION_UVC, + UsbManager.USB_FUNCTION_UVC, + R.string.usb_use_uvc_webcam + ), + NONE_FUNCTION + ) + + fun dlog(msg: String) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, msg) + } + } + } +} + +private data class UsbFunction( + val mask: Long, + val name: String, + val descriptionResId: Int +) + +private class UsbFunctionAdapter( + private val context: Context, + private val items: List, +) : ArrayAdapter( + context, + com.android.internal.R.layout.select_dialog_singlechoice_material, + items +) { + + private val inflater = LayoutInflater.from(context) + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = convertView ?: inflater.inflate( + com.android.internal.R.layout.select_dialog_singlechoice_material, + parent, + false + ) + + val textView = view as CheckedTextView + val function = getItem(position) + textView.text = context.getString( + function?.descriptionResId ?: com.android.internal.R.string.unknownName + ) + + // required for listview to trigger onclick + view.focusable = View.NOT_FOCUSABLE + view.setClickable(false) + + return view + } + + private companion object { + const val TAG = "UsbFunctionAdapter" + + fun dlog(msg: String) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, msg) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/ArcProgressWidget.java b/packages/SystemUI/src/com/android/systemui/util/ArcProgressWidget.java new file mode 100644 index 0000000000000..ed5485b6d5e07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/ArcProgressWidget.java @@ -0,0 +1,83 @@ +package com.android.systemui.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BlendMode; +import android.graphics.BlendModeColorFilter; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import androidx.annotation.Nullable; + +public class ArcProgressWidget { + public static Bitmap generateBitmap(Context context, int percentage, String textInside, int textInsideSizePx, @Nullable String textBottom, int textBottomSizePx, @Nullable String tf) { + return generateBitmap(context, percentage, textInside, textInsideSizePx, null, 28, textBottom, textBottomSizePx, tf); + } + + public static Bitmap generateBitmap(Context context, int percentage, String textInside, int textInsideSizePx, @Nullable Drawable iconDrawable, int iconSizePx, @Nullable String tf) { + return generateBitmap(context, percentage, textInside, textInsideSizePx, iconDrawable, iconSizePx, "Usage", 28, tf); + } + + public static Bitmap generateBitmap(Context context, + int percentage, + String textInside, + int textInsideSizePx, + @Nullable Drawable iconDrawable, + int iconSizePx, + @Nullable String textBottom, + int textBottomSizePx, + @Nullable String tf) { + int width = 400; + int height = 400; + int stroke = 40; + int padding = 5; + int minAngle = 135; + int maxAngle = 275; + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG | Paint.ANTI_ALIAS_FLAG); + paint.setStrokeWidth(stroke); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Paint.Cap.ROUND); + Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setTextSize(dp2px(context, textInsideSizePx)); + mTextPaint.setColor(Color.WHITE); + mTextPaint.setTextAlign(Paint.Align.CENTER); + final RectF arc = new RectF(); + arc.set(((float) stroke / 2) + padding, ((float) stroke / 2) + padding, width - padding - ((float) stroke / 2), height - padding - ((float) stroke / 2)); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + paint.setColor(Color.argb(75, 255, 255, 255)); + canvas.drawArc(arc, minAngle, maxAngle, false, paint); + paint.setColor(Color.WHITE); + canvas.drawArc(arc, minAngle, ((float) maxAngle / 100) * percentage, false, paint); + if (tf != null) { + mTextPaint.setTypeface(Typeface.create(tf, Typeface.BOLD)); + } else { + mTextPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); + } + canvas.drawText(textInside, (float) bitmap.getWidth() / 2, (bitmap.getHeight() - mTextPaint.ascent() * 0.7f) / 2, mTextPaint); + if (iconDrawable != null) { + int size = dp2px(context, iconSizePx); + int left = (bitmap.getWidth() - size) / 2; + int top = bitmap.getHeight() - (int) (size / 1.3) - (stroke + padding) - dp2px(context, 4); + int right = left + size; + int bottom = top + size; + iconDrawable.setBounds(left, top, right, bottom); + iconDrawable.setColorFilter(new BlendModeColorFilter(Color.WHITE, BlendMode.SRC_IN)); + iconDrawable.draw(canvas); + } else if (textBottom != null) { + mTextPaint.setTextSize(dp2px(context, textBottomSizePx)); + canvas.drawText(textBottom, (float) bitmap.getWidth() / 2, bitmap.getHeight() - (stroke + padding), mTextPaint); + } + return bitmap; + } + + private static int dp2px(Context context, float dp) { + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, displayMetrics); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/MediaSessionManagerHelper.kt b/packages/SystemUI/src/com/android/systemui/util/MediaSessionManagerHelper.kt new file mode 100644 index 0000000000000..1e416e40a0171 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/MediaSessionManagerHelper.kt @@ -0,0 +1,263 @@ +/* +* Copyright (C) 2023-2024 The risingOS Android Project +* Copyright (C) 2025 The AxionAOSP Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS 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 + +import android.content.Context +import android.content.res.Configuration +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSessionLegacyHelper +import android.media.session.MediaSessionManager +import android.media.session.PlaybackState +import android.os.SystemClock +import android.provider.Settings +import android.text.TextUtils +import android.view.KeyEvent +import android.view.View + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +class MediaSessionManagerHelper private constructor(private val context: Context) { + + interface MediaMetadataListener { + fun onMediaMetadataChanged() {} + fun onPlaybackStateChanged() {} + } + + private val _mediaMetadata = MutableStateFlow(null) + val mediaMetadata: StateFlow = _mediaMetadata + + private val _playbackState = MutableStateFlow(null) + val playbackState: StateFlow = _playbackState + + private val scope = CoroutineScope(Dispatchers.Main) + private var collectJob: Job? = null + + private var lastSavedPackageName: String? = null + private val mediaSessionManager: MediaSessionManager = context.getSystemService(MediaSessionManager::class.java)!! + private var activeController: MediaController? = null + private val listeners = mutableSetOf() + + private val mediaControllerCallback = object : MediaController.Callback() { + override fun onMetadataChanged(metadata: MediaMetadata?) { + _mediaMetadata.value = metadata + } + + override fun onPlaybackStateChanged(state: PlaybackState?) { + _playbackState.value = state + } + } + + private val tickerFlow = flow { + while (true) { + emit(Unit) + delay(1000) + } + }.flowOn(Dispatchers.Default) + + init { + lastSavedPackageName = Settings.System.getString( + context.contentResolver, + "media_session_last_package_name" + ) + + scope.launch { + tickerFlow + .map { fetchActiveController() } + .distinctUntilChanged { old, new -> sameSessions(old, new) } + .collect { controller -> + activeController?.unregisterCallback(mediaControllerCallback) + activeController = controller + controller?.registerCallback(mediaControllerCallback) + _mediaMetadata.value = controller?.metadata + _playbackState.value = controller?.playbackState + saveLastNonNullPackageName() + } + } + } + + private suspend fun fetchActiveController(): MediaController? = withContext(Dispatchers.IO) { + var localController: MediaController? = null + val remoteSessions = mutableSetOf() + + mediaSessionManager.getActiveSessions(null) + .filter { controller -> + controller.playbackState?.state == PlaybackState.STATE_PLAYING && + controller.playbackInfo != null + } + .forEach { controller -> + when (controller.playbackInfo?.playbackType) { + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> { + remoteSessions.add(controller.packageName) + if (localController?.packageName == controller.packageName) { + localController = null + } + } + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { + if (!remoteSessions.contains(controller.packageName)) { + localController = localController ?: controller + } + } + } + } + localController + } + + fun addMediaMetadataListener(listener: MediaMetadataListener) { + listeners.add(listener) + if (listeners.size == 1) { + startCollecting() + } + listener.onMediaMetadataChanged() + listener.onPlaybackStateChanged() + } + + fun removeMediaMetadataListener(listener: MediaMetadataListener) { + listeners.remove(listener) + if (listeners.isEmpty()) { + stopCollecting() + } + } + + private fun startCollecting() { + collectJob = scope.launch { + launch { mediaMetadata.collect { notifyListeners { onMediaMetadataChanged() } } } + launch { playbackState.collect { notifyListeners { onPlaybackStateChanged() } } } + } + } + + private fun stopCollecting() { + collectJob?.cancel() + collectJob = null + } + + private fun notifyListeners(action: MediaMetadataListener.() -> Unit) { + listeners.forEach { it.action() } + } + + fun seekTo(time: Long) { + activeController?.transportControls?.seekTo(time) + } + + fun getTotalDuration() = mediaMetadata.value?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L + + private fun saveLastNonNullPackageName() { + activeController?.packageName?.takeIf { it.isNotEmpty() }?.let { pkg -> + if (pkg != lastSavedPackageName) { + Settings.System.putString( + context.contentResolver, + "media_session_last_package_name", + pkg + ) + lastSavedPackageName = pkg + } + } + } + + fun getMediaBitmap(): Bitmap? = mediaMetadata.value?.let { + it.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) ?: + it.getBitmap(MediaMetadata.METADATA_KEY_ART) ?: + it.getBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON) + } + + fun getCurrentMediaMetadata(): MediaMetadata? { + return mediaMetadata.value + } + + fun getMediaAppIcon(): Drawable? { + val packageName = activeController?.packageName ?: return null + return try { + val pm = context.packageManager + pm.getApplicationIcon(packageName) + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + fun isMediaControllerAvailable() = activeController?.packageName?.isNotEmpty() ?: false + + fun isMediaPlaying() = playbackState.value?.state == PlaybackState.STATE_PLAYING + + fun getMediaControllerPlaybackState(): PlaybackState? { + return activeController?.playbackState ?: null + } + + private fun sameSessions(a: MediaController?, b: MediaController?): Boolean { + if (a == b) return true + if (a == null) return false + return a.controlsSameSession(b) + } + + private fun dispatchMediaKeyWithWakeLockToMediaSession(keycode: Int) { + val helper = MediaSessionLegacyHelper.getHelper(context) ?: return + var event = KeyEvent( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + KeyEvent.ACTION_DOWN, + keycode, + 0 + ) + helper.sendMediaButtonEvent(event, true) + event = KeyEvent.changeAction(event, KeyEvent.ACTION_UP) + helper.sendMediaButtonEvent(event, true) + } + + fun prevSong() { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PREVIOUS) + } + + fun nextSong() { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_NEXT) + } + + fun toggleMediaPlaybackState() { + if (isMediaPlaying()) { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PAUSE) + } else { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PLAY) + } + } + + fun launchMediaApp() { + lastSavedPackageName?.takeIf { it.isNotEmpty() }?.let { + launchMediaPlayerApp(it) + } + } + + fun launchMediaPlayerApp(packageName: String) { + if (packageName.isNotEmpty()) { + val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) + launchIntent?.let { intent -> + context.startActivity(intent) + } + } + } + + companion object { + @Volatile + private var instance: MediaSessionManagerHelper? = null + + fun getInstance(context: Context): MediaSessionManagerHelper = + instance ?: synchronized(this) { + instance ?: MediaSessionManagerHelper(context).also { instance = it } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/ProgressImageView.kt b/packages/SystemUI/src/com/android/systemui/util/ProgressImageView.kt new file mode 100644 index 0000000000000..1f9edfd320833 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/ProgressImageView.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2023-2024 the risingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 + +import android.app.ActivityManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.ContentResolver +import android.content.Intent +import android.content.IntentFilter +import android.database.ContentObserver +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.media.AudioManager +import android.os.BatteryManager +import android.provider.Settings +import android.util.AttributeSet +import android.widget.ImageView +import androidx.core.content.ContextCompat +import kotlinx.coroutines.* + +import com.android.systemui.res.R + +class ProgressImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ImageView(context, attrs, defStyleAttr) { + + private var progressType: ProgressType = ProgressType.UNKNOWN + private var progressPercent = -1 + private var batteryLevel = -1 + private var batteryTemperature = -1 + private var updateJob: Job? = null + private var receiverRegistered = false + private var typeface: String? = null + + private val batteryReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == Intent.ACTION_BATTERY_CHANGED) { + batteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + batteryLevel = batteryLevel.coerceIn(0, 100) + batteryTemperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) / 10 + updateProgress() + } + } + } + + private val settingsObserver = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + updateVisibility() + } + } + + enum class ProgressType(val iconRes: Int) { + BATTERY(R.drawable.ic_battery), + MEMORY(R.drawable.ic_memory), + TEMPERATURE(R.drawable.ic_temperature), + VOLUME(R.drawable.ic_volume_eq), + UNKNOWN(-1) + } + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressImageView, defStyleAttr, 0) + typeface = typedArray.getString(R.styleable.ProgressImageView_typeface) + typedArray.recycle() + when (id) { + R.id.battery_progress -> progressType = ProgressType.BATTERY + R.id.memory_progress -> progressType = ProgressType.MEMORY + R.id.temperature_progress -> progressType = ProgressType.TEMPERATURE + R.id.volume_progress -> progressType = ProgressType.VOLUME + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + context.contentResolver.registerContentObserver( + Settings.System.getUriFor("lockscreen_info_widgets_enabled"), + false, + settingsObserver + ) + if (!receiverRegistered) { + if (progressType == ProgressType.BATTERY || progressType == ProgressType.TEMPERATURE) { + val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + context.registerReceiver(batteryReceiver, filter, Context.RECEIVER_EXPORTED) + receiverRegistered = true + } + } + startProgressUpdates() + updateVisibility() + updateProgress() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + context.contentResolver.unregisterContentObserver(settingsObserver) + if (receiverRegistered) { + context.unregisterReceiver(batteryReceiver) + } + stopProgressUpdates() + } + + private fun startProgressUpdates() { + if (progressType == ProgressType.MEMORY || progressType == ProgressType.VOLUME) { + updateJob = CoroutineScope(Dispatchers.Main).launch { + while (isActive) { + updateProgress() + delay(1000L) + } + } + } + } + + private fun stopProgressUpdates() { + updateJob?.cancel() + } + + private fun updateProgress() { + val newProgressPercent = when (progressType) { + ProgressType.BATTERY -> batteryLevel + ProgressType.MEMORY -> getMemoryLevel() + ProgressType.TEMPERATURE -> batteryTemperature + ProgressType.VOLUME -> getVolumeLevel() + ProgressType.UNKNOWN -> -1 + } + if (newProgressPercent != progressPercent) { + progressPercent = newProgressPercent + updateImageView() + } + } + + private fun updateImageView() { + val degree = "\u2103" + val progressText = if (progressType == ProgressType.TEMPERATURE) { + if (progressPercent != -1) "$progressPercent$degree" else "N/A" + } else { + if (progressPercent == -1) "..." else "$progressPercent%" + } + val icon: Drawable? = ContextCompat.getDrawable(context, progressType.iconRes) + val widgetBitmap: Bitmap = ArcProgressWidget.generateBitmap( + context, + if (progressPercent == -1) 0 else progressPercent, + progressText, + 40, + icon, + 36, + typeface + ) + setImageBitmap(widgetBitmap) + } + + private fun getMemoryLevel(): Int { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val memoryInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + val usedMemory = memoryInfo.totalMem - memoryInfo.availMem + val usedMemoryPercentage = ((usedMemory * 100) / memoryInfo.totalMem).toInt() + return usedMemoryPercentage.coerceIn(0, 100) + } + + private fun getVolumeLevel(): Int { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager + val maxVolume = audioManager?.getStreamMaxVolume(AudioManager.STREAM_MUSIC) ?: 1 + val currentVolume = audioManager?.getStreamVolume(AudioManager.STREAM_MUSIC) ?: 0 + return ((currentVolume * 100) / maxVolume).coerceIn(0, 100) + } + + private fun updateVisibility() { + val enabled = Settings.System.getInt( + context.contentResolver, + "lockscreen_info_widgets_enabled", 0 + ) == 1 + visibility = if (enabled) VISIBLE else GONE + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt index 6dfca92226ab0..69ad69ef33103 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt @@ -129,7 +129,7 @@ object SysUIConcurrencyModule { ): UiThreadContext { return if (Flags.edgeBackGestureHandlerThread()) { val thread = - HandlerThread("BackPanelUiThread", Process.THREAD_PRIORITY_DISPLAY).apply { + HandlerThread("BackPanelUiThread", Process.THREAD_PRIORITY_URGENT_DISPLAY).apply { start() looper.setSlowLogThresholdMs( LONG_SLOW_DISPATCH_THRESHOLD, diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 5a8d9e0110aa8..c913a3d6934bf 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -23,11 +23,13 @@ import static android.media.AudioManager.STREAM_ACCESSIBILITY; import static android.media.AudioManager.STREAM_ALARM; import static android.media.AudioManager.STREAM_MUSIC; +import static android.media.AudioManager.STREAM_NOTIFICATION; import static android.media.AudioManager.STREAM_RING; import static android.media.AudioManager.STREAM_VOICE_CALL; import static android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE; import static android.view.View.GONE; import static android.view.View.INVISIBLE; +import static android.view.View.LAYOUT_DIRECTION_LTR; import static android.view.View.LAYOUT_DIRECTION_RTL; import static android.view.View.VISIBLE; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; @@ -46,6 +48,8 @@ import android.app.ActivityManager; import android.app.Dialog; import android.app.KeyguardManager; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothProfile; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; @@ -54,6 +58,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; +import android.database.ContentObserver; import android.graphics.Color; import android.graphics.Outline; import android.graphics.PixelFormat; @@ -68,6 +73,9 @@ import android.media.AppVolume; import android.media.AudioManager; import android.media.AudioSystem; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; import android.os.Debug; import android.os.Handler; import android.os.Looper; @@ -79,6 +87,7 @@ import android.provider.Settings; import android.provider.Settings.Global; import android.text.InputFilter; +import android.text.TextUtils; import android.util.Log; import android.util.Slog; import android.util.SparseBooleanArray; @@ -99,6 +108,7 @@ import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -135,6 +145,7 @@ import com.android.systemui.plugins.VolumeDialogController.StreamState; import com.android.systemui.res.R; import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.phone.ExpandableIndicator; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; @@ -144,12 +155,12 @@ import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.volume.domain.interactor.VolumeDialogInteractor; import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor; -import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag; import com.android.systemui.volume.ui.navigation.VolumeNavigator; import com.google.android.msdl.domain.MSDLPlayer; import dagger.Lazy; +import lineageos.providers.LineageSettings; import java.io.PrintWriter; import java.util.ArrayList; @@ -273,6 +284,8 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private ImageButton mSettingsIcon; private View mAppVolumeView; private ImageButton mAppVolumeIcon; + private View mExpandRowsView; + private ExpandableIndicator mExpandRows; private final List mRows = new ArrayList<>(); private ConfigurableTexts mConfigurableTexts; private final SparseBooleanArray mDynamic = new SparseBooleanArray(); @@ -305,6 +318,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private ViewStub mODICaptionsTooltipViewStub; @VisibleForTesting View mODICaptionsTooltipView = null; + // Volume panel placement left or right + private boolean mVolumePanelOnLeft; + private final boolean mUseBackgroundBlur; private Consumer mCrossWindowBlurEnabledListener; private BackgroundBlurDrawable mDialogRowsViewBackground; @@ -312,6 +328,17 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private int mWindowGravity; + // Variable to track the default row with which the panel is initially shown + private VolumeRow mDefaultRow = null; + + private FrameLayout mRoundedBorderBottom; + + // Volume panel expand state + private boolean mExpanded; + + // Number of animating rows + private int mAnimatingRows = 0; + @VisibleForTesting final int mVolumeRingerIconDrawableId = R.drawable.ic_legacy_speaker_on; @VisibleForTesting @@ -327,7 +354,6 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private final VibratorHelper mVibratorHelper; private final MSDLPlayer mMSDLPlayer; private final com.android.systemui.util.time.SystemClock mSystemClock; - private final VolumePanelFlag mVolumePanelFlag; private final VolumeDialogInteractor mInteractor; // Optional actions for soundDose private Optional> @@ -347,7 +373,6 @@ public VolumeDialogImpl( CsdWarningDialog.Factory csdWarningDialogFactory, DevicePostureController devicePostureController, Looper looper, - VolumePanelFlag volumePanelFlag, DumpManager dumpManager, Lazy secureSettings, VibratorHelper vibratorHelper, @@ -387,7 +412,6 @@ public VolumeDialogImpl( mVolumeNavigator = volumeNavigator; mSecureSettings = secureSettings; mDialogTimeoutMillis = DIALOG_TIMEOUT_MILLIS; - mVolumePanelFlag = volumePanelFlag; mInteractor = interactor; dumpManager.registerDumpable("VolumeDialogImpl", this); @@ -404,6 +428,25 @@ public VolumeDialogImpl( }; } + if (!mIsTv) { + ContentObserver volumePanelOnLeftObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + final boolean volumePanelOnLeft = LineageSettings.Secure.getInt( + mContext.getContentResolver(), + LineageSettings.Secure.VOLUME_PANEL_ON_LEFT, 0) != 0; + if (mVolumePanelOnLeft != volumePanelOnLeft) { + mVolumePanelOnLeft = volumePanelOnLeft; + mHandler.post(mControllerCallbackH::onConfigurationChanged); + } + } + }; + mContext.getContentResolver().registerContentObserver( + LineageSettings.Secure.getUriFor(LineageSettings.Secure.VOLUME_PANEL_ON_LEFT), + false, volumePanelOnLeftObserver); + volumePanelOnLeftObserver.onChange(true); + } + initDimens(); mOrientation = mContext.getResources().getConfiguration().orientation; @@ -494,27 +537,75 @@ private void unionViewBoundstoTouchableRegion(final View view) { final int[] locInWindow = new int[2]; view.getLocationInWindow(locInWindow); - float x = locInWindow[0]; - float y = locInWindow[1]; + float xExtraSize = 0; + float yExtraSize = 0; // The ringer and rows container has extra height at the top to fit the expanded ringer // drawer. This area should not be touchable unless the ringer drawer is open. // In landscape the ringer expands to the left and it has to be ensured that if there // are multiple rows they are touchable. - if (view == mTopContainer && !mIsRingerDrawerOpen) { + // The invisible expandable rows reserve space if the panel is not expanded, this space + // needs to be touchable. + if (view == mTopContainer) { if (!isLandscape()) { - y += getRingerDrawerOpenExtraSize(); - } else if (getRingerDrawerOpenExtraSize() > getVisibleRowsExtraSize()) { - x += (getRingerDrawerOpenExtraSize() - getVisibleRowsExtraSize()); + if (!mIsRingerDrawerOpen) { + yExtraSize = getRingerDrawerOpenExtraSize(); + } + if (!mExpanded) { + xExtraSize = getExpandableRowsExtraSize(); + } + } else { + if (!mIsRingerDrawerOpen && !mExpanded) { + xExtraSize = + Math.max(getRingerDrawerOpenExtraSize(), getExpandableRowsExtraSize()); + } else if (!mIsRingerDrawerOpen) { + if (getRingerDrawerOpenExtraSize() > getVisibleRowsExtraSize()) { + xExtraSize = getRingerDrawerOpenExtraSize() - getVisibleRowsExtraSize(); + } + } else if (!mExpanded) { + if ((getVisibleRowsExtraSize() + getExpandableRowsExtraSize()) + > getRingerDrawerOpenExtraSize()) { + xExtraSize = (getVisibleRowsExtraSize() + getExpandableRowsExtraSize()) + - getRingerDrawerOpenExtraSize(); + } + } } } - mTouchableRegion.op( - (int) x, - (int) y, - locInWindow[0] + view.getWidth(), - locInWindow[1] + view.getHeight(), - Region.Op.UNION); + if (isWindowGravityLeft()) { + mTouchableRegion.op( + locInWindow[0], + locInWindow[1] + (int) yExtraSize, + locInWindow[0] + view.getWidth() - (int) xExtraSize, + locInWindow[1] + view.getHeight(), + Region.Op.UNION); + } else { + mTouchableRegion.op( + locInWindow[0] + (int) xExtraSize, + locInWindow[1] + (int) yExtraSize, + locInWindow[0] + view.getWidth(), + locInWindow[1] + view.getHeight(), + Region.Op.UNION); + } + } + + // Helper to set gravity. + private void setGravity(ViewGroup viewGroup, int gravity) { + if (viewGroup instanceof LinearLayout) { + ((LinearLayout) viewGroup).setGravity(gravity); + } + } + + // Helper to set layout gravity. + private void setLayoutGravity(ViewGroup viewGroup, int gravity) { + if (viewGroup != null) { + Object obj = viewGroup.getLayoutParams(); + if (obj instanceof FrameLayout.LayoutParams) { + ((FrameLayout.LayoutParams) obj).gravity = gravity; + } else if (obj instanceof LinearLayout.LayoutParams) { + ((LinearLayout.LayoutParams) obj).gravity = gravity; + } + } } private void initDialog(int lockTaskModeState) { @@ -525,6 +616,7 @@ private void initDialog(int lockTaskModeState) { mConfigurableTexts = new ConfigurableTexts(mContext); mHovering = false; mShowing = false; + mExpanded = false; mWindow = mDialog.getWindow(); mWindow.requestFeature(Window.FEATURE_NO_TITLE); mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); @@ -544,6 +636,12 @@ private void initDialog(int lockTaskModeState) { lp.windowAnimations = -1; mOriginalGravity = mContext.getResources().getInteger(R.integer.volume_dialog_gravity); + if (!mIsTv) { + // Clear the pre-defined gravity for left or right, + // this is handled by mVolumePanelOnLeft + mOriginalGravity &= ~(Gravity.LEFT | Gravity.RIGHT); + mOriginalGravity |= mVolumePanelOnLeft ? Gravity.LEFT : Gravity.RIGHT; + } mWindowGravity = Gravity.getAbsoluteGravity(mOriginalGravity, mContext.getResources().getConfiguration().getLayoutDirection()); lp.gravity = mWindowGravity; @@ -553,6 +651,8 @@ private void initDialog(int lockTaskModeState) { mDialog.setContentView(R.layout.volume_dialog_legacy); mDialogView = mDialog.findViewById(R.id.volume_dialog); mDialogView.setAlpha(0); + mDialogView.setLayoutDirection( + mVolumePanelOnLeft ? LAYOUT_DIRECTION_LTR : LAYOUT_DIRECTION_RTL); mDialogTimeoutMillis = mSecureSettings.get().getInt( Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, DIALOG_TIMEOUT_MILLIS); mDialog.setCanceledOnTouchOutside(true); @@ -656,6 +756,9 @@ public void onViewDetachedFromWindow(View v) { updateBackgroundForDrawerClosedAmount(); setTopContainerBackgroundDrawable(); + + // Rows need to be updated after mRingerAndDrawerContainerBackground is set + updateRowsH(getActiveRow()); } }); } @@ -706,6 +809,31 @@ public void onViewDetachedFromWindow(View v) { mAppVolumeView = mDialog.findViewById(R.id.app_volume_container); mAppVolumeIcon = mDialog.findViewById(R.id.app_volume); + mRoundedBorderBottom = mDialog.findViewById(R.id.rounded_border_bottom); + + mExpandRowsView = mDialog.findViewById(R.id.expandable_indicator_container); + mExpandRows = mDialog.findViewById(R.id.expandable_indicator); + + if (isWindowGravityLeft()) { + ViewGroup container = mDialog.findViewById(R.id.volume_dialog_container); + setGravity(container, Gravity.LEFT); + + setGravity(mDialogView, Gravity.LEFT); + + setGravity((ViewGroup) mTopContainer, Gravity.LEFT); + + setLayoutGravity(mRingerDrawerNewSelectionBg, Gravity.BOTTOM | Gravity.LEFT); + + setLayoutGravity(mSelectedRingerContainer, Gravity.BOTTOM | Gravity.LEFT); + + setGravity(mRinger, Gravity.LEFT); + + setGravity(mDialogRowsViewContainer, Gravity.LEFT); + + setGravity(mODICaptionsView, Gravity.LEFT); + + mExpandRows.setRotation(-90); + } if (mRows.isEmpty()) { if (!AudioSystem.isSingleVolume(mContext)) { @@ -779,8 +907,7 @@ private boolean isLandscape() { } private boolean isRtl() { - return mContext.getResources().getConfiguration().getLayoutDirection() - == LAYOUT_DIRECTION_RTL; + return mDialogView.getLayoutDirection() == LAYOUT_DIRECTION_RTL; } public void setStreamImportant(int stream, boolean important) { @@ -811,6 +938,9 @@ private void addRow(int stream, int iconRes, int iconMuteRes, boolean important, initRow(row, stream, iconRes, iconMuteRes, important, defaultStream); mDialogRowsView.addView(row.view); mRows.add(row); + if (mShowing) { + updateRowsH(getActiveRow()); + } } private void addExistingRows() { @@ -991,6 +1121,12 @@ private void setupRingerDrawer() { mDialogView.getPaddingTop(), mDialogView.getPaddingRight(), mDialogView.getPaddingBottom() + getRingerDrawerOpenExtraSize()); + } else if (isWindowGravityLeft()) { + mDialogView.setPadding( + mDialogView.getPaddingLeft(), + mDialogView.getPaddingTop(), + mDialogView.getPaddingRight() + getRingerDrawerOpenExtraSize(), + mDialogView.getPaddingBottom()); } else { mDialogView.setPadding( mDialogView.getPaddingLeft() + getRingerDrawerOpenExtraSize(), @@ -1061,15 +1197,16 @@ private ImageView getDrawerIconViewForMode(int mode) { } /** - * Translation to apply form the origin (either top or left) to overlap the selection background - * with the given mode in the drawer. + * Translation to apply form the origin (either top or left/right) to overlap the selection + * background with the given mode in the drawer. */ private float getTranslationInDrawerForRingerMode(int mode) { - return mode == RINGER_MODE_VIBRATE - ? -mRingerDrawerItemSize * 2 - : mode == RINGER_MODE_SILENT - ? -mRingerDrawerItemSize - : 0; + final int distantRinger = ((isLandscape() && isWindowGravityLeft()) ? RINGER_MODE_NORMAL + : RINGER_MODE_VIBRATE); + return (mode == distantRinger ? mRingerDrawerItemSize * 2 + : mode == RINGER_MODE_SILENT ? mRingerDrawerItemSize + : 0) + * ((isLandscape() && isWindowGravityLeft()) ? 1 : -1); } @VisibleForTesting String getSelectedRingerContainerDescription() { @@ -1113,12 +1250,13 @@ private void showRingerDrawer() { getTranslationInDrawerForRingerMode(mState.ringerModeInternal)); } - // Move the drawer so that the top/rightmost ringer choice overlaps with the selected ringer + // Move the drawer so that the top/outmost ringer choice overlaps with the selected ringer // icon. if (!isLandscape()) { mRingerDrawerContainer.setTranslationY(mRingerDrawerItemSize * (mRingerCount - 1)); } else { - mRingerDrawerContainer.setTranslationX(mRingerDrawerItemSize * (mRingerCount - 1)); + mRingerDrawerContainer.setTranslationX( + (isWindowGravityLeft() ? -1 : 1) * mRingerDrawerItemSize * (mRingerCount - 1)); } mRingerDrawerContainer.setAlpha(0f); mRingerDrawerContainer.setVisibility(VISIBLE); @@ -1197,7 +1335,7 @@ private void hideRingerDrawer() { .start(); } else { mRingerDrawerContainer.animate() - .translationX(mRingerDrawerItemSize * 2) + .translationX((isWindowGravityLeft() ? -1 : 1) * mRingerDrawerItemSize * 2) .start(); } @@ -1243,19 +1381,102 @@ private void updateSelectedRingerContainerDescription(boolean open) { mSelectedRingerContainer.setContentDescription(currentMode + tapToSelect); } + /** + * Returns a {@link MediaController} that state is playing and type is local playback, + * and also have active sessions. + */ + @Nullable + private MediaController getActiveLocalMediaController() { + MediaSessionManager mediaSessionManager = + mContext.getSystemService(MediaSessionManager.class); + MediaController localController = null; + final List remoteMediaSessionLists = new ArrayList<>(); + for (MediaController controller : mediaSessionManager.getActiveSessions(null)) { + final MediaController.PlaybackInfo pi = controller.getPlaybackInfo(); + if (pi == null) { + // do nothing + continue; + } + final PlaybackState playbackState = controller.getPlaybackState(); + if (playbackState == null) { + // do nothing + continue; + } + if (D.BUG) { + Log.d(TAG, + "getActiveLocalMediaController() package name : " + + controller.getPackageName() + + ", play back type : " + pi.getPlaybackType() + + ", play back state : " + playbackState.getState()); + } + if (playbackState.getState() != PlaybackState.STATE_PLAYING) { + // do nothing + continue; + } + if (pi.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { + if (localController != null + && TextUtils.equals( + localController.getPackageName(), controller.getPackageName())) { + localController = null; + } + if (!remoteMediaSessionLists.contains(controller.getPackageName())) { + remoteMediaSessionLists.add(controller.getPackageName()); + } + continue; + } + if (pi.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) { + if (localController == null + && !remoteMediaSessionLists.contains(controller.getPackageName())) { + localController = controller; + } + } + } + return localController; + } + + private boolean isBluetoothA2dpConnected() { + final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled() + && mBluetoothAdapter.getProfileConnectionState(BluetoothProfile.A2DP) + == BluetoothProfile.STATE_CONNECTED; + } + + private boolean isMediaControllerAvailable() { + final MediaController mediaController = getActiveLocalMediaController(); + return mediaController != null && !TextUtils.isEmpty(mediaController.getPackageName()); + } + private void initSettingsH(int lockTaskModeState) { + final boolean showSettings = mDeviceProvisionedController.isCurrentUserSetup() + && lockTaskModeState == LOCK_TASK_MODE_NONE; + if (mRoundedBorderBottom != null) { + mRoundedBorderBottom.setVisibility(!showSettings ? VISIBLE : GONE); + } if (mSettingsView != null) { mSettingsView.setVisibility( - mDeviceProvisionedController.isCurrentUserSetup() && - lockTaskModeState == LOCK_TASK_MODE_NONE ? VISIBLE : GONE); + showSettings && (isMediaControllerAvailable() || isBluetoothA2dpConnected()) + ? VISIBLE + : GONE); } if (mSettingsIcon != null) { mSettingsIcon.setOnClickListener(v -> { Events.writeEvent(Events.EVENT_SETTINGS_CLICK); + String packageName = isMediaControllerAvailable() + ? getActiveLocalMediaController().getPackageName() + : ""; + mMediaOutputDialogManager.createAndShow(packageName, true, mDialogView, null, null); dismissH(DISMISS_REASON_SETTINGS_CLICKED); - mMediaOutputDialogManager.dismiss(); - mVolumeNavigator.openVolumePanel( - mVolumePanelNavigationInteractor.getVolumePanelRoute()); + }); + } + + if (mExpandRowsView != null) { + mExpandRowsView.setVisibility(showSettings ? VISIBLE : GONE); + } + if (mExpandRows != null) { + mExpandRows.setOnClickListener(v -> { + mExpanded = !mExpanded; + updateRowsH(mDefaultRow, true); + mExpandRows.setExpanded(mExpanded); }); } } @@ -1420,9 +1641,6 @@ protected void tryToRemoveCaptionsTooltip() { } private void updateODICaptionsH(boolean isServiceComponentEnabled, boolean fromTooltip) { - // don't show captions view when the new volume panel is enabled. - isServiceComponentEnabled = - isServiceComponentEnabled && !mVolumePanelFlag.canUseNewVolumePanel(); if (mODICaptionsView != null) { mODICaptionsView.setVisibility(isServiceComponentEnabled ? VISIBLE : GONE); } @@ -1567,6 +1785,10 @@ private void showH(int reason, boolean keyguardLocked, int lockTaskModeState) { mConfigChanged = false; } + if (mDefaultRow == null) { + mDefaultRow = getActiveRow(); + } + initSettingsH(lockTaskModeState); initAppVolumeH(); mShowing = true; @@ -1679,6 +1901,12 @@ protected void dismissH(int reason) { mDialog.dismiss(); } tryToRemoveCaptionsTooltip(); + mExpanded = false; + if (mExpandRows != null) { + mExpandRows.setExpanded(mExpanded); + } + mAnimatingRows = 0; + mDefaultRow = null; mIsAnimatingDismiss = false; hideRingerDrawer(); @@ -1706,6 +1934,13 @@ private boolean isTv() { || mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION); } + private boolean isExpandableRowH(VolumeRow row) { + return row != null && row != mDefaultRow && !row.defaultStream + && (row.stream == STREAM_RING + || row.stream == STREAM_NOTIFICATION + || row.stream == STREAM_ALARM + || row.stream == STREAM_MUSIC); + } private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) { boolean isActive = row.stream == activeRow.stream; @@ -1725,6 +1960,10 @@ private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) { return true; } + if (mExpanded && isExpandableRowH(row)) { + return true; + } + // Always show the stream for audio sharing if it exists. if (row.ss != null && mContext.getString(R.string.volume_dialog_guest_device_volume_description) @@ -1732,10 +1971,14 @@ private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) { return true; } - if (row.defaultStream) { + // if the row is the default stream or the row with which this panel was created, + // show it additonally to the active row if it is one of the following streams + if (row.defaultStream || mDefaultRow == row) { return activeRow.stream == STREAM_RING + || activeRow.stream == STREAM_NOTIFICATION || activeRow.stream == STREAM_ALARM || activeRow.stream == STREAM_VOICE_CALL + || activeRow.stream == STREAM_MUSIC || activeRow.stream == STREAM_ACCESSIBILITY || mDynamic.get(activeRow.stream); } @@ -1751,29 +1994,39 @@ private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) { private void updateRowsH(final VolumeRow activeRow) { Trace.beginSection("VolumeDialogImpl#updateRowsH"); + updateRowsH(activeRow, false); + } + + private void updateRowsH(final VolumeRow activeRow, boolean animate) { if (D.BUG) Log.d(TAG, "updateRowsH"); if (!mShowing) { trimObsoleteH(); } + boolean isOutmostIndexMax = isWindowGravityLeft() ? isRtl() : !isRtl(); + // Index of the last row that is actually visible. - int rightmostVisibleRowIndex = !isRtl() ? -1 : Short.MAX_VALUE; + int outmostVisibleRowIndex = isOutmostIndexMax ? -1 : Short.MAX_VALUE; // apply changes to all rows for (final VolumeRow row : mRows) { final boolean isActive = row == activeRow; + final boolean isExpandableRow = isExpandableRowH(row); final boolean shouldBeVisible = shouldBeVisibleH(row, activeRow); - Util.setVisOrGone(row.view, shouldBeVisible); - if (shouldBeVisible && mRingerAndDrawerContainerBackground != null) { - // For RTL, the rightmost row has the lowest index since child views are laid out + if (!isExpandableRow) { + Util.setVisOrGone(row.view, shouldBeVisible); + } else if (!mExpanded) { + row.view.setVisibility(View.INVISIBLE); + } + + if ((shouldBeVisible || isExpandableRow) + && mRingerAndDrawerContainerBackground != null) { + // For RTL, the outmost row has the lowest index since child views are laid out // from right to left. - rightmostVisibleRowIndex = - !isRtl() - ? Math.max(rightmostVisibleRowIndex, - mDialogRowsView.indexOfChild(row.view)) - : Math.min(rightmostVisibleRowIndex, - mDialogRowsView.indexOfChild(row.view)); + outmostVisibleRowIndex = isOutmostIndexMax + ? Math.max(outmostVisibleRowIndex, mDialogRowsView.indexOfChild(row.view)) + : Math.min(outmostVisibleRowIndex, mDialogRowsView.indexOfChild(row.view)); // Add spacing between each of the visible rows - we'll remove the spacing from the // last row after the loop. @@ -1781,12 +2034,13 @@ private void updateRowsH(final VolumeRow activeRow) { if (layoutParams instanceof LinearLayout.LayoutParams) { final LinearLayout.LayoutParams linearLayoutParams = ((LinearLayout.LayoutParams) layoutParams); - if (!isRtl()) { + if (isOutmostIndexMax) { linearLayoutParams.setMarginEnd(mRingerRowsPadding); } else { linearLayoutParams.setMarginStart(mRingerRowsPadding); } } + row.view.setLayoutParams(layoutParams); // Set the background on each of the rows. We'll remove this from the last row after // the loop, since the last row's background is drawn by the main volume container. @@ -1794,13 +2048,13 @@ private void updateRowsH(final VolumeRow activeRow) { mContext.getDrawable(R.drawable.volume_row_rounded_background)); } - if (row.view.isShown()) { + if (row.view.isShown() || isExpandableRow) { updateVolumeRowTintH(row, isActive); } } - if (rightmostVisibleRowIndex > -1 && rightmostVisibleRowIndex < Short.MAX_VALUE) { - final View lastVisibleChild = mDialogRowsView.getChildAt(rightmostVisibleRowIndex); + if (outmostVisibleRowIndex > -1 && outmostVisibleRowIndex < Short.MAX_VALUE) { + final View lastVisibleChild = mDialogRowsView.getChildAt(outmostVisibleRowIndex); final ViewGroup.LayoutParams layoutParams = lastVisibleChild.getLayoutParams(); // Remove the spacing on the last row, and remove its background since the container is // drawing a background for this row. @@ -1809,8 +2063,106 @@ private void updateRowsH(final VolumeRow activeRow) { ((LinearLayout.LayoutParams) layoutParams); linearLayoutParams.setMarginStart(0); linearLayoutParams.setMarginEnd(0); + lastVisibleChild.setLayoutParams(linearLayoutParams); lastVisibleChild.setBackgroundColor(Color.TRANSPARENT); } + + int elevationCount = 0; + if (animate) { + // Increase the elevation of the outmost row so that other rows animate behind it. + lastVisibleChild.setElevation(1f / ++elevationCount); + + // Add a solid background to the outmost row temporary so that other rows animate + // behind it + lastVisibleChild.setBackgroundDrawable( + mContext.getDrawable(R.drawable.volume_background)); + } + + int[] lastVisibleChildLocation = new int[2]; + lastVisibleChild.getLocationInWindow(lastVisibleChildLocation); + + // Track previous rows to calculate translations + int visibleRowsCount = 0; + int hiddenRowsCount = 0; + int rowWidth = mDialogWidth + mRingerRowsPadding; + + for (final VolumeRow row : mRows) { + final boolean isExpandableRow = isExpandableRowH(row); + final boolean shouldBeVisible = shouldBeVisibleH(row, activeRow); + + if (shouldBeVisible) { + visibleRowsCount++; + } else if (isExpandableRow) { + hiddenRowsCount++; + } + + // Only move rows that are either expandable or visible and not the default stream + if ((!isExpandableRow && !shouldBeVisible) || row.defaultStream) { + continue; + } + + // Cancel any ongoing animations + row.view.animate().cancel(); + + // Expandable rows need to animate behind the last visible row, an additional + // always visible stream animates next to the default stream + float translationX = (isWindowGravityLeft() ? -1 : 1) * hiddenRowsCount * rowWidth; + + if (animate) { + // If the translation is not set and the panel is expanding start the + // animation behind the main row + if (isExpandableRow && mExpanded && row.view.getTranslationX() == 0f) { + row.view.setTranslationX( + (isWindowGravityLeft() ? -1 : 1) * visibleRowsCount * rowWidth); + } + + // The elevation should decrease from the outmost row to the inner rows, so that + // every row animates behind the outer rows + row.view.setElevation(1f / ++elevationCount); + + row.view.setVisibility(View.VISIBLE); + + // Track how many rows are animating to avoid running animation end actions + // if there is still a row animating + mAnimatingRows++; + row.view.animate() + .translationX(translationX) + .setDuration(mExpanded ? mDialogShowAnimationDurationMs + : mDialogHideAnimationDurationMs) + .setInterpolator(mExpanded + ? new SystemUIInterpolators.LogDecelerateInterpolator() + : new SystemUIInterpolators.LogAccelerateInterpolator()) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + mAnimatingRows--; + animation.removeAllListeners(); + } + + @Override + public void onAnimationEnd(Animator animation) { + row.view.setElevation(0); + if (!shouldBeVisible) { + row.view.setVisibility(View.INVISIBLE); + } + mAnimatingRows--; + if (mAnimatingRows == 0) { + // Restore the elevation and background + lastVisibleChild.setElevation(0); + lastVisibleChild.setBackgroundColor(Color.TRANSPARENT); + // Set the active stream to ensure the volume keys change + // the volume of the tinted row. The tint was set before + // already, but setting the active row cancels ongoing + // animations. + mController.setActiveStream(activeRow.stream, false); + } + } + }); + } else { + row.view.setTranslationX(translationX); + row.view.setVisibility(shouldBeVisible ? View.VISIBLE : View.INVISIBLE); + } + } } updateBackgroundForDrawerClosedAmount(); @@ -1920,7 +2272,7 @@ private void trimObsoleteH() { protected void onStateChangedH(State state) { if (D.BUG) Log.d(TAG, "onStateChangedH() state: " + state.toString()); - if (mState != null && state != null + if (mShowing && mState != null && state != null && mState.ringerModeInternal != -1 && mState.ringerModeInternal != state.ringerModeInternal && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) { @@ -1991,6 +2343,7 @@ private void updateVolumeRowH(VolumeRow row) { final boolean isVoiceCallStream = row.stream == AudioManager.STREAM_VOICE_CALL; final boolean isA11yStream = row.stream == STREAM_ACCESSIBILITY; final boolean isRingStream = row.stream == AudioManager.STREAM_RING; + final boolean isNotificationStream = row.stream == AudioManager.STREAM_NOTIFICATION; final boolean isSystemStream = row.stream == AudioManager.STREAM_SYSTEM; final boolean isAlarmStream = row.stream == STREAM_ALARM; final boolean isMusicStream = row.stream == AudioManager.STREAM_MUSIC; @@ -1998,6 +2351,7 @@ private void updateVolumeRowH(VolumeRow row) { && mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE; final boolean isRingSilent = isRingStream && mState.ringerModeInternal == AudioManager.RINGER_MODE_SILENT; + final boolean isNotificationMuted = isNotificationStream && ss.muted; final boolean isZenPriorityOnly = mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; final boolean isZenAlarms = mState.zenMode == Global.ZEN_MODE_ALARMS; final boolean isZenNone = mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS; @@ -2033,7 +2387,7 @@ private void updateVolumeRowH(VolumeRow row) { iconRes = com.android.internal.R.drawable.ic_qs_dnd; } else if (isRingVibrate) { iconRes = R.drawable.ic_legacy_volume_ringer_vibrate; - } else if (isRingSilent) { + } else if (isRingSilent || isNotificationMuted) { iconRes = row.iconMuteRes; } else if (ss.routedToBluetooth) { if (isVoiceCallStream) { @@ -2363,6 +2717,21 @@ private int getVisibleRowsExtraSize() { return (visibleRows - 1) * (mDialogWidth + mRingerRowsPadding); } + /** + * Return the size of invisible rows. + * Expandable rows are invisible while the panel is not expanded. + */ + private int getExpandableRowsExtraSize() { + VolumeRow activeRow = getActiveRow(); + int expandableRows = 0; + for (final VolumeRow row : mRows) { + if (isExpandableRowH(row)) { + expandableRows++; + } + } + return expandableRows * (mDialogWidth + mRingerRowsPadding); + } + private void updateBackgroundForDrawerClosedAmount() { if (mRingerAndDrawerContainerBackground == null) { return; @@ -2371,6 +2740,9 @@ private void updateBackgroundForDrawerClosedAmount() { final Rect bounds = mRingerAndDrawerContainerBackground.copyBounds(); if (!isLandscape()) { bounds.top = (int) (mRingerDrawerClosedAmount * getRingerDrawerOpenExtraSize()); + } else if (isWindowGravityLeft()) { + bounds.right = (int) ((mDialogCornerRadius / 2) + mRingerDrawerItemSize + + (1f - mRingerDrawerClosedAmount) * getRingerDrawerOpenExtraSize()); } else { bounds.left = (int) (mRingerDrawerClosedAmount * getRingerDrawerOpenExtraSize()); } @@ -2378,7 +2750,7 @@ private void updateBackgroundForDrawerClosedAmount() { } /* - * The top container is responsible for drawing the solid color background behind the rightmost + * The top container is responsible for drawing the solid color background behind the outmost * (primary) volume row. This is because the volume drawer animates in from below, initially * overlapping the primary row. We need the drawer to draw below the row's SeekBar, since it * looks strange to overlap it, but above the row's background color, since otherwise it will be @@ -2433,8 +2805,9 @@ private void setTopContainerBackgroundDrawable() { ? mDialogRowsViewContainer.getTop() : mDialogRowsViewContainer.getTop() - mDialogCornerRadius); - // Set gravity to top-right, since additional rows will be added on the left. - background.setLayerGravity(0, Gravity.TOP | Gravity.RIGHT); + // Set gravity to top and opposite side where additional rows will be added. + background.setLayerGravity(0, + isWindowGravityLeft() ? Gravity.TOP | Gravity.LEFT : Gravity.TOP | Gravity.RIGHT); // In landscape, the ringer drawer animates out to the left (instead of down). Since the // drawer comes from the right (beyond the bounds of the dialog), we should clip it so it @@ -2483,7 +2856,6 @@ public void onStateChanged(State state) { @Override public void onLayoutDirectionChanged(int layoutDirection) { - mDialogView.setLayoutDirection(layoutDirection); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index 7cb666ea6fbfa..4a68de885188a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -51,7 +51,6 @@ import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor; import com.android.systemui.volume.panel.dagger.VolumePanelComponent; import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory; -import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag; import com.android.systemui.volume.ui.navigation.VolumeNavigator; import com.google.android.msdl.domain.MSDLPlayer; @@ -130,7 +129,6 @@ static VolumeDialog provideVolumeDialog( VolumeNavigator volumeNavigator, CsdWarningDialog.Factory csdFactory, DevicePostureController devicePostureController, - VolumePanelFlag volumePanelFlag, DumpManager dumpManager, Lazy secureSettings, VibratorHelper vibratorHelper, @@ -162,7 +160,6 @@ static VolumeDialog provideVolumeDialog( csdFactory, devicePostureController, Looper.getMainLooper(), - volumePanelFlag, dumpManager, secureSettings, vibratorHelper, diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/captions/ui/binder/VolumeDialogCaptionsButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/captions/ui/binder/VolumeDialogCaptionsButtonViewBinder.kt index b15476e46a540..f1f3440597672 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/captions/ui/binder/VolumeDialogCaptionsButtonViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/captions/ui/binder/VolumeDialogCaptionsButtonViewBinder.kt @@ -16,11 +16,19 @@ package com.android.systemui.volume.dialog.captions.ui.binder +import android.content.Context +import android.database.ContentObserver +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.InsetDrawable import android.graphics.drawable.TransitionDrawable import android.os.Handler +import android.os.UserHandle +import android.provider.Settings import android.view.View import com.android.app.tracing.coroutines.launchInTraced import com.android.app.tracing.coroutines.launchTraced +import com.android.internal.R as internalR import com.android.systemui.Flags import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.res.R @@ -34,6 +42,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.withIndex +import kotlinx.coroutines.job /** Binds the captions button view. */ @VolumeDialogScope @@ -55,6 +64,39 @@ constructor( dialogViewModel.addTouchableBounds(captionsButton) } + val contentResolver = captionsButton.context.contentResolver + var gradientEnabled = isVolumeGradientEnabled(captionsButton.context) + var gradientColorsForRinger = getGradientColorsForRinger(captionsButton.context) + + val gradientObserver = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + gradientEnabled = isVolumeGradientEnabled(view.context) + gradientColorsForRinger = getGradientColorsForRinger(view.context) + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.VOLUME_SLIDER_GRADIENT), + false, gradientObserver, UserHandle.USER_ALL, + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_COLOR_MODE), + false, gradientObserver, UserHandle.USER_ALL, + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_START_COLOR), + false, gradientObserver, UserHandle.USER_ALL, + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_END_COLOR), + false, gradientObserver, UserHandle.USER_ALL, + ) + + coroutineContext.job.invokeOnCompletion { + contentResolver.unregisterContentObserver(gradientObserver) + } + viewModel.isVisible .onEach { isVisible -> captionsButton.visibility = @@ -88,6 +130,10 @@ constructor( ) ) + if (isEnabled && gradientEnabled) { + applyGradientSelectionBackground(this, gradientColorsForRinger) + } + val transition = background as TransitionDrawable transition.isCrossFadeEnabled = true if (index == 0) { @@ -116,7 +162,70 @@ constructor( ) } + private fun isVolumeGradientEnabled(context: Context): Boolean { + return Settings.System.getIntForUser( + context.contentResolver, Settings.System.VOLUME_SLIDER_GRADIENT, 0, + UserHandle.USER_CURRENT) != 0 + } + + private fun getGradientColorsForRinger(context: Context): Pair { + val resolver = context.contentResolver + + val mode = Settings.System.getIntForUser( + resolver, Settings.System.CUSTOM_GRADIENT_COLOR_MODE, 0, + UserHandle.USER_CURRENT, + ) + + val primary = context.getColor(internalR.color.materialColorPrimary) + val secondary = context.getColor(internalR.color.materialColorSecondary) + + if (mode == 1) { + val start = Settings.System.getIntForUser( + resolver, Settings.System.CUSTOM_GRADIENT_START_COLOR, 0, + UserHandle.USER_CURRENT, + ) + val end = Settings.System.getIntForUser( + resolver, Settings.System.CUSTOM_GRADIENT_END_COLOR, 0, + UserHandle.USER_CURRENT, + ) + + val startColor = if (start != 0) start else primary + val endColor = if (end != 0) end else secondary + + return startColor to endColor + } + + return primary to secondary + } + + private fun applyGradientSelectionBackground( + button: CaptionsToggleImageButton, + gradientColorsForRinger: Pair, + ) { + val (startColor, endColor) = gradientColorsForRinger + val shape = button.backgroundShape() + shape.orientation = GradientDrawable.Orientation.TOP_BOTTOM + shape.colors = intArrayOf(startColor, endColor) + button.background.invalidateSelf() + } + private companion object { const val DURATION_MILLIS = 500 } } + +private fun CaptionsToggleImageButton.backgroundShape(): GradientDrawable { + val td = background as TransitionDrawable + + fun unwrap(d: Drawable): GradientDrawable? { + return when (d) { + is GradientDrawable -> d + is InsetDrawable -> d.drawable as? GradientDrawable + else -> null + } + } + + return unwrap(td.getDrawable(1)) + ?: unwrap(td.getDrawable(0)) + ?: throw IllegalStateException("Captions button background is not a GradientDrawable") +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt index 4fe7e44522faa..d83da96f6875f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt @@ -17,9 +17,14 @@ package com.android.systemui.volume.dialog.ringer.ui.binder import android.animation.ArgbEvaluator +import android.content.Context +import android.content.res.ColorStateList import android.content.res.Configuration +import android.database.ContentObserver import android.graphics.drawable.GradientDrawable import android.graphics.drawable.InsetDrawable +import android.os.UserHandle +import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.widget.ImageButton @@ -54,6 +59,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.Job +import kotlinx.coroutines.job +import kotlinx.coroutines.joinAll private const val CLOSE_DRAWER_DELAY = 300L // Ensure roundness and color of button is updated when progress is changed by a minimum fraction. @@ -124,6 +132,39 @@ constructor( dialogViewModel.addTouchableBounds(ringerBackgroundView) } + val contentResolver = view.context.contentResolver + var gradientEnabled = isVolumeGradientEnabled(view.context) + var gradientColorsForRinger = getGradientColorsForRinger(view.context) + + val gradientObserver = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + gradientEnabled = isVolumeGradientEnabled(view.context) + gradientColorsForRinger = getGradientColorsForRinger(view.context) + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.VOLUME_SLIDER_GRADIENT), + false, gradientObserver, UserHandle.USER_ALL, + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_COLOR_MODE), + false, gradientObserver, UserHandle.USER_ALL, + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_START_COLOR), + false, gradientObserver, UserHandle.USER_ALL, + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_END_COLOR), + false, gradientObserver, UserHandle.USER_ALL, + ) + + coroutineContext.job.invokeOnCompletion { + contentResolver.unregisterContentObserver(gradientObserver) + } + viewModel.ringerViewModel .mapLatest { ringerState -> when (ringerState) { @@ -151,6 +192,8 @@ constructor( uiModel, selectedButtonUiModel, unselectedButtonUiModel, + gradientEnabled, + gradientColorsForRinger, ) ringerDrawerTransitionListener.setProgressChangeEnabled(true) drawerContainer.closeDrawer( @@ -169,6 +212,8 @@ constructor( uiModel, selectedButtonUiModel, unselectedButtonUiModel, + gradientEnabled, + gradientColorsForRinger, onProgressChanged = { progress, isReverse -> // Let's make button progress when switching matches // motionLayout transition progress. When full @@ -209,6 +254,8 @@ constructor( uiModel, selectedButtonUiModel, unselectedButtonUiModel, + gradientEnabled, + gradientColorsForRinger, ) // Open drawer if ( @@ -250,6 +297,8 @@ constructor( uiModel: RingerViewModel, selectedButtonUiModel: RingerButtonUiModel, unselectedButtonUiModel: RingerButtonUiModel, + gradientEnabled: Boolean, + gradientColorsForRinger: Pair, onProgressChanged: (Float, Boolean) -> Unit = { _, _ -> }, onAnimationEnd: Runnable? = null, ) { @@ -269,9 +318,11 @@ constructor( // progress update once because these changes should be applied once on volume dialog // background and ringer drawer views. coroutineScope { + val jobs = ArrayList(2) + val selectedCornerRadius = selectedButton.backgroundShape().cornerRadius if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) { - launchTraced("VDRVB#selectedButtonAnimation") { + jobs += launchTraced("VDRVB#selectedButtonAnimation") { selectedButton.animateTo( selectedButtonUiModel, if (uiModel.currentButtonIndex == count - 1) { @@ -282,9 +333,10 @@ constructor( ) } } + val unselectedCornerRadius = unselectedButton.backgroundShape().cornerRadius if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) { - launchTraced("VDRVB#unselectedButtonAnimation") { + jobs += launchTraced("VDRVB#unselectedButtonAnimation") { unselectedButton.animateTo( unselectedButtonUiModel, if (previousIndex == count - 1) { @@ -295,19 +347,48 @@ constructor( ) } } - launchTraced("VDRVB#bindButtons") { + + launchTraced("VDRVB#bindButtonsAnimated") { delay(CLOSE_DRAWER_DELAY) - bindButtons(viewModel, uiModel, onAnimationEnd, isAnimated = true) + bindButtons( + viewModel, + uiModel, + gradientEnabled, + gradientColorsForRinger, + onAnimationEnd = null, + isAnimated = true, + ) + } + + jobs.joinAll() + + launchTraced("VDRVB#bindButtonsFinal") { + bindButtons( + viewModel, + uiModel, + gradientEnabled, + gradientColorsForRinger, + onAnimationEnd, + isAnimated = false, + ) } } } else { - bindButtons(viewModel, uiModel, onAnimationEnd) + bindButtons( + viewModel, + uiModel, + gradientEnabled, + gradientColorsForRinger, + onAnimationEnd + ) } } private fun MotionLayout.bindButtons( viewModel: VolumeDialogRingerDrawerViewModel, uiModel: RingerViewModel, + gradientEnabled: Boolean, + gradientColorsForRinger: Pair, onAnimationEnd: Runnable? = null, isAnimated: Boolean = false, ) { @@ -320,11 +401,20 @@ constructor( if (isOpen) ringerButton else uiModel.selectedButton, viewModel, isOpen, + gradientEnabled, + gradientColorsForRinger, isSelected = true, isAnimated = isAnimated, ) } else { - view.bindDrawerButton(ringerButton, viewModel, isOpen, isAnimated = isAnimated) + view.bindDrawerButton( + ringerButton, + viewModel, + isOpen, + gradientEnabled, + gradientColorsForRinger, + isAnimated = isAnimated, + ) } } onAnimationEnd?.run() @@ -334,6 +424,8 @@ constructor( buttonViewModel: RingerButtonViewModel, viewModel: VolumeDialogRingerDrawerViewModel, isOpen: Boolean, + gradientEnabled: Boolean, + gradientColorsForRinger: Pair, isSelected: Boolean = false, isAnimated: Boolean = false, ) { @@ -352,11 +444,14 @@ constructor( } if (isSelected && !isAnimated) { setBackgroundResource(R.drawable.volume_drawer_selection_bg) - setColorFilter(context.getColor(internalR.color.materialColorOnPrimary)) + imageTintList = ColorStateList.valueOf(context.getColor(internalR.color.materialColorOnPrimary)) background = background.mutate() + if (gradientEnabled) { + applyGradientSelectionBackground(this, gradientColorsForRinger) + } } else if (!isAnimated) { setBackgroundResource(R.drawable.volume_ringer_item_bg) - setColorFilter(context.getColor(internalR.color.materialColorOnSurface)) + imageTintList = ColorStateList.valueOf(context.getColor(internalR.color.materialColorOnSurface)) background = background.mutate() } setOnClickListener { @@ -411,22 +506,20 @@ constructor( coroutineScope { launchTraced("VDRVB#colorAnimation") { colorAnimation.suspendAnimate { value -> - val currentIconColor = - rgbEvaluator.evaluate( - value.coerceIn(0F, 1F), - imageTintList?.colors?.first(), - ringerButtonUiModel.tintColor, - ) as Int - val currentBgColor = - rgbEvaluator.evaluate( - value.coerceIn(0F, 1F), - backgroundShape().color?.colors?.get(0), - ringerButtonUiModel.backgroundColor, - ) as Int + val t = value.coerceIn(0F, 1F) + + val startIconColor = imageTintList?.defaultColor ?: ringerButtonUiModel.tintColor + val endIconColor = ringerButtonUiModel.tintColor + + val startBgColor = backgroundShape().firstColorOrNull() ?: ringerButtonUiModel.backgroundColor + val endBgColor = ringerButtonUiModel.backgroundColor + + val currentIconColor = rgbEvaluator.evaluate(t, startIconColor, endIconColor) as Int + val currentBgColor = rgbEvaluator.evaluate(t, startBgColor, endBgColor) as Int backgroundShape().setColor(currentBgColor) background.invalidateSelf() - setColorFilter(currentIconColor) + imageTintList = ColorStateList.valueOf(currentIconColor) } } roundnessAnimation.suspendAnimate { value -> @@ -442,7 +535,64 @@ constructor( (background as GradientDrawable).cornerRadius = radius background.invalidateSelf() } + + private fun isVolumeGradientEnabled(context: Context): Boolean { + return Settings.System.getIntForUser( + context.contentResolver, Settings.System.VOLUME_SLIDER_GRADIENT, 0, + UserHandle.USER_CURRENT) != 0 + } + + private fun getGradientColorsForRinger(context: Context): Pair { + val resolver = context.contentResolver + + val mode = Settings.System.getIntForUser( + resolver, Settings.System.CUSTOM_GRADIENT_COLOR_MODE, 0, + UserHandle.USER_CURRENT, + ) + + val primary = context.getColor(internalR.color.materialColorPrimary) + val secondary = context.getColor(internalR.color.materialColorSecondary) + + if (mode == 1) { + val start = Settings.System.getIntForUser( + resolver, Settings.System.CUSTOM_GRADIENT_START_COLOR, 0, + UserHandle.USER_CURRENT, + ) + val end = Settings.System.getIntForUser( + resolver, Settings.System.CUSTOM_GRADIENT_END_COLOR, 0, + UserHandle.USER_CURRENT, + ) + + val startColor = if (start != 0) start else primary + val endColor = if (end != 0) end else secondary + + return startColor to endColor + } + + return primary to secondary + } + + private fun applyGradientSelectionBackground( + button: ImageButton, + gradientColorsForRinger: Pair, + ) { + val (startColor, endColor) = gradientColorsForRinger + val shape = button.backgroundShape() + shape.orientation = GradientDrawable.Orientation.TOP_BOTTOM + shape.colors = intArrayOf(startColor, endColor) + button.background.invalidateSelf() + } } private fun ImageButton.backgroundShape(): GradientDrawable = (background as InsetDrawable).drawable as GradientDrawable + +private fun GradientDrawable.firstColorOrNull(): Int? { + val gradient = colors + if (gradient != null && gradient.isNotEmpty()) return gradient[0] + + val solid = color + if (solid != null) return solid.defaultColor + + return null +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index bb6b59c08e970..7bdffaee73027 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext @@ -46,6 +47,9 @@ import com.android.systemui.res.R import com.android.systemui.volume.dialog.domain.interactor.DesktopAudioTileDetailsFeatureInteractor import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope import com.android.systemui.volume.dialog.sliders.ui.compose.SliderTrack +import com.android.systemui.volume.dialog.sliders.ui.compose.rememberGradientColorMode +import com.android.systemui.volume.dialog.sliders.ui.compose.rememberGradientCustomColors +import com.android.systemui.volume.dialog.sliders.ui.compose.rememberVolumeGradientEnabled import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider @@ -128,6 +132,16 @@ private fun VolumeDialogSlider( } } + val thumbColorOverride: Color? = + if (!rememberVolumeGradientEnabled()) { + null + } else if (rememberGradientColorMode() == 1) { + val (customStart, _) = rememberGradientCustomColors() + customStart + } else { + MaterialTheme.colorScheme.primary + } + Slider( value = sliderStateModel.value, valueRange = sliderStateModel.valueRange, @@ -191,6 +205,7 @@ private fun VolumeDialogSlider( isVisible = iconsState.isInactiveTrackEndIconVisible, ) }, + ignoreGradient = false, ) }, thumb = { sliderState, interactions -> @@ -198,7 +213,9 @@ private fun VolumeDialogSlider( sliderState = sliderState, interactionSource = interactions, enabled = !sliderStateModel.isDisabled, - colors = colors, + colors = SliderDefaults.colors( + thumbColor = thumbColorOverride ?: SliderDefaults.colors().thumbColor + ), thumbSize = if (isVolumeDialogVertical) { DpSize(52.dp, 4.dp) diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt index 28557c854e1a6..c8234357c2ebe 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt @@ -16,29 +16,48 @@ package com.android.systemui.volume.dialog.sliders.ui.compose +import android.database.ContentObserver +import android.os.UserHandle +import android.provider.Settings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp @@ -63,6 +82,7 @@ fun SliderTrack( activeTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, inactiveTrackStartIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, inactiveTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, + ignoreGradient: Boolean = true, ) { val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val measurePolicy = @@ -77,21 +97,39 @@ fun SliderTrack( Layout( measurePolicy = measurePolicy, content = { - SliderDefaults.Track( + + val gradientColors = if (rememberGradientColorMode() == 1) { + val (start, end) = rememberGradientCustomColors() + listOf(start, end) + } else { + listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.secondary + ) + } + + val activeBrush = + if (!ignoreGradient && rememberVolumeGradientEnabled()) + gradientColors + else + null + + GradientSliderTrack( sliderState = sliderState, + isEnabled = isEnabled, colors = colors, - enabled = isEnabled, trackCornerSize = trackCornerSize, trackInsideCornerSize = trackInsideCornerSize, - drawStopIndicator = null, thumbTrackGapSize = thumbTrackGapSize, - drawTick = { _, _ -> }, + isVertical = isVertical, + gradientColors = activeBrush, modifier = - Modifier.then( + Modifier + .then( if (isVertical) { - Modifier.width(trackSize) + Modifier.width(trackSize).fillMaxHeight() } else { - Modifier.height(trackSize) + Modifier.height(trackSize).fillMaxWidth() } ) .layoutId(Contents.Track), @@ -130,6 +168,269 @@ fun SliderTrack( ) } +private data class TrackGradient( + val brush: Brush, +) + +@Composable +fun rememberVolumeGradientEnabled(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readEnabled(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.VOLUME_SLIDER_GRADIENT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var enabled by remember { mutableStateOf(readEnabled()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + enabled = readEnabled() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.VOLUME_SLIDER_GRADIENT), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return enabled +} + +@Composable +fun rememberGradientColorMode(): Int { + val contentResolver = LocalContext.current.contentResolver + + fun readMode(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_COLOR_MODE, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var mode by remember { mutableIntStateOf(readMode()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + mode = readMode() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_COLOR_MODE), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return mode +} + +@Composable +fun rememberGradientCustomColors(): Pair { + val contentResolver = LocalContext.current.contentResolver + + fun readStart(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_START_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + fun readEnd(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_END_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var startInt by remember { mutableIntStateOf(readStart()) } + var endInt by remember { mutableIntStateOf(readEnd()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + startInt = readStart() + endInt = readEnd() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_START_COLOR), + false, observer, UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_END_COLOR), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + val start = if (startInt != 0) Color(startInt) else MaterialTheme.colorScheme.primary + val end = if (endInt != 0) Color(endInt) else MaterialTheme.colorScheme.secondary + return start to end +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun GradientSliderTrack( + sliderState: SliderState, + isEnabled: Boolean, + colors: SliderColors, + trackCornerSize: Dp, + trackInsideCornerSize: Dp, + thumbTrackGapSize: Dp, + isVertical: Boolean, + gradientColors: List?, + modifier: Modifier = Modifier, +) { + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + + val inactiveColor = + if (isEnabled) colors.inactiveTrackColor else colors.disabledInactiveTrackColor + val activeSolidColor = + if (isEnabled) colors.activeTrackColor else colors.disabledActiveTrackColor + + Box( + modifier = + modifier.drawBehind { + val w = size.width + val h = size.height + if (w <= 0f || h <= 0f) return@drawBehind + + val frac = sliderState.coercedValueAsFraction.coerceIn(0f, 1f) + val outerR = trackCornerSize.toPx().coerceAtMost(minOf(w, h) / 2f) + val innerR = trackInsideCornerSize.toPx().coerceAtMost(minOf(w, h) / 2f) + val halfGap = (thumbTrackGapSize.toPx() / 2f).coerceAtLeast(0f) + + drawRoundRect( + color = inactiveColor, + size = size, + cornerRadius = CornerRadius(outerR, outerR) + ) + + if (!isVertical) { + val splitX = if (isRtl) w * (1f - frac) else w * frac + + val gapEff = + if (isRtl) { + minOf(halfGap, splitX) + } else { + minOf(halfGap, w - splitX) + } + + val activeStart = if (isRtl) splitX + gapEff else 0f + val activeEnd = if (isRtl) w else splitX - gapEff + + if (activeEnd <= activeStart) return@drawBehind + + val eps = 0.5f + + val hitsStart = activeStart <= eps + val hitsEnd = activeEnd >= (w - eps) + + val leftR = + if (isRtl) { + if (hitsStart) outerR else innerR // rounded only at max (when it reaches start) + } else { + outerR // far end always rounded + } + + val rightR = + if (isRtl) { + outerR // far end always rounded + } else { + if (hitsEnd) outerR else innerR // rounded only at max (when it reaches end) + } + + val path = Path().apply { + addRoundRect( + RoundRect( + rect = Rect(activeStart, 0f, activeEnd, h), + topLeft = CornerRadius(leftR, leftR), + bottomLeft = CornerRadius(leftR, leftR), + topRight = CornerRadius(rightR, rightR), + bottomRight = CornerRadius(rightR, rightR), + ) + ) + } + + val brush = + if (gradientColors != null) Brush.horizontalGradient(gradientColors) + else Brush.linearGradient(listOf(activeSolidColor, activeSolidColor)) + + drawPath(path = path, brush = brush) + } else { + val frac = sliderState.coercedValueAsFraction.coerceIn(0f, 1f) + val splitY = h * (1f - frac) + + val gapEff = minOf(halfGap, splitY) + + val activeTop = splitY + gapEff + val activeBottom = h + if (activeBottom <= activeTop) return@drawBehind + + val eps = 0.5f + val hitsTop = activeTop <= eps + + val topR = if (hitsTop) outerR else innerR + val bottomR = outerR + + val path = Path().apply { + addRoundRect( + RoundRect( + rect = Rect(0f, activeTop, w, activeBottom), + topLeft = CornerRadius(topR, topR), + bottomLeft = CornerRadius(bottomR, bottomR), + topRight = CornerRadius(topR, topR), + bottomRight = CornerRadius(bottomR, bottomR), + ) + ) + } + + val brush = + if (gradientColors != null) { + Brush.verticalGradient( + colors = gradientColors, + startY = activeBottom, + endY = activeTop + ) + } else { + Brush.linearGradient(listOf(activeSolidColor, activeSolidColor)) + } + + drawPath(path = path, brush = brush) + } + } + ) +} + @Composable private fun TrackIcon( icon: (@Composable BoxScope.(sliderIconsState: SliderIconsState) -> Unit)?, diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperUtils.java b/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperUtils.java index 759044f3e0bf5..d9af73b33596b 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperUtils.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperUtils.java @@ -45,35 +45,22 @@ public static Bitmap resizeAndCompress(Bitmap bitmap, Context context) { float maxScale = 1.10f; int targetWidth = Math.round(screenWidth * maxScale); int targetHeight = Math.round(screenHeight * maxScale); - - Bitmap resized = bitmap; - if (bitmap.getWidth() != targetWidth || bitmap.getHeight() != targetHeight) { - resized = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true); - } - - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - resized.compress(Bitmap.CompressFormat.PNG, 90, byteArrayOutputStream); - byte[] byteArray = byteArrayOutputStream.toByteArray(); - - if (resized != bitmap) { - resized.recycle(); + if (bitmap.getWidth() == targetWidth && bitmap.getHeight() == targetHeight) { + return bitmap; } - - return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); + return Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true); } public static Bitmap getDimmedBitmap(Bitmap bitmap, int dimLevel) { - Bitmap mutableBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false); float dimFactor = 1 - (Math.max(0, Math.min(dimLevel, 100)) / 100f); - Bitmap dimmedBitmap = Bitmap.createBitmap(mutableBitmap.getWidth(), mutableBitmap.getHeight(), Bitmap.Config.ARGB_8888); + Bitmap dimmedBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(dimmedBitmap); Paint paint = new Paint(); ColorMatrix colorMatrix = new ColorMatrix(); colorMatrix.setScale(dimFactor, dimFactor, dimFactor, 1.0f); ColorMatrixColorFilter colorFilter = new ColorMatrixColorFilter(colorMatrix); paint.setColorFilter(colorFilter); - canvas.drawBitmap(mutableBitmap, 0, 0, paint); - mutableBitmap.recycle(); + canvas.drawBitmap(bitmap, 0, 0, paint); return dimmedBitmap; } @@ -193,9 +180,10 @@ public static Bitmap getVignetteEffect(Bitmap bitmap, float intensity) { float centerY = height / 2f; float maxRadius = (float) Math.sqrt(centerX * centerX + centerY * centerY); + int vignetteAlpha = (int)(intensity * 255) & 0xff; RadialGradient gradient = new RadialGradient( centerX, centerY, maxRadius, - new int[]{0x00000000, (int)(intensity * 255) << 24}, + new int[]{0x00000000, (vignetteAlpha << 24) | 0x00000000}, new float[]{0.5f, 1.0f}, Shader.TileMode.CLAMP ); @@ -277,6 +265,7 @@ public static Bitmap getSharpenEffect(Bitmap bitmap) { }; int[] result = new int[width * height]; + System.arraycopy(pixels, 0, result, 0, pixels.length); for (int y = 1; y < height - 1; y++) { for (int x = 1; x < width - 1; x++) { @@ -361,6 +350,12 @@ public static Bitmap getRadialBlurEffect(Bitmap bitmap) { float dirX = x - centerX; float dirY = y - centerY; float distance = (float) Math.sqrt(dirX * dirX + dirY * dirY); + + if (distance == 0) { + result.setPixel(x, y, pixels[y * width + x]); + continue; + } + float blurAmount = distance * strength; float totalRed = 0, totalGreen = 0, totalBlue = 0, totalAlpha = 0; diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt new file mode 100644 index 0000000000000..d8b151f528572 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023-2024 risingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.weather + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.View.MeasureSpec +import android.widget.ImageView +import android.widget.TextView +import com.android.systemui.res.R + +class WeatherImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, + isCustomClock: Boolean = true, +) : ImageView(context, attrs, defStyle) { + + private val maxSizePx: Int = + context.resources.getDimension(R.dimen.weather_image_max_size).toInt() + + private val weatherViewController: WeatherViewController + + init { + visibility = View.GONE + + val stubText = TextView(context) + + weatherViewController = WeatherViewController( + context = context, + weatherIcon = this, + weatherTemp = stubText, + weatherInfoView = this, + isCustomClock = isCustomClock, + ) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + weatherViewController.init() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + weatherViewController.removeObserver() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val width = maxSizePx.coerceAtMost(MeasureSpec.getSize(widthMeasureSpec)) + val height = maxSizePx.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec)) + setMeasuredDimension(width, height) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt new file mode 100644 index 0000000000000..a603256bf48c1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023-2024 risingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.weather + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.TextView +import com.android.systemui.res.R + +class WeatherTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, + isCustomClock: Boolean = true, +) : TextView(context, attrs, defStyle) { + + private val mWeatherViewController: WeatherViewController + + init { + visibility = View.GONE + + val stubIcon = android.widget.ImageView(context) + + mWeatherViewController = WeatherViewController( + context = context, + weatherIcon = stubIcon, + weatherTemp = this, + weatherInfoView = this, + isCustomClock = isCustomClock, + ) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + mWeatherViewController.init() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + mWeatherViewController.removeObserver() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt index de191bb923887..2603ab84fb411 100644 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt @@ -38,6 +38,7 @@ class WeatherViewController( private val weatherIcon: ImageView, private val weatherTemp: TextView, private val weatherInfoView: View, + private val isCustomClock: Boolean = false, ) : OmniJawsClient.OmniJawsObserver { private var weatherInfo: OmniJawsClient.WeatherInfo? = null @@ -94,12 +95,16 @@ class WeatherViewController( showWeatherLocation = getSystemSetting(LOCKSCREEN_WEATHER_LOCATION), showWeatherText = getSystemSetting(LOCKSCREEN_WEATHER_TEXT, defaultValue = 1), showWindInfo = getSystemSetting(LOCKSCREEN_WEATHER_WIND_INFO), - showHumidityInfo = getSystemSetting(LOCKSCREEN_WEATHER_HUMIDITY_INFO) + showHumidityInfo = getSystemSetting(LOCKSCREEN_WEATHER_HUMIDITY_INFO), + customClockWeather = getSecureSetting(CUSTOM_CLOCK_WEATHER, defaultValue = 1), ) private fun getSystemSetting(setting: String, defaultValue: Int = 0) = Settings.System.getIntForUser(context.contentResolver, setting, defaultValue, UserHandle.USER_CURRENT) != 0 + private fun getSecureSetting(setting: String, defaultValue: Int = 0) = + Settings.Secure.getIntForUser(context.contentResolver, setting, defaultValue, UserHandle.USER_CURRENT) != 0 + private fun applyWeatherSettings(settings: WeatherSettings) { if (!settings.weatherEnabled) { hideAllViews() @@ -141,6 +146,8 @@ class WeatherViewController( weatherTemp.isSelected = true } } catch (e: Exception) {} + + applyElementVisibility(weatherSettingsFlow.value) } private fun hideAllViews() { @@ -156,6 +163,18 @@ class WeatherViewController( listOf(weatherInfoView, weatherIcon, weatherTemp).forEach { updateViewVisibility(it, true) } + applyElementVisibility(weatherSettingsFlow.value) + } + } + + private fun applyElementVisibility(settings: WeatherSettings) { + scope.launch { + val show = settings.weatherEnabled && ( + if (isCustomClock) settings.customClockWeather + else !settings.customClockWeather + ) + updateViewVisibility(weatherIcon, show) + updateViewVisibility(weatherTemp, show) } } @@ -197,7 +216,8 @@ class WeatherViewController( val showWeatherLocation: Boolean, val showWeatherText: Boolean, val showWindInfo: Boolean, - val showHumidityInfo: Boolean + val showHumidityInfo: Boolean, + val customClockWeather: Boolean, ) companion object { @@ -206,6 +226,7 @@ class WeatherViewController( private const val LOCKSCREEN_WEATHER_TEXT = "lockscreen_weather_text" private const val LOCKSCREEN_WEATHER_WIND_INFO = "lockscreen_weather_wind_info" private const val LOCKSCREEN_WEATHER_HUMIDITY_INFO = "lockscreen_weather_humidity_info" + const val CUSTOM_CLOCK_WEATHER = "custom_clock_weather" private val WEATHER_CONDITIONS = mapOf( "clouds" to R.string.weather_condition_clouds, diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt index 0e01ca1588b34..3eacec83124b4 100644 --- a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.combine typealias BlurAppliedListener = Consumer @@ -106,58 +107,70 @@ constructor( .stateIn(scope, SharingStarted.WhileSubscribed(), false) override val isTranslucentSupported: StateFlow = - conflatedCallbackFlow { - val sendUpdate = { - trySendWithFailureLogging( - isTranslucentEnabled(), - TAG, - "unable to send notificationRowTransparency state change", - ) - } - val observer = - object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) = sendUpdate() + combine( + conflatedCallbackFlow { + val sendUpdate = { + trySendWithFailureLogging( + isTranslucentEnabledInSettings(), + TAG, + "unable to send notificationRowTransparency state change", + ) } - val resolver = context.contentResolver - resolver.registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY), - true, - observer, - ) - sendUpdate() - awaitClose { resolver.unregisterContentObserver(observer) } + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) = sendUpdate() + } + val resolver = context.contentResolver + resolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY), + true, + observer, + ) + sendUpdate() + awaitClose { resolver.unregisterContentObserver(observer) } + }, + isBlurSupported + ) { settingEnabled, blurSupported -> + settingEnabled && blurSupported && isSystemBlurEnabled() } - .stateIn(scope, SharingStarted.WhileSubscribed(), isTranslucentEnabled()) + .stateIn(scope, SharingStarted.WhileSubscribed(), + isTranslucentEnabledInSettings() && isSystemBlurEnabled()) override val isLockscreenTranslucentSupported: StateFlow = - conflatedCallbackFlow { - val sendUpdate = { - trySendWithFailureLogging( - isLockscreenTranslucentEnabled(), - TAG, - "unable to send notificationRowTransparency lockscreen state change", - ) - } - val observer = - object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) = sendUpdate() + combine( + conflatedCallbackFlow { + val sendUpdate = { + trySendWithFailureLogging( + isLockscreenTranslucentEnabledInSettings(), + TAG, + "unable to send notificationRowTransparency lockscreen state change", + ) } - val resolver = context.contentResolver - resolver.registerContentObserver( - Settings.Secure.getUriFor( - Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY_LOCKSCREEN, - ), - true, - observer, - ) - sendUpdate() - awaitClose { resolver.unregisterContentObserver(observer) } + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) = sendUpdate() + } + val resolver = context.contentResolver + resolver.registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY_LOCKSCREEN, + ), + true, + observer, + ) + sendUpdate() + awaitClose { resolver.unregisterContentObserver(observer) } + }, + isBlurSupported + ) { settingEnabled, blurSupported -> + settingEnabled && blurSupported && isSystemBlurEnabled() } - .stateIn(scope, SharingStarted.WhileSubscribed(), isLockscreenTranslucentEnabled()) + .stateIn(scope, SharingStarted.WhileSubscribed(), + isLockscreenTranslucentEnabledInSettings() && isSystemBlurEnabled()) override var blurAppliedListener: BlurAppliedListener? = null - private fun isTranslucentEnabled(): Boolean = + private fun isTranslucentEnabledInSettings(): Boolean = Settings.Secure.getIntForUser( context.contentResolver, Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY, @@ -165,12 +178,22 @@ constructor( UserHandle.USER_CURRENT, ) == 1 - private fun isLockscreenTranslucentEnabled(): Boolean = + private fun isLockscreenTranslucentEnabledInSettings(): Boolean = Settings.Secure.getIntForUser( context.contentResolver, Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY_LOCKSCREEN, 1, UserHandle.USER_CURRENT) == 1 + private fun isSystemBlurEnabled(): Boolean { + val blurEnabledByDefault = SystemProperties.getBoolean("ro.custom.blur.enable", false) + val blurEnabled = Settings.Global.getInt( + context.contentResolver, + Settings.Global.DISABLE_WINDOW_BLURS, + if (blurEnabledByDefault) 0 else 1 + ) != 1 + return blurEnabled + } + private fun isBlurAllowed(): Boolean { return ActivityManager.isHighEndGfx() && !isDisableBlurSysPropSet() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt index b607266fdefa1..a467e83cb7008 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt @@ -86,9 +86,13 @@ import kotlin.test.assertEquals import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runCurrent import org.junit.Assert +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.invocation.InvocationOnMock @@ -514,6 +518,87 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { ) } + @Test + fun testShowGuts_lockedPrimary_yes() { + whenever(userManager.isManagedProfile(anyInt())).thenReturn(false) + whenever(notificationLockscreenUserManager.isLockscreenPublicMode(anyInt())) + .thenReturn(true) + + val guts = spy(NotificationGuts(mContext)) + whenever(guts.post(any())).thenAnswer { invocation: InvocationOnMock -> + handler.post(((invocation.arguments[0] as Runnable))) + null + } + + // Test doesn't support animation since the guts view is not attached. + doNothing().whenever(guts).openControls(anyInt(), anyInt(), anyBoolean(), any()) + + val realRow = createTestNotificationRow() + val menuItem = createTestMenuItem(realRow) + + val row = spy(realRow) + whenever(row.windowToken).thenReturn(Binder()) + whenever(row.guts).thenReturn(guts) + + assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem)) + executor.runAllReady() + verify(guts).openControls(anyInt(), anyInt(), anyBoolean(), any()) + } + + @Test + fun testShowGuts_unlockedWork_yes() { + whenever(userManager.isManagedProfile(anyInt())).thenReturn(true) + whenever(notificationLockscreenUserManager.isLockscreenPublicMode(anyInt())) + .thenReturn(false) + + val guts = spy(NotificationGuts(mContext)) + whenever(guts.post(any())).thenAnswer { invocation: InvocationOnMock -> + handler.post(((invocation.arguments[0] as Runnable))) + null + } + + // Test doesn't support animation since the guts view is not attached. + doNothing().whenever(guts).openControls(anyInt(), anyInt(), anyBoolean(), any()) + + val realRow = createTestNotificationRow() + val menuItem = createTestMenuItem(realRow) + + val row = spy(realRow) + whenever(row.windowToken).thenReturn(Binder()) + whenever(row.guts).thenReturn(guts) + + assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem)) + executor.runAllReady() + verify(guts).openControls(anyInt(), anyInt(), anyBoolean(), any()) + } + + @Test + fun testShowGuts_lockedWork_no() { + whenever(userManager.isManagedProfile(anyInt())).thenReturn(true) + whenever(notificationLockscreenUserManager.isLockscreenPublicMode(anyInt())) + .thenReturn(true) + + val guts = spy(NotificationGuts(mContext)) + whenever(guts.post(any())).thenAnswer { invocation: InvocationOnMock -> + handler.post(((invocation.arguments[0] as Runnable))) + null + } + + // Test doesn't support animation since the guts view is not attached. + doNothing().whenever(guts).openControls(anyInt(), anyInt(), anyBoolean(), any()) + + val realRow = createTestNotificationRow() + val menuItem = createTestMenuItem(realRow) + + val row = spy(realRow) + whenever(row.windowToken).thenReturn(Binder()) + whenever(row.guts).thenReturn(guts) + + assertFalse(gutsManager.openGutsInternal(row, 0, 0, menuItem)) + executor.runAllReady() + verify(guts, never()).openControls(anyInt(), anyInt(), anyBoolean(), any()) + } + private fun createTestNotificationRow( block: NotificationEntryBuilder.() -> Unit = {} ): ExpandableNotificationRow { diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 8478276094043..79731097ceff5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -87,7 +87,6 @@ import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.volume.domain.interactor.VolumeDialogInteractor; import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor; -import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag; import com.android.systemui.volume.ui.navigation.VolumeNavigator; import com.google.android.msdl.domain.MSDLPlayer; @@ -155,8 +154,6 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Mock private VolumeNavigator mVolumeNavigator; @Mock - private VolumePanelFlag mVolumePanelFlag; - @Mock private VolumeDialogInteractor mVolumeDialogInteractor; private final CsdWarningDialog.Factory mCsdWarningDialogFactory = @@ -222,7 +219,6 @@ public void setup() throws Exception { mCsdWarningDialogFactory, mPostureController, mTestableLooper.getLooper(), - mVolumePanelFlag, mDumpManager, mLazySecureSettings, mVibratorHelper, diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index c9680b0340101..96b24d8867e4e 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -990,13 +990,15 @@ void setBindInstantServiceAllowed(int userId, boolean allowed) { private void onSomePackagesChangedLocked( @Nullable List parsedAccessibilityServiceInfos, - @Nullable List parsedAccessibilityShortcutInfos) { + @Nullable List parsedAccessibilityShortcutInfos, + @NonNull Set validA11yTileServices) { final AccessibilityUserState userState = getCurrentUserStateLocked(); // Reload the installed services since some services may have different attributes // or resolve info (does not support equals), etc. Remove them then to force reload. userState.mInstalledServices.clear(); - if (readConfigurationForUserStateLocked(userState, - parsedAccessibilityServiceInfos, parsedAccessibilityShortcutInfos)) { + if (readConfigurationForUserStateLocked( + userState, parsedAccessibilityServiceInfos, parsedAccessibilityShortcutInfos, + validA11yTileServices)) { onUserStateChangedLocked(userState); } } @@ -2268,6 +2270,13 @@ void switchUser(int userId) { // parse outside of a lock, but after verifying userId parsedAccessibilityServiceInfos = parseAccessibilityServiceInfos(userId); parsedAccessibilityShortcutInfos = parseAccessibilityShortcutInfos(userId); + Set validA11yTileServices = AccessibilityTileUtils.getValidA11yTileServices( + mContext, + LocalServices.getService(PackageManagerInternal.class), + parsedAccessibilityServiceInfos, + parsedAccessibilityShortcutInfos, + userId + ); synchronized (mLock) { // Disconnect from services for the old user. @@ -2286,7 +2295,8 @@ void switchUser(int userId) { AccessibilityUserState userState = getCurrentUserStateLocked(); readConfigurationForUserStateLocked(userState, - parsedAccessibilityServiceInfos, parsedAccessibilityShortcutInfos); + parsedAccessibilityServiceInfos, parsedAccessibilityShortcutInfos, + validA11yTileServices); mSecurityPolicy.onSwitchUserLocked(mCurrentUserId, userState.mEnabledServices); // Even if reading did not yield change, we have to update // the state since the context in which the current user @@ -2699,7 +2709,8 @@ private List parseAccessibilityServiceInfos(int userId } private boolean readInstalledAccessibilityServiceLocked(AccessibilityUserState userState, - @Nullable List parsedAccessibilityServiceInfos) { + @Nullable List parsedAccessibilityServiceInfos, + @NonNull Set validA11yTileServices) { for (int i = 0, count = parsedAccessibilityServiceInfos.size(); i < count; i++) { AccessibilityServiceInfo accessibilityServiceInfo = parsedAccessibilityServiceInfos.get(i); @@ -2708,14 +2719,18 @@ private boolean readInstalledAccessibilityServiceLocked(AccessibilityUserState u accessibilityServiceInfo.crashed = true; } } + boolean serviceInfosChanged = false; if (!parsedAccessibilityServiceInfos.equals(userState.mInstalledServices)) { userState.mInstalledServices.clear(); userState.mInstalledServices.addAll(parsedAccessibilityServiceInfos); - userState.updateTileServiceMapForAccessibilityServiceLocked(); - return true; + serviceInfosChanged = true; } - return false; + // Sometimes when the package changes is called (especially for the initial load), the + // package manager may not be able to resolve the TileService at that time. Always + // rebuild the feature to tileService map could solve the problem. + userState.updateTileServiceMapForAccessibilityServiceLocked(validA11yTileServices); + return serviceInfosChanged; } /** @@ -2734,7 +2749,9 @@ private List parseAccessibilityShortcutInfos(int user } private boolean readInstalledAccessibilityShortcutLocked(AccessibilityUserState userState, - List parsedAccessibilityShortcutInfos) { + List parsedAccessibilityShortcutInfos, + @NonNull Set validA11yTileServices) { + boolean shortcutInfosChanged = false; if (!parsedAccessibilityShortcutInfos.equals(userState.mInstalledShortcuts)) { List componentNames = userState.mInstalledShortcuts.stream() .filter(a11yActivity -> @@ -2749,10 +2766,13 @@ private boolean readInstalledAccessibilityShortcutLocked(AccessibilityUserState userState.mInstalledShortcuts.clear(); userState.mInstalledShortcuts.addAll(parsedAccessibilityShortcutInfos); - userState.updateTileServiceMapForAccessibilityActivityLocked(); - return true; + shortcutInfosChanged = true; } - return false; + // Sometimes when the package changes is called (especially for the initial load), the + // package manager may not be able to resolve the TileService at that time. Always + // rebuild the feature to tileService map could solve the problem. + userState.updateTileServiceMapForAccessibilityActivityLocked(validA11yTileServices); + return shortcutInfosChanged; } private boolean readEnabledAccessibilityServicesLocked(AccessibilityUserState userState) { @@ -3577,11 +3597,12 @@ private void updateFilterKeyEventsLocked(AccessibilityUserState userState) { private boolean readConfigurationForUserStateLocked( AccessibilityUserState userState, List parsedAccessibilityServiceInfos, - List parsedAccessibilityShortcutInfos) { + List parsedAccessibilityShortcutInfos, + @NonNull Set validA11yTileServices) { boolean somethingChanged = readInstalledAccessibilityServiceLocked( - userState, parsedAccessibilityServiceInfos); + userState, parsedAccessibilityServiceInfos, validA11yTileServices); somethingChanged |= readInstalledAccessibilityShortcutLocked( - userState, parsedAccessibilityShortcutInfos); + userState, parsedAccessibilityShortcutInfos, validA11yTileServices); somethingChanged |= readEnabledAccessibilityServicesLocked(userState); somethingChanged |= readTouchExplorationGrantedAccessibilityServicesLocked(userState); somethingChanged |= readTouchExplorationEnabledSettingLocked(userState); @@ -6846,6 +6867,14 @@ public void onSomePackagesChanged() { .parseAccessibilityServiceInfos(userId); List parsedAccessibilityShortcutInfos = mManagerService .parseAccessibilityShortcutInfos(userId); + Set validA11yTileServices = + AccessibilityTileUtils.getValidA11yTileServices( + mManagerService.mContext, + LocalServices.getService(PackageManagerInternal.class), + parsedAccessibilityServiceInfos, + parsedAccessibilityShortcutInfos, + userId + ); synchronized (mManagerService.getLock()) { // Only the profile parent can install accessibility services. // Therefore we ignore packages from linked profiles. @@ -6861,7 +6890,7 @@ public void onSomePackagesChanged() { return; } mManagerService.onSomePackagesChangedLocked(parsedAccessibilityServiceInfos, - parsedAccessibilityShortcutInfos); + parsedAccessibilityShortcutInfos, validA11yTileServices); } } @@ -6883,6 +6912,14 @@ public void onPackageUpdateFinished(String packageName, int uid) { .parseAccessibilityServiceInfos(userId); List parsedAccessibilityShortcutInfos = mManagerService.parseAccessibilityShortcutInfos(userId); + Set validA11yTileServices = + AccessibilityTileUtils.getValidA11yTileServices( + mManagerService.mContext, + LocalServices.getService(PackageManagerInternal.class), + parsedAccessibilityServiceInfos, + parsedAccessibilityShortcutInfos, + userId + ); synchronized (mManagerService.getLock()) { if (userId != mManagerService.getCurrentUserIdLocked()) { return; @@ -6899,7 +6936,9 @@ public void onPackageUpdateFinished(String packageName, int uid) { final boolean configurationChanged; configurationChanged = mManagerService.readConfigurationForUserStateLocked( userState, parsedAccessibilityServiceInfos, - parsedAccessibilityShortcutInfos); + parsedAccessibilityShortcutInfos, + validA11yTileServices + ); if (reboundAService || configurationChanged) { mManagerService.onUserStateChangedLocked(userState); } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityTileUtils.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityTileUtils.java new file mode 100644 index 0000000000000..68e2b32ba7053 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityTileUtils.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.accessibility; + +import android.Manifest; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.AccessibilityShortcutInfo; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Process; +import android.service.quicksettings.TileService; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Slog; + +import java.util.List; +import java.util.Set; + +/** + * Collection of utilities for Accessibility feature's TileServices. + */ +public class AccessibilityTileUtils { + private AccessibilityTileUtils() {} + private static final String TAG = "AccessibilityTileUtils"; + + /** + * Checks if a given {@link ComponentName} corresponds to a valid and enabled TileService + * for the current user. + * + * @param componentName The {@link ComponentName} of the service to validate. + * @return {@code true} if the component is a valid and enabled TileService, {@code false} + * otherwise. + */ + private static boolean isComponentValidTileService( + @NonNull Context context, @NonNull PackageManagerInternal pm, + @NonNull ComponentName componentName, @UserIdInt int userId) { + Intent intent = new Intent(TileService.ACTION_QS_TILE); + intent.setComponent(componentName); + + ResolveInfo resolveInfo = pm.resolveService(intent, + intent.resolveTypeIfNeeded(context.getContentResolver()), + /* flags= */ 0L, + userId, + android.os.Process.myUid()); + + if (resolveInfo == null || resolveInfo.serviceInfo == null) { + Slog.w(TAG, "TileService could not be resolved: " + componentName); + return false; + } + + ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (!serviceInfo.exported) { + Slog.w(TAG, "TileService is not exported: " + componentName); + return false; + } + + if (!Manifest.permission.BIND_QUICK_SETTINGS_TILE.equals(serviceInfo.permission)) { + Slog.w(TAG, "TileService is not protected by BIND_QUICK_SETTINGS_TILE permission: " + + componentName); + return false; + } + + int enabledSetting = pm.getComponentEnabledSetting(componentName, Process.myUid(), userId); + if (!resolveEnabledComponent(enabledSetting, serviceInfo.enabled)) { + Slog.w(TAG, "TileService is not enabled: " + componentName.flattenToShortString()); + return false; + } + + return true; + } + + /** + * Resolves the effective enabled state of a component by considering both its dynamic setting + * and its static manifest declaration. + * + * @param pmResult The component's dynamic enabled state, as returned by + * {@link PackageManager#getComponentEnabledSetting(ComponentName)}. + * @param defaultValue The component's static enabled state from its manifest (e.g., + * {@link android.content.pm.ServiceInfo#enabled}). This value is used + * when {@code pmResult} is + * {@link PackageManager#COMPONENT_ENABLED_STATE_DEFAULT}. + * @return {@code true} if the component is considered enabled, {@code false} otherwise. + */ + private static boolean resolveEnabledComponent(int pmResult, boolean defaultValue) { + if (pmResult == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + return true; + } + if (pmResult == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) { + return defaultValue; + } + return false; + } + + /** + * Returns a set of {@link ComponentName}s for valid Accessibility TileServices. + * A TileService is considered valid if it is properly declared, exported, protected by the + * {@link Manifest.permission#BIND_QUICK_SETTINGS_TILE} permission, and enabled. + * + * @param context The current context. + * @param pm The {@link PackageManagerInternal} instance. + * @param accessibilityServiceInfos A list of installed {@link AccessibilityServiceInfo}s. + * @param accessibilityShortcutInfos A list of installed {@link AccessibilityShortcutInfo}s. + * @param userId The user ID for which to retrieve the TileServices. + * @return A {@link Set} of valid {@link ComponentName}s for Accessibility TileServices. + */ + @NonNull + public static Set getValidA11yTileServices( + @NonNull Context context, + @Nullable PackageManagerInternal pm, + @Nullable List accessibilityServiceInfos, + @Nullable List accessibilityShortcutInfos, + @UserIdInt int userId + ) { + Set validA11yTileServices = new ArraySet<>(); + if (pm == null) { + return validA11yTileServices; + } + + if (accessibilityServiceInfos != null) { + accessibilityServiceInfos.forEach( + a11yServiceInfo -> { + String tileServiceName = a11yServiceInfo.getTileServiceName(); + if (!TextUtils.isEmpty(tileServiceName)) { + ResolveInfo resolveInfo = a11yServiceInfo.getResolveInfo(); + ComponentName a11yFeature = new ComponentName( + resolveInfo.serviceInfo.packageName, + resolveInfo.serviceInfo.name + ); + ComponentName tileService = new ComponentName( + a11yFeature.getPackageName(), + tileServiceName + ); + if (isComponentValidTileService(context, pm, tileService, userId)) { + validA11yTileServices.add(tileService); + } + } + } + ); + } + + if (accessibilityShortcutInfos != null) { + accessibilityShortcutInfos.forEach( + a11yShortcutInfo -> { + String tileServiceName = a11yShortcutInfo.getTileServiceName(); + if (!TextUtils.isEmpty(tileServiceName)) { + ComponentName a11yFeature = a11yShortcutInfo.getComponentName(); + ComponentName tileService = new ComponentName( + a11yFeature.getPackageName(), + tileServiceName); + if (isComponentValidTileService(context, pm, tileService, userId)) { + validA11yTileServices.add(tileService); + } + } + } + ); + } + + return validA11yTileServices; + } + +} diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 178fefbd23c32..9400ba5296cb5 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -1175,8 +1175,18 @@ public boolean isServiceDetectsGesturesEnabled(int displayId) { return false; } - public void updateTileServiceMapForAccessibilityServiceLocked() { + /** + * Updates the internal map of accessibility services to their corresponding tile services. + * + * @param validA11yTileServices A set of valid {@link ComponentName}s for accessibility tile + * services. + */ + public void updateTileServiceMapForAccessibilityServiceLocked( + @NonNull Set validA11yTileServices) { mA11yServiceToTileService.clear(); + if (validA11yTileServices.isEmpty()) { + return; + } mInstalledServices.forEach( a11yServiceInfo -> { String tileServiceName = a11yServiceInfo.getTileServiceName(); @@ -1190,14 +1200,26 @@ public void updateTileServiceMapForAccessibilityServiceLocked() { a11yFeature.getPackageName(), tileServiceName ); - mA11yServiceToTileService.put(a11yFeature, tileService); + if (validA11yTileServices.contains(tileService)) { + mA11yServiceToTileService.put(a11yFeature, tileService); + } } } ); } - public void updateTileServiceMapForAccessibilityActivityLocked() { + /** + * Updates the internal map of accessibility activities to their corresponding tile services. + * + * @param validA11yTileServices A set of valid {@link ComponentName}s for accessibility tile + * services. + */ + public void updateTileServiceMapForAccessibilityActivityLocked( + @NonNull Set validA11yTileServices) { mA11yActivityToTileService.clear(); + if (validA11yTileServices.isEmpty()) { + return; + } mInstalledShortcuts.forEach( a11yShortcutInfo -> { String tileServiceName = a11yShortcutInfo.getTileServiceName(); @@ -1206,7 +1228,9 @@ public void updateTileServiceMapForAccessibilityActivityLocked() { ComponentName tileService = new ComponentName( a11yFeature.getPackageName(), tileServiceName); - mA11yActivityToTileService.put(a11yFeature, tileService); + if (validA11yTileServices.contains(tileService)) { + mA11yActivityToTileService.put(a11yFeature, tileService); + } } } ); diff --git a/services/applock/java/com/android/server/app/AppLockManagerService.kt b/services/applock/java/com/android/server/app/AppLockManagerService.kt index 0294aa8dcea42..6dad498540bf2 100644 --- a/services/applock/java/com/android/server/app/AppLockManagerService.kt +++ b/services/applock/java/com/android/server/app/AppLockManagerService.kt @@ -814,6 +814,9 @@ class AppLockManagerService( } private fun enforceCallingPermission(msg: String) { + if (com.android.internal.util.lunaris.PixelPropsUtils.isSystemLauncher(Binder.getCallingUid())) { + return; + } context.enforceCallingPermission(Manifest.permission.MANAGE_APP_LOCK, msg) } diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index dcae8edd80ad0..a91b4cb1cc7f0 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -605,7 +605,7 @@ private UserBackupManagerService( backupManagerMonitorDumpsysUtils::deleteExpiredBMMEvents, INITIALIZATION_DELAY_MILLIS); - mBackupPreferences = new UserBackupPreferences(mContext, mBaseStateDir); + mBackupPreferences = new UserBackupPreferences(userContext, mBaseStateDir); // Power management mWakelock = diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index a25d13207e8ec..0879ec7ad5418 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -34,6 +34,7 @@ import static com.android.internal.util.Preconditions.checkState; import static com.android.server.companion.association.DisassociationProcessor.REASON_API; import static com.android.server.companion.association.DisassociationProcessor.REASON_PKG_DATA_CLEARED; +import static com.android.server.companion.association.DisassociationProcessor.REASON_REVOKED; import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature; import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; @@ -219,6 +220,11 @@ public void onStart() { // Init association stores mAssociationStore.refreshCache(); + // Remove any revoked associations after reboot. + for (AssociationInfo ai : mAssociationStore.getRevokedAssociations()) { + mDisassociationProcessor.disassociate(ai.getId(), REASON_REVOKED); + } + // Init UUID store mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId()); diff --git a/services/core/java/com/android/server/AnimationThread.java b/services/core/java/com/android/server/AnimationThread.java index 826e7b52a9df6..8f7643a13f1d4 100644 --- a/services/core/java/com/android/server/AnimationThread.java +++ b/services/core/java/com/android/server/AnimationThread.java @@ -16,7 +16,7 @@ package com.android.server; -import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY; import android.os.Handler; import android.os.Trace; @@ -32,7 +32,7 @@ public final class AnimationThread extends ServiceThread { private static Handler sHandler; private AnimationThread() { - super("android.anim", THREAD_PRIORITY_DISPLAY, false /*allowIo*/); + super("android.anim", THREAD_PRIORITY_URGENT_DISPLAY, false /*allowIo*/); } private static void ensureThreadLocked() { diff --git a/services/core/java/com/android/server/DisplayThread.java b/services/core/java/com/android/server/DisplayThread.java index 13e9550a7bae6..8d1c976a17d80 100644 --- a/services/core/java/com/android/server/DisplayThread.java +++ b/services/core/java/com/android/server/DisplayThread.java @@ -35,7 +35,7 @@ public final class DisplayThread extends ServiceThread { private DisplayThread() { // DisplayThread runs important stuff, but these are not as important as things running in // AnimationThread. Thus, set the priority to one lower. - super("android.display", Process.THREAD_PRIORITY_DISPLAY + 1, false /*allowIo*/); + super("android.display", Process.THREAD_PRIORITY_URGENT_DISPLAY + 1, false /*allowIo*/); } private static void ensureThreadLocked() { diff --git a/services/core/java/com/android/server/HideAppListService.java b/services/core/java/com/android/server/HideAppListService.java new file mode 100644 index 0000000000000..5253424858bcf --- /dev/null +++ b/services/core/java/com/android/server/HideAppListService.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2025 the AxionAOSP Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Slog; +import com.android.server.SystemService; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class HideAppListService extends SystemService { + private static final String TAG = "HideAppListService"; + + private final Context mContext; + private final Handler mHandler = new Handler(); + + public HideAppListService(Context context) { + super(context); + mContext = context; + } + + @Override + public void onStart() { + Slog.i(TAG, "Starting HideAppListService"); + } + + @Override + public void onBootPhase(int phase) { + if (phase == SystemService.PHASE_BOOT_COMPLETED) { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); + filter.addDataScheme("package"); + mContext.registerReceiver(new PackageUninstallReceiver(), filter); + } + } + + private class PackageUninstallReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String packageName = intent.getData().getSchemeSpecificPart(); + if (packageName != null) { + Slog.i(TAG, "Package uninstalled: " + packageName); + removeFromHideAppList(packageName); + } + } + } + + private void removeFromHideAppList(String packageName) { + ContentResolver cr = mContext.getContentResolver(); + String apps = Settings.Secure.getString(cr, Settings.Secure.HIDE_APPLIST); + + if (apps == null || apps.isEmpty() || apps.equals(",")) { + return; + } + + Set appSet = new HashSet<>(Arrays.asList(apps.split(","))); + if (appSet.remove(packageName)) { + Slog.i(TAG, "Removing package due to reason: UNINSTALLED: " + packageName); + Settings.Secure.putString(cr, Settings.Secure.HIDE_APPLIST, String.join(",", appSet)); + } + } +} diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index a2e41ecdd0012..aa9d658c79804 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -3779,9 +3779,14 @@ public void mkdirs(String callingPkg, String appPath) { final int userId = UserHandle.getUserId(callingUid); final String propertyName = "sys.user." + userId + ".ce_available"; - // Ignore requests to create directories while CE storage is locked if (!isCeStorageUnlocked(userId)) { - throw new IllegalStateException("Failed to prepare " + appPath); + // If the directory already exists, the hardware is clearly unlocked. + // We log a warning but allow the code to continue to the vold call. + if (new File(appPath).exists()) { + Slog.w(TAG, "Storage reported locked, but path exists. Proceeding for: " + appPath); + } else { + throw new IllegalStateException("Failed to prepare " + appPath); + } } // Ignore requests to create directories if CE storage is not available @@ -3900,18 +3905,22 @@ public StorageVolume[] getVolumeList(int userId, String callingPackage, int flag } } - // Report all volumes as unmounted until we've recorded that user 0 has unlocked. There + // Report all volumes as unmounted until we've recorded that the parent has unlocked. There // are no guarantees that callers will see a consistent view of the volume before that // point - final boolean systemUserUnlocked = isSystemUnlocked(UserHandle.USER_SYSTEM); + final boolean systemUserUnlocked; final boolean userIsDemo; final boolean storagePermission; final boolean ceStorageUnlocked; final long token = Binder.clearCallingIdentity(); try { - userIsDemo = LocalServices.getService(UserManagerInternal.class) - .getUserInfo(userId).isDemo(); + final UserInfo userInfo = LocalServices.getService(UserManagerInternal.class) + .getUserInfo(userId); + final int parentUserId = userInfo.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID + ? userInfo.profileGroupId : userId; + systemUserUnlocked = isSystemUnlocked(parentUserId); + userIsDemo = userInfo.isDemo(); storagePermission = mStorageManagerInternal.hasExternalStorage(callingUid, callingPackage); ceStorageUnlocked = isCeStorageUnlocked(userId); diff --git a/services/core/java/com/android/server/UiThread.java b/services/core/java/com/android/server/UiThread.java index 88004bd5f6190..722e47dd58b71 100644 --- a/services/core/java/com/android/server/UiThread.java +++ b/services/core/java/com/android/server/UiThread.java @@ -35,7 +35,7 @@ public final class UiThread extends ServiceThread { private static Handler sHandler; private UiThread() { - super("android.ui", Process.THREAD_PRIORITY_FOREGROUND, false /*allowIo*/); + super("android.ui", Process.THREAD_PRIORITY_URGENT_DISPLAY, false /*allowIo*/); } @Override diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 1ae9c6b72bdcc..fd2cd51be5bb8 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -68,7 +68,7 @@ import static android.os.Process.THREAD_GROUP_FOREGROUND_WINDOW; import static android.os.Process.THREAD_GROUP_RESTRICTED; import static android.os.Process.THREAD_GROUP_TOP_APP; -import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALL; @@ -2182,7 +2182,7 @@ protected boolean applyOomAdjLSP(ProcessRecordInternal state, boolean doingAll, } if (renderThreadTid != 0) { - mInjector.setThreadPriority(renderThreadTid, THREAD_PRIORITY_DISPLAY); + mInjector.setThreadPriority(renderThreadTid, THREAD_PRIORITY_URGENT_DISPLAY); } } } catch (Exception e) { diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 70037dac3aeae..45c0f9ba393e3 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -39,6 +39,7 @@ import static android.app.AppOpsManager.OP_FLAGS_ALL; import static android.app.AppOpsManager.OP_FLAG_SELF; import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED; +import static android.app.AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED; import static android.app.AppOpsManager.OP_NONE; import static android.app.AppOpsManager.OP_PLAY_AUDIO; import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; @@ -3187,7 +3188,7 @@ public void setCameraAudioRestriction(@CAMERA_AUDIO_RESTRICTION int mode) { public int checkPackage(int uid, String packageName) { Objects.requireNonNull(packageName); try { - verifyAndGetBypass(uid, packageName, null, Process.INVALID_UID, null, true); + verifyAndGetBypass(uid, packageName, null, Process.INVALID_UID, null, true, true); // When the caller is the system, it's possible that the packageName is the special // one (e.g., "root") which isn't actually existed. if (resolveNonAppUid(packageName) == uid @@ -3405,8 +3406,10 @@ private SyncNotedAppOp noteOperationUnchecked(int code, int uid, @NonNull String @OpFlags int flags, boolean shouldCollectAsyncNotedOp, @Nullable String message, boolean shouldCollectMessage, int notedCount) { PackageVerificationResult pvr; + boolean proxyTrusted = (flags & OP_FLAG_UNTRUSTED_PROXIED) == 0; try { - pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName); + pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName, + proxyTrusted); if (!pvr.isAttributionTagValid) { attributionTag = null; } @@ -4070,8 +4073,10 @@ private SyncNotedAppOp startOperationUnchecked(IBinder clientId, int code, int u boolean shouldCollectMessage, @AttributionFlags int attributionFlags, int attributionChainId) { PackageVerificationResult pvr; + boolean proxyTrusted = (flags & OP_FLAG_UNTRUSTED_PROXIED) == 0; try { - pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName); + pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName, + proxyTrusted); if (!pvr.isAttributionTagValid) { attributionTag = null; } @@ -4204,8 +4209,10 @@ private SyncNotedAppOp startOperationDryRun(int code, int uid, int proxyUid, String proxyPackageName, @OpFlags int flags, boolean startIfModeDefault) { PackageVerificationResult pvr; + boolean proxyTrusted = (flags & OP_FLAG_UNTRUSTED_PROXIED) == 0; try { - pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName); + pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName, + proxyTrusted); if (!pvr.isAttributionTagValid) { attributionTag = null; } @@ -4221,7 +4228,9 @@ private SyncNotedAppOp startOperationDryRun(int code, int uid, boolean isRestricted = false; synchronized (this) { - final Ops ops = getOpsLocked(uid, packageName, attributionTag, + // Edit is true (so we create the Ops object if needed), but attribution tag is given as + // null, so we don't cache any information about it. + final Ops ops = getOpsLocked(uid, packageName, null, pvr.isAttributionTagValid, pvr.bypass, /* edit */ true); if (ops == null) { if (DEBUG) { @@ -4400,8 +4409,11 @@ private void finishOperationUnchecked(IBinder clientId, int code, int proxyUid, int virtualDeviceId) { PackageVerificationResult pvr; try { + // assume the proxy is trusted, since we aren't sure. We'll search for the attribution + // tag with trusted flags, and if we don't find it, search for a null tag with + // untrusted flags pvr = verifyAndGetBypass(proxiedUid, proxiedPackageName, attributionTag, - proxyUid, proxyPackageName); + proxyUid, proxyPackageName, /* isProxyTrusted */ true); if (!pvr.isAttributionTagValid) { attributionTag = null; } @@ -4411,8 +4423,9 @@ private void finishOperationUnchecked(IBinder clientId, int code, int proxyUid, } synchronized (this) { + boolean hasProxy = proxyUid != Process.INVALID_UID; Op op = getOpLocked(code, proxiedUid, proxiedPackageName, attributionTag, - pvr.isAttributionTagValid, pvr.bypass, /* edit */ true); + pvr.isAttributionTagValid, pvr.bypass, /* edit */ false); if (op == null) { Slog.e(TAG, "Operation not found: uid=" + proxiedUid + " pkg=" + proxiedPackageName + "(" @@ -4420,9 +4433,8 @@ private void finishOperationUnchecked(IBinder clientId, int code, int proxyUid, return; } final AttributedOp attributedOp = - op.mDeviceAttributedOps.getOrDefault( - getPersistentDeviceIdForOp(virtualDeviceId, code), - new ArrayMap<>()).get(attributionTag); + getAttributedOpWithClientId(op, clientId, attributionTag, virtualDeviceId, + hasProxy); if (attributedOp == null) { Slog.e(TAG, "Attribution not found: uid=" + proxiedUid + " pkg=" + proxiedPackageName + "(" @@ -4440,6 +4452,54 @@ private void finishOperationUnchecked(IBinder clientId, int code, int proxyUid, } } + private AttributedOp getAttributedOpWithClientId(Op op, IBinder clientId, + String attributionTag, int virtualDeviceId, boolean hasProxy) { + AttributedOp attributedOp = + op.mDeviceAttributedOps.getOrDefault( + getPersistentDeviceIdForOp(virtualDeviceId, op.op), + new ArrayMap<>()).get(attributionTag); + if (!hasProxy) { + return attributedOp; + } + boolean hasTrustedInProgressEvent = attributedOp != null + && attributedOp.hasInProgressEvent((event -> event.getClientId() == clientId + && (event.getFlags() & OP_FLAG_UNTRUSTED_PROXIED) == 0)); + if (hasTrustedInProgressEvent) { + return attributedOp; + } + + // We failed to find a trusted in progress event that matches the clientId. Check if the + // tag is valid in the package, and look for an untrusted access matching that tag, if so + boolean tagValid = false; + try { + tagValid = verifyAndGetBypass(op.uid, op.packageName, attributionTag) + .isAttributionTagValid; + } catch (SecurityException e) { + // assume tag is invalid + } + if (tagValid) { + boolean hasUntrustedInProgressEvent = attributedOp != null + && attributedOp.hasInProgressEvent((event -> event.getClientId() == clientId + && (event.getFlags() & OP_FLAG_UNTRUSTED_PROXIED) != 0)); + if (hasUntrustedInProgressEvent) { + return attributedOp; + } + } + + // The tag was not valid, or we failed to find an untrusted event. Look for an untrusted + // event with the null attribution tag + attributedOp = op.mDeviceAttributedOps.getOrDefault( + getPersistentDeviceIdForOp(virtualDeviceId, op.op), + new ArrayMap<>()).get(null); + boolean hasUntrustedNullEvent = attributedOp != null + && attributedOp.hasInProgressEvent((event -> event.getClientId() == clientId + && (event.getFlags() & OP_FLAG_UNTRUSTED_PROXIED) != 0)); + if (hasUntrustedNullEvent) { + return attributedOp; + } + return null; + } + void scheduleOpActiveChangedIfNeededLocked(int code, int uid, @NonNull String packageName, @Nullable String attributionTag, int virtualDeviceId, boolean active, @AttributionFlags int attributionFlags, int attributionChainId) { @@ -4879,20 +4939,22 @@ private RestrictionBypass getBypassforPackage(@NonNull PackageState packageState } /** - * @see #verifyAndGetBypass(int, String, String, int, String, boolean) + * @see #verifyAndGetBypass(int, String, String, int, String, boolean, boolean) */ private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName, @Nullable String attributionTag) { - return verifyAndGetBypass(uid, packageName, attributionTag, Process.INVALID_UID, null); + return verifyAndGetBypass(uid, packageName, attributionTag, Process.INVALID_UID, null, + true); } /** - * @see #verifyAndGetBypass(int, String, String, int, String, boolean) + * @see #verifyAndGetBypass(int, String, String, int, String, boolean, boolean) */ private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName, - @Nullable String attributionTag, int proxyUid, @Nullable String proxyPackageName) { + @Nullable String attributionTag, int proxyUid, @Nullable String proxyPackageName, + boolean isProxyTrusted) { return verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName, - false); + isProxyTrusted, false); } /** @@ -4905,6 +4967,8 @@ private RestrictionBypass getBypassforPackage(@NonNull PackageState packageState * @param attributionTag attribution tag or {@code null} if no need to verify * @param proxyUid The proxy uid, from which the attribution tag is to be pulled * @param proxyPackageName The proxy package, from which the attribution tag may be pulled + * @param isProxyTrusted Whether or not the proxy package is trusted. If it isn't, then the + * proxy attribution tag will not be used * @param suppressErrorLogs Whether to print to logcat about nonmatching parameters * * @return PackageVerificationResult containing {@link RestrictionBypass} and whether the @@ -4912,7 +4976,7 @@ private RestrictionBypass getBypassforPackage(@NonNull PackageState packageState */ private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName, @Nullable String attributionTag, int proxyUid, @Nullable String proxyPackageName, - boolean suppressErrorLogs) { + boolean isProxyTrusted, boolean suppressErrorLogs) { if (uid == Process.ROOT_UID) { // For backwards compatibility, don't check package name for root UID, unless someone // is claiming to be a proxy for root, which should never happen in normal usage. @@ -4920,7 +4984,8 @@ private RestrictionBypass getBypassforPackage(@NonNull PackageState packageState // system app (or is null), in order to prevent abusive apps clogging the appops // system with unlimited attribution tags via proxy calls. return new PackageVerificationResult(null, - /* isAttributionTagValid */ isPackageNullOrSystem(proxyPackageName, proxyUid)); + /* isAttributionTagValid */ isProxyTrusted + && isPackageNullOrSystem(proxyPackageName, proxyUid)); } if (Process.isSdkSandboxUid(uid)) { // SDK sandbox processes run in their own UID range, but their associated @@ -4984,7 +5049,8 @@ private RestrictionBypass getBypassforPackage(@NonNull PackageState packageState // system app (or is null), in order to prevent abusive apps clogging the appops // system with unlimited attribution tags via proxy calls. return new PackageVerificationResult(RestrictionBypass.UNRESTRICTED, - /* isAttributionTagValid */ isPackageNullOrSystem(proxyPackageName, proxyUid)); + /* isAttributionTagValid */ isProxyTrusted + && isPackageNullOrSystem(proxyPackageName, proxyUid)); } int userId = UserHandle.getUserId(uid); @@ -5005,8 +5071,9 @@ private RestrictionBypass getBypassforPackage(@NonNull PackageState packageState if (!isAttributionTagValid) { AndroidPackage proxyPkg = proxyPackageName != null ? pmInt.getPackage(proxyPackageName) : null; - // Re-check in proxy. - isAttributionTagValid = isAttributionInPackage(proxyPkg, attributionTag); + // Re-check in proxy, if trusted. + isAttributionTagValid = + isProxyTrusted && isAttributionInPackage(proxyPkg, attributionTag); String msg; if (pkg != null && isAttributionTagValid) { msg = "attributionTag " + attributionTag + " declared in manifest of the proxy" @@ -5033,7 +5100,6 @@ private RestrictionBypass getBypassforPackage(@NonNull PackageState packageState throw new SecurityException("Specified package \"" + packageName + "\" under uid " + uid + otherUidMessage); } - return new PackageVerificationResult(bypass, isAttributionTagValid); } @@ -5041,10 +5107,17 @@ private boolean isPackageNullOrSystem(String packageName, int uid) { if (packageName == null) { return true; } + if (Process.isSdkSandboxUid(uid)) { + return false; + } int appId = UserHandle.getAppId(uid); if (appId > 0 && appId < Process.FIRST_APPLICATION_UID) { return true; } + if (mPackageManagerInternal.getPackageUid(packageName, PackageManager.MATCH_ALL, + UserHandle.getUserId(uid)) != uid) { + return false; + } return mPackageManagerInternal.isSystemPackage(packageName); } diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java index 2cbf89893ff16..2b40cc38f44d0 100644 --- a/services/core/java/com/android/server/appop/AttributedOp.java +++ b/services/core/java/com/android/server/appop/AttributedOp.java @@ -40,6 +40,7 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.function.Consumer; +import java.util.function.Predicate; final class AttributedOp { private final @NonNull AppOpsService mAppOpsService; @@ -624,6 +625,19 @@ public boolean isPaused() { return mPausedInProgressEvents != null && !mPausedInProgressEvents.isEmpty(); } + public boolean hasInProgressEvent(Predicate predicate) { + ArrayMap events = + isPaused() ? mPausedInProgressEvents : mInProgressEvents; + if (events == null || events.isEmpty()) { + return false; + } + for (int i = 0; i < events.size(); i++) { + if (predicate.test(events.valueAt(i))) { + return true; + } + } + return false; + } boolean hasAnyTime() { return (mAccessEvents != null && mAccessEvents.size() > 0) || (mRejectEvents != null && mRejectEvents.size() > 0); diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index dd0b6366cae6e..44351d0b36300 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -438,20 +438,39 @@ public void onChange(boolean selfChange, Uri uri, int userId) { notifyEnabledOnKeyguardCallbacks(userId, TYPE_ANY_BIOMETRIC); } } else if (FACE_KEYGUARD_ENABLED.equals(uri)) { + final int biometricKeyguardEnabled = Settings.Secure.getIntForUser( + mContentResolver, + Settings.Secure.BIOMETRIC_KEYGUARD_ENABLED, + -1 /* default */, + userId); + // For OTA case: if FACE_KEYGUARD_ENABLED is not set and BIOMETRIC_APP_ENABLED is + // set, set the default value of the former to that of the latter. + final boolean defaultValue = biometricKeyguardEnabled == -1 + ? DEFAULT_KEYGUARD_ENABLED : biometricKeyguardEnabled == 1; mFaceEnabledOnKeyguard.put(userId, Settings.Secure.getIntForUser( mContentResolver, Settings.Secure.FACE_KEYGUARD_ENABLED, - DEFAULT_KEYGUARD_ENABLED ? 1 : 0 /* default */, + defaultValue ? 1 : 0 /* default */, userId) != 0); if (userId == ActivityManager.getCurrentUser() && !selfChange) { notifyEnabledOnKeyguardCallbacks(userId, TYPE_FACE); } } else if (FINGERPRINT_KEYGUARD_ENABLED.equals(uri)) { + final int biometricKeyguardEnabled = Settings.Secure.getIntForUser( + mContentResolver, + Settings.Secure.BIOMETRIC_KEYGUARD_ENABLED, + -1 /* default */, + userId); + // For OTA case: if FINGERPRINT_KEYGUARD_ENABLED is not set and + // BIOMETRIC_APP_ENABLED is set, set the default value of the former to that of the + // latter. + final boolean defaultValue = biometricKeyguardEnabled == -1 + ? DEFAULT_KEYGUARD_ENABLED : biometricKeyguardEnabled == 1; mFingerprintEnabledOnKeyguard.put(userId, Settings.Secure.getIntForUser( mContentResolver, Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED, - DEFAULT_KEYGUARD_ENABLED ? 1 : 0 /* default */, + defaultValue ? 1 : 0 /* default */, userId) != 0); if (userId == ActivityManager.getCurrentUser() && !selfChange) { @@ -464,16 +483,34 @@ public void onChange(boolean selfChange, Uri uri, int userId) { DEFAULT_APP_ENABLED ? 1 : 0 /* default */, userId) != 0); } else if (FACE_APP_ENABLED.equals(uri)) { + final int biometricAppEnabled = Settings.Secure.getIntForUser( + mContentResolver, + Settings.Secure.BIOMETRIC_APP_ENABLED, + -1 /* default */, + userId); + // For OTA case: if FACE_APP_ENABLED is not set and BIOMETRIC_APP_ENABLED is set, + // set the default value of the former to that of the latter. + final boolean defaultValue = biometricAppEnabled == -1 + ? DEFAULT_APP_ENABLED : biometricAppEnabled == 1; mFaceEnabledForApps.put(userId, Settings.Secure.getIntForUser( mContentResolver, Settings.Secure.FACE_APP_ENABLED, - DEFAULT_APP_ENABLED ? 1 : 0 /* default */, + defaultValue ? 1 : 0 /* default */, userId) != 0); } else if (FINGERPRINT_APP_ENABLED.equals(uri)) { + final int biometricAppEnabled = Settings.Secure.getIntForUser( + mContentResolver, + Settings.Secure.BIOMETRIC_APP_ENABLED, + -1 /* default */, + userId); + // For OTA case: if FINGERPRINT_APP_ENABLED is not set and BIOMETRIC_APP_ENABLED is + // set, set the default value of the former to that of the latter. + final boolean defaultValue = biometricAppEnabled == -1 + ? DEFAULT_APP_ENABLED : biometricAppEnabled == 1; mFingerprintEnabledForApps.put(userId, Settings.Secure.getIntForUser( mContentResolver, Settings.Secure.FINGERPRINT_APP_ENABLED, - DEFAULT_APP_ENABLED ? 1 : 0 /* default */, + defaultValue ? 1 : 0 /* default */, userId) != 0); } else if (MANDATORY_BIOMETRICS_ENABLED.equals(uri)) { updateMandatoryBiometricsForAllProfiles(userId); diff --git a/services/core/java/com/android/server/display/AutoAODService.java b/services/core/java/com/android/server/display/AutoAODService.java index f30ddc1b7d052..020a4253fc071 100644 --- a/services/core/java/com/android/server/display/AutoAODService.java +++ b/services/core/java/com/android/server/display/AutoAODService.java @@ -78,7 +78,7 @@ public class AutoAODService extends SystemService { private final AlarmManager mAlarmManager; private final Context mContext; - private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final Handler mHandler = new Handler(); private TwilightManager mTwilightManager; private TwilightState mTwilightState; private SharedPreferences mSharedPreferences; @@ -234,7 +234,7 @@ public AutoAODService(Context context) { super(context); mContext = context; mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - mSettingsObserver = new SettingsObserver(mHandler); + mSettingsObserver = new SettingsObserver(null); mActive = Settings.Secure.getIntForUser( mContext.getContentResolver(), Settings.Secure.DOZE_ALWAYS_ON, 0, diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java index baa1d1ca77c0f..419d37fd791a1 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java @@ -45,6 +45,7 @@ import com.android.internal.inputmethod.IRemoteComputerControlInputConnection; import com.android.internal.inputmethod.IRemoteInputConnection; import com.android.internal.inputmethod.InputMethodInfoSafeList; +import com.android.internal.inputmethod.InputMethodSubtypeSafeList; import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; import com.android.internal.view.IInputMethodManager; @@ -104,7 +105,8 @@ List getInputMethodListLegacy(@UserIdInt int userId, @NonNull List getEnabledInputMethodListLegacy(@UserIdInt int userId); - List getEnabledInputMethodSubtypeList(String imiId, + @NonNull + InputMethodSubtypeSafeList getEnabledInputMethodSubtypeList(String imiId, boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId); InputMethodSubtype getLastInputMethodSubtype(@UserIdInt int userId); @@ -255,8 +257,9 @@ public List getEnabledInputMethodListLegacy(@UserIdInt int user return mCallback.getEnabledInputMethodListLegacy(userId); } + @NonNull @Override - public List getEnabledInputMethodSubtypeList(String imiId, + public InputMethodSubtypeSafeList getEnabledInputMethodSubtypeList(String imiId, boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) { return mCallback.getEnabledInputMethodSubtypeList(imiId, allowsImplicitlyEnabledSubtypes, userId); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 0938da7d8681c..c7766ccca7b06 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -172,6 +172,7 @@ import com.android.internal.inputmethod.InputMethodInfoSafeList; import com.android.internal.inputmethod.InputMethodNavButtonFlags; import com.android.internal.inputmethod.InputMethodSubtypeHandle; +import com.android.internal.inputmethod.InputMethodSubtypeSafeList; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; @@ -1594,7 +1595,7 @@ public InputMethodInfoSafeList getInputMethodList(@UserIdInt int userId, Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } if (!mUserManagerInternal.exists(userId)) { - return InputMethodInfoSafeList.empty(); + return InputMethodInfoSafeList.create(null); } final int callingUid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); @@ -1615,7 +1616,7 @@ public InputMethodInfoSafeList getEnabledInputMethodList(@UserIdInt int userId) Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } if (!mUserManagerInternal.exists(userId)) { - return InputMethodInfoSafeList.empty(); + return InputMethodInfoSafeList.create(null); } final int callingUid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); @@ -1739,8 +1740,9 @@ private List getEnabledInputMethodListInternal(@UserIdInt int u * subtypes * @param userId the user ID to be queried about */ + @NonNull @Override - public List getEnabledInputMethodSubtypeList(String imiId, + public InputMethodSubtypeSafeList getEnabledInputMethodSubtypeList(String imiId, boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) { if (UserHandle.getCallingUserId() != userId) { mContext.enforceCallingOrSelfPermission( @@ -1750,8 +1752,9 @@ public List getEnabledInputMethodSubtypeList(String imiId, final int callingUid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); try { - return getEnabledInputMethodSubtypeListInternal(imiId, - allowsImplicitlyEnabledSubtypes, userId, callingUid); + return InputMethodSubtypeSafeList.create( + getEnabledInputMethodSubtypeListInternal(imiId, + allowsImplicitlyEnabledSubtypes, userId, callingUid)); } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java index 910c9a688969b..e8a3da5f0199b 100644 --- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java +++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java @@ -61,6 +61,7 @@ import com.android.internal.inputmethod.IRemoteComputerControlInputConnection; import com.android.internal.inputmethod.IRemoteInputConnection; import com.android.internal.inputmethod.InputMethodInfoSafeList; +import com.android.internal.inputmethod.InputMethodSubtypeSafeList; import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; import com.android.internal.util.FunctionalUtils.ThrowingRunnable; @@ -149,8 +150,9 @@ public List getEnabledInputMethodListLegacy(int userId) { return mInner.getEnabledInputMethodListLegacy(userId); } + @NonNull @Override - public List getEnabledInputMethodSubtypeList(String imiId, + public InputMethodSubtypeSafeList getEnabledInputMethodSubtypeList(String imiId, boolean allowsImplicitlyEnabledSubtypes, int userId) { return mInner.getEnabledInputMethodSubtypeList(imiId, allowsImplicitlyEnabledSubtypes, userId); diff --git a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java index 866651d4c3715..94a0eb0654261 100644 --- a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java +++ b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java @@ -137,6 +137,9 @@ public static MediaButtonReceiverHolder create( } public static MediaButtonReceiverHolder create(int userId, ComponentName broadcastReceiver) { + if (componentNameTooLong(broadcastReceiver)) { + throw new IllegalArgumentException("receiver name too long"); + } return new MediaButtonReceiverHolder(userId, null, broadcastReceiver, COMPONENT_TYPE_BROADCAST); } @@ -403,20 +406,27 @@ private static ComponentName getComponentName(PendingIntent pendingIntent, int c if (componentInfo != null && TextUtils.equals(componentInfo.packageName, pendingIntent.getCreatorPackage()) && componentInfo.packageName != null && componentInfo.name != null) { - int componentNameLength = - componentInfo.packageName.length() + componentInfo.name.length() + 1; - if (componentNameLength > MAX_COMPONENT_NAME_LENGTH) { + ComponentName componentName = + new ComponentName(componentInfo.packageName, componentInfo.name); + if (componentNameTooLong(componentName)) { Log.w(TAG, "detected and ignored component name with overly long package" + " or name, pi=" + pendingIntent); continue; } - return new ComponentName(componentInfo.packageName, componentInfo.name); + return componentName; } } return null; } + private static boolean componentNameTooLong(ComponentName componentName) { + return componentName.getPackageName().length() + + componentName.getClassName().length() + + 1 + > MAX_COMPONENT_NAME_LENGTH; + } + /** * Retrieves the {@link ComponentInfo} from a {@link ResolveInfo} instance. Similar to {@link * ResolveInfo#getComponentInfo()}, but returns {@code null} if this {@link ResolveInfo} points diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index 54cf810fc0397..1dd298cb67b33 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -978,8 +978,7 @@ protected boolean setPackageOrComponentEnabled(String pkgOrComponent, int userId if (approvedItem != null) { int uid = getUidForPackageOrComponent(pkgOrComponent, userId); if (enabled) { - if (!Flags.limitManagedServicesCount() - || approved.size() < MAX_SERVICE_ENTRIES) { + if (approved.size() < MAX_SERVICE_ENTRIES) { approved.add(approvedItem); if (uid != Process.INVALID_UID) { approvedUids.add(uid); @@ -1006,8 +1005,7 @@ protected boolean setPackageOrComponentEnabled(String pkgOrComponent, int userId mUserSetServices.put(userId, userSetServices); } if (userSet) { - if (!Flags.limitManagedServicesCount() - || userSetServices.size() < MAX_SERVICE_ENTRIES) { + if (userSetServices.size() < MAX_SERVICE_ENTRIES) { userSetServices.add(pkgOrComponent); } } else { @@ -1016,7 +1014,7 @@ protected boolean setPackageOrComponentEnabled(String pkgOrComponent, int userId } } - if (!Flags.limitManagedServicesCount() || changed) { + if (changed) { rebindServices(false, userId); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index fc2e513acefb5..378a00630eab1 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -7012,7 +7012,7 @@ public void setNotificationPolicyAccessGrantedForUser( pkg, userId, mConditionProviders.getRequiredPermission())) { boolean changed = mConditionProviders.setPackageOrComponentEnabled(pkg, userId, /* isPrimary= */ true, granted); - if (Flags.limitManagedServicesCount() && !changed) { + if (!changed) { return; } @@ -7282,7 +7282,7 @@ public void setNotificationListenerAccessGrantedForUser(ComponentName listener, boolean changed = mListeners.setPackageOrComponentEnabled( listener.flattenToString(), userId, /* isPrimary= */ true, granted, userSet); - if (Flags.limitManagedServicesCount() && !changed) { + if (!changed) { return; } @@ -13851,7 +13851,7 @@ protected boolean setPackageOrComponentEnabled(String pkgOrComponent, int userId boolean isPrimary, boolean enabled, boolean userSet) { boolean changed = super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled, userSet); - if (Flags.limitManagedServicesCount() && !changed) { + if (!changed) { return false; } diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 30c2067772542..9eddb61bece1f 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -164,6 +164,8 @@ import com.android.server.utils.WatchedSparseIntArray; import com.android.server.wm.ActivityTaskManagerInternal; +import org.rising.server.QuickSwitchService; + import libcore.util.EmptyArray; import java.io.BufferedOutputStream; @@ -183,6 +185,8 @@ import java.util.Set; import java.util.UUID; +import com.android.internal.util.lunaris.HideAppListUtils; + /** * This class contains the implementation of the Computer functions. It * is entirely self-contained - it has no implicit access to @@ -991,6 +995,12 @@ public final ApplicationInfo generateApplicationInfoFromSettings(String packageN public final ApplicationInfo getApplicationInfo(String packageName, @PackageManager.ApplicationInfoFlagsBits long flags, int userId) { + if (canHideApp(Binder.getCallingUid(), packageName) && + HideAppListUtils.shouldHideAppList(mContext, packageName)) { + return null; + } + if (QuickSwitchService.shouldHide(userId, packageName)) + return null; return getApplicationInfoInternal(packageName, flags, Binder.getCallingUid(), userId); } @@ -1004,6 +1014,12 @@ public final ApplicationInfo getApplicationInfoInternal(String packageName, @PackageManager.ApplicationInfoFlagsBits long flags, int filterCallingUid, int userId) { if (!mUserManager.exists(userId)) return null; + if (canHideApp(Binder.getCallingUid(), packageName) && + HideAppListUtils.shouldHideAppList(mContext, packageName)) { + return null; + } + if (QuickSwitchService.shouldHide(userId, packageName)) + return null; flags = updateFlagsForApplication(flags, userId); if (!isRecentsAccessingChildProfiles(Binder.getCallingUid(), userId)) { @@ -1014,6 +1030,64 @@ public final ApplicationInfo getApplicationInfoInternal(String packageName, return getApplicationInfoInternalBody(packageName, flags, filterCallingUid, userId); } + + private boolean canHideApp(int callingUid, String packageName) { + if (!isBootCompleted() || mContext == null || mContext.getPackageManager() == null) { + return false; + } + + String callingPackage = mContext.getPackageManager().getNameForUid(callingUid); + + if (callingPackage == null || TextUtils.isEmpty(callingPackage)) { + return false; + } + + // app can be always hidden if calling package is play store + boolean isFinsky = callingPackage.contains("com.android.vending"); + + if (isFinsky) return true; + + if (packageName == null || TextUtils.isEmpty(packageName)) { + return false; + } + + // the calling package is itself, no need to hide + if (callingPackage.contains(packageName)) return false; + + // we only want to hide these apps from playstore + // to avoid these apps from being updated, so abort if + // calling package is not finsky + if (packageName.contains("youtube") + || packageName.contains("microg") + || packageName.contains("revanced") + || packageName.contains("gms")) { + return false; + } + + // this is for banking apps, but we need to make sure first that + // we arent hiding app infos from sandbox/system processes + return !isCallerSystem(callingUid) + && !Process.isIsolated(callingUid) + && !Process.isSdkSandboxUid(callingUid); + } + + public ParceledListSlice recreatePackageList( + int callingUid, Context context, int userId, ParceledListSlice list) { + List appList = new ArrayList<>(list.getList()); + if (!canHideApp(callingUid, null)) return new ParceledListSlice<>(appList); + Set hiddenApps = HideAppListUtils.getApps(context); + appList.removeIf(info -> hiddenApps.contains(info.packageName)); + return new ParceledListSlice<>(appList); + } + + public List recreateApplicationList( + int callingUid, Context context, int userId, List list) { + List appList = new ArrayList<>(list); + if (!canHideApp(callingUid, null)) return appList; + Set hiddenApps = HideAppListUtils.getApps(context); + appList.removeIf(info -> hiddenApps.contains(info.packageName)); + return appList; + } protected ApplicationInfo getApplicationInfoInternalBody(String packageName, @PackageManager.ApplicationInfoFlagsBits long flags, @@ -1548,12 +1622,18 @@ public final PackageInfo generatePackageInfo(PackageStateInternal ps, return null; } - if ((flags & MATCH_UNINSTALLED_PACKAGES) != 0 - && ps.isSystem()) { - flags |= MATCH_ANY_USER; + final PackageUserStateInternal state = ps.getUserStateOrDefault(userId); + if ((flags & MATCH_UNINSTALLED_PACKAGES) != 0) { + final SettingBase callingSetting = + mSettings.getSettingBase(UserHandle.getAppId(callingUid)); + if (state.isHidden() && callingSetting != null && (callingSetting.getFlags() & + ApplicationInfo.FLAG_SYSTEM) != ApplicationInfo.FLAG_SYSTEM) { + return null; + } else if (ps.isSystem()) { + flags |= MATCH_ANY_USER; + } } - final PackageUserStateInternal state = ps.getUserStateOrDefault(userId); AndroidPackage p = ps.getPkg(); if (p != null) { // Compute GIDs only if requested @@ -1653,6 +1733,12 @@ public final PackageInfo generatePackageInfo(PackageStateInternal ps, public final PackageInfo getPackageInfo(String packageName, @PackageManager.PackageInfoFlagsBits long flags, int userId) { + if (canHideApp(Binder.getCallingUid(), packageName) && + HideAppListUtils.shouldHideAppList(mContext, packageName)) { + return null; + } + if (QuickSwitchService.shouldHide(userId, packageName)) + return null; return getPackageInfoInternal(packageName, PackageManager.VERSION_CODE_HIGHEST, flags, Binder.getCallingUid(), userId); } @@ -1776,7 +1862,8 @@ public final ParceledListSlice getInstalledPackages(long flags, int enforceCrossUserPermission(callingUid, userId, false /* requireFullPermission */, false /* checkShell */, "get installed packages"); - return getInstalledPackagesBody(flags, userId, callingUid); + return QuickSwitchService.recreatePackageList(callingUid, mContext, + userId, getInstalledPackagesBody(flags, userId, callingUid)); } protected ParceledListSlice getInstalledPackagesBody(long flags, int userId, @@ -2578,6 +2665,62 @@ public final boolean isSameProfileGroup(@UserIdInt int callerUserId, } } + private static boolean isBootCompleted() { + return android.os.SystemProperties.getBoolean("sys.boot_completed", false); + } + + /** + * Returns whether caller is home. + */ + private final boolean isCallerHome(int callingUid, int userId) { + final String home = mDefaultAppProvider.getDefaultHome(userId); + if (home == null) return false; + return isCallerSameApp(home, callingUid); + } + + /** + * Returns whether caller is system, root, shell, or updated system app. + */ + private final boolean isCallerSystem(int callingUid) { + if (isSystemOrRootOrShell(callingUid)) { + return true; + } + final SettingBase callingPs = mSettings.getSettingBase(UserHandle.getAppId(callingUid)); + if (callingPs == null) return false; + final int callingFlags = callingPs.getFlags(); + if (((callingFlags & ApplicationInfo.FLAG_SYSTEM) == ApplicationInfo.FLAG_SYSTEM) + || ((callingFlags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) + == ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) { + return true; + } + return false; + } + + private final boolean shouldFilterApplicationCustom( + @Nullable PackageStateInternal ps, int callingUid, int userId) { + if (!isBootCompleted()) return false; + if (ps == null) return false; + + final String packageName = ps.getPackageName(); + if (packageName == null) return false; + + // if the target and caller are the same application, skip + if (isCallerSameApp(packageName, callingUid) + // if the caller is system, root, shell, or updated system app, skip + || isCallerSystem(callingUid) + // if the caller is the current default home, skip + || isCallerHome(callingUid, userId)) { + return false; + } + // if the target is included in Settings.Secure.HIDE_APPLIST, do filter + if (canHideApp(Binder.getCallingUid(), packageName) && HideAppListUtils.shouldHideAppList( + mContext, packageName)) { + return true; + } + + return false; + } + /** * Returns whether or not access to the application should be filtered. *

@@ -2605,6 +2748,9 @@ public final boolean shouldFilterApplication(@Nullable PackageStateInternal ps, final boolean callerIsInstantApp = instantAppPkgName != null; final boolean packageArchivedForUser = ps != null && PackageArchiver.isArchived( ps.getUserStateOrDefault(userId)); + if (shouldFilterApplicationCustom(ps, callingUid, userId)) { + return true; + } // Don't treat hiddenUntilInstalled as an uninstalled state, phone app needs to access // these hidden application details to customize carrier apps. Also, allowing the system // caller accessing to application across users. @@ -4773,7 +4919,7 @@ public List getInstalledApplications( } } - return list; + return QuickSwitchService.recreateApplicationList(callingUid, mContext, userId, list); } @Nullable diff --git a/services/core/java/com/android/server/pm/DefaultAppProvider.java b/services/core/java/com/android/server/pm/DefaultAppProvider.java index fc61451b0289d..1ed23ceea5a66 100644 --- a/services/core/java/com/android/server/pm/DefaultAppProvider.java +++ b/services/core/java/com/android/server/pm/DefaultAppProvider.java @@ -113,9 +113,16 @@ public String getDefaultDialer(@NonNull int userId) { * @return the package name of the default home, or {@code null} if none */ @Nullable - public String getDefaultHome(@NonNull int userId) { - return getRoleHolder(RoleManager.ROLE_HOME, - mUserManagerInternalSupplier.get().getProfileParentId(userId)); + public String getDefaultHome(int userId) { + String currentHome = getRoleHolder( + RoleManager.ROLE_HOME, + mUserManagerInternalSupplier.get().getProfileParentId(userId) + ); + return maybeOverrideDefaultHome(currentHome); + } + + private String maybeOverrideDefaultHome(String packageName) { + return packageName; } /** @@ -135,7 +142,7 @@ public boolean setDefaultHome(@NonNull String packageName, @UserIdInt int userId } final long identity = Binder.clearCallingIdentity(); try { - roleManager.addRoleHolderAsUser(RoleManager.ROLE_HOME, packageName, 0, + roleManager.addRoleHolderAsUser(RoleManager.ROLE_HOME, maybeOverrideDefaultHome(packageName), 0, UserHandle.of(userId), executor, callback); } finally { Binder.restoreCallingIdentity(identity); diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index db5e49d9de7ae..e106761990b49 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -1130,6 +1130,8 @@ int createSessionInternal(SessionParams params, String installerPackageName, } final var dpmi = LocalServices.getService(DevicePolicyManagerInternal.class); + // Only the system should be able to set this flag - so ensure it is unset when not needed. + params.installFlags &= ~PackageManager.INSTALL_FROM_MANAGED_USER_OR_PROFILE; if (dpmi != null && dpmi.isUserOrganizationManaged(userId)) { params.installFlags |= PackageManager.INSTALL_FROM_MANAGED_USER_OR_PROFILE; } diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index d4bc94593c38e..479d62c96965f 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -945,8 +945,8 @@ PackageSetting enableSystemPackageLPw(String name) { p.getPkgState().setUpdatedSystemApp(false); final AndroidPackageInternal pkg = p.getPkg(); PackageSetting ret = addPackageLPw(name, p.getRealName(), p.getPath(), p.getAppId(), - p.getFlags(), p.getPrivateFlags(), mDomainVerificationManager.generateNewId(), - pkg == null ? false : pkg.isSdkLibrary()); + p.getFlags(), p.getPrivateFlags(), mDomainVerificationManager.generateNewId(), + pkg == null ? false : pkg.isSdkLibrary(), p.hasSharedUser()); if (ret != null) { ret.setLegacyNativeLibraryPath(p.getLegacyNativeLibraryPath()); ret.setPrimaryCpuAbi(p.getPrimaryCpuAbiLegacy()); @@ -966,6 +966,7 @@ PackageSetting enableSystemPackageLPw(String name) { ret.setRestrictUpdateHash(p.getRestrictUpdateHash()); ret.setScannedAsStoppedSystemApp(p.isScannedAsStoppedSystemApp()); ret.setInstallSource(p.getInstallSource()); + ret.setSharedUserAppId(p.getSharedUserAppId()); } mDisabledSysPackages.remove(name); return ret; @@ -987,7 +988,8 @@ void removeDisabledSystemPackageLPw(String name) { } PackageSetting addPackageLPw(String name, String realName, File codePath, int uid, - int pkgFlags, int pkgPrivateFlags, @NonNull UUID domainSetId, boolean isSdkLibrary) { + int pkgFlags, int pkgPrivateFlags, @NonNull UUID domainSetId, boolean isSdkLibrary, + boolean hasSharedUser) { PackageSetting p = mPackages.get(name); if (p != null) { if (p.getAppId() == uid) { @@ -1000,7 +1002,8 @@ PackageSetting addPackageLPw(String name, String realName, File codePath, int ui p = new PackageSetting(name, realName, codePath, pkgFlags, pkgPrivateFlags, domainSetId) .setAppId(uid); if ((uid == Process.INVALID_UID && isSdkLibrary && Flags.disallowSdkLibsToBeApps()) - || mAppIds.registerExistingAppId(uid, p, name)) { + || mAppIds.registerExistingAppId(uid, p, name) + || hasSharedUser) { mPackages.put(name, p); return p; } @@ -4332,7 +4335,8 @@ private void readPackageLPw(TypedXmlPullParser parser, ArrayList read } else if (appId > 0 || (appId == Process.INVALID_UID && isSdkLibrary && Flags.disallowSdkLibsToBeApps())) { packageSetting = addPackageLPw(name.intern(), realName, new File(codePathStr), - appId, pkgFlags, pkgPrivateFlags, domainSetId, isSdkLibrary); + appId, pkgFlags, pkgPrivateFlags, domainSetId, isSdkLibrary, + /* hasSharedUser= */ false); if (PackageManagerService.DEBUG_SETTINGS) Log.i(PackageManagerService.TAG, "Reading package " + name + ": appId=" + appId + " pkg=" + packageSetting); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 3c6a3709079ea..04f32eee1cfde 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -283,6 +283,8 @@ import org.lineageos.internal.buttons.LineageButtons; import org.lineageos.internal.util.ActionUtils; +import org.rising.server.ShakeGestureService; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; @@ -713,6 +715,8 @@ public void onDrawn() { ANBIHandler mANBIHandler; private boolean mANBIEnabled; + private ShakeGestureService mShakeGestures; + // Tracks user-customisable behavior for certain key events private Action mBackLongPressAction; private Action mHomeLongPressAction; @@ -727,6 +731,7 @@ public void onDrawn() { private Action mCornerLongSwipeAction; private Action mEdgeLongSwipeAction; private Action mThreeFingersSwipeAction; + private Action mShakeGestureAction; // support for activating the lock screen while the screen is on private HashSet mAllowLockscreenWhenOnDisplays = new HashSet<>(); @@ -1155,6 +1160,9 @@ void observe() { resolver.registerContentObserver(Settings.System.getUriFor( "doze_trigger_doubletap"), false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(LineageSettings.System.getUriFor( + LineageSettings.System.KEY_SHAKE_GESTURE_ACTION), false, this, + UserHandle.USER_ALL); updateSettings(); } @@ -3502,6 +3510,10 @@ private void updateKeyAssignments() { } } + mShakeGestureAction = Action.fromSettings(resolver, + LineageSettings.System.KEY_SHAKE_GESTURE_ACTION, + Action.NOTHING); + mShortPressOnWindowBehavior = SHORT_PRESS_WINDOW_NOTHING; if (mPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { mShortPressOnWindowBehavior = SHORT_PRESS_WINDOW_PICTURE_IN_PICTURE; @@ -7100,6 +7112,21 @@ public void systemReady() { if (mVrManagerInternal != null) { mVrManagerInternal.addPersistentVrModeStateListener(mPersistentVrModeListener); } + + mShakeGestures = ShakeGestureService.getInstance(mContext, new ShakeGestureService.ShakeGesturesCallbacks() { + @Override + public void onShake() { + if (mShakeGestureAction == Action.NOTHING) + return; + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SYSRQ, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_FROM_SYSTEM, InputDevice.SOURCE_TOUCHSCREEN); + performKeyAction(mShakeGestureAction, event); + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, "Shake Gesture"); + } + }); + mShakeGestures.onStart(); mDockObserverInternal = LocalServices.getService(DockObserverInternal.class); if (mDockObserverInternal != null) { diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java index ddc92c3fb3697..2ff3fca430ece 100644 --- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java +++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java @@ -240,7 +240,10 @@ public void onServiceConnected(ComponentName name, IBinder service) { public void onServiceDisconnected(ComponentName name) { if (DEBUG) Log.v(TAG, "*** Keyguard disconnected (boo!)"); mKeyguardService = null; + // Remember the keyguard enabled state when the service is disconnected. + boolean wasEnabled = mKeyguardState.enabled; mKeyguardState.reset(); + mKeyguardState.enabled = wasEnabled; mHandler.post(() -> { try { ActivityTaskManager.getService().setLockScreenShown(true /* keyguardShowing */, diff --git a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java index e09ab600a1dc3..1678142f61cfd 100644 --- a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java +++ b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java @@ -274,7 +274,7 @@ private Map> readFromLegacySettings(@UserIdInt int userId) { homePackageName = null; } if (homePackageName != null) { - roles.put(RoleManager.ROLE_HOME, Collections.singleton(homePackageName)); + roles.put(RoleManager.ROLE_HOME, Collections.singleton(maybeOverrideDefaultHome(homePackageName))); } // Emergency @@ -286,6 +286,10 @@ private Map> readFromLegacySettings(@UserIdInt int userId) { return roles; } + + private String maybeOverrideDefaultHome(String packageName) { + return packageName; + } private boolean isSettingsApplication(@NonNull String packageName, @UserIdInt int userId) { PackageManager packageManager = mContext.getPackageManager(); diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index 9174855b6466e..0a505dbc48b61 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -1028,10 +1028,11 @@ public void closeChannel() { if (mConfig != null) { try { mPowerHal.closeSessionChannel(mTgid, mUid); - } catch (RemoteException e) { - throw new IllegalStateException("Failed to close session channel!", e); + } catch (Exception e) { + Slog.w(TAG, "Session channel already dead for uid " + mUid); + } finally { + mConfig = null; } - mConfig = null; } } diff --git a/services/core/java/com/android/server/storage/StorageSessionController.java b/services/core/java/com/android/server/storage/StorageSessionController.java index 281aeb68f224d..f1551fc684459 100644 --- a/services/core/java/com/android/server/storage/StorageSessionController.java +++ b/services/core/java/com/android/server/storage/StorageSessionController.java @@ -272,8 +272,11 @@ public void onVolumeUnmount(ImmutableVolumeInfo vol) { */ public void onUnlockUser(int userId) throws ExternalStorageServiceException { Slog.i(TAG, "On user unlock " + userId); - if (userId == 0) { - initExternalStorageServiceComponent(); + if (mExternalStorageServiceComponent == null) { + final UserInfo info = mUserManager.getUserInfo(userId); + if (info != null && info.isFull()) { + initExternalStorageServiceComponent(userId); + } } } @@ -358,7 +361,8 @@ public void onReset(IVold vold, Runnable resetHandlerRunnable) { } } - private void initExternalStorageServiceComponent() throws ExternalStorageServiceException { + private void initExternalStorageServiceComponent(int userId) + throws ExternalStorageServiceException { Slog.i(TAG, "Initialialising..."); ProviderInfo provider = mContext.getPackageManager().resolveContentProvider( MediaStore.AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE @@ -371,7 +375,7 @@ private void initExternalStorageServiceComponent() throws ExternalStorageService mExternalStorageServicePackageName = provider.applicationInfo.packageName; mExternalStorageServiceAppId = UserHandle.getAppId(provider.applicationInfo.uid); - ServiceInfo serviceInfo = resolveExternalStorageServiceAsUser(UserHandle.USER_SYSTEM); + ServiceInfo serviceInfo = resolveExternalStorageServiceAsUser(userId); if (serviceInfo == null) { throw new ExternalStorageServiceException( "No valid ExternalStorageService component found"); diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java index 3877f63588280..c7f8185549f74 100644 --- a/services/core/java/com/android/server/webkit/SystemImpl.java +++ b/services/core/java/com/android/server/webkit/SystemImpl.java @@ -143,7 +143,7 @@ public class SystemImpl implements SystemInterface { @Override public WebViewProviderInfo[] getWebViewPackages() { return Arrays.stream(mWebViewProviderPackages) - .filter(x -> isProviderAvailable(x.packageName)) + .filter(this::isProviderAvailable) .toArray(WebViewProviderInfo[]::new); } @@ -153,9 +153,9 @@ public long getFactoryPackageVersion(String packageName) throws NameNotFoundExce .getLongVersionCode(); } - private boolean isProviderAvailable(String packageName) { + private boolean isProviderAvailable(WebViewProviderInfo configInfo) { try { - getFactoryPackageVersion(packageName); + getPackageInfoForProvider(configInfo); return true; } catch (NameNotFoundException e) { return false; diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 139a5d939adb6..163cfb35b284e 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -672,6 +672,8 @@ enum State { * @see WindowContainer#providesOrientation() */ final boolean mStyleFillsParent; + + private int mSyncTimeoutCounter = 0; // The input dispatching timeout for this application token in milliseconds. long mInputDispatchingTimeoutMillis = DEFAULT_DISPATCHING_TIMEOUT_MILLIS; @@ -6048,8 +6050,8 @@ void makeInvisible() { case PAUSING: case PAUSED: case STARTED: - addToStopping(true /* scheduleIdle */, - canEnterPictureInPicture /* idleDelayed */, "makeInvisible"); + final boolean idleDelayed = canEnterPictureInPicture || inTransition(); + addToStopping(true /* scheduleIdle */, idleDelayed, "makeInvisible"); break; default: @@ -9395,6 +9397,25 @@ void setShouldDockBigOverlays(boolean shouldDockBigOverlays) { getTask().getRootTask().onShouldDockBigOverlaysChanged(); } + void checkSyncTimeout(BLASTSyncEngine.SyncGroup group) { + if (attachedToProcess() && "com.android.launcher3".equals(packageName)) { + mAtmService.mH.post(() -> { + synchronized (mAtmService.mGlobalLock) { + if (!hasProcess()) { + return; + } + WindowProcessController wpc = app; + mSyncTimeoutCounter++; + if (mSyncTimeoutCounter < 3) { + return; + } + Slog.d(TAG, "Sync timeout, try kill process"); + mAtmService.mAmInternal.killProcess(wpc.mName, wpc.mUid, "syncTimeout"); + } + }); + } + } + @Override boolean isSyncFinished(BLASTSyncEngine.SyncGroup group) { if (task != null && task.mSharedStartingData != null) { diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 7c943fe9639ab..026d571405b5b 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1131,14 +1131,24 @@ private int executeRequest(Request request) { // in the flow, and asking to forward its result back to the previous. In this // case the activity is serving as a trampoline between the two, so we also want // to update its launchedFromPackage to be the same as the previous activity. - // Note that this is safe, since we know these two packages come from the same - // uid; the caller could just as well have supplied that same package name itself - // . This specifially deals with the case of an intent picker/chooser being + // This specifically deals with the case of an intent picker/chooser being // launched in the app flow to redirect to an activity picked by the user, where // we want the final activity to consider it to have been launched by the // previous app activity. - callingPackage = sourceRecord.launchedFromPackage; - callingFeatureId = sourceRecord.launchedFromFeatureId; + final String launchedFromPackage = sourceRecord.launchedFromPackage; + if (launchedFromPackage != null) { + final PackageManagerInternal pmInternal = + mService.getPackageManagerInternalLocked(); + final int packageUid = pmInternal.getPackageUid( + launchedFromPackage, 0 /* flags */, + UserHandle.getUserId(callingUid)); + // Only override callingPackage and callingFeatureId based on package UID check. + // This is to prevent spoofing. See b/457742426. + if (UserHandle.isSameApp(packageUid, callingUid)) { + callingPackage = launchedFromPackage; + callingFeatureId = sourceRecord.launchedFromFeatureId; + } + } } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index f7aad2633592d..a90f437a5d6a4 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -2798,6 +2798,20 @@ public void stopSystemLockTaskMode() throws RemoteException { stopLockTaskModeInternal(null, true /* isSystemCaller */); } + @Override + public void rebuildSystemLockTaskPinnedMode() { + enforceTaskPermission("rebuildSystemLockTaskPinnedMode"); + // This makes inner call to look as if it was initiated by system. + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + getLockTaskController().rebuildSystemLockTaskPinnedMode(); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + void startLockTaskMode(@Nullable Task task, boolean isSystemCaller) { ProtoLog.w(WM_DEBUG_LOCKTASK, "startLockTaskMode: %s", task); if (task == null || task.mLockTaskAuth == LOCK_TASK_AUTH_DONT_LOCK) { @@ -5516,6 +5530,11 @@ void continueWindowLayout() { // ClientTransactions is queued during #deferWindowLayout() for performance. // Notify to continue. mLifecycleManager.onLayoutContinued(); + + if (mRootWindowContainer.mTaskLayersChanged + && !mWindowManager.mWindowPlacerLocked.isLayoutDeferred()) { + mRootWindowContainer.rankTaskLayers(); + } } /** diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java index 70f075658df45..6828d3be3c45a 100644 --- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java +++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java @@ -20,6 +20,7 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_SYNC_ENGINE; import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import android.annotation.NonNull; import android.annotation.Nullable; @@ -207,9 +208,14 @@ private boolean tryFinish() { for (int i = mRootMembers.size() - 1; i >= 0; --i) { final WindowContainer wc = mRootMembers.valueAt(i); if (!wc.isSyncFinished(this)) { - ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Unfinished container: %s", - mSyncId, wc); - return false; + if(canIgnoreFromRecents(wc)){ + //if wallpaper has drawn, ignore unfinished container when window is animating by recents. + Slog.w(TAG, "Sync group " + mSyncId + " ignoring " + wc + "when wallpaper has drawn"); + } else { + ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Unfinished container: %s", + mSyncId, wc); + return false; + } } } finishNow(); @@ -427,6 +433,10 @@ private void onTimeout() { .mUnknownAppVisibilityController.getDebugMessage()); } }); + ActivityRecord r = wc.asActivityRecord(); + if (r != null) { + r.checkSyncTimeout(this); + } } } @@ -656,4 +666,17 @@ boolean hasPendingSyncSets() { void addOnIdleListener(Runnable onIdleListener) { mOnIdleListeners.add(onIdleListener); } + + /** @return {@code true} if wallpaper has drawn when window is animating by recents.*/ + private boolean canIgnoreFromRecents(WindowContainer wc){ + + if(wc.asActivityRecord() != null && wc.getDisplayContent() != null + && wc.asActivityRecord().isActivityTypeHomeOrRecents() + && mWm.mAtmService.getTransitionController().getCollectingTransitionType() == TRANSIT_TO_FRONT + && wc.getDisplayContent().mWallpaperController.wallpaperTransitionReady() + && wc.getDisplayContent().mWallpaperController.getTopVisibleWallpaper() != null){ + return true; + } + return false; + } } diff --git a/services/core/java/com/android/server/wm/LockTaskController.java b/services/core/java/com/android/server/wm/LockTaskController.java index a3e77a6cf0d62..546e9fcbd2535 100644 --- a/services/core/java/com/android/server/wm/LockTaskController.java +++ b/services/core/java/com/android/server/wm/LockTaskController.java @@ -688,6 +688,41 @@ void startLockTaskMode(@NonNull Task task, boolean isSystemCaller, int callingUi "startLockTask", true); } + /** + * Method to rebuild Lock Task Pinned Mode. This uses the existing locked tasks. + */ + void rebuildSystemLockTaskPinnedMode() { + int lockTaskModeState = mLockTaskModeState; + if (lockTaskModeState != LOCK_TASK_MODE_PINNED) { + Slog.e(TAG_LOCKTASK, + "rebuildSystemLockTaskPinnedMode: Attempt to rebuild pinned mode but not in " + + "pinned mode."); + return; + } + if (mLockTaskModeTasks.isEmpty()) { + Slog.i(TAG_LOCKTASK, + "rebuildSystemLockTaskPinnedMode: mLockTaskModeTasks empty, nothing to " + + "rebuild."); + return; + } + Task task = mLockTaskModeTasks.getFirst(); + mSupervisor.mRecentTasks.onLockTaskModeStateChanged(LOCK_TASK_MODE_PINNED, task.mUserId); + // rebuild pinned mode on the handler thread + mHandler.post(() -> { + try { + final IStatusBarService statusBarService = getStatusBarService(); + if (statusBarService != null) { + statusBarService.showPinningEnterExitToast(true /* entering */); + } + mTaskChangeNotificationController.notifyLockTaskModeChanged(lockTaskModeState); + setStatusBarState(lockTaskModeState, task.mUserId); + setKeyguardState(lockTaskModeState, task.mUserId); + } catch (RemoteException ex) { + throw new RuntimeException(ex); + } + }); + } + /** * Start lock task mode on the given task. * @param lockTaskModeState whether fully locked or pinned mode. diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 6f3f08cf4ed74..2e79cb56d6da8 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -69,6 +69,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; +import android.provider.Settings; import android.text.TextUtils; import android.util.ArraySet; import android.util.IntArray; @@ -408,8 +409,8 @@ void loadParametersFromResources(Resources res) { * any dependent services (like SystemUI) is started. */ void loadRecentsComponent(Resources res) { - final String rawRecentsComponent = res.getString( - com.android.internal.R.string.config_recentsComponentName); + int defaultLauncher = android.os.SystemProperties.getInt("persist.sys.default_launcher", 0); + final String rawRecentsComponent = res.getStringArray(com.android.internal.R.array.config_launcherComponents)[defaultLauncher]; if (TextUtils.isEmpty(rawRecentsComponent)) { return; } @@ -768,8 +769,15 @@ void removeTasksByPackageName(String packageName, int userId) { void removeAllVisibleTasks(int userId) { Set profileIds = getProfileIds(userId); + String lockedTasks = Settings.System.getStringForUser( + mService.mContext.getContentResolver(), + Settings.System.RECENTS_LOCKED_TASKS, + userId); for (int i = mTasks.size() - 1; i >= 0; --i) { final Task task = mTasks.get(i); + ComponentName cn = task.intent != null ? task.intent.getComponent() : null; + if (lockedTasks != null && !lockedTasks.isEmpty() && + cn != null && lockedTasks.contains(cn.getPackageName())) continue; if (!profileIds.contains(task.mUserId)) continue; if (isVisibleRecentTask(task)) { mTasks.remove(i); diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index d4d46be9fa802..930e1d173963a 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -253,7 +253,7 @@ class RootWindowContainer extends WindowContainer DeviceStateAutoRotateSettingController mDeviceStateAutoRotateSettingController; // Whether tasks have moved and we need to rank the tasks before next OOM scoring - private boolean mTaskLayersChanged = true; + boolean mTaskLayersChanged = true; private int mTmpTaskLayerRank; private final RankTaskLayersRunnable mRankTaskLayersRunnable = new RankTaskLayersRunnable(); private Region mTmpOccludingRegion; @@ -2991,7 +2991,9 @@ void invalidateTaskLayersAndUpdateOomAdjIfNeeded() { void invalidateTaskLayers() { if (!mTaskLayersChanged) { mTaskLayersChanged = true; - mService.mH.post(mRankTaskLayersRunnable); + if (!mWindowManager.mWindowPlacerLocked.isLayoutDeferred()) { + mService.mH.post(mRankTaskLayersRunnable); + } } } diff --git a/services/core/java/com/android/server/wm/SurfaceAnimationThread.java b/services/core/java/com/android/server/wm/SurfaceAnimationThread.java index 8ea715c4084e0..2806ff9bc9121 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimationThread.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimationThread.java @@ -16,7 +16,7 @@ package com.android.server.wm; -import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY; import android.os.Handler; import android.os.Trace; @@ -32,7 +32,7 @@ public final class SurfaceAnimationThread extends ServiceThread { private static Handler sHandler; private SurfaceAnimationThread() { - super("android.anim.lf", THREAD_PRIORITY_DISPLAY, false /*allowIo*/); + super("android.anim.lf", THREAD_PRIORITY_URGENT_DISPLAY, false /*allowIo*/); } private static void ensureThreadLocked() { diff --git a/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java b/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java index 1b70d1d4a8b64..057abe81dcf8c 100644 --- a/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java +++ b/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java @@ -16,7 +16,7 @@ package com.android.server.wm; -import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; import static android.os.Process.myTid; import static android.os.Process.setThreadPriority; @@ -44,7 +44,7 @@ class WindowManagerThreadPriorityBooster extends ThreadPriorityBooster { private boolean mBoundsAnimationRunning; WindowManagerThreadPriorityBooster() { - super(THREAD_PRIORITY_DISPLAY, INDEX_WINDOW); + super(THREAD_PRIORITY_URGENT_DISPLAY, INDEX_WINDOW); mAnimationThreadId = AnimationThread.get().getThreadId(); mSurfaceAnimationThreadId = SurfaceAnimationThread.get().getThreadId(); } @@ -93,7 +93,7 @@ void setBoundsAnimationRunning(boolean running) { @GuardedBy("mLock") private void updatePriorityLocked() { int priority = (mAppTransitionRunning || mBoundsAnimationRunning) - ? THREAD_PRIORITY_TOP_APP_BOOST : THREAD_PRIORITY_DISPLAY; + ? THREAD_PRIORITY_TOP_APP_BOOST : THREAD_PRIORITY_URGENT_DISPLAY; setBoostToPriority(priority); setThreadPriority(mAnimationThreadId, priority); setThreadPriority(mSurfaceAnimationThreadId, priority); diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 74cfc773ae1d4..c9981c2bbcc28 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -91,6 +91,7 @@ import com.android.internal.app.HeavyWeightSwitcherActivity; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.function.pooled.PooledLambda; +import com.android.server.DisplayThread; import com.android.server.Watchdog; import com.android.server.am.Flags; import com.android.server.am.psc.AsyncBatchSession; @@ -567,7 +568,7 @@ void postPendingUiCleanMsg(boolean pendingUiClean) { // Posting on handler so WM lock isn't held when we call into AM. final Message m = PooledLambda.obtainMessage( WindowProcessListener::setPendingUiClean, mListener, pendingUiClean); - mAtm.mH.sendMessage(m); + DisplayThread.getHandler().sendMessage(m); } long getInteractionEventTime() { @@ -1443,7 +1444,7 @@ public long getInputDispatchingTimeoutMillis() { void clearProfilerIfNeeded() { // Posting on handler so WM lock isn't held when we call into AM. - mAtm.mH.sendMessage(PooledLambda.obtainMessage( + DisplayThread.getHandler().sendMessage(PooledLambda.obtainMessage( WindowProcessListener::clearProfilerIfNeeded, mListener)); } @@ -1490,7 +1491,7 @@ void addToPendingTop() { void updateServiceConnectionActivities() { // Posting on handler so WM lock isn't held when we call into AM. - mAtm.mH.sendMessage(PooledLambda.obtainMessage( + DisplayThread.getHandler().sendMessage(PooledLambda.obtainMessage( WindowProcessListener::updateServiceConnectionActivities, mListener)); } @@ -1499,7 +1500,7 @@ void setPendingUiCleanAndForceProcessStateUpTo(int newState) { final Message m = PooledLambda.obtainMessage( WindowProcessListener::setPendingUiCleanAndForceProcessStateUpTo, mListener, newState); - mAtm.mH.sendMessage(m); + DisplayThread.getHandler().sendMessage(m); } boolean isRemoved() { @@ -1568,7 +1569,7 @@ void appDied(String reason) { // Posting on handler so WM lock isn't held when we call into AM. final Message m = PooledLambda.obtainMessage( WindowProcessListener::appDied, mListener, reason); - mAtm.mH.sendMessage(m); + DisplayThread.getHandler().sendMessage(m); } /** @@ -2211,7 +2212,7 @@ void removeAnimatingReason(@AnimatingReason int reason) { /** Applies the animating state to activity manager for updating process priority. */ private void setAnimating(boolean animating) { // Posting on handler so WM lock isn't held when we call into AM. - mAtm.mH.post(() -> mListener.setRunningRemoteAnimation(animating)); + DisplayThread.getHandler().post(() -> mListener.setRunningRemoteAnimation(animating)); } boolean isRunningRemoteTransition() { diff --git a/services/core/java/org/rising/server/QuickSwitchService.java b/services/core/java/org/rising/server/QuickSwitchService.java new file mode 100644 index 0000000000000..aaa9a11de8496 --- /dev/null +++ b/services/core/java/org/rising/server/QuickSwitchService.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2023 The RisingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.rising.server; + +import static android.os.Process.THREAD_PRIORITY_DEFAULT; + +import android.app.ActivityManager; +import android.app.ActivityThread; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; +import android.content.pm.UserInfo; +import android.os.Handler; +import android.os.IUserManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; + +import com.android.server.ServiceThread; +import com.android.server.SystemService; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public final class QuickSwitchService extends SystemService { + + private static final String TAG = "QuickSwitchService"; + private static final int THREAD_PRIORITY_DEFAULT = android.os.Process.THREAD_PRIORITY_DEFAULT; + + private final Context mContext; + private final IPackageManager mPM; + private final IUserManager mUM; + private final ContentResolver mResolver; + private final String mOpPackageName; + + private ServiceThread mWorker; + private Handler mHandler; + + private static List LAUNCHER_PACKAGES = null; + private static List disabledLaunchersCache = null; + private static int lastDefaultLauncher = -1; + + static { + if (LAUNCHER_PACKAGES == null) { + try { + Context context = ActivityThread.currentApplication() != null ? + ActivityThread.currentApplication().getApplicationContext() : null; + if (context != null) { + String[] launcherPackages = context.getResources().getStringArray(com.android.internal.R.array.config_launcherPackages); + LAUNCHER_PACKAGES = new ArrayList<>(); + for (String packageName : launcherPackages) { + LAUNCHER_PACKAGES.add(packageName); + } + } else { + LAUNCHER_PACKAGES = new ArrayList<>(); + } + } catch (Exception e) { + LAUNCHER_PACKAGES = new ArrayList<>(); + } + } + } + + public static boolean shouldHide(int userId, String packageName) { + return packageName != null && getDisabledDefaultLaunchers().contains(packageName); + } + + public static ParceledListSlice recreatePackageList( + int callingUid, Context context, int userId, ParceledListSlice list) { + List appList = list.getList(); + List disabledLaunchers = getDisabledDefaultLaunchers(); + appList.removeIf(info -> disabledLaunchers.contains(info.packageName)); + return new ParceledListSlice<>(appList); + } + + public static List recreateApplicationList( + int callingUid, Context context, int userId, List list) { + List appList = new ArrayList<>(list); + List disabledLaunchers = getDisabledDefaultLaunchers(); + appList.removeIf(info -> disabledLaunchers.contains(info.packageName)); + return appList; + } + + private void updateStateForUser(int userId) { + int defaultLauncher = SystemProperties.getInt("persist.sys.default_launcher", 0); + try { + for (String packageName : LAUNCHER_PACKAGES) { + if (packageName.equals("com.android.launcher3") && defaultLauncher == 2) { + mPM.setApplicationEnabledSetting(packageName, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + 0, userId, mOpPackageName); + } else if (packageName.equals(LAUNCHER_PACKAGES.get(defaultLauncher))) { + mPM.setApplicationEnabledSetting(packageName, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + 0, userId, mOpPackageName); + } else { + mPM.setApplicationEnabledSetting(packageName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + 0, userId, mOpPackageName); + } + } + } catch (IllegalArgumentException ignored) {} + catch (RemoteException e) {} + } + + public static List getDisabledDefaultLaunchers() { + int defaultLauncher = SystemProperties.getInt("persist.sys.default_launcher", 0); + if (defaultLauncher != lastDefaultLauncher || disabledLaunchersCache == null) { + lastDefaultLauncher = defaultLauncher; + List disabledDefaultLaunchers = new ArrayList<>(); + for (int i = 0; i < LAUNCHER_PACKAGES.size(); i++) { + if (i != defaultLauncher && !(i == 0 && defaultLauncher == 2)) { + disabledDefaultLaunchers.add(LAUNCHER_PACKAGES.get(i)); + } + } + disabledLaunchersCache = disabledDefaultLaunchers; + } + return disabledLaunchersCache; + } + + private void initForUser(int userId) { + if (userId < 0) + return; + updateStateForUser(userId); + } + + private void init() { + try { + for (UserInfo user : mUM.getUsers(false)) { + initForUser(user.id); + } + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_USER_ADDED); + filter.addAction(Intent.ACTION_USER_REMOVED); + mContext.registerReceiver(new UserReceiver(), filter, + android.Manifest.permission.MANAGE_USERS, mHandler); + } + + @Override + public void onStart() { + mWorker = new ServiceThread(TAG, THREAD_PRIORITY_DEFAULT, false); + mWorker.start(); + mHandler = new Handler(mWorker.getLooper()); + init(); + } + + @Override + public void onBootPhase(int phase) { + super.onBootPhase(phase); + if (phase == SystemService.PHASE_BOOT_COMPLETED) { + IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT); + } + } + + public QuickSwitchService(Context context) { + super(context); + mContext = context; + mResolver = context.getContentResolver(); + mPM = IPackageManager.Stub.asInterface(ServiceManager.getService("package")); + mUM = IUserManager.Stub.asInterface(ServiceManager.getService(Context.USER_SERVICE)); + mOpPackageName = context.getOpPackageName(); + } + + private final class UserReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (Intent.ACTION_USER_ADDED.equals(intent.getAction())) { + initForUser(userId); + } + } + } +} diff --git a/services/core/java/org/rising/server/RisingServicesStarter.java b/services/core/java/org/rising/server/RisingServicesStarter.java new file mode 100644 index 0000000000000..1190be4bad0c5 --- /dev/null +++ b/services/core/java/org/rising/server/RisingServicesStarter.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023-2024 The RisingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.rising.server; + +import com.android.server.SystemServiceManager; + +public class RisingServicesStarter { + + private final SystemServiceManager mSystemServiceManager; + + private static final String QUICKSWITCH_SERVICE_CLASS = + "org.rising.server.QuickSwitchService"; + + public RisingServicesStarter(SystemServiceManager systemServiceManager) { + this.mSystemServiceManager = systemServiceManager; + } + + public void startAllServices() { + startService(QUICKSWITCH_SERVICE_CLASS); + } + + private void startService(String serviceClassName) { + try { + mSystemServiceManager.startService(serviceClassName); + } catch (Exception e) {} + } +} + diff --git a/services/core/java/org/rising/server/ShakeGestureService.java b/services/core/java/org/rising/server/ShakeGestureService.java new file mode 100644 index 0000000000000..6dce9663f1ec5 --- /dev/null +++ b/services/core/java/org/rising/server/ShakeGestureService.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023-2024 The RisingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.rising.server; + +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.provider.Settings; + +public final class ShakeGestureService { + + private static final String TAG = "ShakeGestureService"; + + private static final String SHAKE_GESTURES_ENABLED = "shake_gestures_enabled"; + private static final String SHAKE_GESTURES_ACTION = "shake_gestures_action"; + private static final int USER_ALL = UserHandle.USER_ALL; + + private final Context mContext; + private ShakeGestureUtils mShakeGestureUtils; + private static volatile ShakeGestureService instance; + private final ShakeGesturesCallbacks mShakeCallbacks; + + private final SettingsObserver mSettingsObserver; + private boolean mShakeServiceEnabled = false; + + private ShakeGestureUtils.OnShakeListener mShakeListener; + + public interface ShakeGesturesCallbacks { + void onShake(); + } + + private ShakeGestureService(Context context, ShakeGesturesCallbacks callback) { + mContext = context; + mShakeCallbacks = callback; + mShakeListener = () -> { + if (mShakeServiceEnabled && mShakeCallbacks != null) { + mShakeCallbacks.onShake(); + } + }; + mSettingsObserver = new SettingsObserver(null); + } + + public static synchronized ShakeGestureService getInstance(Context context, ShakeGesturesCallbacks callback) { + if (instance == null) { + synchronized (ShakeGestureService.class) { + if (instance == null) { + instance = new ShakeGestureService(context, callback); + } + } + } + return instance; + } + + public void onStart() { + if (mShakeGestureUtils == null) { + mShakeGestureUtils = new ShakeGestureUtils(mContext); + } + updateSettings(); + mSettingsObserver.observe(); + if (mShakeServiceEnabled) { + mShakeGestureUtils.registerListener(mShakeListener); + } + } + + private void updateSettings() { + boolean wasShakeServiceEnabled = mShakeServiceEnabled; + mShakeServiceEnabled = Settings.System.getInt(mContext.getContentResolver(), + SHAKE_GESTURES_ENABLED, 0) == 1; + if (mShakeServiceEnabled && !wasShakeServiceEnabled) { + mShakeGestureUtils.registerListener(mShakeListener); + } else if (!mShakeServiceEnabled && wasShakeServiceEnabled) { + mShakeGestureUtils.unregisterListener(mShakeListener); + } + } + + class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + void observe() { + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(SHAKE_GESTURES_ENABLED), false, this, USER_ALL); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(SHAKE_GESTURES_ACTION), false, this, USER_ALL); + } + @Override + public void onChange(boolean selfChange) { + updateSettings(); + } + } +} diff --git a/services/core/java/org/rising/server/ShakeGestureUtils.java b/services/core/java/org/rising/server/ShakeGestureUtils.java new file mode 100644 index 0000000000000..76e9e02047d7e --- /dev/null +++ b/services/core/java/org/rising/server/ShakeGestureUtils.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2023-2024 The RisingOS Android Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.rising.server; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.SystemClock; +import android.provider.Settings; + +import java.util.ArrayList; + +public class ShakeGestureUtils implements SensorEventListener { + + private static final String TAG = "ShakeGestureUtils"; + + private static final String SHAKE_GESTURES_SHAKE_INTENSITY = "shake_gestures_intensity"; + + private Context mContext; + private SensorManager mSensorManager; + private Sensor mAccelerometer; + private ArrayList mListeners = new ArrayList<>(); + private long mLastShakeTime = 0L; + private long mLastUpdateTime = 0L; + private int mShakeCount = 0; + private float mLastX = 0f; + private float mLastY = 0f; + private float mLastZ = 0f; + + public ShakeGestureUtils(Context context) { + mContext = context; + mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + if (mSensorManager != null) { + mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + } + + public interface OnShakeListener { + void onShake(); + } + + public void registerListener(OnShakeListener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + startListening(); + } + } + + public void unregisterListener(OnShakeListener listener) { + mListeners.remove(listener); + if (mListeners.isEmpty()) { + stopListening(); + } + } + + private void startListening() { + if (mAccelerometer != null) { + mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_UI); + } + } + + private void stopListening() { + mSensorManager.unregisterListener(this); + } + + private int getShakeIntensity() { + return Settings.System.getInt(mContext.getContentResolver(), + SHAKE_GESTURES_SHAKE_INTENSITY, 3); + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (event == null) { + return; + } + long curUpdateTime = System.currentTimeMillis(); + long timeInterval = curUpdateTime - mLastUpdateTime; + if (timeInterval < (getShakeIntensity() * 14f)) { + return; + } + if (event.values.length < 3) { + return; + } + mLastUpdateTime = curUpdateTime; + float x = event.values[0]; + float y = event.values[1]; + float z = event.values[2]; + float deltaX = x - mLastX; + float deltaY = y - mLastY; + float deltaZ = z - mLastZ; + mLastX = x; + mLastY = y; + mLastZ = z; + double speed = Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 1000.0 / timeInterval; + if (speed >= getShakeIntensity() * 100f) { + notifyShakeListeners(); + } + } + + private void notifyShakeListeners() { + if (SystemClock.elapsedRealtime() - mLastShakeTime < 1000) { + return; + } + for (OnShakeListener listener : mListeners) { + listener.onShake(); + } + mLastShakeTime = SystemClock.elapsedRealtime(); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 82d776b86526d..ab4d8af6dc006 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -328,6 +328,8 @@ import dalvik.system.VMDebug; import dalvik.system.VMRuntime; +import org.rising.server.RisingServicesStarter; + import java.io.File; import java.io.FileDescriptor; import java.io.IOException; @@ -2926,6 +2928,8 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { mSystemServiceManager.startService(CustomDeviceConfigService.class); t.traceEnd(); + mSystemServiceManager.startService(HideAppListService.class); + boolean hbmSupported = SystemProperties.getBoolean("persist.sys.hbmservice_support", false); String hbmFile = SystemProperties.get("persist.sys.hbmservice_file"); if (hbmSupported && hbmFile != null && !hbmFile.isEmpty()) { @@ -3392,6 +3396,9 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { mSystemServiceManager.startService(HEALTHCONNECT_MANAGER_SERVICE_CLASS); t.traceEnd(); + RisingServicesStarter risingServiceStarter = new RisingServicesStarter(mSystemServiceManager); + risingServiceStarter.startAllServices(); + if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_DEVICE_LOCK)) { t.traceBegin("DeviceLockService"); mSystemServiceManager.startServiceFromJar(DEVICE_LOCK_SERVICE_CLASS, diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt index 520ab62374ca2..554ae0f48e2a8 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt @@ -171,7 +171,7 @@ class MockSystem(withSession: (StaticMockitoSessionBuilder) -> Unit = {}) { null } whenever(mocks.settings.addPackageLPw(nullable(), nullable(), nullable(), nullable(), - nullable(), nullable(), nullable(), nullable())) { + nullable(), nullable(), nullable(), nullable(), nullable())) { val name: String = getArgument(0) val pendingAdd = mPendingPackageAdds.firstOrNull { it.first == name } ?: return@whenever null diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index f3e85dd1e8e0c..0cf9fc3948b2b 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -146,6 +146,7 @@ import com.android.server.accessibility.magnification.MagnificationConnectionManager; import com.android.server.accessibility.magnification.MagnificationController; import com.android.server.accessibility.magnification.MagnificationProcessor; +import com.android.server.accessibility.utils.TileServiceUtil; import com.android.server.pm.UserManagerInternal; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal; @@ -2659,7 +2660,17 @@ private void setupShortcutTargetServices(AccessibilityUserState userState) { /* isAlwaysOnService= */ false); userState.mInstalledServices.addAll( List.of(alwaysOnServiceInfo, standardServiceInfo)); - userState.updateTileServiceMapForAccessibilityServiceLocked(); + ComponentName alwaysOnA11yServiceTile = + new ComponentName(TARGET_ALWAYS_ON_A11Y_SERVICE.getPackageName(), + alwaysOnServiceInfo.getTileServiceName()); + TileServiceUtil.setupPackageManagerForValidTileService( + mMockPackageManagerInternal, + userState.mUserId, + alwaysOnA11yServiceTile + ); + userState.updateTileServiceMapForAccessibilityServiceLocked( + Set.of(alwaysOnA11yServiceTile) + ); } private void sendBroadcastToAccessibilityManagerService(Intent intent, @UserIdInt int userId) { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityTileUtilsTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityTileUtilsTest.java new file mode 100644 index 0000000000000..c06f17ef60b6d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityTileUtilsTest.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.accessibility; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.AccessibilityShortcutInfo; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.test.mock.MockContentResolver; +import android.util.AttributeSet; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.server.LocalServices; +import com.android.server.accessibility.utils.TileServiceUtil; + +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.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +/** + * Tests for {@link AccessibilityTileUtils}. + */ +@RunWith(AndroidJUnit4.class) +public class AccessibilityTileUtilsTest { + private static final String A11Y_FEATURE_PACKAGE = "com.example.test"; + private static final String A11Y_SERVICE_NAME = "TestAccessibilityService"; + private static final String A11Y_SHORTCUT_NAME = "TestAccessibilityShortcut"; + private static final String TILE_SERVICE_NAME = "TestTileService"; + private static final String TILE_SERVICE_NAME2 = "TestTileService2"; + private static final int USER_ID = 0; + + private final ComponentName mTileServiceComponent = + new ComponentName(A11Y_FEATURE_PACKAGE, TILE_SERVICE_NAME); + private final ComponentName mTileServiceComponent2 = + new ComponentName(A11Y_FEATURE_PACKAGE, TILE_SERVICE_NAME2); + private final ComponentName mA11yFeatureComponent = + new ComponentName(A11Y_FEATURE_PACKAGE, A11Y_SERVICE_NAME); + private final ComponentName mA11yShortcutComponent = + new ComponentName(A11Y_FEATURE_PACKAGE, A11Y_SHORTCUT_NAME); + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + private Context mMockContext; + @Mock + private PackageManagerInternal mMockPackageManagerInternal; + private final MockContentResolver mMockResolver = new MockContentResolver(); + + @Before + public void setUp() { + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, mMockPackageManagerInternal); + when(mMockContext.getContentResolver()).thenReturn(mMockResolver); + } + + @After + public void cleanUp() { + LocalServices.removeServiceForTest(PackageManagerInternal.class); + } + + @Test + public void getValidA11yTileServices_pmIsNull_returnsEmptySet() { + TileServiceUtil.setupPackageManagerForValidTileService(mMockPackageManagerInternal, USER_ID, + mTileServiceComponent); + + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, /* pm= */ null, Collections.singletonList( + createMockAccessibilityServiceInfo(mA11yFeatureComponent, + mTileServiceComponent)), + Collections.emptyList(), USER_ID); + + assertThat(result).isEmpty(); + } + + @Test + public void getValidA11yTileServices_nullLists_returnsEmptySet() { + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, null, null, USER_ID); + + assertThat(result).isEmpty(); + } + + @Test + public void getValidA11yTileServices_emptyLists_returnsEmptySet() { + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, Collections.emptyList(), + Collections.emptyList(), USER_ID); + + assertThat(result).isEmpty(); + } + + @Test + public void getValidA11yTileServices_withA11yService_returnsComponent() { + TileServiceUtil.setupPackageManagerForValidTileService(mMockPackageManagerInternal, USER_ID, + mTileServiceComponent); + AccessibilityServiceInfo a11yServiceInfo = createMockAccessibilityServiceInfo( + mA11yFeatureComponent, mTileServiceComponent + ); + + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, + Collections.singletonList(a11yServiceInfo), /* accessibilityShortcutInfos=*/null, + USER_ID); + + assertThat(result).hasSize(1); + assertThat(result).contains(mTileServiceComponent); + } + + @Test + public void getValidA11yTileServices_withShortcutInfo_returnsComponent() throws Exception { + TileServiceUtil.setupPackageManagerForValidTileService(mMockPackageManagerInternal, USER_ID, + mTileServiceComponent2); + AccessibilityShortcutInfo a11yShortcutInfo = createFakeAccessibilityShortcutInfo( + mA11yShortcutComponent, mTileServiceComponent2 + ); + + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, + /* accessibilityServiceInfos=*/ null, Collections.singletonList(a11yShortcutInfo), + USER_ID); + + assertThat(result).hasSize(1); + assertThat(result).contains(mTileServiceComponent2); + } + + @Test + public void getValidA11yTileServices_serviceWithNoTileName_returnsEmptySet() { + TileServiceUtil.setupPackageManagerForValidTileService(mMockPackageManagerInternal, USER_ID, + mTileServiceComponent); + AccessibilityServiceInfo a11yServiceInfo = createMockAccessibilityServiceInfo( + mA11yFeatureComponent, /* tileServiceName= */ null + ); + + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, + Collections.singletonList(a11yServiceInfo), /* accessibilityShortcutInfos=*/ null, + USER_ID); + + assertThat(result).isEmpty(); + } + + @Test + public void getValidA11yTileServices_shortcutWithNoTileName_returnsEmptySet() throws Exception { + TileServiceUtil.setupPackageManagerForValidTileService(mMockPackageManagerInternal, USER_ID, + mTileServiceComponent2); + AccessibilityShortcutInfo a11yShortcutInfo = createFakeAccessibilityShortcutInfo( + mA11yShortcutComponent, /* tileServiceName= */ null + ); + + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, + /* accessibilityServiceInfos=*/ null, Collections.singletonList(a11yShortcutInfo), + USER_ID); + + assertThat(result).isEmpty(); + } + + @Test + public void getValidA11yTileServices_tileServiceNotExist_returnsEmptySet() { + TileServiceUtil.setupPackageManagerForValidTileService( + mMockPackageManagerInternal, USER_ID, mTileServiceComponent); + AccessibilityServiceInfo a11yServiceInfo = createMockAccessibilityServiceInfo( + mA11yFeatureComponent, /* tileServiceName= */ mTileServiceComponent2 + ); + + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, + Collections.singletonList(a11yServiceInfo), /* accessibilityShortcutInfos=*/ null, + USER_ID); + + assertThat(result).isEmpty(); + } + + @Test + public void getValidA11yTileServices_tileServiceNotExported_returnsEmptySet() { + TileServiceUtil.setupPackageManagerForTileService( + mMockPackageManagerInternal, USER_ID, mTileServiceComponent, + serviceInfo -> serviceInfo.exported = false); + AccessibilityServiceInfo a11yServiceInfo = createMockAccessibilityServiceInfo( + mA11yFeatureComponent, /* tileServiceName= */ mTileServiceComponent + ); + + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, + Collections.singletonList(a11yServiceInfo), /* accessibilityShortcutInfos=*/ null, + USER_ID); + + assertThat(result).isEmpty(); + } + + @Test + public void getValidA11yTileServices_tileServiceWrongPermission_returnsEmptySet() { + TileServiceUtil.setupPackageManagerForTileService( + mMockPackageManagerInternal, USER_ID, mTileServiceComponent, + serviceInfo -> serviceInfo.permission = "some.wrong.permission"); + AccessibilityServiceInfo a11yServiceInfo = createMockAccessibilityServiceInfo( + mA11yFeatureComponent, /* tileServiceName= */ mTileServiceComponent + ); + + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, + Collections.singletonList(a11yServiceInfo), /* accessibilityShortcutInfos=*/ null, + USER_ID); + + assertThat(result).isEmpty(); + } + + @Test + public void getValidA11yTileServices_tileServiceDisabled_returnsEmptySet() { + TileServiceUtil.setupPackageManagerForTileService( + mMockPackageManagerInternal, USER_ID, mTileServiceComponent, + serviceInfo -> serviceInfo.enabled = false); + AccessibilityServiceInfo a11yServiceInfo = createMockAccessibilityServiceInfo( + mA11yFeatureComponent, /* tileServiceName= */ mTileServiceComponent + ); + + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, + Collections.singletonList(a11yServiceInfo), /* accessibilityShortcutInfos=*/ null, + USER_ID); + + assertThat(result).isEmpty(); + } + + @Test + public void getValidA11yTileServices_tileServiceEnabledByDefault_returnsComponent() { + TileServiceUtil.setupPackageManagerForTileService( + mMockPackageManagerInternal, USER_ID, mTileServiceComponent, + serviceInfo -> serviceInfo.enabled = true); + AccessibilityServiceInfo a11yServiceInfo = createMockAccessibilityServiceInfo( + mA11yFeatureComponent, /* tileServiceName= */ mTileServiceComponent + ); + when(mMockPackageManagerInternal.getComponentEnabledSetting( + eq(mTileServiceComponent), anyInt(), anyInt())) + .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT); + + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, + Collections.singletonList(a11yServiceInfo), /* accessibilityShortcutInfos=*/ null, + USER_ID); + + assertThat(result).hasSize(1); + assertThat(result).contains(mTileServiceComponent); + } + + @Test + public void getValidA11yTileServices_tileServiceDisabledByDefault_returnsEmptySet() { + TileServiceUtil.setupPackageManagerForTileService( + mMockPackageManagerInternal, USER_ID, mTileServiceComponent, + serviceInfo -> serviceInfo.enabled = false); + AccessibilityServiceInfo a11yServiceInfo = createMockAccessibilityServiceInfo( + mA11yFeatureComponent, /* tileServiceName= */ mTileServiceComponent + ); + when(mMockPackageManagerInternal.getComponentEnabledSetting( + eq(mTileServiceComponent), anyInt(), anyInt())) + .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT); + + Set result = AccessibilityTileUtils.getValidA11yTileServices( + mMockContext, mMockPackageManagerInternal, + Collections.singletonList(a11yServiceInfo), /* accessibilityShortcutInfos=*/ null, + USER_ID); + + assertThat(result).isEmpty(); + } + + private AccessibilityShortcutInfo createFakeAccessibilityShortcutInfo( + @NonNull ComponentName shortcutInfoComponentName, + @Nullable ComponentName tileServiceName) + throws XmlPullParserException, IOException, PackageManager.NameNotFoundException { + Context context = mock(Context.class); + Resources resources = mock(Resources.class); + PackageManager packageManager = mock(PackageManager.class); + ActivityInfo activityInfo = mock(ActivityInfo.class); + XmlResourceParser xmlParser = mock(XmlResourceParser.class); + TypedArray typedArray = mock(TypedArray.class); + + // 1. Configure the mock ActivityInfo + activityInfo.applicationInfo = mock(ApplicationInfo.class); + when(activityInfo.getComponentName()).thenReturn(shortcutInfoComponentName); + // 2. Mock the context to return the mock PackageManager + when(context.getPackageManager()).thenReturn(packageManager); + // 3. Mock the PackageManager to return mock Resources + when(packageManager.getResourcesForApplication(any(ApplicationInfo.class))).thenReturn( + resources); + + // 4. Mock the XML parsing flow + // Simulate finding the tag + when(activityInfo.loadXmlMetaData(packageManager, + AccessibilityShortcutInfo.META_DATA)).thenReturn(xmlParser); + when(xmlParser.next()) + .thenReturn(XmlPullParser.START_TAG) + .thenReturn(XmlPullParser.END_DOCUMENT); + when(xmlParser.getName()).thenReturn("accessibility-shortcut-target"); + + // 5. Mock the Resources to return our mock TypedArray for attributes + when(resources.obtainAttributes(any(AttributeSet.class), any(int[].class))) + .thenReturn(typedArray); + + // 6. Configure the mock TypedArray to return our test data + if (tileServiceName != null) { + when(typedArray.getString( + com.android.internal.R.styleable.AccessibilityShortcutTarget_tileService)) + .thenReturn(tileServiceName.getClassName()); + } + + return new AccessibilityShortcutInfo(context, activityInfo); + } + + private AccessibilityServiceInfo createMockAccessibilityServiceInfo( + @NonNull ComponentName a11yServiceComponent, @Nullable ComponentName tileServiceName) { + AccessibilityServiceInfo serviceInfo = mock(AccessibilityServiceInfo.class); + when(serviceInfo.getComponentName()).thenReturn(a11yServiceComponent); + if (tileServiceName != null) { + when(serviceInfo.getTileServiceName()).thenReturn(tileServiceName.getClassName()); + } + ResolveInfo a11yResolveInfo = new ResolveInfo(); + a11yResolveInfo.serviceInfo = new ServiceInfo(); + a11yResolveInfo.serviceInfo.packageName = a11yServiceComponent.getPackageName(); + a11yResolveInfo.serviceInfo.name = a11yServiceComponent.getClassName(); + when(serviceInfo.getResolveInfo()).thenReturn(a11yResolveInfo); + return serviceInfo; + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java index a22cf3cbaf92f..fcecc02d25da0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java @@ -538,7 +538,7 @@ public void getTileServiceToA11yServiceInfoMapLocked() { when(mMockServiceInfo.getTileServiceName()).thenReturn(tileComponent.getClassName()); when(mMockServiceInfo.getResolveInfo()).thenReturn(resolveInfo); mUserState.mInstalledServices.add(mMockServiceInfo); - mUserState.updateTileServiceMapForAccessibilityServiceLocked(); + mUserState.updateTileServiceMapForAccessibilityServiceLocked(Set.of(tileComponent)); Map actual = mUserState.getTileServiceToA11yServiceInfoMapLocked(); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/utils/TileServiceUtil.java b/services/tests/servicestests/src/com/android/server/accessibility/utils/TileServiceUtil.java new file mode 100644 index 0000000000000..a6a61b7936136 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/utils/TileServiceUtil.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.accessibility.utils; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Process; + +import java.util.function.Consumer; + +/** + * A test utility for setting up a mock {@link PackageManagerInternal} for TileService queries. + */ +public class TileServiceUtil { + private TileServiceUtil() { + // Utility class + } + + /** + * Sets up a mock {@link PackageManagerInternal} to respond to queries for a tile service. + * + *

This is a convenience method that configures a valid, enabled TileService. + * + * @param pm The mock {@link PackageManagerInternal} instance. + * @param userId The user ID for the query. + * @param tileComponent The {@link ComponentName} of the TileService to mock. + */ + public static void setupPackageManagerForValidTileService(@NonNull PackageManagerInternal pm, + @UserIdInt int userId, + @NonNull ComponentName tileComponent) { + // A valid service is one with no special mutations. + setupPackageManagerForTileService(pm, userId, tileComponent, serviceInfo -> {}); + } + + /** + * Sets up a mock {@link PackageManagerInternal} to respond to queries for a tile service, + * allowing for custom modifications to simulate different states. + * + *

By default, this prepares a valid TileService. The {@code serviceInfoMutator} can be + * used to configure invalid states (e.g., disabled, missing permission) for testing. + * + * @param pm The mock {@link PackageManagerInternal} instance. + * @param userId The user ID for the query. + * @param tileComponent The {@link ComponentName} of the TileService to mock. + * @param serviceInfoMutator A {@link Consumer} that can modify the default {@link ServiceInfo}. + */ + public static void setupPackageManagerForTileService(@NonNull PackageManagerInternal pm, + @UserIdInt int userId, + @NonNull ComponentName tileComponent, + @NonNull Consumer serviceInfoMutator) { + // Create the base ServiceInfo for a valid TileService. + final ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.exported = true; + serviceInfo.permission = Manifest.permission.BIND_QUICK_SETTINGS_TILE; + serviceInfo.enabled = true; + + final ResolveInfo tileResolveInfo = new ResolveInfo(); + tileResolveInfo.serviceInfo = serviceInfo; + + // Allow the caller to mutate the ServiceInfo to create invalid or custom states. + serviceInfoMutator.accept(serviceInfo); + + when(pm.resolveService( + argThat(intent -> intent != null && tileComponent.equals(intent.getComponent())), + any(), eq(0L), eq(userId), eq(Process.myUid()))).thenReturn(tileResolveInfo); + + when(pm.getComponentEnabledSetting( + eq(tileComponent), eq(Process.myUid()), eq(userId))) + .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 96336d31b8697..b977c7732da81 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -21,6 +21,12 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.Authenticators; +import static android.provider.Settings.Secure.BIOMETRIC_APP_ENABLED; +import static android.provider.Settings.Secure.BIOMETRIC_KEYGUARD_ENABLED; +import static android.provider.Settings.Secure.FACE_APP_ENABLED; +import static android.provider.Settings.Secure.FACE_KEYGUARD_ENABLED; +import static android.provider.Settings.Secure.FINGERPRINT_APP_ENABLED; +import static android.provider.Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED; import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI; @@ -42,6 +48,7 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -2296,6 +2303,90 @@ public void testCanAuthenticate_fingerprintsDisabledForApps() throws Exception { invokeCanAuthenticate(mBiometricService, Authenticators.BIOMETRIC_STRONG)); } + @Test + @RequiresFlagsEnabled(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void + testEnabledForApps_biometricAppEnableOff_fpAppEnabledNotSet_returnFalse() + throws Exception { + final Context context = ApplicationProvider.getApplicationContext(); + final int value = Settings.Secure.getIntForUser(context.getContentResolver(), + FINGERPRINT_APP_ENABLED, -1, context.getUserId()); + assumeTrue("FINGERPRINT_APP_ENABLED is set. Skipped", value == -1); + + Settings.Secure.putIntForUser(context.getContentResolver(), + BIOMETRIC_APP_ENABLED, 0, context.getUserId()); + + final BiometricService.SettingObserver settingObserver = + new BiometricService.SettingObserver( + context, mBiometricHandlerProvider.getBiometricCallbackHandler(), + new ArrayList<>(), mUserManager, mFingerprintManager, mFaceManager); + + assertFalse(settingObserver.getEnabledForApps(context.getUserId(), TYPE_FINGERPRINT)); + } + + @Test + @RequiresFlagsEnabled(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void + testKeyguardEnabled_biometricKeyguardEnableOff_fpKeyguardEnabledNotSet_returnFalse() + throws Exception { + final Context context = ApplicationProvider.getApplicationContext(); + final int value = Settings.Secure.getIntForUser(context.getContentResolver(), + FINGERPRINT_KEYGUARD_ENABLED, -1, context.getUserId()); + assumeTrue("FINGERPRINT_KEYGUARD_ENABLED is set. Skipped", value == -1); + + Settings.Secure.putIntForUser(context.getContentResolver(), + BIOMETRIC_KEYGUARD_ENABLED, 0, context.getUserId()); + + final BiometricService.SettingObserver settingObserver = + new BiometricService.SettingObserver( + context, mBiometricHandlerProvider.getBiometricCallbackHandler(), + new ArrayList<>(), mUserManager, mFingerprintManager, mFaceManager); + + assertFalse(settingObserver.getEnabledOnKeyguard(context.getUserId(), TYPE_FINGERPRINT)); + } + + @Test + @RequiresFlagsEnabled(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void + testEnabledForApps_biometricAppEnableOff_faceAppEnabledNotSet_returnFalse() + throws Exception { + final Context context = ApplicationProvider.getApplicationContext(); + final int value = Settings.Secure.getIntForUser(context.getContentResolver(), + FACE_APP_ENABLED, -1, context.getUserId()); + assumeTrue("FACE_APP_ENABLED is set. Skipped", value == -1); + + Settings.Secure.putIntForUser(context.getContentResolver(), + BIOMETRIC_APP_ENABLED, 0, context.getUserId()); + + final BiometricService.SettingObserver settingObserver = + new BiometricService.SettingObserver( + context, mBiometricHandlerProvider.getBiometricCallbackHandler(), + new ArrayList<>(), mUserManager, mFingerprintManager, mFaceManager); + + assertFalse(settingObserver.getEnabledForApps(context.getUserId(), TYPE_FACE)); + } + + @Test + @RequiresFlagsEnabled(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void + testKeyguardEnabled_biometricKeyguardEnableOff_faceKeyguardEnabledNotSet_returnFalse() + throws Exception { + final Context context = ApplicationProvider.getApplicationContext(); + final int value = Settings.Secure.getIntForUser(context.getContentResolver(), + FACE_KEYGUARD_ENABLED, -1, context.getUserId()); + assumeTrue("FACE_KEYGUARD_ENABLED is set. Skipped", value == -1); + + Settings.Secure.putIntForUser(context.getContentResolver(), + BIOMETRIC_KEYGUARD_ENABLED, 0, context.getUserId()); + + final BiometricService.SettingObserver settingObserver = + new BiometricService.SettingObserver( + context, mBiometricHandlerProvider.getBiometricCallbackHandler(), + new ArrayList<>(), mUserManager, mFingerprintManager, mFaceManager); + + assertFalse(settingObserver.getEnabledOnKeyguard(context.getUserId(), TYPE_FACE)); + } + // Helper methods private int invokeCanAuthenticate(BiometricService service, int authenticators) diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index 97f9f9ceb4aff..0de1d377bd09a 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -2586,7 +2586,6 @@ public void isUidAllowed_multipleApprovedUids_returnsTrueForBoth() { } @Test - @EnableFlags(Flags.FLAG_LIMIT_MANAGED_SERVICES_COUNT) public void setPackageOrComponentEnabled_tooManyPackages_stopsAdding() { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, APPROVAL_BY_PACKAGE); @@ -2614,7 +2613,6 @@ public void setPackageOrComponentEnabled_tooManyPackages_stopsAdding() { } @Test - @EnableFlags(Flags.FLAG_LIMIT_MANAGED_SERVICES_COUNT) public void setPackageOrComponentEnabled_tooManyChanges_stopsAddingToUserSet() { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, APPROVAL_BY_PACKAGE); 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 7dc0921db4104..f96fd66caca6d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -30,6 +30,8 @@ import static android.app.Flags.FLAG_NM_SUMMARIZATION_UI; import static android.app.Flags.FLAG_UI_RICH_ONGOING; import static android.app.Notification.EXTRA_ALLOW_DURING_SETUP; +import static android.app.Notification.EXTRA_MESSAGES; +import static android.app.Notification.EXTRA_MESSAGING_PERSON; import static android.app.Notification.EXTRA_PICTURE; import static android.app.Notification.EXTRA_PICTURE_ICON; import static android.app.Notification.EXTRA_PREFER_SMALL_ICON; @@ -87,6 +89,7 @@ import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_MUTABLE; import static android.app.PendingIntent.FLAG_ONE_SHOT; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.app.StatusBarManager.ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED; import static android.app.StatusBarManager.EXTRA_KM_PRIVATE_NOTIFS_ALLOWED; import static android.app.backup.NotificationLoggingConstants.DATA_TYPE_ZEN_CONFIG; @@ -246,11 +249,13 @@ import android.compat.testing.PlatformCompatChangeRule; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.ContentProvider; import android.content.ContentUris; import android.content.Context; import android.content.IIntentSender; import android.content.Intent; import android.content.IntentFilter; +import android.content.UriPermission; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; @@ -267,6 +272,7 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Color; +import android.graphics.Rect; import android.graphics.drawable.Icon; import android.media.AudioAttributes; import android.media.AudioManager; @@ -1535,6 +1541,96 @@ private void verifyToastShownForTestPackage(String text, int displayId) { eq(TOAST_DURATION), any(), eq(displayId)); } + @Test + public void testNoUriGrantsForBadMessagesList() throws RemoteException { + Uri targetUri = Uri.parse("content://com.android.contacts/display_photo/1"); + + // create message person + Person person = new Person.Builder() + .setName("Name") + .setIcon(Icon.createWithContentUri(targetUri)) + .setKey("user_123") + .setBot(false) + .build(); + + // create MessagingStyle + Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle(person) + .setConversationTitle("Bug discussion") + .setGroupConversation(true) + .addMessage("Hi,look my photo", System.currentTimeMillis() - 60000, person) + .addMessage("Oho, you used my contacts photo", + System.currentTimeMillis() - 30000, "Friend"); + + // create Notification + Notification notification = new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setSmallIcon(R.drawable.sym_def_app_icon) + .setContentTitle("") + .setContentText("") + .setAutoCancel(true) + .setStyle(messagingStyle) + .setCategory(Notification.CATEGORY_MESSAGE) + .setFlag(Notification.FLAG_GROUP_SUMMARY, true) + .build(); + notification.contentIntent = createPendingIntent("open"); + + notification.extras.remove(EXTRA_MESSAGING_PERSON); + + // add BadClipDescription to avoid visitUri check uris in EXTRA_MESSAGES value + ArrayList parcelableArray = + new ArrayList<>(List.of(notification.extras.getParcelableArray(EXTRA_MESSAGES))); + parcelableArray.add(new MyParceledListSlice()); + notification.extras.putParcelableArray( + EXTRA_MESSAGES, parcelableArray.toArray(new Parcelable[0])); + try { + mBinderService.enqueueNotificationWithTag(mPkg, mPkg, + "testNoUriGrantsForBadMessagesList", + 1, notification, mContext.getUserId()); + waitForIdle(); + fail("should have failed to parse messages"); + } catch (java.lang.ArrayStoreException e) { + verify(mUgmInternal, never()).checkGrantUriPermission( + anyInt(), any(), eq(ContentProvider.getUriWithoutUserId(targetUri)), + anyInt(), anyInt()); + } + } + + private class MyParceledListSlice extends Intent { + @Override + public void writeToParcel(Parcel dest, int i) { + Parcel test = Parcel.obtain(); + test.writeString(this.getClass().getName()); + int strLength = test.dataSize(); + test.recycle(); + dest.setDataPosition(dest.dataPosition() - strLength); + dest.writeString("android.content.pm.ParceledListSlice"); + + dest.writeInt(1); + dest.writeString(UriPermission.class.getName()); + dest.writeInt(0); // use binder + dest.writeStrongBinder(new Binder() { + private int callingPid = -1; + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + if (code == 1) { + reply.writeNoException(); + reply.writeInt(1); + if (getCallingUid() == 1000 && callingPid == -1) { + reply.writeParcelable(new Rect(), 0); + callingPid = getCallingPid(); + } else { + reply.writeInt(-1); + reply.writeInt(-1); + reply.writeLong(0); + } + return true; + } + return super.onTransact(code, data, reply, flags); + } + }); + } + } + @Test public void testDefaultAssistant_overrideDefault() { final int userId = mContext.getUserId(); @@ -6382,7 +6478,6 @@ public void testSetListenerAccessForUser_revokeWithNameTooLong_okay() throws Exc } @Test - @EnableFlags(Flags.FLAG_LIMIT_MANAGED_SERVICES_COUNT) public void testSetListenerAccessForUser_tooManyListeners_skipsFollowups() throws Exception { UserHandle user = UserHandle.of(mContext.getUserId() + 10); ComponentName c = ComponentName.unflattenFromString("package/Component"); @@ -8589,7 +8684,7 @@ public void testVisitUris() throws Exception { Bundle extras = new Bundle(); extras.putParcelable(Notification.EXTRA_AUDIO_CONTENTS_URI, audioContents); extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI, backgroundImage.toString()); - extras.putParcelable(Notification.EXTRA_MESSAGING_PERSON, person1); + extras.putParcelable(EXTRA_MESSAGING_PERSON, person1); extras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, new ArrayList<>(Arrays.asList(person2, person3))); extras.putParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, @@ -8727,13 +8822,13 @@ public void testVisitUris_styleExtrasWithoutStyle() { .setSmallIcon(android.R.drawable.sym_def_app_icon); Bundle messagingExtras = new Bundle(); - messagingExtras.putParcelable(Notification.EXTRA_MESSAGING_PERSON, + messagingExtras.putParcelable(EXTRA_MESSAGING_PERSON, personWithIcon("content://user")); messagingExtras.putParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES, new Bundle[] { new Notification.MessagingStyle.Message("Heyhey!", System.currentTimeMillis() - 100, personWithIcon("content://historicalMessenger")).toBundle()}); - messagingExtras.putParcelableArray(Notification.EXTRA_MESSAGES, + messagingExtras.putParcelableArray(EXTRA_MESSAGES, new Bundle[] { new Notification.MessagingStyle.Message("Are you there?", System.currentTimeMillis(), personWithIcon("content://messenger")).toBundle()}); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index e4ad38689daa4..6d59fe5b21bda 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -1997,6 +1997,88 @@ private ActivityRecord createBubbledActivity() { .build(); } + /** + * This test simulates the following scenario: + * 1. Privileged app (P) starts malicious app's activity (M1). + * 2. M1 starts M2 (also in malicious app) using startNextMatchingActivity(). + * This causes M2's launchedFromPackage to be P. + * 3. M2 starts an activity in P (P2) using startActivity() with + * FLAG_ACTIVITY_FORWARD_RESULT. + * The test verifies that P2's launchedFromPackage is M, not P. + * See b/457742426 for details. + */ + @Test + public void testLaunchedFromPackage_nextMatchingActivity_forwardResult() { + final String privilegedPackage = "com.test.privileged"; + final int privilegedUid = 10001; + final String maliciousPackage = "com.test.malicious"; + final int maliciousUid = 10002; + + // Setup P1 activity + final ActivityRecord p1 = new ActivityBuilder(mAtm) + .setComponent(new ComponentName(privilegedPackage, "P1Activity")) + .setUid(privilegedUid) + .setCreateTask(true) + .build(); + + // Setup M1 activity, launched by P1 + final ActivityRecord m1 = new ActivityBuilder(mAtm) + .setComponent(new ComponentName(maliciousPackage, "M1Activity")) + .setUid(maliciousUid) + .setCreateTask(true) + .setLaunchedFromPackage(privilegedPackage) + .setLaunchedFromUid(privilegedUid) + .build(); + m1.resultTo = p1; + + // Setup M2 activity, as if launched from M1 via startNextMatchingActivity() + final ActivityRecord m2 = new ActivityBuilder(mAtm) + .setComponent(new ComponentName(maliciousPackage, "M2Activity")) + .setUid(maliciousUid) + .setCreateTask(true) + .setLaunchedFromPackage(privilegedPackage) // Spoofed package name + .setLaunchedFromUid(maliciousUid) + .build(); + m2.resultTo = p1; // result is forwarded + + // M2 starts P2 + final ActivityStarter starter = prepareStarter(0); + doReturn(privilegedUid).when(mMockPackageManager).getPackageUid( + eq(privilegedPackage), anyLong(), anyInt()); + doReturn(maliciousUid).when(mMockPackageManager).getPackageUid( + eq(maliciousPackage), anyLong(), anyInt()); + starter.setCallingPackage(maliciousPackage); + starter.setCallingUid(maliciousUid); + + final Intent p2Intent = new Intent(); + p2Intent.setComponent(new ComponentName(privilegedPackage, "P2Activity")); + p2Intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + + final ActivityInfo p2ActivityInfo = new ActivityInfo(); + p2ActivityInfo.applicationInfo = new ApplicationInfo(); + p2ActivityInfo.applicationInfo.packageName = privilegedPackage; + p2ActivityInfo.applicationInfo.uid = privilegedUid; + p2ActivityInfo.name = "P2Activity"; + + final ActivityRecord[] outActivity = new ActivityRecord[1]; + + // The request simulates M2 starting P2 + starter.setIntent(p2Intent) + .setActivityInfo(p2ActivityInfo) + .setResultTo(m2.token) // sourceRecord is m2 + .setRequestCode(-1) // for startActivity() + .setOutActivity(outActivity) + .execute(); + + final ActivityRecord p2 = outActivity[0]; + + assertNotNull(p2); + assertEquals("launchedFromPackage should be the immediate caller", + maliciousPackage, p2.launchedFromPackage); + assertEquals("launchedFromUid should be the immediate caller", + maliciousUid, p2.launchedFromUid); + } + private static void startActivityInner(ActivityStarter starter, ActivityRecord target, ActivityRecord source, ActivityOptions options, Task inTask, TaskFragment inTaskFragment) { diff --git a/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java index 774a2ba72fe46..b9e1612359d57 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java @@ -318,6 +318,67 @@ public void testLockTaskViolation_wirelessEmergencyAlerts() { assertFalse(mLockTaskController.isLockTaskModeViolation(cellbroadcastreceiver)); } + @Test + public void testRebuildSystemLockTaskPinnedMode_lockTaskModeTasksEmpty_noop() { + // GIVEN no tasks in lockTaskMode + + // WHEN calling rebuildSystemLockTaskPinnedMode + mLockTaskController.rebuildSystemLockTaskPinnedMode(); + + // THEN mSupervisor should not be interacted with + verify(mSupervisor, never()).mRecentTasks.onLockTaskModeStateChanged(anyInt(), anyInt()); + } + + @Test + public void testRebuildSystemLockTaskPinnedMode_taskDontLock_noop() { + // GIVEN in started lock task mode (DONT_LOCK) + Task tr = getTask(LOCK_TASK_AUTH_DONT_LOCK); + mLockTaskController.startLockTaskMode(tr, true, TEST_UID); + + // WHEN calling rebuildSystemLockTaskPinnedMode + mLockTaskController.rebuildSystemLockTaskPinnedMode(); + + // THEN mSupervisor should not be interacted with + verify(mSupervisor, never()).mRecentTasks.onLockTaskModeStateChanged(anyInt(), anyInt()); + } + + @Test + public void testRebuildSystemLockTaskPinnedMode_notInPinnedMode_noop() { + // GIVEN in lock task mode (not PINNED) + Task tr = getTask(LOCK_TASK_AUTH_LAUNCHABLE_PRIV); + mLockTaskController.startLockTaskMode(tr, false, TEST_UID); + + // WHEN calling rebuildSystemLockTaskPinnedMode + mLockTaskController.rebuildSystemLockTaskPinnedMode(); + + // THEN notifyLockTaskModeChanged called only once, for the startLockTaskMode call + verify(mTaskChangeNotificationController).notifyLockTaskModeChanged(anyInt()); + } + + @Test + public void testRebuildSystemLockTaskPinnedMode_pinnedMode_rebuild() throws Exception { + // GIVEN in lock task mode (PINNED) + Task tr = getTask(LOCK_TASK_AUTH_PINNABLE); + mLockTaskController.startLockTaskMode(tr, true, TEST_UID); + + // WHEN calling rebuildSystemLockTaskPinnedMode + mLockTaskController.rebuildSystemLockTaskPinnedMode(); + + // THEN these below items are called TWICE, once for startLocktaskMode, once for rebuild + // THEN notifyLockTaskModeChanged + verify(mTaskChangeNotificationController, times(2)).notifyLockTaskModeChanged(anyInt()); + // THEN the keyguard should have been disabled + verify(mWindowManager, times(2)).disableKeyguard(any(IBinder.class), anyString(), + eq(TEST_USER_ID)); + // THEN the status bar should have been disabled + verify(mStatusBarService, times(2)).disable(eq(STATUS_BAR_MASK_PINNED), + any(IBinder.class), eq(mPackageName)); + verify(mStatusBarService, times(2)).disable2(eq(DISABLE2_NONE), any(IBinder.class), + eq(mPackageName)); + // THEN recents should have been notified + verify(mRecentTasks, times(2)).onLockTaskModeStateChanged(anyInt(), eq(TEST_USER_ID)); + } + @Test public void testStopLockTaskMode() throws Exception { // GIVEN one task record with allowlisted auth that is in lock task mode diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java index 9a2183128c3fb..208056397c647 100644 --- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java +++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java @@ -33,6 +33,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; +import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -699,6 +700,9 @@ abstract static class UsbHandler extends Handler { protected int mCurrentGadgetHalVersion; protected boolean mPendingBootAccessoryHandshakeBroadcast; protected boolean mUserUnlockedAfterBoot; + protected boolean mShowedFunctionDialog; + protected boolean mIsFirstUnlock = true; + /** * The persistent property which stores whether adb is enabled or not. * May also contain vendor-specific default functions for testing purposes. @@ -1289,6 +1293,12 @@ public void handleMessage(Message msg) { } } updateUsbFunctions(); + if (mConnected && !mScreenLocked) { + showFunctionDialog(); + } else if (!mConnected) { + // reset for the next usb connection + mShowedFunctionDialog = false; + } } else { mPendingBootBroadcast = true; } @@ -1452,6 +1462,15 @@ public void handleMessage(Message msg) { // Set the screen unlocked functions if current function is charging. setScreenUnlockedFunctions(operationId); } + if (mIsFirstUnlock) { + // skip showing function dialog at the first unlock + if (mConnected) { + mShowedFunctionDialog = true; + } + mIsFirstUnlock = false; + } else if (mConnected) { + showFunctionDialog(); + } } break; case MSG_UPDATE_USER_RESTRICTIONS: @@ -1781,6 +1800,23 @@ protected void updateUsbNotification(boolean force) { } } + private void showFunctionDialog() { + if (mShowedFunctionDialog) return; + + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setComponent(ComponentName.unflattenFromString(mContext.getString( + com.android.internal.R.string.config_usbFunctionActivity))); + + try { + mContext.startActivityAsUser(intent, UserHandle.CURRENT); + } catch (ActivityNotFoundException e) { + Slog.e(TAG, "unable to start activity " + intent, e); + } + + mShowedFunctionDialog = true; + } + protected boolean isAdbEnabled() { return LocalServices.getService(AdbManagerInternal.class) .isAdbEnabled(AdbTransportType.USB); diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 62f736cc943c8..a0baf495889f5 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -18222,6 +18222,23 @@ public void clearSignalStrengthUpdateRequest(@NonNull SignalStrengthUpdateReques } } + /** + * Get the modem service name. + * @return the service name of the modem service which bind to. + * @hide + */ + public String getModemService() { + try { + ITelephony telephony = getITelephony(); + if (telephony != null) { + return telephony.getModemService(); + } + } catch (RemoteException ex) { + Rlog.e(TAG, "getModemService RemoteException", ex); + } + return null; + } + /** * The unattended reboot was prepared successfully. * @hide