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
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 4a14ccf..5a46b8d 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.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.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 @@
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 @@
mSplitSelectStateController.onDestroy();
}
+ if (mAsyncClockEventDelegate != null) {
+ mAsyncClockEventDelegate.onDestroy();
+ }
+
super.onDestroy();
mHotseatPredictionController.destroy();
mSplitWithKeyboardShortcutController.onDestroy();
@@ -1305,4 +1315,27 @@
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 0000000..0dee5b3
--- /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<BroadcastReceiver, Handler> mTimeEventReceivers = new ArrayMap<>();
+ private final List<ContentObserver> 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));
+ }
+}