Revert "Adding main thread binder tracing in development build."
Revert submission 23482347-launcher-binder-tracing
Reason for revert: Broken Build 10242119 on git_master on errorprone b/285434612
Reverted changes: /q/submissionid:23482347-launcher-binder-tracing
Change-Id: Id9726474265e839a59d6eb34822e7a86d40f0fc9
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java b/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java
new file mode 100644
index 0000000..d8aa235
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides;
+
+import static android.os.IBinder.FLAG_ONEWAY;
+
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+
+/**
+ * A binder proxy transaction listener for tracking non-whitelisted binder calls.
+ */
+public class DejankBinderTracker implements Binder.ProxyTransactListener {
+ private static final String TAG = "DejankBinderTracker";
+
+ private static final Object sLock = new Object();
+ private static final HashSet<String> sWhitelistedFrameworkClasses = new HashSet<>();
+ static {
+ // Common IPCs that are ok to block the main thread.
+ sWhitelistedFrameworkClasses.add("android.view.IWindowSession");
+ sWhitelistedFrameworkClasses.add("android.os.IPowerManager");
+ }
+ private static boolean sTemporarilyIgnoreTracking = false;
+
+ // Used by the client to limit binder tracking to specific regions
+ private static boolean sTrackingAllowed = false;
+
+ private BiConsumer<String, Integer> mUnexpectedTransactionCallback;
+ private boolean mIsTracking = false;
+
+ /**
+ * Temporarily ignore blocking binder calls for the duration of this {@link Runnable}.
+ */
+ @MainThread
+ public static void whitelistIpcs(Runnable runnable) {
+ sTemporarilyIgnoreTracking = true;
+ runnable.run();
+ sTemporarilyIgnoreTracking = false;
+ }
+
+ /**
+ * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
+ */
+ @MainThread
+ public static <T> T whitelistIpcs(Supplier<T> supplier) {
+ sTemporarilyIgnoreTracking = true;
+ T value = supplier.get();
+ sTemporarilyIgnoreTracking = false;
+ return value;
+ }
+
+ /**
+ * Enables binder tracking during a test.
+ */
+ @MainThread
+ public static void allowBinderTrackingInTests() {
+ sTrackingAllowed = true;
+ }
+
+ /**
+ * Disables binder tracking during a test.
+ */
+ @MainThread
+ public static void disallowBinderTrackingInTests() {
+ sTrackingAllowed = false;
+ }
+
+ public DejankBinderTracker(BiConsumer<String, Integer> unexpectedTransactionCallback) {
+ mUnexpectedTransactionCallback = unexpectedTransactionCallback;
+ }
+
+ @MainThread
+ public void startTracking() {
+ if (!Build.TYPE.toLowerCase(Locale.ROOT).contains("debug")
+ && !Build.TYPE.toLowerCase(Locale.ROOT).equals("eng")) {
+ Log.wtf(TAG, "Unexpected use of binder tracker in non-debug build", new Exception());
+ return;
+ }
+ if (mIsTracking) {
+ return;
+ }
+ mIsTracking = true;
+ Binder.setProxyTransactListener(this);
+ }
+
+ @MainThread
+ public void stopTracking() {
+ if (!mIsTracking) {
+ return;
+ }
+ mIsTracking = false;
+ Binder.setProxyTransactListener(null);
+ }
+
+ // Override the hidden Binder#onTransactStarted method
+ public synchronized Object onTransactStarted(IBinder binder, int transactionCode, int flags) {
+ if (!mIsTracking
+ || !sTrackingAllowed
+ || sTemporarilyIgnoreTracking
+ || (flags & FLAG_ONEWAY) == FLAG_ONEWAY
+ || !isMainThread()) {
+ return null;
+ }
+
+ String descriptor;
+ try {
+ descriptor = binder.getInterfaceDescriptor();
+ if (sWhitelistedFrameworkClasses.contains(descriptor)) {
+ return null;
+ }
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ descriptor = binder.getClass().getSimpleName();
+ }
+
+ mUnexpectedTransactionCallback.accept(descriptor, transactionCode);
+ return null;
+ }
+
+ @Override
+ public Object onTransactStarted(IBinder binder, int transactionCode) {
+ // Do nothing
+ return null;
+ }
+
+ @Override
+ public void onTransactEnded(Object session) {
+ // Do nothing
+ }
+
+ public static boolean isMainThread() {
+ return Thread.currentThread() == Looper.getMainLooper().getThread();
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index bf4896d..2b92188 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -112,7 +112,6 @@
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.TraceHelper;
import com.android.launcher3.util.VibratorWrapper;
import com.android.launcher3.util.WindowBounds;
@@ -588,7 +587,7 @@
if (mWasLauncherAlreadyVisible) {
mStateCallback.setState(STATE_LAUNCHER_DRAWN);
} else {
- SafeCloseable traceToken = TraceHelper.INSTANCE.beginAsyncSection("WTS-init");
+ Object traceToken = TraceHelper.INSTANCE.beginSection("WTS-init");
View dragLayer = activity.getDragLayer();
dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
boolean mHandled = false;
@@ -600,7 +599,7 @@
}
mHandled = true;
- traceToken.close();
+ TraceHelper.INSTANCE.endSection(traceToken);
dragLayer.post(() ->
dragLayer.getViewTreeObserver().removeOnDrawListener(this));
if (activity != mActivity) {
@@ -682,10 +681,11 @@
private void initializeLauncherAnimationController() {
buildAnimationController();
- try (SafeCloseable c = TraceHelper.INSTANCE.allowIpcs("logToggleRecents")) {
- LatencyTracker.getInstance(mContext).logAction(LatencyTracker.ACTION_TOGGLE_RECENTS,
- (int) (mLauncherFrameDrawnTime - mTouchTimeMs));
- }
+ Object traceToken = TraceHelper.INSTANCE.beginSection("logToggleRecents",
+ TraceHelper.FLAG_IGNORE_BINDERS);
+ LatencyTracker.getInstance(mContext).logAction(LatencyTracker.ACTION_TOGGLE_RECENTS,
+ (int) (mLauncherFrameDrawnTime - mTouchTimeMs));
+ TraceHelper.INSTANCE.endSection(traceToken);
// This method is only called when STATE_GESTURE_STARTED is set, so we can enable the
// high-res thumbnail loader here once we are sure that we will end up in an overview state
@@ -2039,9 +2039,10 @@
private void setScreenshotCapturedState() {
// If we haven't posted a draw callback, set the state immediately.
- TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT);
+ Object traceToken = TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT,
+ TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS);
mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
- TraceHelper.INSTANCE.endSection();
+ TraceHelper.INSTANCE.endSection(traceToken);
}
private void finishCurrentTransitionToRecents() {
diff --git a/quickstep/src/com/android/quickstep/BinderTracker.java b/quickstep/src/com/android/quickstep/BinderTracker.java
deleted file mode 100644
index a876cd8..0000000
--- a/quickstep/src/com/android/quickstep/BinderTracker.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/**
- * 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;
-
-import static android.os.IBinder.FLAG_ONEWAY;
-
-import android.os.Binder;
-import android.os.Binder.ProxyTransactListener;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.RemoteException;
-import android.os.Trace;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.util.TraceHelper;
-
-import java.util.LinkedList;
-import java.util.Set;
-import java.util.function.Consumer;
-
-import kotlin.random.Random;
-
-/**
- * A binder proxy transaction listener for tracking binder calls on main thread.
- */
-public class BinderTracker {
-
- private static final String TAG = "BinderTracker";
-
- // Common IPCs that are ok to block the main thread.
- private static final Set<String> sAllowedFrameworkClasses = Set.of(
- "android.view.IWindowSession",
- "android.os.IPowerManager",
- "android.os.IServiceManager");
-
- /**
- * Starts tracking binder class and returns a {@link SafeCloseable} to end tracking
- */
- public static SafeCloseable startTracking(Consumer<BinderCallSite> callback) {
- TraceHelper current = TraceHelper.INSTANCE;
-
- TraceHelperExtension helper = new TraceHelperExtension(callback);
- TraceHelper.INSTANCE = helper;
- Binder.setProxyTransactListener(helper);
-
- return () -> {
- Binder.setProxyTransactListener(null);
- TraceHelper.INSTANCE = current;
- };
- }
-
- private static final LinkedList<String> mMainThreadTraceStack = new LinkedList<>();
- private static final LinkedList<String> mMainThreadIgnoreIpcStack = new LinkedList<>();
-
- private static class TraceHelperExtension extends TraceHelper implements ProxyTransactListener {
-
- private final Consumer<BinderCallSite> mUnexpectedTransactionCallback;
-
- TraceHelperExtension(Consumer<BinderCallSite> unexpectedTransactionCallback) {
- mUnexpectedTransactionCallback = unexpectedTransactionCallback;
- }
-
- @Override
- public void beginSection(String sectionName) {
- if (isMainThread()) {
- mMainThreadTraceStack.add(sectionName);
- }
- super.beginSection(sectionName);
- }
-
- @Override
- public SafeCloseable beginAsyncSection(String sectionName) {
- if (!isMainThread()) {
- return super.beginAsyncSection(sectionName);
- }
-
- mMainThreadTraceStack.add(sectionName);
- int cookie = Random.Default.nextInt();
- Trace.beginAsyncSection(sectionName, cookie);
- return () -> {
- Trace.endAsyncSection(sectionName, cookie);
- mMainThreadTraceStack.remove(sectionName);
- };
- }
-
- @Override
- public void endSection() {
- super.endSection();
- if (isMainThread()) {
- mMainThreadTraceStack.pollLast();
- }
- }
-
- @Override
- public SafeCloseable allowIpcs(String rpcName) {
- if (!isMainThread()) {
- return super.allowIpcs(rpcName);
- }
-
- mMainThreadTraceStack.add(rpcName);
- mMainThreadIgnoreIpcStack.add(rpcName);
- int cookie = Random.Default.nextInt();
- Trace.beginAsyncSection(rpcName, cookie);
- return () -> {
- Trace.endAsyncSection(rpcName, cookie);
- mMainThreadTraceStack.remove(rpcName);
- mMainThreadIgnoreIpcStack.remove(rpcName);
- };
- }
-
- @Override
- public Object onTransactStarted(IBinder binder, int transactionCode, int flags) {
- if (!isMainThread() || (flags & FLAG_ONEWAY) == FLAG_ONEWAY) {
- return null;
- }
-
- String ipcBypass = mMainThreadIgnoreIpcStack.peekLast();
- String descriptor;
- try {
- descriptor = binder.getInterfaceDescriptor();
- if (sAllowedFrameworkClasses.contains(descriptor)) {
- return null;
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Error getting IPC descriptor", e);
- descriptor = binder.getClass().getSimpleName();
- }
-
- if (ipcBypass == null) {
- mUnexpectedTransactionCallback.accept(new BinderCallSite(
- mMainThreadTraceStack.peekLast(), descriptor, transactionCode));
- } else {
- Log.d(TAG, "MainThread-IPC " + descriptor + " ignored due to " + ipcBypass);
- }
- return null;
- }
-
- @Override
- public Object onTransactStarted(IBinder binder, int transactionCode) {
- // Do nothing
- return null;
- }
-
- @Override
- public void onTransactEnded(Object session) {
- // Do nothing
- }
- }
-
- private static boolean isMainThread() {
- return Thread.currentThread() == Looper.getMainLooper().getThread();
- }
-
- /**
- * Information about a binder call
- */
- public static class BinderCallSite {
-
- @Nullable
- public final String activeTrace;
- public final String descriptor;
- public final int transactionCode;
-
- BinderCallSite(String activeTrace, String descriptor, int transactionCode) {
- this.activeTrace = activeTrace;
- this.descriptor = descriptor;
- this.transactionCode = transactionCode;
- }
- }
-}
diff --git a/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
index 529213c..7638541 100644
--- a/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
+++ b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
@@ -16,13 +16,10 @@
package com.android.quickstep;
-import android.app.ActivityThread;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.os.RemoteException;
-import android.util.Log;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.util.InstantAppResolver;
@@ -52,14 +49,4 @@
ComponentName cn = info.getTargetComponent();
return cn != null && cn.getClassName().equals(COMPONENT_CLASS_MARKER);
}
-
- @Override
- public boolean isInstantApp(String packageName, int userId) {
- try {
- return ActivityThread.getPackageManager().isInstantApp(packageName, userId);
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to determine whether package is instant app " + packageName, e);
- return false;
- }
- }
}
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index 128b045..5f589bf 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -60,10 +60,5 @@
// Elevate GPU priority for Quickstep and Remote animations.
ThreadedRenderer.setContextPriority(
ThreadedRenderer.EGL_CONTEXT_PRIORITY_HIGH_IMG);
-
- if (BuildConfig.IS_STUDIO_BUILD) {
- BinderTracker.startTracking(call -> Log.e("BinderCall",
- call.descriptor + " called on mainthread under " + call.activeTrace));
- }
}
}
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 810c028..8135238 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -393,12 +393,11 @@
@Override
public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
TaskIdAttributeContainer taskContainer) {
- Task t = taskContainer.getTask();
- return InstantAppResolver.newInstance(activity).isInstantApp(
- t.getTopComponent().getPackageName(), t.getKey().userId)
- ? Collections.singletonList(new SystemShortcut.Install(activity,
- taskContainer.getItemInfo(), taskContainer.getTaskView()))
- : null;
+ return InstantAppResolver.newInstance(activity).isInstantApp(activity,
+ taskContainer.getTask().getTopComponent().getPackageName()) ?
+ Collections.singletonList(new SystemShortcut.Install(activity,
+ taskContainer.getItemInfo(), taskContainer.getTaskView())) :
+ null;
}
};
diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java
index 80a449b..67360c4 100644
--- a/quickstep/src/com/android/quickstep/TaskUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskUtils.java
@@ -33,7 +33,6 @@
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageManagerHelper;
-import com.android.launcher3.util.TraceHelper;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -52,8 +51,7 @@
* TODO: remove this once we switch to getting the icon and label from IconCache.
*/
public static CharSequence getTitle(Context context, Task task) {
- return TraceHelper.allowIpcs("TaskUtils.getTitle", () ->
- getTitle(context, task.key.userId, task.getTopComponent().getPackageName()));
+ return getTitle(context, task.key.userId, task.getTopComponent().getPackageName());
}
public static CharSequence getTitle(
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index a060c7c..8d05fa9 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -102,7 +102,6 @@
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.LockedUserState;
import com.android.launcher3.util.OnboardingPrefs;
-import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.TraceHelper;
import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
import com.android.quickstep.inputconsumers.AssistantInputConsumer;
@@ -655,7 +654,8 @@
return;
}
- SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
+ Object traceToken = TraceHelper.INSTANCE.beginFlagsOverride(
+ TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
final int action = event.getActionMasked();
// Note this will create a new consumer every mouse click, as after ACTION_UP from the click
@@ -751,7 +751,7 @@
if (cleanUpConsumer) {
reset();
}
- traceToken.close();
+ TraceHelper.INSTANCE.endFlagsOverride(traceToken);
ProtoTracer.INSTANCE.get(this).scheduleFrameUpdate();
}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 10c6316..5b27f9b 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -28,6 +28,7 @@
import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
import static com.android.launcher3.Utilities.squaredHypot;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
import static com.android.launcher3.util.VelocityUtils.PX_PER_MS;
import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
@@ -228,7 +229,8 @@
// Until we detect the gesture, handle events as we receive them
mInputEventReceiver.setBatchingEnabled(false);
- TraceHelper.INSTANCE.beginSection(DOWN_EVT);
+ Object traceToken = TraceHelper.INSTANCE.beginSection(DOWN_EVT,
+ FLAG_CHECK_FOR_RACE_CONDITIONS);
mActivePointerId = ev.getPointerId(0);
mDownPos.set(ev.getX(), ev.getY());
mLastPos.set(mDownPos);
@@ -239,7 +241,7 @@
startTouchTrackingForWindowAnimation(ev.getEventTime());
}
- TraceHelper.INSTANCE.endSection();
+ TraceHelper.INSTANCE.endSection(traceToken);
break;
}
case ACTION_POINTER_DOWN: {
@@ -415,7 +417,8 @@
* the animation can still be running.
*/
private void finishTouchTracking(MotionEvent ev) {
- TraceHelper.INSTANCE.beginSection(UP_EVT);
+ Object traceToken = TraceHelper.INSTANCE.beginSection(UP_EVT,
+ FLAG_CHECK_FOR_RACE_CONDITIONS);
if (mPassedWindowMoveSlop && mInteractionHandler != null) {
if (ev.getActionMasked() == ACTION_CANCEL) {
@@ -452,7 +455,7 @@
onInteractionGestureFinished();
}
cleanupAfterGesture();
- TraceHelper.INSTANCE.endSection();
+ TraceHelper.INSTANCE.endSection(traceToken);
}
private void cleanupAfterGesture() {
diff --git a/quickstep/src/com/android/quickstep/util/BinderTracker.java b/quickstep/src/com/android/quickstep/util/BinderTracker.java
new file mode 100644
index 0000000..cb04e5b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/BinderTracker.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Looper;
+import android.util.Log;
+
+import com.android.launcher3.config.FeatureFlags;
+
+/**
+ * Utility class to test and check binder calls during development.
+ */
+public class BinderTracker {
+
+ private static final String TAG = "BinderTracker";
+
+ public static void start() {
+ if (!FeatureFlags.IS_STUDIO_BUILD) {
+ Log.wtf(TAG, "Accessing tracker in released code.", new Exception());
+ return;
+ }
+
+ Binder.setProxyTransactListener(new Tracker());
+ }
+
+ public static void stop() {
+ if (!FeatureFlags.IS_STUDIO_BUILD) {
+ Log.wtf(TAG, "Accessing tracker in released code.", new Exception());
+ return;
+ }
+ Binder.setProxyTransactListener(null);
+ }
+
+ private static class Tracker implements Binder.ProxyTransactListener {
+
+ @Override
+ public Object onTransactStarted(IBinder iBinder, int code) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ Log.e(TAG, "Binder call on ui thread", new Exception());
+ }
+ return null;
+ }
+
+ @Override
+ public void onTransactEnded(Object session) { }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 7f035a2..f8893bd 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -112,7 +112,8 @@
mContext = context;
mSizeStrategy = sizeStrategy;
- mOrientationState = TraceHelper.allowIpcs("TaskViewSimulator.init",
+ // TODO(b/187074722): Don't create this per-TaskViewSimulator
+ mOrientationState = TraceHelper.allowIpcs("",
() -> new RecentsOrientedState(context, sizeStrategy, i -> { }));
mOrientationState.setGestureActive(true);
mCurrentFullscreenParams = new FullscreenDrawParams(context);
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 46dd94b..200252a 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -32,7 +32,6 @@
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition;
-import static com.android.quickstep.TaskOverlayFactory.getEnabledShortcuts;
import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR;
import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -89,7 +88,6 @@
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
-import com.android.launcher3.util.TraceHelper;
import com.android.launcher3.util.TransformingTouchDelegate;
import com.android.launcher3.util.ViewPool.Reusable;
import com.android.quickstep.RecentsModel;
@@ -1558,8 +1556,8 @@
if (taskContainer == null) {
continue;
}
- for (SystemShortcut s : TraceHelper.allowIpcs(
- "TV.a11yInfo", () -> getEnabledShortcuts(this, taskContainer))) {
+ for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
+ taskContainer)) {
info.addAction(s.createAccessibilityAction(context));
}
}
@@ -1596,7 +1594,7 @@
if (taskContainer == null) {
continue;
}
- for (SystemShortcut s : getEnabledShortcuts(this,
+ for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
taskContainer)) {
if (s.hasHandlerForAction(action)) {
s.onClick(this);
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index 5127190..df5303f 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -20,6 +20,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.util.RaceConditionReproducer;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
import org.junit.Before;
@@ -44,6 +45,18 @@
startTestActivity(2);
}
+ private void runTest(String... eventSequence) {
+ final RaceConditionReproducer eventProcessor = new RaceConditionReproducer(eventSequence);
+
+ // Destroy Launcher activity.
+ closeLauncherActivity();
+
+ // The test action.
+ eventProcessor.startIteration();
+ mLauncher.goHome();
+ eventProcessor.finishIteration();
+ }
+
@Ignore
@Test
@NavigationModeSwitch
diff --git a/res/drawable/ic_wallpaper.xml b/res/drawable/ic_wallpaper.xml
new file mode 100644
index 0000000..9543f88
--- /dev/null
+++ b/res/drawable/ic_wallpaper.xml
@@ -0,0 +1,27 @@
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/options_menu_icon_size"
+ android:height="@dimen/options_menu_icon_size"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/textColorPrimary">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M9,12.71l2.14,2.58l3-3.87L18,16.57H6L9,12.71z M5,5h6V3H5C3.9,3,3,3.9,3,5v6h2V5z M19,19h-6v2h6c1.1,0,2-0.9,2-2v-6h-2V19z
+ M5,19v-6H3v6c0,1.1,0.9,2,2,2h6v-2H5z M19,5v6h2V5c0-1.1-0.9-2-2-2h-6v2H19z M16,9c0.55,0,1-0.45,1-1s-0.45-1-1-1
+ c-0.55,0-1,0.45-1,1S15.45,9,16,9z"/>
+</vector>
diff --git a/res/values/config.xml b/res/values/config.xml
index 83f840d..5a6698b 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -102,6 +102,8 @@
<!-- Default packages -->
<string name="wallpaper_picker_package" translatable="false"></string>
+ <string name="custom_activity_picker" translatable="false">
+ com.android.customization.picker.CustomizationPickerActivity</string>
<string name="local_colors_extraction_class" translatable="false"></string>
<string name="search_session_manager_class" translatable="false"></string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1b46b4d..c2eb373 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -250,6 +250,8 @@
<!-- Strings for the customization mode -->
<!-- Text for wallpaper change button [CHAR LIMIT=30]-->
+ <string name="wallpaper_button_text">Wallpapers</string>
+ <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
<string name="styles_wallpaper_button_text">Wallpaper & style</string>
<!-- Text for edit home screen button [CHAR LIMIT=30]-->
<string name="edit_home_screen">Edit Home Screen</string>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index ac46c82..db5a27a 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -442,7 +442,8 @@
Trace.beginAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
DISPLAY_ALL_APPS_TRACE_COOKIE);
}
- TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT);
+ Object traceToken = TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT,
+ TraceHelper.FLAG_UI_EVENT);
if (DEBUG_STRICT_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
@@ -575,7 +576,7 @@
LauncherOverlayPlugin.class, false /* allowedMultiple */);
mRotationHelper.initialize();
- TraceHelper.INSTANCE.endSection();
+ TraceHelper.INSTANCE.endSection(traceToken);
mUserChangedCallbackCloseable = UserCache.INSTANCE.get(this).addUserChangeListener(
() -> getStateManager().goToState(NORMAL));
@@ -1073,14 +1074,15 @@
@Override
protected void onStart() {
- TraceHelper.INSTANCE.beginSection(ON_START_EVT);
+ Object traceToken = TraceHelper.INSTANCE.beginSection(ON_START_EVT,
+ TraceHelper.FLAG_UI_EVENT);
super.onStart();
if (!mDeferOverlayCallbacks) {
mOverlayManager.onActivityStarted(this);
}
mAppWidgetHolder.setActivityStarted(true);
- TraceHelper.INSTANCE.endSection();
+ TraceHelper.INSTANCE.endSection(traceToken);
}
@Override
@@ -1251,7 +1253,8 @@
@Override
protected void onResume() {
- TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT);
+ Object traceToken = TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT,
+ TraceHelper.FLAG_UI_EVENT);
super.onResume();
if (mDeferOverlayCallbacks) {
@@ -1261,7 +1264,7 @@
}
DragView.removeAllViews(this);
- TraceHelper.INSTANCE.endSection();
+ TraceHelper.INSTANCE.endSection(traceToken);
}
@Override
@@ -1649,7 +1652,7 @@
if (Utilities.isRunningInTestHarness()) {
Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Launcher.onNewIntent: " + intent);
}
- TraceHelper.INSTANCE.beginSection(ON_NEW_INTENT_EVT);
+ Object traceToken = TraceHelper.INSTANCE.beginSection(ON_NEW_INTENT_EVT);
super.onNewIntent(intent);
boolean alreadyOnHome = hasWindowFocus() && ((intent.getFlags() &
@@ -1696,7 +1699,7 @@
showAllAppsWorkTabFromIntent(alreadyOnHome);
}
- TraceHelper.INSTANCE.endSection();
+ TraceHelper.INSTANCE.endSection(traceToken);
}
protected void toggleAllAppsFromIntent(boolean alreadyOnHome) {
@@ -2295,7 +2298,7 @@
* Implementation of the method from LauncherModel.Callbacks.
*/
public void startBinding() {
- TraceHelper.INSTANCE.beginSection("startBinding");
+ Object traceToken = TraceHelper.INSTANCE.beginSection("startBinding");
// Floating panels (except the full widget sheet) are associated with individual icons. If
// we are starting a fresh bind, close all such panels as all the icons are about
// to go away.
@@ -2313,7 +2316,7 @@
if (mHotseat != null) {
mHotseat.resetLayout(getDeviceProfile().isVerticalBarLayout());
}
- TraceHelper.INSTANCE.endSection();
+ TraceHelper.INSTANCE.endSection(traceToken);
}
@Override
@@ -2566,7 +2569,7 @@
return view;
}
- TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId);
+ Object traceToken = TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId);
try {
final LauncherAppWidgetProviderInfo appWidgetInfo;
@@ -2696,7 +2699,7 @@
}
prepareAppWidget(view, item);
} finally {
- TraceHelper.INSTANCE.endSection();
+ TraceHelper.INSTANCE.endSection(traceToken);
}
return view;
@@ -2789,7 +2792,7 @@
* Implementation of the method from LauncherModel.Callbacks.
*/
public void finishBindingItems(IntSet pagesBoundFirst) {
- TraceHelper.INSTANCE.beginSection("finishBindingItems");
+ Object traceToken = TraceHelper.INSTANCE.beginSection("finishBindingItems");
mWorkspace.restoreInstanceStateForRemainingPages();
setWorkspaceLoading(false);
@@ -2814,7 +2817,7 @@
mDeviceProfile.inv.numFolderColumns * mDeviceProfile.inv.numFolderRows);
getViewCache().setCacheSize(R.layout.folder_page, 2);
- TraceHelper.INSTANCE.endSection();
+ TraceHelper.INSTANCE.endSection(traceToken);
mWorkspace.removeExtraEmptyScreen(true);
}
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 73a06b6..d2a8174 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -201,7 +201,7 @@
}
}
- TraceHelper.INSTANCE.beginSection(TAG);
+ Object traceToken = TraceHelper.INSTANCE.beginSection(TAG);
LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger();
try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
List<ShortcutInfo> allShortcuts = new ArrayList<>();
@@ -325,7 +325,7 @@
memoryLogger.printLogs();
throw e;
}
- TraceHelper.INSTANCE.endSection();
+ TraceHelper.INSTANCE.endSection(traceToken);
}
public synchronized void stopLocked() {
diff --git a/src/com/android/launcher3/util/InstantAppResolver.java b/src/com/android/launcher3/util/InstantAppResolver.java
index bdb5e77..6f706d2 100644
--- a/src/com/android/launcher3/util/InstantAppResolver.java
+++ b/src/com/android/launcher3/util/InstantAppResolver.java
@@ -42,7 +42,14 @@
return false;
}
- public boolean isInstantApp(String packageName, int userId) {
+ public boolean isInstantApp(Context context, String packageName) {
+ PackageManager packageManager = context.getPackageManager();
+ try {
+ return isInstantApp(packageManager.getPackageInfo(packageName, 0).applicationInfo);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e("InstantAppResolver", "Failed to determine whether package is instant app "
+ + packageName, e);
+ }
return false;
}
}
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 91203a7..1d6bc25 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -164,6 +164,13 @@
}
}
+ public static Intent getStyleWallpapersIntent(Context context) {
+ return new Intent(Intent.ACTION_SET_WALLPAPER).setComponent(
+ new ComponentName(context.getString(R.string.wallpaper_picker_package),
+ context.getString(R.string.custom_activity_picker)
+ ));
+ }
+
/**
* Starts the details activity for {@code info}
*/
diff --git a/src/com/android/launcher3/util/TraceHelper.java b/src/com/android/launcher3/util/TraceHelper.java
index d5056ee..c23df77 100644
--- a/src/com/android/launcher3/util/TraceHelper.java
+++ b/src/com/android/launcher3/util/TraceHelper.java
@@ -21,8 +21,6 @@
import java.util.function.Supplier;
-import kotlin.random.Random;
-
/**
* A wrapper around {@link Trace} to allow better testing.
*
@@ -38,53 +36,54 @@
// Temporarily ignore blocking binder calls for this trace.
public static final int FLAG_IGNORE_BINDERS = 1 << 1;
+ public static final int FLAG_CHECK_FOR_RACE_CONDITIONS = 1 << 2;
+
+ public static final int FLAG_UI_EVENT =
+ FLAG_ALLOW_BINDER_TRACKING | FLAG_CHECK_FOR_RACE_CONDITIONS;
+
/**
* Static instance of Trace helper, overridden in tests.
*/
public static TraceHelper INSTANCE = new TraceHelper();
/**
- * @see Trace#beginSection(String)
+ * @return a token to pass into {@link #endSection(Object)}.
*/
- public void beginSection(String sectionName) {
+ public Object beginSection(String sectionName) {
+ return beginSection(sectionName, 0);
+ }
+
+ public Object beginSection(String sectionName, int flags) {
Trace.beginSection(sectionName);
+ return null;
}
/**
- * @see Trace#endSection()
+ * @param token the token returned from {@link #beginSection(String, int)}
*/
- public void endSection() {
+ public void endSection(Object token) {
Trace.endSection();
}
/**
- * @see Trace#beginAsyncSection(String, int)
- * @return a SafeCloseable that can be used to end the session
+ * Similar to {@link #beginSection} but doesn't add a trace section.
*/
- public SafeCloseable beginAsyncSection(String sectionName) {
- int cookie = Random.Default.nextInt();
- Trace.beginAsyncSection(sectionName, cookie);
- return () -> Trace.endAsyncSection(sectionName, cookie);
+ public Object beginFlagsOverride(int flags) {
+ return null;
}
- /**
- * Returns a SafeCloseable to temporarily ignore blocking binder calls.
- */
- public SafeCloseable allowIpcs(String rpcName) {
- int cookie = Random.Default.nextInt();
- Trace.beginAsyncSection(rpcName, cookie);
- return () -> Trace.endAsyncSection(rpcName, cookie);
- }
+ public void endFlagsOverride(Object token) { }
/**
* Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
- *
- * Note, new features should be designed to not rely on mainThread RPCs.
*/
@MainThread
public static <T> T allowIpcs(String rpcName, Supplier<T> supplier) {
- try (SafeCloseable c = INSTANCE.allowIpcs(rpcName)) {
+ Object traceToken = INSTANCE.beginSection(rpcName, FLAG_IGNORE_BINDERS);
+ try {
return supplier.get();
+ } finally {
+ INSTANCE.endSection(traceToken);
}
}
}
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 55febc7..aebf752 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -55,6 +55,7 @@
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import java.util.ArrayList;
@@ -189,9 +190,14 @@
*/
public static ArrayList<OptionItem> getOptions(Launcher launcher) {
ArrayList<OptionItem> options = new ArrayList<>();
+ boolean styleWallpaperExists = styleWallpapersExists(launcher);
+ int resString = styleWallpaperExists
+ ? R.string.styles_wallpaper_button_text : R.string.wallpaper_button_text;
+ int resDrawable = styleWallpaperExists
+ ? R.drawable.ic_palette : R.drawable.ic_wallpaper;
options.add(new OptionItem(launcher,
- R.string.styles_wallpaper_button_text,
- R.drawable.ic_palette,
+ resString,
+ resDrawable,
IGNORE,
OptionsPopupView::startWallpaperPicker));
if (!WidgetsModel.GO_DISABLE_WIDGETS) {
@@ -268,8 +274,12 @@
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.putExtra(EXTRA_WALLPAPER_OFFSET,
launcher.getWorkspace().getWallpaperOffsetForCenterPage())
- .putExtra(EXTRA_WALLPAPER_LAUNCH_SOURCE, "app_launched_launcher")
- .putExtra(EXTRA_WALLPAPER_FLAVOR, "focus_wallpaper");
+ .putExtra(EXTRA_WALLPAPER_LAUNCH_SOURCE, "app_launched_launcher");
+ if (!styleWallpapersExists(launcher)) {
+ intent.putExtra(EXTRA_WALLPAPER_FLAVOR, "wallpaper_only");
+ } else {
+ intent.putExtra(EXTRA_WALLPAPER_FLAVOR, "focus_wallpaper");
+ }
String pickerPackage = launcher.getString(R.string.wallpaper_picker_package);
if (!TextUtils.isEmpty(pickerPackage)) {
intent.setPackage(pickerPackage);
@@ -312,4 +322,9 @@
this.clickListener = clickListener;
}
}
+
+ private static boolean styleWallpapersExists(Context context) {
+ return context.getPackageManager().resolveActivity(
+ PackageManagerHelper.getStyleWallpapersIntent(context), 0) != null;
+ }
}
diff --git a/tests/src/com/android/launcher3/util/RaceConditionReproducer.java b/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
new file mode 100644
index 0000000..ed2ec7b
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import static com.android.launcher3.util.Executors.createAndStartNewLooper;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Event processor for reliably reproducing multithreaded apps race conditions in tests.
+ *
+ * The app notifies us about “events” that happen in its threads. The race condition test runs the
+ * test action multiple times (aka iterations), trying to generate all possible permutations of
+ * these events. It keeps a set of all seen event sequences and steers the execution towards
+ * executing events in previously unseen order. It does it by postponing execution of threads that
+ * would lead to an already seen sequence.
+ *
+ * If an event A occurs before event B in the sequence, this is how execution order looks like:
+ * Events: ... A ... B ...
+ * Events and instructions, guaranteed order:
+ * (instructions executed prior to A) A ... B (instructions executed after B)
+ *
+ * Each iteration has 3 parts (phases).
+ * Phase 1. Picking a previously seen event subsequence that we believe can have previously unseen
+ * continuations. Reproducing this sequence by pausing threads that would lead to other sequences.
+ * Phase 2. Trying to generate previously unseen continuation of the sequence from Phase 1. We need
+ * one new event after that sequence. All threads leading to seen continuations will be postponed
+ * for some short period of time. The phase ends once the new event is registered, or after the
+ * period of time ends (in which case we declare that the sequence can’t have new continuations).
+ * Phase 3. Releasing all threads and letting the test iteration run till its end.
+ *
+ * The iterations end when all seen paths have been declared “uncontinuable”.
+ *
+ * When we register event XXX:enter, we hold all other events until we register XXX:exit.
+ */
+public class RaceConditionReproducer {
+ private static final String TAG = "RaceConditionReproducer";
+
+ private static final boolean ENTER = true;
+ private static final boolean EXIT = false;
+ private static final String ENTER_POSTFIX = "enter";
+ private static final String EXIT_POSTFIX = "exit";
+
+ private static final long SHORT_TIMEOUT_MS = 2000;
+ private static final long LONG_TIMEOUT_MS = 60000;
+ // Handler used to resume postponed events.
+ private static final Handler POSTPONED_EVENT_RESUME_HANDLER =
+ new Handler(createAndStartNewLooper("RaceConditionEventResumer"));
+
+ public static String enterExitEvt(String eventName, boolean isEnter) {
+ return eventName + ":" + (isEnter ? ENTER_POSTFIX : EXIT_POSTFIX);
+ }
+
+ public static String enterEvt(String eventName) {
+ return enterExitEvt(eventName, ENTER);
+ }
+
+ public static String exitEvt(String eventName) {
+ return enterExitEvt(eventName, EXIT);
+ }
+
+ /**
+ * Event in a particular sequence of events. A node in the prefix tree of all seen event
+ * sequences.
+ */
+ private class EventNode {
+ // Events that were seen just after this event.
+ private final Map<String, EventNode> mNextEvents = new HashMap<>();
+ // Whether we believe that further iterations will not be able to add more events to
+ // mNextEvents.
+ private boolean mStoppedAddingChildren = true;
+
+ private void debugDump(StringBuilder sb, int indent, String name) {
+ for (int i = 0; i < indent; ++i) sb.append('.');
+ sb.append(!mStoppedAddingChildren ? "+" : "-");
+ sb.append(" : ");
+ sb.append(name);
+ if (mLastRegisteredEvent == this) sb.append(" <");
+ sb.append('\n');
+
+ for (String key : mNextEvents.keySet()) {
+ mNextEvents.get(key).debugDump(sb, indent + 2, key);
+ }
+ }
+
+ /** Number of leaves in the subtree with this node as a root. */
+ private int numberOfLeafNodes() {
+ if (mNextEvents.isEmpty()) return 1;
+
+ int leaves = 0;
+ for (String event : mNextEvents.keySet()) {
+ leaves += mNextEvents.get(event).numberOfLeafNodes();
+ }
+ return leaves;
+ }
+
+ /**
+ * Whether we believe that further iterations will not be able add nodes to the subtree with
+ * this node as a root.
+ */
+ private boolean stoppedAddingChildrenToTree() {
+ if (!mStoppedAddingChildren) return false;
+
+ for (String event : mNextEvents.keySet()) {
+ if (!mNextEvents.get(event).stoppedAddingChildrenToTree()) return false;
+ }
+ return true;
+ }
+
+ /**
+ * In the subtree with this node as a root, tries finding a node where we may have a
+ * chance to add new children.
+ * If succeeds, returns true and fills 'path' with the sequence of events to that node;
+ * otherwise returns false.
+ */
+ private boolean populatePathToGrowthPoint(List<String> path) {
+ for (String event : mNextEvents.keySet()) {
+ if (mNextEvents.get(event).populatePathToGrowthPoint(path)) {
+ path.add(0, event);
+ return true;
+ }
+ }
+ if (!mStoppedAddingChildren) {
+ // Mark that we have finished adding children. It will remain true if no new
+ // children are added, or will be set to false upon adding a new child.
+ mStoppedAddingChildren = true;
+ return true;
+ }
+ return false;
+ }
+ }
+
+ // Starting point of all event sequences; the root of the prefix tree representation all
+ // sequences generated by test iterations. A test iteration can add nodes int it.
+ private EventNode mRoot = new EventNode();
+ // During a test iteration, the last event that was registered.
+ private EventNode mLastRegisteredEvent;
+ // Length of the current sequence of registered events for the current test iteration.
+ private int mRegisteredEventCount = 0;
+ // During the first part of a test iteration, we go to a specific node under mRoot by
+ // 'playing back' mSequenceToFollow. During this part, all events that don't belong to this
+ // sequence get postponed.
+ private List<String> mSequenceToFollow = new ArrayList<>();
+ // Collection of events that got postponed, with corresponding wait objects used to let them go.
+ private Map<String, Semaphore> mPostponedEvents = new HashMap<>();
+ // Callback to run by POSTPONED_EVENT_RESUME_HANDLER, used to let go of all currently
+ // postponed events.
+ private Runnable mResumeAllEventsCallback;
+ // String representation of the sequence of events registered so far for the current test
+ // iteration. After registering any event, we output it to the log. The last output before
+ // the test failure can be later played back to reliable reproduce the exact sequence of
+ // events that broke the test.
+ // Format: EV1|EV2|...\EVN
+ private StringBuilder mCurrentSequence;
+ // When not null, we are in a repro mode. We run only one test iteration, and are trying to
+ // reproduce the event sequence represented by this string. The format is same as for
+ // mCurrentSequence.
+ private final String mReproString;
+
+ /* Constructor for a normal test. */
+ public RaceConditionReproducer() {
+ mReproString = null;
+ }
+
+ /**
+ * Constructor for reliably reproducing a race condition failure. The developer should find in
+ * the log the latest "Repro sequence:" record and locally modify the test by passing that
+ * string to the constructor. Running the test will have only one iteration that will reliably
+ * "play back" that sequence.
+ */
+ public RaceConditionReproducer(String reproString) {
+ mReproString = reproString;
+ }
+
+ public RaceConditionReproducer(String... reproSequence) {
+ this(String.join("|", reproSequence));
+ }
+
+ public synchronized String getCurrentSequenceString() {
+ return mCurrentSequence.toString();
+ }
+
+ /**
+ * Starts a new test iteration. Events reported via RaceConditionTracker.onEvent before this
+ * call will be ignored.
+ */
+ public synchronized void startIteration() {
+ mLastRegisteredEvent = mRoot;
+ mRegisteredEventCount = 0;
+ mCurrentSequence = new StringBuilder();
+ Log.d(TAG, "Repro sequence: " + mCurrentSequence);
+ mSequenceToFollow = mReproString != null ?
+ parseReproString(mReproString) : generateSequenceToFollowLocked();
+ Log.e(TAG, "---- Start of iteration; state:\n" + dumpStateLocked());
+ checkIfCompletedSequenceToFollowLocked();
+
+ TraceHelperForTest.setRaceConditionReproducer(this);
+ }
+
+ /**
+ * Ends a new test iteration. Events reported via RaceConditionTracker.onEvent after this call
+ * will be ignored.
+ * Returns whether we need more iterations.
+ */
+ public synchronized boolean finishIteration() {
+ TraceHelperForTest.setRaceConditionReproducer(null);
+
+ runResumeAllEventsCallbackLocked();
+ assertTrue("Non-empty postponed events", mPostponedEvents.isEmpty());
+ assertTrue("Last registered event is :enter", lastEventAsEnter() == null);
+
+ // No events came after mLastRegisteredEvent. It doesn't make sense to come to it again
+ // because we won't see new continuations.
+ mLastRegisteredEvent.mStoppedAddingChildren = true;
+ Log.e(TAG, "---- End of iteration; state:\n" + dumpStateLocked());
+ if (mReproString != null) {
+ assertTrue("Repro mode: failed to reproduce the sequence",
+ mCurrentSequence.toString().startsWith(mReproString));
+ }
+ // If we are in a repro mode, we need only one iteration. Otherwise, continue if the tree
+ // has prospective growth points.
+ return mReproString == null && !mRoot.stoppedAddingChildrenToTree();
+ }
+
+ private static List<String> parseReproString(String reproString) {
+ return Arrays.asList(reproString.split("\\|"));
+ }
+
+ /**
+ * Called when the app issues an event.
+ */
+ public void onEvent(String event) {
+ final Semaphore waitObject = tryRegisterEvent(event);
+ if (waitObject != null) {
+ waitUntilCanRegister(event, waitObject);
+ }
+ }
+
+ /**
+ * Returns whether the last event was not an XXX:enter, or this event is a matching XXX:exit.
+ */
+ private boolean canRegisterEventNowLocked(String event) {
+ final String lastEventAsEnter = lastEventAsEnter();
+ final String thisEventAsExit = eventAsExit(event);
+
+ if (lastEventAsEnter != null) {
+ if (!lastEventAsEnter.equals(thisEventAsExit)) {
+ assertTrue("YYY:exit after XXX:enter", thisEventAsExit == null);
+ // Last event was :enter, but this event is not :exit.
+ return false;
+ }
+ } else {
+ // Previous event was not :enter.
+ assertTrue(":exit after a non-enter event", thisEventAsExit == null);
+ }
+ return true;
+ }
+
+ /**
+ * Registers an event issued by the app and returns null or decides that the event must be
+ * postponed, and returns an object to wait on.
+ */
+ private synchronized Semaphore tryRegisterEvent(String event) {
+ Log.d(TAG, "Event issued by the app: " + event);
+
+ if (!canRegisterEventNowLocked(event)) {
+ return createWaitObjectForPostponedEventLocked(event);
+ }
+
+ if (mRegisteredEventCount < mSequenceToFollow.size()) {
+ // We are in the first part of the iteration. We only register events that follow the
+ // mSequenceToFollow and postponing all other events.
+ if (event.equals(mSequenceToFollow.get(mRegisteredEventCount))) {
+ // The event is the next one expected in the sequence. Register it.
+ registerEventLocked(event);
+
+ // If there are postponed events that could continue the sequence, register them.
+ while (mRegisteredEventCount < mSequenceToFollow.size() &&
+ mPostponedEvents.containsKey(
+ mSequenceToFollow.get(mRegisteredEventCount))) {
+ registerPostponedEventLocked(mSequenceToFollow.get(mRegisteredEventCount));
+ }
+
+ // Perhaps we just completed the required sequence...
+ checkIfCompletedSequenceToFollowLocked();
+ } else {
+ // The event is not the next one in the sequence. Postpone it.
+ return createWaitObjectForPostponedEventLocked(event);
+ }
+ } else if (mRegisteredEventCount == mSequenceToFollow.size()) {
+ // The second phase of the iteration. We have just registered the whole
+ // mSequenceToFollow, and want to add previously not seen continuations for the last
+ // node in the sequence aka 'growth point'.
+ if (!mLastRegisteredEvent.mNextEvents.containsKey(event) || mReproString != null) {
+ // The event was never seen as a continuation for the current node.
+ // Or we are in repro mode, in which case we are not in business of generating
+ // new sequences after we've played back the required sequence.
+ // Register it immediately.
+ registerEventLocked(event);
+ } else {
+ // The event was seen as a continuation for the current node. Postpone it, hoping
+ // that a new event will come from other threads.
+ return createWaitObjectForPostponedEventLocked(event);
+ }
+ } else {
+ // The third phase of the iteration. We are past the growth point and register
+ // everything that comes.
+ registerEventLocked(event);
+ // Register events that may have been postponed while waiting for an :exit event
+ // during the third phase. We don't do this if just registered event is :enter.
+ if (eventAsEnter(event) == null && mRegisteredEventCount > mSequenceToFollow.size()) {
+ registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet()));
+ }
+ }
+ return null;
+ }
+
+ /** Called when there are chances that we just have registered the whole mSequenceToFollow. */
+ private void checkIfCompletedSequenceToFollowLocked() {
+ if (mRegisteredEventCount == mSequenceToFollow.size()) {
+ // We just entered the second phase of the iteration. We have just registered the
+ // whole mSequenceToFollow, and want to add previously not seen continuations for the
+ // last node in the sequence aka 'growth point'. All seen continuations will be
+ // postponed for SHORT_TIMEOUT_MS. At the end of this time period, we'll let them go.
+ scheduleResumeAllEventsLocked();
+
+ // Among the events that were postponed during the first stage, there may be an event
+ // that wasn't seen after the current. If so, register it immediately because this
+ // creates a new sequence.
+ final Set<String> keys = new HashSet<>(mPostponedEvents.keySet());
+ keys.removeAll(mLastRegisteredEvent.mNextEvents.keySet());
+ if (!keys.isEmpty()) {
+ registerPostponedEventLocked(keys.iterator().next());
+ }
+ }
+ }
+
+ private Semaphore createWaitObjectForPostponedEventLocked(String event) {
+ final Semaphore waitObject = new Semaphore(0);
+ assertTrue("Event already postponed: " + event, !mPostponedEvents.containsKey(event));
+ mPostponedEvents.put(event, waitObject);
+ return waitObject;
+ }
+
+ private void waitUntilCanRegister(String event, Semaphore waitObject) {
+ try {
+ assertTrue("Never registered event: " + event,
+ waitObject.tryAcquire(LONG_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail("Wait was interrupted");
+ }
+ }
+
+ /** Schedules resuming all postponed events after SHORT_TIMEOUT_MS */
+ private void scheduleResumeAllEventsLocked() {
+ assertTrue(mResumeAllEventsCallback == null);
+ mResumeAllEventsCallback = this::allEventsResumeCallback;
+ POSTPONED_EVENT_RESUME_HANDLER.postDelayed(mResumeAllEventsCallback, SHORT_TIMEOUT_MS);
+ }
+
+ private synchronized void allEventsResumeCallback() {
+ assertTrue("In callback, but callback is not set", mResumeAllEventsCallback != null);
+ mResumeAllEventsCallback = null;
+ registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet()));
+ }
+
+ private void registerPostponedEventsLocked(Collection<String> events) {
+ for (String event : events) {
+ registerPostponedEventLocked(event);
+ if (eventAsEnter(event) != null) {
+ // Once :enter is registered, switch to waiting for :exit to come. Won't register
+ // other postponed events.
+ break;
+ }
+ }
+ }
+
+ private void registerPostponedEventLocked(String event) {
+ mPostponedEvents.remove(event).release();
+ registerEventLocked(event);
+ }
+
+ /**
+ * If the last registered event was XXX:enter, returns XXX, otherwise, null.
+ */
+ private String lastEventAsEnter() {
+ return eventAsEnter(mCurrentSequence.substring(mCurrentSequence.lastIndexOf("|") + 1));
+ }
+
+ /**
+ * If the event is XXX:postfix, returns XXX, otherwise, null.
+ */
+ private static String prefixFromPostfixedEvent(String event, String postfix) {
+ final int columnPos = event.indexOf(':');
+ if (columnPos != -1 && postfix.equals(event.substring(columnPos + 1))) {
+ return event.substring(0, columnPos);
+ }
+ return null;
+ }
+
+ /**
+ * If the event is XXX:enter, returns XXX, otherwise, null.
+ */
+ private static String eventAsEnter(String event) {
+ return prefixFromPostfixedEvent(event, ENTER_POSTFIX);
+ }
+
+ /**
+ * If the event is XXX:exit, returns XXX, otherwise, null.
+ */
+ private static String eventAsExit(String event) {
+ return prefixFromPostfixedEvent(event, EXIT_POSTFIX);
+ }
+
+ private void registerEventLocked(String event) {
+ assertTrue(canRegisterEventNowLocked(event));
+
+ Log.d(TAG, "Actually registering event: " + event);
+ EventNode next = mLastRegisteredEvent.mNextEvents.get(event);
+ if (next == null) {
+ // This event wasn't seen after mLastRegisteredEvent.
+ next = new EventNode();
+ mLastRegisteredEvent.mNextEvents.put(event, next);
+ // The fact that we've added a new event after the previous one means that the
+ // previous event is still a growth point, unless this event is :exit, which means
+ // that the previous event is :enter.
+ mLastRegisteredEvent.mStoppedAddingChildren = eventAsExit(event) != null;
+ }
+
+ mLastRegisteredEvent = next;
+ mRegisteredEventCount++;
+
+ if (mCurrentSequence.length() > 0) mCurrentSequence.append("|");
+ mCurrentSequence.append(event);
+ Log.d(TAG, "Repro sequence: " + mCurrentSequence);
+ }
+
+ private void runResumeAllEventsCallbackLocked() {
+ if (mResumeAllEventsCallback != null) {
+ POSTPONED_EVENT_RESUME_HANDLER.removeCallbacks(mResumeAllEventsCallback);
+ mResumeAllEventsCallback.run();
+ }
+ }
+
+ private CharSequence dumpStateLocked() {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("Sequence to follow: ");
+ for (String event : mSequenceToFollow) sb.append(" " + event);
+ sb.append(".\n");
+ sb.append("Registered event count: " + mRegisteredEventCount);
+
+ sb.append("\nPostponed events: ");
+ for (String event : mPostponedEvents.keySet()) sb.append(" " + event);
+ sb.append(".");
+
+ sb.append("\nNodes: \n");
+ mRoot.debugDump(sb, 0, "");
+ return sb;
+ }
+
+ public int numberOfLeafNodes() {
+ return mRoot.numberOfLeafNodes();
+ }
+
+ private List<String> generateSequenceToFollowLocked() {
+ ArrayList<String> sequence = new ArrayList<>();
+ mRoot.populatePathToGrowthPoint(sequence);
+ return sequence;
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java b/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java
new file mode 100644
index 0000000..59f2173
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class RaceConditionReproducerTest {
+ private final static String SOME_VALID_SEQUENCE_3_3 = "B1|A1|A2|B2|A3|B3";
+
+ private static int factorial(int n) {
+ int res = 1;
+ for (int i = 2; i <= n; ++i) res *= i;
+ return res;
+ }
+
+ RaceConditionReproducer eventProcessor;
+
+ @Before
+ public void setup() {
+ eventProcessor = new RaceConditionReproducer();
+ }
+
+ @After
+ public void tearDown() {
+ TraceHelperForTest.cleanup();
+ }
+
+ private void run3_3_TestAction() throws InterruptedException {
+ Thread tb = new Thread(() -> {
+ eventProcessor.onEvent("B1");
+ eventProcessor.onEvent("B2");
+ eventProcessor.onEvent("B3");
+ });
+ tb.start();
+
+ eventProcessor.onEvent("A1");
+ eventProcessor.onEvent("A2");
+ eventProcessor.onEvent("A3");
+
+ tb.join();
+ }
+
+ @Test
+ @Ignore // The test is too long for continuous testing.
+ // 2 threads, 3 events each.
+ public void test3_3() throws Exception {
+ boolean sawTheValidSequence = false;
+
+ for (; ; ) {
+ eventProcessor.startIteration();
+ run3_3_TestAction();
+ final boolean needMoreIterations = eventProcessor.finishIteration();
+
+ sawTheValidSequence = sawTheValidSequence ||
+ SOME_VALID_SEQUENCE_3_3.equals(eventProcessor.getCurrentSequenceString());
+
+ if (!needMoreIterations) break;
+ }
+
+ assertEquals("Wrong number of leaf nodes",
+ factorial(3 + 3) / (factorial(3) * factorial(3)),
+ eventProcessor.numberOfLeafNodes());
+ assertTrue(sawTheValidSequence);
+ }
+
+ @Test
+ @Ignore // The test is too long for continuous testing.
+ // 2 threads, 3 events, including enter-exit pairs each.
+ public void test3_3_enter_exit() throws Exception {
+ boolean sawTheValidSequence = false;
+
+ for (; ; ) {
+ eventProcessor.startIteration();
+ Thread tb = new Thread(() -> {
+ eventProcessor.onEvent("B1:enter");
+ eventProcessor.onEvent("B1:exit");
+ eventProcessor.onEvent("B2");
+ eventProcessor.onEvent("B3:enter");
+ eventProcessor.onEvent("B3:exit");
+ });
+ tb.start();
+
+ eventProcessor.onEvent("A1");
+ eventProcessor.onEvent("A2:enter");
+ eventProcessor.onEvent("A2:exit");
+ eventProcessor.onEvent("A3:enter");
+ eventProcessor.onEvent("A3:exit");
+
+ tb.join();
+ final boolean needMoreIterations = eventProcessor.finishIteration();
+
+ sawTheValidSequence = sawTheValidSequence ||
+ "B1:enter|B1:exit|A1|A2:enter|A2:exit|B2|A3:enter|A3:exit|B3:enter|B3:exit".
+ equals(eventProcessor.getCurrentSequenceString());
+
+ if (!needMoreIterations) break;
+ }
+
+ assertEquals("Wrong number of leaf nodes",
+ factorial(3 + 3) / (factorial(3) * factorial(3)),
+ eventProcessor.numberOfLeafNodes());
+ assertTrue(sawTheValidSequence);
+ }
+
+ @Test
+ // 2 threads, 3 events each; reproducing a particular event sequence.
+ public void test3_3_ReproMode() throws Exception {
+ eventProcessor = new RaceConditionReproducer(SOME_VALID_SEQUENCE_3_3);
+ eventProcessor.startIteration();
+ run3_3_TestAction();
+ assertTrue(!eventProcessor.finishIteration());
+ assertEquals(SOME_VALID_SEQUENCE_3_3, eventProcessor.getCurrentSequenceString());
+
+ assertEquals("Wrong number of leaf nodes", 1, eventProcessor.numberOfLeafNodes());
+ }
+
+ @Test
+ @Ignore // The test is too long for continuous testing.
+ // 2 threads with 2 events; 1 thread with 1 event.
+ public void test2_1_2() throws Exception {
+ for (; ; ) {
+ eventProcessor.startIteration();
+ Thread tb = new Thread(() -> {
+ eventProcessor.onEvent("B1");
+ eventProcessor.onEvent("B2");
+ });
+ tb.start();
+
+ Thread tc = new Thread(() -> {
+ eventProcessor.onEvent("C1");
+ });
+ tc.start();
+
+ eventProcessor.onEvent("A1");
+ eventProcessor.onEvent("A2");
+
+ tb.join();
+ tc.join();
+
+ if (!eventProcessor.finishIteration()) break;
+ }
+
+ assertEquals("Wrong number of leaf nodes",
+ factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)),
+ eventProcessor.numberOfLeafNodes());
+ }
+
+ @Test
+ @Ignore // The test is too long for continuous testing.
+ // 2 threads with 2 events; 1 thread with 1 event. Includes enter-exit pairs.
+ public void test2_1_2_enter_exit() throws Exception {
+ for (; ; ) {
+ eventProcessor.startIteration();
+ Thread tb = new Thread(() -> {
+ eventProcessor.onEvent("B1:enter");
+ eventProcessor.onEvent("B1:exit");
+ eventProcessor.onEvent("B2:enter");
+ eventProcessor.onEvent("B2:exit");
+ });
+ tb.start();
+
+ Thread tc = new Thread(() -> {
+ eventProcessor.onEvent("C1:enter");
+ eventProcessor.onEvent("C1:exit");
+ });
+ tc.start();
+
+ eventProcessor.onEvent("A1:enter");
+ eventProcessor.onEvent("A1:exit");
+ eventProcessor.onEvent("A2:enter");
+ eventProcessor.onEvent("A2:exit");
+
+ tb.join();
+ tc.join();
+
+ if (!eventProcessor.finishIteration()) break;
+ }
+
+ assertEquals("Wrong number of leaf nodes",
+ factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)),
+ eventProcessor.numberOfLeafNodes());
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/TraceHelperForTest.java b/tests/src/com/android/launcher3/util/TraceHelperForTest.java
new file mode 100644
index 0000000..f1c8a67
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/TraceHelperForTest.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import java.util.LinkedList;
+import java.util.function.IntConsumer;
+
+public class TraceHelperForTest extends TraceHelper {
+
+ private static final TraceHelperForTest INSTANCE_FOR_TEST = new TraceHelperForTest();
+
+ private final ThreadLocal<LinkedList<TraceInfo>> mStack =
+ ThreadLocal.withInitial(LinkedList::new);
+
+ private RaceConditionReproducer mRaceConditionReproducer;
+ private IntConsumer mFlagsChangeListener;
+
+ public static void setRaceConditionReproducer(RaceConditionReproducer reproducer) {
+ TraceHelper.INSTANCE = INSTANCE_FOR_TEST;
+ INSTANCE_FOR_TEST.mRaceConditionReproducer = reproducer;
+ }
+
+ public static void cleanup() {
+ INSTANCE_FOR_TEST.mRaceConditionReproducer = null;
+ INSTANCE_FOR_TEST.mFlagsChangeListener = null;
+ }
+
+ public static void setFlagsChangeListener(IntConsumer listener) {
+ TraceHelper.INSTANCE = INSTANCE_FOR_TEST;
+ INSTANCE_FOR_TEST.mFlagsChangeListener = listener;
+ }
+
+ private TraceHelperForTest() { }
+
+ @Override
+ public Object beginSection(String sectionName, int flags) {
+ LinkedList<TraceInfo> stack = mStack.get();
+ TraceInfo info = new TraceInfo(sectionName, flags);
+ stack.add(info);
+
+ if ((flags & TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS) != 0
+ && mRaceConditionReproducer != null) {
+ mRaceConditionReproducer.onEvent(RaceConditionReproducer.enterEvt(sectionName));
+ }
+ updateBinderTracking(stack);
+
+ super.beginSection(sectionName, flags);
+ return info;
+ }
+
+ @Override
+ public void endSection(Object token) {
+ LinkedList<TraceInfo> stack = mStack.get();
+ if (stack.size() == 0) {
+ new Throwable().printStackTrace();
+ }
+ TraceInfo info = (TraceInfo) token;
+ stack.remove(info);
+ if ((info.flags & TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS) != 0
+ && mRaceConditionReproducer != null) {
+ mRaceConditionReproducer.onEvent(RaceConditionReproducer.exitEvt(info.sectionName));
+ }
+ updateBinderTracking(stack);
+
+ super.endSection(token);
+ }
+
+ @Override
+ public Object beginFlagsOverride(int flags) {
+ LinkedList<TraceInfo> stack = mStack.get();
+ TraceInfo info = new TraceInfo(null, flags);
+ stack.add(info);
+ updateBinderTracking(stack);
+ super.beginFlagsOverride(flags);
+ return info;
+ }
+
+ @Override
+ public void endFlagsOverride(Object token) {
+ super.endFlagsOverride(token);
+ LinkedList<TraceInfo> stack = mStack.get();
+ TraceInfo info = (TraceInfo) token;
+ stack.remove(info);
+ updateBinderTracking(stack);
+ }
+
+ private void updateBinderTracking(LinkedList<TraceInfo> stack) {
+ if (mFlagsChangeListener != null) {
+ mFlagsChangeListener.accept(stack.stream()
+ .mapToInt(info -> info.flags).reduce(0, (a, b) -> a | b));
+ }
+ }
+
+ private static class TraceInfo {
+ public final String sectionName;
+ public final int flags;
+
+ TraceInfo(String sectionName, int flags) {
+ this.sectionName = sectionName;
+ this.flags = flags;
+ }
+ }
+}