From 5b86162853974d2c1661192f098d69c3f1a68e4b Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Mon, 18 Sep 2023 12:49:12 -0700 Subject: [PATCH] Overriding the default TextClock and AnalogClock behavior to avoid RPCs on main thread during onAttachToWindow Bug: 294352799 Test: Verified on device Flag: N/A Change-Id: I3cce6900cd62a6e9a57c155b74c15c2340c6011b --- .../uioverrides/QuickstepLauncher.java | 33 +++++ .../util/AsyncClockEventDelegate.java | 125 ++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 4a14ccf704..5a46b8d8e9 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -73,10 +73,13 @@ import android.os.Bundle; import android.os.IBinder; import android.os.SystemProperties; import android.os.Trace; +import android.util.AttributeSet; import android.util.Log; import android.view.Display; import android.view.HapticFeedbackConstants; import android.view.View; +import android.widget.AnalogClock; +import android.widget.TextClock; import android.window.BackEvent; import android.window.OnBackAnimationCallback; import android.window.OnBackInvokedDispatcher; @@ -152,6 +155,7 @@ import com.android.quickstep.RecentsModel; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; import com.android.quickstep.TouchInteractionService.TISBinder; +import com.android.quickstep.util.AsyncClockEventDelegate; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.LauncherUnfoldAnimationController; import com.android.quickstep.util.QuickstepOnboardingPrefs; @@ -213,6 +217,8 @@ public class QuickstepLauncher extends Launcher { private SplitWithKeyboardShortcutController mSplitWithKeyboardShortcutController; private SplitToWorkspaceController mSplitToWorkspaceController; + private AsyncClockEventDelegate mAsyncClockEventDelegate; + /** * If Launcher restarted while in the middle of an Overview split select, it needs this data to * recover. In all other cases this will remain null. @@ -478,6 +484,10 @@ public class QuickstepLauncher extends Launcher { mSplitSelectStateController.onDestroy(); } + if (mAsyncClockEventDelegate != null) { + mAsyncClockEventDelegate.onDestroy(); + } + super.onDestroy(); mHotseatPredictionController.destroy(); mSplitWithKeyboardShortcutController.onDestroy(); @@ -1305,4 +1315,27 @@ public class QuickstepLauncher extends Launcher { mHotseatPredictionController.dump(prefix, writer); } } + + @Override + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + switch (name) { + case "TextClock", "android.widget.TextClock" -> { + TextClock tc = new TextClock(context, attrs); + if (mAsyncClockEventDelegate == null) { + mAsyncClockEventDelegate = new AsyncClockEventDelegate(this); + } + tc.setClockEventDelegate(mAsyncClockEventDelegate); + return tc; + } + case "AnalogClock", "android.widget.AnalogClock" -> { + AnalogClock ac = new AnalogClock(context, attrs); + if (mAsyncClockEventDelegate == null) { + mAsyncClockEventDelegate = new AsyncClockEventDelegate(this); + } + ac.setClockEventDelegate(mAsyncClockEventDelegate); + return ac; + } + } + return super.onCreateView(parent, name, context, attrs); + } } diff --git a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java new file mode 100644 index 0000000000..0dee5b3103 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import static android.content.Intent.ACTION_TIMEZONE_CHANGED; +import static android.content.Intent.ACTION_TIME_CHANGED; + +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.util.ArrayMap; +import android.widget.TextClock.ClockEventDelegate; + +import androidx.annotation.WorkerThread; + +import com.android.launcher3.util.SettingsCache; +import com.android.launcher3.util.SettingsCache.OnChangeListener; +import com.android.launcher3.util.SimpleBroadcastReceiver; + +import java.util.ArrayList; +import java.util.List; + +/** + * Extension of {@link ClockEventDelegate} to support async event registration + */ +public class AsyncClockEventDelegate extends ClockEventDelegate implements OnChangeListener { + + private final Context mContext; + private final SimpleBroadcastReceiver mReceiver = + new SimpleBroadcastReceiver(this::onClockEventReceived); + + private final ArrayMap mTimeEventReceivers = new ArrayMap<>(); + private final List mFormatObservers = new ArrayList<>(); + private final Uri mFormatUri = Settings.System.getUriFor(Settings.System.TIME_12_24); + + private boolean mFormatRegistered = false; + private boolean mDestroyed = false; + + public AsyncClockEventDelegate(Context context) { + super(context); + mContext = context; + + UI_HELPER_EXECUTOR.execute(() -> + mReceiver.register(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED)); + } + + @Override + public void registerTimeChangeReceiver(BroadcastReceiver receiver, Handler handler) { + synchronized (mTimeEventReceivers) { + mTimeEventReceivers.put(receiver, handler == null ? new Handler() : handler); + } + } + + @Override + public void unregisterTimeChangeReceiver(BroadcastReceiver receiver) { + synchronized (mTimeEventReceivers) { + mTimeEventReceivers.remove(receiver); + } + } + + @Override + public void registerFormatChangeObserver(ContentObserver observer, int userHandle) { + synchronized (mFormatObservers) { + if (!mFormatRegistered && !mDestroyed) { + SettingsCache.INSTANCE.get(mContext).register(mFormatUri, this); + mFormatRegistered = true; + } + mFormatObservers.add(observer); + } + } + + @Override + public void unregisterFormatChangeObserver(ContentObserver observer) { + synchronized (mFormatObservers) { + mFormatObservers.remove(observer); + } + } + + @Override + public void onSettingsChanged(boolean isEnabled) { + if (mDestroyed) { + return; + } + synchronized (mFormatObservers) { + mFormatObservers.forEach(o -> o.dispatchChange(false, mFormatUri)); + } + } + @WorkerThread + private void onClockEventReceived(Intent intent) { + if (mDestroyed) { + return; + } + synchronized (mReceiver) { + mTimeEventReceivers.forEach((r, h) -> h.post(() -> r.onReceive(mContext, intent))); + } + } + + /** + * Unregisters all system callbacks and destroys this delegate + */ + public void onDestroy() { + mDestroyed = true; + SettingsCache.INSTANCE.get(mContext).unregister(mFormatUri, this); + UI_HELPER_EXECUTOR.execute(() -> mReceiver.unregisterReceiverSafely(mContext)); + } +}