1package com.android.server.am;
2
3import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
4import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
5import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
6import static android.app.ActivityManager.StackId.HOME_STACK_ID;
7import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
8import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
9import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
10import static com.android.server.am.ActivityStack.STACK_INVISIBLE;
11
12import android.annotation.Nullable;
13import android.app.ActivityManager.StackId;
14import android.content.Context;
15import android.os.SystemClock;
16import android.util.Slog;
17
18import com.android.internal.logging.MetricsLogger;
19import com.android.internal.logging.MetricsProto.MetricsEvent;
20
21import java.util.ArrayList;
22
23/**
24 * Handles logging into Tron.
25 */
26class ActivityMetricsLogger {
27
28    private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityMetricsLogger" : TAG_AM;
29
30    // Window modes we are interested in logging. If we ever introduce a new type, we need to add
31    // a value here and increase the {@link #TRON_WINDOW_STATE_VARZ_STRINGS} array.
32    private static final int WINDOW_STATE_STANDARD = 0;
33    private static final int WINDOW_STATE_SIDE_BY_SIDE = 1;
34    private static final int WINDOW_STATE_FREEFORM = 2;
35    private static final int WINDOW_STATE_INVALID = -1;
36
37    private static final long INVALID_START_TIME = -1;
38
39    // Preallocated strings we are sending to tron, so we don't have to allocate a new one every
40    // time we log.
41    private static final String[] TRON_WINDOW_STATE_VARZ_STRINGS = {
42            "window_time_0", "window_time_1", "window_time_2"};
43
44    private int mWindowState = WINDOW_STATE_STANDARD;
45    private long mLastLogTimeSecs;
46    private final ActivityStackSupervisor mSupervisor;
47    private final Context mContext;
48
49    private long mCurrentTransitionStartTime = INVALID_START_TIME;
50    private boolean mLoggedWindowsDrawn;
51    private boolean mLoggedStartingWindowDrawn;
52    private boolean mLoggedTransitionStarting;
53
54    ActivityMetricsLogger(ActivityStackSupervisor supervisor, Context context) {
55        mLastLogTimeSecs = SystemClock.elapsedRealtime() / 1000;
56        mSupervisor = supervisor;
57        mContext = context;
58    }
59
60    void logWindowState() {
61        final long now = SystemClock.elapsedRealtime() / 1000;
62        if (mWindowState != WINDOW_STATE_INVALID) {
63            // We log even if the window state hasn't changed, because the user might remain in
64            // home/fullscreen move forever and we would like to track this kind of behavior
65            // too.
66            MetricsLogger.count(mContext, TRON_WINDOW_STATE_VARZ_STRINGS[mWindowState],
67                    (int) (now - mLastLogTimeSecs));
68        }
69        mLastLogTimeSecs = now;
70
71        ActivityStack stack = mSupervisor.getStack(DOCKED_STACK_ID);
72        if (stack != null && stack.getStackVisibilityLocked(null) != STACK_INVISIBLE) {
73            mWindowState = WINDOW_STATE_SIDE_BY_SIDE;
74            return;
75        }
76        mWindowState = WINDOW_STATE_INVALID;
77        stack = mSupervisor.getFocusedStack();
78        if (stack.mStackId == PINNED_STACK_ID) {
79            stack = mSupervisor.findStackBehind(stack);
80        }
81        if (stack.mStackId == HOME_STACK_ID
82                || stack.mStackId == FULLSCREEN_WORKSPACE_STACK_ID) {
83            mWindowState = WINDOW_STATE_STANDARD;
84        } else if (stack.mStackId == DOCKED_STACK_ID) {
85            Slog.wtf(TAG, "Docked stack shouldn't be the focused stack, because it reported not"
86                    + " being visible.");
87            mWindowState = WINDOW_STATE_INVALID;
88        } else if (stack.mStackId == FREEFORM_WORKSPACE_STACK_ID) {
89            mWindowState = WINDOW_STATE_FREEFORM;
90        } else if (StackId.isStaticStack(stack.mStackId)) {
91            throw new IllegalStateException("Unknown stack=" + stack);
92        }
93    }
94
95    /**
96     * Notifies the tracker at the earliest possible point when we are starting to launch an
97     * activity.
98     */
99    void notifyActivityLaunching() {
100        mCurrentTransitionStartTime = System.currentTimeMillis();
101    }
102
103    /**
104     * Notifies the tracker that the activity is actually launching.
105     *
106     * @param resultCode one of the ActivityManager.START_* flags, indicating the result of the
107     *                   launch
108     * @param launchedActivity the activity that is being launched
109     */
110    void notifyActivityLaunched(int resultCode, ActivityRecord launchedActivity) {
111        final ProcessRecord processRecord = launchedActivity != null
112                ? mSupervisor.mService.mProcessNames.get(launchedActivity.processName,
113                        launchedActivity.appInfo.uid)
114                : null;
115        final boolean processRunning = processRecord != null;
116        final String componentName = launchedActivity != null
117                ? launchedActivity.shortComponentName
118                : null;
119
120        // We consider this a "process switch" if the process of the activity that gets launched
121        // didn't have an activity that was in started state. In this case, we assume that lot
122        // of caches might be purged so the time until it produces the first frame is very
123        // interesting.
124        final boolean processSwitch = processRecord == null
125                || !hasStartedActivity(processRecord, launchedActivity);
126
127        notifyActivityLaunched(resultCode, componentName, processRunning, processSwitch);
128    }
129
130    private boolean hasStartedActivity(ProcessRecord record, ActivityRecord launchedActivity) {
131        final ArrayList<ActivityRecord> activities = record.activities;
132        for (int i = activities.size() - 1; i >= 0; i--) {
133            final ActivityRecord activity = activities.get(i);
134            if (launchedActivity == activity) {
135                continue;
136            }
137            if (!activity.stopped) {
138                return true;
139            }
140        }
141        return false;
142    }
143
144    /**
145     * Notifies the tracker the the activity is actually launching.
146     *
147     * @param resultCode one of the ActivityManager.START_* flags, indicating the result of the
148     *                   launch
149     * @param componentName the component name of the activity being launched
150     * @param processRunning whether the process that will contains the activity is already running
151     * @param processSwitch whether the process that will contain the activity didn't have any
152     *                      activity that was stopped, i.e. the started activity is "switching"
153     *                      processes
154     */
155    private void notifyActivityLaunched(int resultCode, @Nullable String componentName,
156            boolean processRunning, boolean processSwitch) {
157
158        if (resultCode < 0 || componentName == null || !processSwitch) {
159
160            // Failed to launch or it was not a process switch, so we don't care about the timing.
161            reset();
162            return;
163        }
164
165        MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_COMPONENT_NAME,
166                componentName);
167        MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_PROCESS_RUNNING,
168                processRunning);
169        MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_DEVICE_UPTIME_SECONDS,
170                (int) (SystemClock.uptimeMillis() / 1000));
171    }
172
173    /**
174     * Notifies the tracker that all windows of the app have been drawn.
175     */
176    void notifyWindowsDrawn() {
177        if (!isTransitionActive() || mLoggedWindowsDrawn) {
178            return;
179        }
180        MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_WINDOWS_DRAWN_DELAY_MS,
181                calculateCurrentDelay());
182        mLoggedWindowsDrawn = true;
183        if (mLoggedTransitionStarting) {
184            reset();
185        }
186    }
187
188    /**
189     * Notifies the tracker that the starting window was drawn.
190     */
191    void notifyStartingWindowDrawn() {
192        if (!isTransitionActive() || mLoggedStartingWindowDrawn) {
193            return;
194        }
195        mLoggedStartingWindowDrawn = true;
196        MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_STARTING_WINDOW_DELAY_MS,
197                calculateCurrentDelay());
198    }
199
200    /**
201     * Notifies the tracker that the app transition is starting.
202     *
203     * @param reason The reason why we started it. Must be on of
204     *               ActivityManagerInternal.APP_TRANSITION_* reasons.
205     */
206    void notifyTransitionStarting(int reason) {
207        if (!isTransitionActive() || mLoggedTransitionStarting) {
208            return;
209        }
210        MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_REASON, reason);
211        MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_DELAY_MS,
212                calculateCurrentDelay());
213        mLoggedTransitionStarting = true;
214        if (mLoggedWindowsDrawn) {
215            reset();
216        }
217    }
218
219    private boolean isTransitionActive() {
220        return mCurrentTransitionStartTime != INVALID_START_TIME;
221    }
222
223    private void reset() {
224        mCurrentTransitionStartTime = INVALID_START_TIME;
225        mLoggedWindowsDrawn = false;
226        mLoggedTransitionStarting = false;
227        mLoggedStartingWindowDrawn = false;
228    }
229
230    private int calculateCurrentDelay() {
231
232        // Shouldn't take more than 25 days to launch an app, so int is fine here.
233        return (int) (System.currentTimeMillis() - mCurrentTransitionStartTime);
234    }
235}
236