1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.scheduling;
18 
19 import static com.android.server.scheduling.SchedulingStatsLog.LONG_REBOOT_BLOCKING_REPORTED;
20 import static com.android.server.scheduling.SchedulingStatsLog.LONG_REBOOT_BLOCKING_REPORTED__REBOOT_BLOCK_REASON__APP_UID;
21 import static com.android.server.scheduling.SchedulingStatsLog.LONG_REBOOT_BLOCKING_REPORTED__REBOOT_BLOCK_REASON__SYSTEM_COMPONENT;
22 
23 import android.annotation.CurrentTimeMillisLong;
24 import android.annotation.IntDef;
25 import android.content.ApexEnvironment;
26 import android.content.Context;
27 import android.content.pm.PackageManager;
28 import android.os.SystemClock;
29 import android.util.ArrayMap;
30 import android.util.AtomicFile;
31 import android.util.Log;
32 import android.util.SparseArray;
33 
34 import com.android.internal.annotations.GuardedBy;
35 import com.android.internal.annotations.VisibleForTesting;
36 
37 import java.io.File;
38 import java.io.FileInputStream;
39 import java.io.FileOutputStream;
40 import java.io.PrintWriter;
41 import java.lang.annotation.Retention;
42 import java.lang.annotation.RetentionPolicy;
43 import java.util.ArrayList;
44 import java.util.List;
45 
46 
47 /**
48  * Class for handling the storage of logging information related to reboot readiness.
49  */
50 final class RebootReadinessLogger {
51     private static final String TAG = "RebootReadinessLogger";
52 
53     private static final String MODULE_NAME = "com.android.scheduling";
54 
55     private static final String REBOOT_STATS_FILE = "reboot-readiness/reboot-stats.xml";
56 
57     private final Object mLock = new Object();
58 
59     @GuardedBy("mLock")
60     private final SparseArray<BlockingEntityRecord> mBlockingApps = new SparseArray<>();
61 
62     @GuardedBy("mLock")
63     private final ArrayMap<String, BlockingEntityRecord> mBlockingComponents = new ArrayMap<>();
64 
65     @GuardedBy("mLock")
66     private long mStartTime;
67 
68     @GuardedBy("mLock")
69     private long mReadyTime;
70 
71     @GuardedBy("mLock")
72     private int mTimesBlockedByInteractivity;
73 
74     @GuardedBy("mLock")
75     private int mTimesBlockedBySubsystems;
76 
77     @GuardedBy("mLock")
78     private int mTimesBlockedByAppActivity;
79 
80     @GuardedBy("mLock")
81     private long mTimeUntilNextInteractionMs;
82 
83     @GuardedBy("mLock")
84     private boolean mNeedsToLogMetrics;
85 
86     @GuardedBy("mLock")
87     private boolean mShouldDumpMetrics;
88 
89     // Directory for storing reboot readiness metrics. By default, this is the module's DE
90     // data directory, but a different directory may be injected for testing purposes.
91     private final File mDeDir;
92 
93     private final Context mContext;
94 
95     private final PackageManager mPackageManager;
96 
97     @VisibleForTesting
RebootReadinessLogger(File deDir, Context context)98     RebootReadinessLogger(File deDir, Context context) {
99         mDeDir = deDir;
100         mContext = context;
101         mPackageManager = mContext.getPackageManager();
102     }
103 
RebootReadinessLogger(Context context)104     RebootReadinessLogger(Context context) {
105         mDeDir = ApexEnvironment.getApexEnvironment(MODULE_NAME).getDeviceProtectedDataDir();
106         mContext = context;
107         mPackageManager = mContext.getPackageManager();
108     }
109 
110     /**
111      * Persists important information about recent reboot readiness checks. This information is
112      * stored when the device first becomes reboot-ready.
113      *
114      * @param startTime the time the reboot readiness state started to be polled
115      * @param readyTime the time the device became reboot-ready
116      * @param timesBlockedByInteractivity the number of times the reboot was blocked by device
117      *                                    interactivity
118      * @param timesBlockedBySubsystems the number of times the reboot was blocked by subsystems
119      *                                 that registered callbacks
120      * @param timesBlockedByAppActivity the number of times the reboot was blocked by background
121      *                                  app activity
122      */
writeAfterRebootReadyBroadcast(@urrentTimeMillisLong long startTime, @CurrentTimeMillisLong long readyTime, int timesBlockedByInteractivity, int timesBlockedBySubsystems, int timesBlockedByAppActivity)123     void writeAfterRebootReadyBroadcast(@CurrentTimeMillisLong long startTime,
124             @CurrentTimeMillisLong long readyTime, int timesBlockedByInteractivity,
125             int timesBlockedBySubsystems, int timesBlockedByAppActivity) {
126         synchronized (mLock) {
127             AtomicFile rebootStatsFile = new AtomicFile(new File(mDeDir, REBOOT_STATS_FILE));
128 
129             mStartTime = startTime;
130             mReadyTime = readyTime;
131             mTimesBlockedByInteractivity = timesBlockedByInteractivity;
132             mTimesBlockedBySubsystems = timesBlockedBySubsystems;
133             mTimesBlockedByAppActivity = timesBlockedByAppActivity;
134             mShouldDumpMetrics = true;
135 
136             RebootStats rebootStats = new RebootStats();
137             rebootStats.setStartTimeMs(startTime);
138             rebootStats.setReadyTimeMs(readyTime);
139             rebootStats.setTimesBlockedByInteractivity(timesBlockedByInteractivity);
140             rebootStats.setTimesBlockedBySubsystems(timesBlockedBySubsystems);
141             rebootStats.setTimesBlockedByAppActivity(timesBlockedByAppActivity);
142             writeRebootStatsToFile(rebootStats, rebootStatsFile);
143         }
144     }
145 
146     /**
147      * Deletes any information that would have been logged post-reboot. This is called when
148      * the device becomes not ready to reboot.
149      */
deleteLoggingInformation()150     void deleteLoggingInformation() {
151         synchronized (mLock) {
152             AtomicFile rebootStatsFile = getRebootStatsFile();
153             if (rebootStatsFile != null) {
154                 rebootStatsFile.delete();
155                 mNeedsToLogMetrics = false;
156             }
157         }
158     }
159 
160     /**
161      * Amend logging file when the device becomes not ready to reboot. If a logging file exists,
162      * add information about the time between the device becoming ready to reboot and subsequently
163      * not ready to reboot.
164      */
writeAfterNotRebootReadyBroadcast()165     void writeAfterNotRebootReadyBroadcast() {
166         synchronized (mLock) {
167             AtomicFile rebootStatsFile = getRebootStatsFile();
168             if (rebootStatsFile != null) {
169                 RebootStats rebootStats;
170                 try (FileInputStream stream = rebootStatsFile.openRead()) {
171                     rebootStats = XmlParser.read(stream);
172                 } catch (Exception e) {
173                     Log.e(TAG, "Could not read reboot readiness stats: " + e);
174                     return;
175                 }
176 
177                 mTimeUntilNextInteractionMs = System.currentTimeMillis() - mReadyTime;
178                 rebootStats.setTimeUntilNextInteractionMs(mTimeUntilNextInteractionMs);
179                 writeRebootStatsToFile(rebootStats, rebootStatsFile);
180             }
181 
182         }
183     }
184 
185     /**
186      * If any metrics were stored before the last reboot, reads them into local variables. These
187      * local variables will be logged when the device is first unlocked after reboot.
188      */
readMetricsPostReboot()189     void readMetricsPostReboot() {
190         synchronized (mLock) {
191             AtomicFile rebootStatsFile = getRebootStatsFile();
192             if (rebootStatsFile != null) {
193                 try (FileInputStream stream = rebootStatsFile.openRead()) {
194                     RebootStats rebootStats = XmlParser.read(stream);
195                     mReadyTime = rebootStats.getReadyTimeMs();
196                     mStartTime = rebootStats.getStartTimeMs();
197                     mTimesBlockedByInteractivity = rebootStats.getTimesBlockedByInteractivity();
198                     mTimesBlockedBySubsystems = rebootStats.getTimesBlockedBySubsystems();
199                     mTimesBlockedByAppActivity = rebootStats.getTimesBlockedByAppActivity();
200                     if (rebootStats.hasTimeUntilNextInteractionMs()) {
201                         mTimeUntilNextInteractionMs = rebootStats.getTimeUntilNextInteractionMs();
202                     }
203                     mNeedsToLogMetrics = true;
204                 } catch (Exception e) {
205                     Log.e(TAG, "Could not read reboot readiness stats: " + e);
206                 } finally {
207                     rebootStatsFile.delete();
208                 }
209             }
210         }
211     }
212 
213     /**
214      * Logs metrics which have been stored across reboots, if any exist. This method will be called
215      * after the first time the device is unlocked after reboot.
216      */
writePostRebootMetrics()217     void writePostRebootMetrics() {
218         synchronized (mLock) {
219             if (mNeedsToLogMetrics) {
220                 mNeedsToLogMetrics = false;
221                 long timeToUnlockMs = SystemClock.elapsedRealtime();
222                 long timeToRebootReadyMs = mReadyTime - mStartTime;
223                 long timeToNextInteractionMs = System.currentTimeMillis() - mReadyTime;
224                 if (mTimeUntilNextInteractionMs != 0) {
225                     timeToNextInteractionMs = mTimeUntilNextInteractionMs;
226                 }
227                 Log.i(TAG, "UnattendedRebootOccurred"
228                         + " rebootReadyMs=" + timeToRebootReadyMs
229                         + " timeUntilFirstUnlockMs=" + timeToUnlockMs
230                         + " blockedByInteractivity=" + mTimesBlockedByInteractivity
231                         + " blockedBySubsystems=" + mTimesBlockedBySubsystems
232                         + " blockedByAppActivity=" + mTimesBlockedByAppActivity
233                         + " timeToNextInteractionMs=" + timeToNextInteractionMs);
234                 SchedulingStatsLog.write(SchedulingStatsLog.UNATTENDED_REBOOT_OCCURRED,
235                         timeToRebootReadyMs,
236                         timeToUnlockMs,
237                         mTimesBlockedByAppActivity,
238                         mTimesBlockedBySubsystems,
239                         mTimesBlockedByInteractivity,
240                         timeToNextInteractionMs);
241                 mShouldDumpMetrics = true;
242             }
243         }
244     }
245 
246     /**
247      * Tracks any components which are currently blocking the reboot. If any of the components have
248      * been blocking the reboot for longer than the given threshold, this information will be logged
249      * to SchedulingStatsLog. Any components which previously blocked the reboot but are currently
250      * not blocking the reboot will be pruned from the set of tracked components.
251      *
252      * @param blockingComponentNames list of component names which are blocking the reboot.
253      * @param thresholdMs the time a component may block the reboot for before being logged.
254      */
maybeLogLongBlockingComponents(List<String> blockingComponentNames, long thresholdMs)255     void maybeLogLongBlockingComponents(List<String> blockingComponentNames, long thresholdMs) {
256         synchronized (mLock) {
257             for (String component : blockingComponentNames) {
258                 BlockingEntityRecord record = mBlockingComponents.get(component);
259                 if (record == null) {
260                     record = new BlockingEntityRecord(component);
261                     mBlockingComponents.put(component, record);
262                 }
263                 record.logLongRebootBlockingIfNecessary(thresholdMs);
264             }
265 
266             for (String existingRecordName : mBlockingComponents.keySet()) {
267                 if (!blockingComponentNames.contains(existingRecordName)) {
268                     mBlockingComponents.remove(existingRecordName);
269                 }
270             }
271         }
272     }
273 
274     /**
275      * Tracks any app uids which are currently blocking the reboot. If any of the apps have been
276      * blocking the reboot for longer than the given threshold, this information will be logged
277      * to SchedulingStatsLog. Any apps which previously blocked the reboot but are currently
278      * not blocking the reboot will be pruned from the set of tracked uids.
279      *
280      * @param blockingAppUids list of app uids which are blocking the reboot.
281      * @param thresholdMs the time an app may block the reboot for before being logged.
282      *
283      * TODO(b/184165442): Use IntArray instead.
284      */
maybeLogLongBlockingApps(List<Integer> blockingAppUids, long thresholdMs)285     void maybeLogLongBlockingApps(List<Integer> blockingAppUids, long thresholdMs) {
286         synchronized (mLock) {
287             for (Integer uid : blockingAppUids) {
288                 BlockingEntityRecord record = mBlockingApps.get(uid);
289                 if (record == null) {
290                     record = new BlockingEntityRecord(uid);
291                     mBlockingApps.put(uid, record);
292                 }
293                 record.logLongRebootBlockingIfNecessary(thresholdMs);
294             }
295 
296             List<Integer> uidsToPrune = new ArrayList<>();
297             for (int i = 0; i < mBlockingApps.size(); i++) {
298                 int uid = mBlockingApps.keyAt(i);
299                 if (!blockingAppUids.contains(uid)) {
300                     uidsToPrune.add(uid);
301                 }
302             }
303 
304             for (Integer uid : uidsToPrune) {
305                 mBlockingApps.remove(uid);
306             }
307         }
308     }
309 
310     @VisibleForTesting
getRebootStatsFile()311     AtomicFile getRebootStatsFile() {
312         File file = new File(mDeDir, REBOOT_STATS_FILE);
313         if (file.exists()) {
314             return new AtomicFile(new File(mDeDir, REBOOT_STATS_FILE));
315         } else {
316             return null;
317         }
318     }
319 
320     /** Writes information about any UIDs which are blocking the reboot. */
writeBlockingUids(PrintWriter pw)321     void writeBlockingUids(PrintWriter pw) {
322         synchronized (mLock) {
323             for (int i = 0; i < mBlockingApps.size(); i++) {
324                 int appUid = mBlockingApps.valueAt(i).mAppUid;
325                 String[] pkgs = mPackageManager.getPackagesForUid(appUid);
326                 pw.println("Blocking uid: " + appUid + ", package(s): " + String.join(",", pkgs));
327             }
328         }
329     }
330 
331     /** Writes information about any subsystems which are blocking the reboot. */
writeBlockingSubsystems(PrintWriter pw)332     void writeBlockingSubsystems(PrintWriter pw) {
333         synchronized (mLock) {
334             for (String subsystem : mBlockingComponents.keySet()) {
335                 pw.println("Blocking subsystem: " + subsystem);
336             }
337         }
338     }
339 
writeRebootStatsToFile(RebootStats rebootStats, AtomicFile rebootStatsFile)340     private void writeRebootStatsToFile(RebootStats rebootStats, AtomicFile rebootStatsFile) {
341         FileOutputStream stream = null;
342         try {
343             stream = rebootStatsFile.startWrite();
344             PrintWriter pw = new PrintWriter(stream);
345             XmlWriter writer = new XmlWriter(pw);
346             XmlWriter.write(writer, rebootStats);
347             pw.flush();
348             rebootStatsFile.finishWrite(stream);
349         } catch (Exception e) {
350             Log.e(TAG, "Could not write reboot readiness stats: " + e);
351             rebootStatsFile.failWrite(stream);
352         }
353     }
354 
dump(PrintWriter pw)355     void dump(PrintWriter pw) {
356         synchronized (mLock) {
357             if (mBlockingComponents.size() > 0) {
358                 writeBlockingSubsystems(pw);
359             }
360 
361             if (mBlockingApps.size() > 0) {
362                 writeBlockingUids(pw);
363             }
364 
365             if (mShouldDumpMetrics) {
366                 pw.println("Previous reboot readiness checks:");
367                 pw.println("    Start timestamp: " + mStartTime);
368                 pw.println("    Ready timestamp: " +  mReadyTime);
369                 pw.println("    Times blocked by interactivity " + mTimesBlockedByInteractivity);
370                 pw.println("    Times blocked by subsystems " + mTimesBlockedBySubsystems);
371                 pw.println("    Times blocked by app activity " + mTimesBlockedByAppActivity);
372                 pw.println("    Time until next interaction " + mTimeUntilNextInteractionMs);
373             }
374         }
375     }
376 
377 
378     /**
379      * Class for tracking system components or app uids which are blocking the reboot. Handles
380      * the tracking of how long an entity has blocked the reboot for, and handles the logging
381      * of LongRebootBlockingReported events to SchedulingStatsLog.
382      */
383     private static final class BlockingEntityRecord {
384 
385         private final int mType;
386         private String mComponentName;
387         private int mAppUid;
388         @CurrentTimeMillisLong private long mLastMetricLoggedTime;
389 
390         @Retention(RetentionPolicy.SOURCE)
391         @IntDef({
392             ENTITY_TYPE_COMPONENT,
393             ENTITY_TYPE_APP,
394         })
395         private @interface EntityType {}
396 
397         private static final int ENTITY_TYPE_COMPONENT = 1;
398         private static final int ENTITY_TYPE_APP = 2;
399 
BlockingEntityRecord(String name)400         private BlockingEntityRecord(String name) {
401             mType = ENTITY_TYPE_COMPONENT;
402             mComponentName = name;
403             mLastMetricLoggedTime = System.currentTimeMillis();
404         }
405 
BlockingEntityRecord(int uid)406         private BlockingEntityRecord(int uid) {
407             mType = ENTITY_TYPE_APP;
408             mComponentName = "";
409             mAppUid = uid;
410             mLastMetricLoggedTime = System.currentTimeMillis();
411         }
412 
413         /**
414          * Writes to SchedulingStatsLog if this entity has been blocking the reboot for longer
415          * than the given threshold. If this entity has been previously written to
416          * SchedulingStatsLog, the threshold will be compared with the time since the previous
417          * metric was recorded.
418          */
logLongRebootBlockingIfNecessary(long thresholdMs)419         private void logLongRebootBlockingIfNecessary(long thresholdMs) {
420             final long now = System.currentTimeMillis();
421             if ((now - mLastMetricLoggedTime) > thresholdMs) {
422                 int rebootBlockReason = mapEntityTypeToRebootBlockReason(mType);
423                 SchedulingStatsLog.write(LONG_REBOOT_BLOCKING_REPORTED, rebootBlockReason,
424                         mComponentName, mAppUid);
425                 Log.i(TAG, "LongRebootBlockingReported "
426                         + " rebootBlockReason=" + rebootBlockReasonToString(rebootBlockReason)
427                         + " componentName=" + mComponentName
428                         + " appUid=" + mAppUid);
429                 mLastMetricLoggedTime = now;
430             }
431         }
432 
433         /**
434          * Returns the reboot block reason which should be logged to SchedulingStatsLog if a given
435          * EntityType is blocking the reboot for a long time.
436          */
mapEntityTypeToRebootBlockReason(@ntityType int type)437         private static int mapEntityTypeToRebootBlockReason(@EntityType int type) {
438             if (type == ENTITY_TYPE_COMPONENT) {
439                 return LONG_REBOOT_BLOCKING_REPORTED__REBOOT_BLOCK_REASON__SYSTEM_COMPONENT;
440             } else {
441                 return LONG_REBOOT_BLOCKING_REPORTED__REBOOT_BLOCK_REASON__APP_UID;
442             }
443         }
444 
445         /**
446          * Maps a reboot block reason to a readable string for logging purposes.
447          */
rebootBlockReasonToString(int rebootBlockReason)448         private static String rebootBlockReasonToString(int rebootBlockReason) {
449             switch (rebootBlockReason) {
450                 case LONG_REBOOT_BLOCKING_REPORTED__REBOOT_BLOCK_REASON__APP_UID:
451                     return "APP_UID";
452                 case LONG_REBOOT_BLOCKING_REPORTED__REBOOT_BLOCK_REASON__SYSTEM_COMPONENT:
453                     return "SYSTEM_COMPONENT";
454                 default:
455                     return "UNKNOWN";
456             }
457         }
458     }
459 }
460