Merge changes Ie8685a12,I87abde9f
* changes:
am: Add unit tests for ActivityMetricsLaunchObserver
am: Add new LaunchObserver to track app launch sequences
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLaunchObserver.java b/services/core/java/com/android/server/wm/ActivityMetricsLaunchObserver.java
new file mode 100644
index 0000000..e3133ef
--- /dev/null
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLaunchObserver.java
@@ -0,0 +1,182 @@
+/*
+ * 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.server.wm;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Intent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Observe activity manager launch sequences.
+ *
+ * The activity manager can have at most 1 concurrent launch sequences. Calls to this interface
+ * are ordered by a happens-before relation for each defined state transition (see below).
+ *
+ * When a new launch sequence is made, that sequence is in the {@code INTENT_STARTED} state which
+ * is communicated by the {@link #onIntentStarted} callback. This is a transient state.
+ *
+ * The intent can fail to launch the activity, in which case the sequence's state transitions to
+ * {@code INTENT_FAILED} via {@link #onIntentFailed}. This is a terminal state.
+ *
+ * If an activity is successfully started, the launch sequence's state will transition into
+ * {@code STARTED} via {@link #onActivityLaunched}. This is a transient state.
+ *
+ * It must then transition to either {@code CANCELLED} with {@link #onActivityLaunchCancelled}
+ * or into {@code FINISHED} with {@link #onActivityLaunchFinished}. These are terminal states.
+ *
+ * Note that the {@link ActivityRecord} provided as a parameter to some state transitions isn't
+ * necessarily the same within a single launch sequence: it is only the top-most activity at the
+ * time (if any). Trampoline activities coalesce several activity starts into a single launch
+ * sequence.
+ *
+ * Upon reaching a terminal state, it is considered that there are no active launch sequences
+ * until a subsequent transition into {@code INTENT_STARTED} initiates a new launch sequence.
+ *
+ * <pre>
+ * ┌⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯┐ ┌⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯┐ ╔══════════════════════════╗
+ * ╴╴▶ ⋮ INTENT_STARTED ⋮ ──▶ ⋮ ACTIVITY_LAUNCHED ⋮ ──▶ ║ ACTIVITY_LAUNCH_FINISHED ║
+ * └⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯┘ └⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯┘ ╚══════════════════════════╝
+ * : :
+ * : :
+ * ▼ ▼
+ * ╔════════════════╗ ╔═══════════════════════════╗
+ * ║ INTENT_FAILED ║ ║ ACTIVITY_LAUNCH_CANCELLED ║
+ * ╚════════════════╝ ╚═══════════════════════════╝
+ * </pre>
+ */
+public interface ActivityMetricsLaunchObserver {
+ /**
+ * The 'temperature' at which a launch sequence had started.
+ *
+ * The lower the temperature the more work has to be done during start-up.
+ * A 'cold' temperature means that a new process has been started and likely
+ * nothing is cached.
+ *
+ * A hot temperature means the existing activity is brought to the foreground.
+ * It may need to regenerate some objects as a result of {@code onTrimMemory}.
+ *
+ * A warm temperature is in the middle; an existing process is used, but the activity
+ * has to be created from scratch with {@code #onCreate}.
+ *
+ * @see https://developer.android.com/topic/performance/vitals/launch-time
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ TEMPERATURE_COLD,
+ TEMPERATURE_WARM,
+ TEMPERATURE_HOT
+ })
+ @interface Temperature {}
+
+ /** Cold launch sequence: a new process has started. */
+ public static final int TEMPERATURE_COLD = 1;
+ /** Warm launch sequence: process reused, but activity has to be created. */
+ public static final int TEMPERATURE_WARM = 2;
+ /** Hot launch sequence: process reused, activity brought-to-top. */
+ public static final int TEMPERATURE_HOT = 3;
+
+ /**
+ * Notifies the observer that a new launch sequence has begun as a result of a new intent.
+ *
+ * Once a launch sequence begins, the resolved activity will either subsequently start with
+ * {@link #onActivityLaunched} or abort early (for example due to a resolution error or due to
+ * a security error) with {@link #onIntentFailed}.
+ *
+ * Multiple calls to this method cannot occur without first terminating the current
+ * launch sequence.
+ */
+ public void onIntentStarted(@NonNull Intent intent);
+
+ /**
+ * Notifies the observer that the current launch sequence has failed to launch an activity.
+ *
+ * This function call terminates the current launch sequence. The next method call, if any,
+ * must be {@link #onIntentStarted}.
+ *
+ * Examples of this happening:
+ * - Failure to resolve to an activity
+ * - Calling package did not have the security permissions to call the requested activity
+ * - Resolved activity was already running and only needed to be brought to the top
+ *
+ * Multiple calls to this method cannot occur without first terminating the current
+ * launch sequence.
+ */
+ public void onIntentFailed();
+
+ /**
+ * Notifies the observer that the current launch sequence had begun starting an activity.
+ *
+ * This is an intermediate state: once an activity begins starting, the entire launch sequence
+ * will later terminate by either finishing or cancelling.
+ *
+ * The initial activity is the first activity to be started as part of a launch sequence:
+ * it is represented by {@param activity} However, it isn't
+ * necessarily the activity which will be considered as displayed when the activity
+ * finishes launching (e.g. {@code activity} in {@link #onActivityLaunchFinished}).
+ *
+ * Multiple calls to this method cannot occur without first terminating the current
+ * launch sequence.
+ */
+ public void onActivityLaunched(@NonNull ActivityRecord activity,
+ @Temperature int temperature);
+
+ /**
+ * Notifies the observer that the current launch sequence has been aborted.
+ *
+ * This function call terminates the current launch sequence. The next method call, if any,
+ * must be {@link #onIntentStarted}.
+ *
+ * This can happen for many reasons, for example the user switches away to another app
+ * prior to the launch sequence completing, or the application being killed.
+ *
+ * Multiple calls to this method cannot occur without first terminating the current
+ * launch sequence.
+ *
+ * @param abortingActivity the last activity that had the top-most window during abort
+ * (this can be {@code null} in rare situations its unknown).
+ *
+ * @apiNote The aborting activity isn't necessarily the same as the starting activity;
+ * in the case of a trampoline, multiple activities could've been started
+ * and only the latest activity is reported here.
+ */
+ public void onActivityLaunchCancelled(@Nullable ActivityRecord abortingActivity);
+
+ /**
+ * Notifies the observer that the current launch sequence has been successfully finished.
+ *
+ * This function call terminates the current launch sequence. The next method call, if any,
+ * must be {@link #onIntentStarted}.
+ *
+ * A launch sequence is considered to be successfully finished when a frame is fully
+ * drawn for the first time: the top-most activity at the time is what's reported here.
+ *
+ * @param finalActivity the top-most activity whose windows were first to fully draw
+ *
+ * Multiple calls to this method cannot occur without first terminating the current
+ * launch sequence.
+ *
+ * @apiNote The finishing activity isn't necessarily the same as the starting activity;
+ * in the case of a trampoline, multiple activities could've been started
+ * and only the latest activity that was top-most during first-frame drawn
+ * is reported here.
+ */
+ public void onActivityLaunchFinished(@NonNull ActivityRecord finalActivity);
+}
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index 8bde7dd..9b01dfd 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -80,6 +80,7 @@
import static com.android.server.am.MemoryStatUtil.readMemoryStatFromFilesystem;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_TIMEOUT;
+import android.app.WindowConfiguration.WindowingMode;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -141,15 +142,21 @@
private final Context mContext;
private final MetricsLogger mMetricsLogger = new MetricsLogger();
+ // set to INVALID_START_TIME in reset.
+ // set to valid value in notifyActivityLaunching
private long mCurrentTransitionStartTime = INVALID_START_TIME;
private long mLastTransitionStartTime = INVALID_START_TIME;
private int mCurrentTransitionDeviceUptime;
private int mCurrentTransitionDelayMs;
+
+ /** If the any app transitions have been logged as starting, after the latest reset. */
private boolean mLoggedTransitionStarting;
+ /** Map : @WindowingMode int => WindowingModeTransitionInfo */
private final SparseArray<WindowingModeTransitionInfo> mWindowingModeTransitionInfo =
new SparseArray<>();
+ /** Map : @WindowingMode int => WindowingModeTransitionInfo */
private final SparseArray<WindowingModeTransitionInfo> mLastWindowingModeTransitionInfo =
new SparseArray<>();
private final H mHandler;
@@ -157,6 +164,12 @@
private ArtManagerInternal mArtManagerInternal;
private final StringBuilder mStringBuilder = new StringBuilder();
+ /**
+ * Due to the global single concurrent launch sequence, all calls to this observer must be made
+ * in-order on the same thread to fulfill the "happens-before" guarantee in LaunchObserver.
+ */
+ private final ActivityMetricsLaunchObserver mLaunchObserver = null;
+
private final class H extends Handler {
public H(Looper looper) {
@@ -175,6 +188,7 @@
}
private final class WindowingModeTransitionInfo {
+ /** The latest activity to have been launched. */
private ActivityRecord launchedActivity;
private int startResult;
private boolean currentTransitionProcessRunning;
@@ -273,7 +287,7 @@
return;
}
- int windowingMode = stack.getWindowingMode();
+ @WindowingMode int windowingMode = stack.getWindowingMode();
if (windowingMode == WINDOWING_MODE_PINNED) {
stack = mSupervisor.findStackBehind(stack);
windowingMode = stack.getWindowingMode();
@@ -301,11 +315,19 @@
* Notifies the tracker at the earliest possible point when we are starting to launch an
* activity.
*/
- void notifyActivityLaunching() {
+ void notifyActivityLaunching(Intent intent) {
+ if (DEBUG_METRICS) {
+ Slog.i(TAG, String.format("notifyActivityLaunching: active:%b, intent:%s",
+ isAnyTransitionActive(),
+ intent));
+ }
+
if (!isAnyTransitionActive()) {
- if (DEBUG_METRICS) Slog.i(TAG, "notifyActivityLaunching");
+
mCurrentTransitionStartTime = SystemClock.uptimeMillis();
mLastTransitionStartTime = mCurrentTransitionStartTime;
+
+ launchObserverNotifyIntentStarted(intent);
}
}
@@ -350,7 +372,9 @@
+ " processRunning=" + processRunning
+ " processSwitch=" + processSwitch);
- final int windowingMode = launchedActivity != null
+ // If we are already in an existing transition, only update the activity name, but not the
+ // other attributes.
+ final @WindowingMode int windowingMode = launchedActivity != null
? launchedActivity.getWindowingMode()
: WINDOWING_MODE_UNDEFINED;
final WindowingModeTransitionInfo info = mWindowingModeTransitionInfo.get(windowingMode);
@@ -361,13 +385,15 @@
if (launchedActivity != null && launchedActivity.nowVisible) {
// Launched activity is already visible. We cannot measure windows drawn delay.
- reset(true /* abort */, info);
+ reset(true /* abort */, info, "launched activity already visible");
return;
}
if (launchedActivity != null && info != null) {
// If we are already in an existing transition, only update the activity name, but not
// the other attributes.
+
+ // Coalesce multiple (trampoline) activities from a single sequence together.
info.launchedActivity = launchedActivity;
return;
}
@@ -377,7 +403,7 @@
if ((!isLoggableResultCode(resultCode) || launchedActivity == null || !processSwitch
|| windowingMode == WINDOWING_MODE_UNDEFINED) && !otherWindowModesLaunching) {
// Failed to launch or it was not a process switch, so we don't care about the timing.
- reset(true /* abort */, info);
+ reset(true /* abort */, info, "failed to launch or not a process switch");
return;
} else if (otherWindowModesLaunching) {
// Don't log this windowing mode but continue with the other windowing modes.
@@ -386,6 +412,8 @@
if (DEBUG_METRICS) Slog.i(TAG, "notifyActivityLaunched successful");
+ // A new launch sequence [with the windowingMode] has begun.
+ // Start tracking it.
final WindowingModeTransitionInfo newInfo = new WindowingModeTransitionInfo();
newInfo.launchedActivity = launchedActivity;
newInfo.currentTransitionProcessRunning = processRunning;
@@ -394,6 +422,7 @@
mLastWindowingModeTransitionInfo.put(windowingMode, newInfo);
mCurrentTransitionDeviceUptime = (int) (SystemClock.uptimeMillis() / 1000);
startTraces(newInfo);
+ launchObserverNotifyActivityLaunched(newInfo);
}
/**
@@ -407,7 +436,8 @@
/**
* Notifies the tracker that all windows of the app have been drawn.
*/
- WindowingModeTransitionInfoSnapshot notifyWindowsDrawn(int windowingMode, long timestamp) {
+ WindowingModeTransitionInfoSnapshot notifyWindowsDrawn(@WindowingMode int windowingMode,
+ long timestamp) {
if (DEBUG_METRICS) Slog.i(TAG, "notifyWindowsDrawn windowingMode=" + windowingMode);
final WindowingModeTransitionInfo info = mWindowingModeTransitionInfo.get(windowingMode);
@@ -419,7 +449,7 @@
final WindowingModeTransitionInfoSnapshot infoSnapshot =
new WindowingModeTransitionInfoSnapshot(info);
if (allWindowsDrawn() && mLoggedTransitionStarting) {
- reset(false /* abort */, info);
+ reset(false /* abort */, info, "notifyWindowsDrawn - all windows drawn");
}
return infoSnapshot;
}
@@ -427,7 +457,7 @@
/**
* Notifies the tracker that the starting window was drawn.
*/
- void notifyStartingWindowDrawn(int windowingMode, long timestamp) {
+ void notifyStartingWindowDrawn(@WindowingMode int windowingMode, long timestamp) {
final WindowingModeTransitionInfo info = mWindowingModeTransitionInfo.get(windowingMode);
if (info == null || info.loggedStartingWindowDrawn) {
return;
@@ -444,22 +474,28 @@
*/
void notifyTransitionStarting(SparseIntArray windowingModeToReason, long timestamp) {
if (!isAnyTransitionActive() || mLoggedTransitionStarting) {
+ // Ignore calls to this made after a reset and prior to notifyActivityLaunching.
+
+ // Ignore any subsequent notifyTransitionStarting until the next reset.
return;
}
if (DEBUG_METRICS) Slog.i(TAG, "notifyTransitionStarting");
mCurrentTransitionDelayMs = calculateDelay(timestamp);
mLoggedTransitionStarting = true;
+
+ WindowingModeTransitionInfo foundInfo = null;
for (int index = windowingModeToReason.size() - 1; index >= 0; index--) {
- final int windowingMode = windowingModeToReason.keyAt(index);
+ final @WindowingMode int windowingMode = windowingModeToReason.keyAt(index);
final WindowingModeTransitionInfo info = mWindowingModeTransitionInfo.get(
windowingMode);
if (info == null) {
continue;
}
info.reason = windowingModeToReason.valueAt(index);
+ foundInfo = info;
}
if (allWindowsDrawn()) {
- reset(false /* abort */, null /* WindowingModeTransitionInfo */);
+ reset(false /* abort */, foundInfo, "notifyTransitionStarting - all windows drawn");
}
}
@@ -498,7 +534,7 @@
logAppTransitionCancel(info);
mWindowingModeTransitionInfo.remove(r.getWindowingMode());
if (mWindowingModeTransitionInfo.size() == 0) {
- reset(true /* abort */, info);
+ reset(true /* abort */, info, "notifyVisibilityChanged to invisible");
}
}
}
@@ -534,12 +570,25 @@
&& mWindowingModeTransitionInfo.size() > 0;
}
- private void reset(boolean abort, WindowingModeTransitionInfo info) {
- if (DEBUG_METRICS) Slog.i(TAG, "reset abort=" + abort);
+ private void reset(boolean abort, WindowingModeTransitionInfo info, String cause) {
+ if (DEBUG_METRICS) Slog.i(TAG, "reset abort=" + abort + ",cause=" + cause);
if (!abort && isAnyTransitionActive()) {
logAppTransitionMultiEvents();
}
stopLaunchTrace(info);
+
+ // Ignore reset-after reset.
+ if (isAnyTransitionActive()) {
+ // LaunchObserver callbacks.
+ if (abort) {
+ launchObserverNotifyActivityLaunchCancelled(info);
+ } else {
+ launchObserverNotifyActivityLaunchFinished(info);
+ }
+ } else {
+ launchObserverNotifyIntentFailed();
+ }
+
mCurrentTransitionStartTime = INVALID_START_TIME;
mCurrentTransitionDelayMs = INVALID_DELAY;
mLoggedTransitionStarting = false;
@@ -572,6 +621,13 @@
info.launchedActivity.packageName,
convertAppStartTransitionType(type),
info.launchedActivity.info.name);
+ if (DEBUG_METRICS) {
+ Slog.i(TAG, String.format("APP_START_CANCELED(%s, %s, %s, %s)",
+ info.launchedActivity.appInfo.uid,
+ info.launchedActivity.packageName,
+ convertAppStartTransitionType(type),
+ info.launchedActivity.info.name));
+ }
}
private void logAppTransitionMultiEvents() {
@@ -656,6 +712,17 @@
launchToken,
packageOptimizationInfo.getCompilationReason(),
packageOptimizationInfo.getCompilationFilter());
+
+ if (DEBUG_METRICS) {
+ Slog.i(TAG, String.format("APP_START_OCCURRED(%s, %s, %s, %s, %s)",
+ info.applicationInfo.uid,
+ info.packageName,
+ convertAppStartTransitionType(info.type),
+ info.launchedActivityName,
+ info.launchedActivityLaunchedFromPackage));
+ }
+
+
logAppStartMemoryStateCapture(info);
}
@@ -923,4 +990,76 @@
info.launchTraceActive = false;
}
}
+
+ /** Notify the {@link ActivityMetricsLaunchObserver} that a new launch sequence has begun. */
+ private void launchObserverNotifyIntentStarted(Intent intent) {
+ if (mLaunchObserver != null) {
+ // Beginning a launch is timing sensitive and so should be observed as soon as possible.
+ mLaunchObserver.onIntentStarted(intent);
+ }
+ }
+
+ /**
+ * Notify the {@link ActivityMetricsLaunchObserver} that the previous launch sequence has
+ * aborted due to intent failure (e.g. intent resolve failed or security error, etc) or
+ * intent being delivered to the top running activity.
+ */
+ private void launchObserverNotifyIntentFailed() {
+ if (mLaunchObserver != null) {
+ mLaunchObserver.onIntentFailed();
+ }
+ }
+
+ /**
+ * Notify the {@link ActivityMetricsLaunchObserver} that the current launch sequence's activity
+ * has started.
+ */
+ private void launchObserverNotifyActivityLaunched(WindowingModeTransitionInfo info) {
+ @ActivityMetricsLaunchObserver.Temperature int temperature =
+ convertTransitionTypeToLaunchObserverTemperature(getTransitionType(info));
+
+ if (mLaunchObserver != null) {
+ // Beginning a launch is timing sensitive and so should be observed as soon as possible.
+ mLaunchObserver.onActivityLaunched(info.launchedActivity,
+ temperature);
+ }
+ }
+
+ /**
+ * Notify the {@link ActivityMetricsLaunchObserver} that the current launch sequence is
+ * cancelled.
+ */
+ private void launchObserverNotifyActivityLaunchCancelled(WindowingModeTransitionInfo info) {
+ final ActivityRecord launchedActivity = info != null ? info.launchedActivity : null;
+
+ if (mLaunchObserver != null) {
+ mLaunchObserver.onActivityLaunchCancelled(launchedActivity);
+ }
+ }
+
+ /**
+ * Notify the {@link ActivityMetricsLaunchObserver} that the current launch sequence's activity
+ * has fully finished (successfully).
+ */
+ private void launchObserverNotifyActivityLaunchFinished(WindowingModeTransitionInfo info) {
+ final ActivityRecord launchedActivity = info.launchedActivity;
+
+ if (mLaunchObserver != null) {
+ mLaunchObserver.onActivityLaunchFinished(launchedActivity);
+ }
+ }
+
+ private static @ActivityMetricsLaunchObserver.Temperature int
+ convertTransitionTypeToLaunchObserverTemperature(int transitionType) {
+ switch (transitionType) {
+ case TYPE_TRANSITION_WARM_LAUNCH:
+ return ActivityMetricsLaunchObserver.TEMPERATURE_WARM;
+ case TYPE_TRANSITION_HOT_LAUNCH:
+ return ActivityMetricsLaunchObserver.TEMPERATURE_HOT;
+ case TYPE_TRANSITION_COLD_LAUNCH:
+ return ActivityMetricsLaunchObserver.TEMPERATURE_COLD;
+ default:
+ return -1;
+ }
+ }
}
diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
index 77b331e..de203c9 100644
--- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
@@ -4745,7 +4745,7 @@
final ActivityRecord targetActivity = task.getTopActivity();
sendPowerHintForLaunchStartIfNeeded(true /* forceSend */, targetActivity);
- mActivityMetricsLogger.notifyActivityLaunching();
+ mActivityMetricsLogger.notifyActivityLaunching(task.intent);
try {
mService.moveTaskToFrontLocked(task.taskId, 0, options,
true /* fromRecents */);
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index e43a79a..afc946b 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -996,7 +996,7 @@
if (intent != null && intent.hasFileDescriptors()) {
throw new IllegalArgumentException("File descriptors passed in Intent");
}
- mSupervisor.getActivityMetricsLogger().notifyActivityLaunching();
+ mSupervisor.getActivityMetricsLogger().notifyActivityLaunching(intent);
boolean componentSpecified = intent.getComponent() != null;
final int realCallingPid = Binder.getCallingPid();
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index 7a1ebf2..9aeb025 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -122,7 +122,7 @@
targetActivity);
}
- mStackSupervisor.getActivityMetricsLogger().notifyActivityLaunching();
+ mStackSupervisor.getActivityMetricsLogger().notifyActivityLaunching(intent);
mService.mAmInternal.setRunningRemoteAnimation(mCallingPid, true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
new file mode 100644
index 0000000..215c51d
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
@@ -0,0 +1,190 @@
+/*
+ * 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.server.wm;
+
+import static android.app.ActivityManager.START_SUCCESS;
+import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.content.Intent;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseIntArray;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the {@link ActivityMetricsLaunchObserver} class.
+ *
+ * Build/Install/Run:
+ * atest WmTests:ActivityMetricsLaunchObserverTests
+ */
+@SmallTest
+@Presubmit
+@FlakyTest(detail="promote once confirmed non-flaky")
+public class ActivityMetricsLaunchObserverTests extends ActivityTestsBase {
+ private ActivityMetricsLogger mActivityMetricsLogger;
+ private ActivityMetricsLaunchObserver mLaunchObserver;
+
+ private TestActivityStack mStack;
+ private TaskRecord mTask;
+ private ActivityRecord mActivityRecord;
+ private ActivityRecord mActivityRecordTrampoline;
+
+ @Before
+ public void setUpAMLO() throws Exception {
+ setupActivityTaskManagerService();
+
+ mActivityMetricsLogger =
+ new ActivityMetricsLogger(mSupervisor, mService.mContext, mService.mH.getLooper());
+
+ mLaunchObserver = mock(ActivityMetricsLaunchObserver.class);
+
+ // TODO: Use ActivityMetricsLaunchObserverRegistry .
+ java.lang.reflect.Field f =
+ mActivityMetricsLogger.getClass().getDeclaredField("mLaunchObserver");
+ f.setAccessible(true);
+ f.set(mActivityMetricsLogger, mLaunchObserver);
+
+ // Sometimes we need an ActivityRecord for ActivityMetricsLogger to do anything useful.
+ // This seems to be the easiest way to create an ActivityRecord.
+ mStack = mSupervisor.getDefaultDisplay().createStack(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+ mTask = new TaskBuilder(mSupervisor).setStack(mStack).build();
+ mActivityRecord = new ActivityBuilder(mService).setTask(mTask).build();
+ mActivityRecordTrampoline = new ActivityBuilder(mService).setTask(mTask).build();
+ }
+
+ @Test
+ public void testOnIntentStarted() throws Exception {
+ Intent intent = new Intent("action 1");
+
+ mActivityMetricsLogger.notifyActivityLaunching(intent);
+
+ verify(mLaunchObserver).onIntentStarted(eq(intent));
+ verifyNoMoreInteractions(mLaunchObserver);
+ }
+
+ @Test
+ public void testOnIntentFailed() throws Exception {
+ testOnIntentStarted();
+
+ ActivityRecord activityRecord = null;
+
+ // Bringing an intent that's already running 'to front' is not considered
+ // as an ACTIVITY_LAUNCHED state transition.
+ mActivityMetricsLogger.notifyActivityLaunched(START_TASK_TO_FRONT,
+ activityRecord);
+
+ verify(mLaunchObserver).onIntentFailed();
+ verifyNoMoreInteractions(mLaunchObserver);
+ }
+
+ @Test
+ public void testOnActivityLaunched() throws Exception {
+ testOnIntentStarted();
+
+ mActivityMetricsLogger.notifyActivityLaunched(START_SUCCESS,
+ mActivityRecord);
+
+ verify(mLaunchObserver).onActivityLaunched(eq(mActivityRecord), anyInt());
+ verifyNoMoreInteractions(mLaunchObserver);
+ }
+
+ @Test
+ public void testOnActivityLaunchFinished() throws Exception {
+ testOnActivityLaunched();
+
+ mActivityMetricsLogger.notifyTransitionStarting(new SparseIntArray(),
+ SystemClock.uptimeMillis());
+
+ mActivityMetricsLogger.notifyWindowsDrawn(mActivityRecord.getWindowingMode(),
+ SystemClock.uptimeMillis());
+
+ verify(mLaunchObserver).onActivityLaunchFinished(eq(mActivityRecord));
+ verifyNoMoreInteractions(mLaunchObserver);
+ }
+
+ @Test
+ public void testOnActivityLaunchCancelled() throws Exception {
+ testOnActivityLaunched();
+
+ mActivityRecord.nowVisible = true;
+
+ // Cannot time already-visible activities.
+ mActivityMetricsLogger.notifyActivityLaunched(START_TASK_TO_FRONT, mActivityRecord);
+
+ verify(mLaunchObserver).onActivityLaunchCancelled(eq(mActivityRecord));
+ verifyNoMoreInteractions(mLaunchObserver);
+ }
+
+ @Test
+ public void testOnActivityLaunchedTrampoline() throws Exception {
+ testOnIntentStarted();
+
+ mActivityMetricsLogger.notifyActivityLaunched(START_SUCCESS,
+ mActivityRecord);
+
+ verify(mLaunchObserver).onActivityLaunched(eq(mActivityRecord), anyInt());
+
+ // A second, distinct, activity launch is coalesced into the the current app launch sequence
+ mActivityMetricsLogger.notifyActivityLaunched(START_SUCCESS,
+ mActivityRecordTrampoline);
+
+ verifyNoMoreInteractions(mLaunchObserver);
+ }
+
+ @Test
+ public void testOnActivityLaunchFinishedTrampoline() throws Exception {
+ testOnActivityLaunchedTrampoline();
+
+ mActivityMetricsLogger.notifyTransitionStarting(new SparseIntArray(),
+ SystemClock.uptimeMillis());
+
+ mActivityMetricsLogger.notifyWindowsDrawn(mActivityRecordTrampoline.getWindowingMode(),
+ SystemClock.uptimeMillis());
+
+ verify(mLaunchObserver).onActivityLaunchFinished(eq(mActivityRecordTrampoline));
+ verifyNoMoreInteractions(mLaunchObserver);
+ }
+
+ @Test
+ public void testOnActivityLaunchCancelledTrampoline() throws Exception {
+ testOnActivityLaunchedTrampoline();
+
+ mActivityRecordTrampoline.nowVisible = true;
+
+ // Cannot time already-visible activities.
+ mActivityMetricsLogger.notifyActivityLaunched(START_TASK_TO_FRONT,
+ mActivityRecordTrampoline);
+
+ verify(mLaunchObserver).onActivityLaunchCancelled(eq(mActivityRecordTrampoline));
+ verifyNoMoreInteractions(mLaunchObserver);
+ }
+}