AppIdleHistory.java revision a93542f9d341897f3206f775fd5720663b17504f
1/**
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16
17package com.android.server.usage;
18
19import android.os.Environment;
20import android.os.SystemClock;
21import android.os.UserHandle;
22import android.util.ArrayMap;
23import android.util.AtomicFile;
24import android.util.Slog;
25import android.util.SparseArray;
26import android.util.TimeUtils;
27import android.util.Xml;
28
29import com.android.internal.annotations.VisibleForTesting;
30import com.android.internal.util.FastXmlSerializer;
31import com.android.internal.util.IndentingPrintWriter;
32
33import libcore.io.IoUtils;
34
35import org.xmlpull.v1.XmlPullParser;
36import org.xmlpull.v1.XmlPullParserException;
37
38import java.io.BufferedOutputStream;
39import java.io.BufferedReader;
40import java.io.File;
41import java.io.FileInputStream;
42import java.io.FileOutputStream;
43import java.io.FileReader;
44import java.io.IOException;
45import java.nio.charset.StandardCharsets;
46
47/**
48 * Keeps track of recent active state changes in apps.
49 * Access should be guarded by a lock by the caller.
50 */
51public class AppIdleHistory {
52
53    private static final String TAG = "AppIdleHistory";
54
55    // History for all users and all packages
56    private SparseArray<ArrayMap<String,PackageHistory>> mIdleHistory = new SparseArray<>();
57    private long mLastPeriod = 0;
58    private static final long ONE_MINUTE = 60 * 1000;
59    private static final int HISTORY_SIZE = 100;
60    private static final int FLAG_LAST_STATE = 2;
61    private static final int FLAG_PARTIAL_ACTIVE = 1;
62    private static final long PERIOD_DURATION = UsageStatsService.COMPRESS_TIME ? ONE_MINUTE
63            : 60 * ONE_MINUTE;
64
65    @VisibleForTesting
66    static final String APP_IDLE_FILENAME = "app_idle_stats.xml";
67    private static final String TAG_PACKAGES = "packages";
68    private static final String TAG_PACKAGE = "package";
69    private static final String ATTR_NAME = "name";
70    // Screen on timebase time when app was last used
71    private static final String ATTR_SCREEN_IDLE = "screenIdleTime";
72    // Elapsed timebase time when app was last used
73    private static final String ATTR_ELAPSED_IDLE = "elapsedIdleTime";
74
75    // device on time = mElapsedDuration + (timeNow - mElapsedSnapshot)
76    private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration
77    private long mElapsedDuration; // Total device on duration since device was "born"
78
79    // screen on time = mScreenOnDuration + (timeNow - mScreenOnSnapshot)
80    private long mScreenOnSnapshot; // Elapsed time snapshot when last write of mScreenOnDuration
81    private long mScreenOnDuration; // Total screen on duration since device was "born"
82
83    private long mElapsedTimeThreshold;
84    private long mScreenOnTimeThreshold;
85    private final File mStorageDir;
86
87    private boolean mScreenOn;
88
89    private static class PackageHistory {
90        final byte[] recent = new byte[HISTORY_SIZE];
91        long lastUsedElapsedTime;
92        long lastUsedScreenTime;
93    }
94
95    AppIdleHistory(long elapsedRealtime) {
96        this(Environment.getDataSystemDirectory(), elapsedRealtime);
97    }
98
99    @VisibleForTesting
100    AppIdleHistory(File storageDir, long elapsedRealtime) {
101        mElapsedSnapshot = elapsedRealtime;
102        mScreenOnSnapshot = elapsedRealtime;
103        mStorageDir = storageDir;
104        readScreenOnTimeLocked();
105    }
106
107    public void setThresholds(long elapsedTimeThreshold, long screenOnTimeThreshold) {
108        mElapsedTimeThreshold = elapsedTimeThreshold;
109        mScreenOnTimeThreshold = screenOnTimeThreshold;
110    }
111
112    public void updateDisplayLocked(boolean screenOn, long elapsedRealtime) {
113        if (screenOn == mScreenOn) return;
114
115        mScreenOn = screenOn;
116        if (mScreenOn) {
117            mScreenOnSnapshot = elapsedRealtime;
118        } else {
119            mScreenOnDuration += elapsedRealtime - mScreenOnSnapshot;
120            mElapsedDuration += elapsedRealtime - mElapsedSnapshot;
121            writeScreenOnTimeLocked();
122            mElapsedSnapshot = elapsedRealtime;
123        }
124    }
125
126    public long getScreenOnTimeLocked(long elapsedRealtime) {
127        long screenOnTime = mScreenOnDuration;
128        if (mScreenOn) {
129            screenOnTime += elapsedRealtime - mScreenOnSnapshot;
130        }
131        return screenOnTime;
132    }
133
134    @VisibleForTesting
135    File getScreenOnTimeFile() {
136        return new File(mStorageDir, "screen_on_time");
137    }
138
139    private void readScreenOnTimeLocked() {
140        File screenOnTimeFile = getScreenOnTimeFile();
141        if (screenOnTimeFile.exists()) {
142            try {
143                BufferedReader reader = new BufferedReader(new FileReader(screenOnTimeFile));
144                mScreenOnDuration = Long.parseLong(reader.readLine());
145                mElapsedDuration = Long.parseLong(reader.readLine());
146                reader.close();
147            } catch (IOException | NumberFormatException e) {
148            }
149        } else {
150            writeScreenOnTimeLocked();
151        }
152    }
153
154    private void writeScreenOnTimeLocked() {
155        AtomicFile screenOnTimeFile = new AtomicFile(getScreenOnTimeFile());
156        FileOutputStream fos = null;
157        try {
158            fos = screenOnTimeFile.startWrite();
159            fos.write((Long.toString(mScreenOnDuration) + "\n"
160                    + Long.toString(mElapsedDuration) + "\n").getBytes());
161            screenOnTimeFile.finishWrite(fos);
162        } catch (IOException ioe) {
163            screenOnTimeFile.failWrite(fos);
164        }
165    }
166
167    /**
168     * To be called periodically to keep track of elapsed time when app idle times are written
169     */
170    public void writeElapsedTimeLocked() {
171        final long elapsedRealtime = SystemClock.elapsedRealtime();
172        // Only bump up and snapshot the elapsed time. Don't change screen on duration.
173        mElapsedDuration += elapsedRealtime - mElapsedSnapshot;
174        mElapsedSnapshot = elapsedRealtime;
175        writeScreenOnTimeLocked();
176    }
177
178    public void reportUsageLocked(String packageName, int userId, long elapsedRealtime) {
179        ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId);
180        PackageHistory packageHistory = getPackageHistoryLocked(userHistory, packageName,
181                elapsedRealtime);
182
183        shiftHistoryToNow(userHistory, elapsedRealtime);
184
185        packageHistory.lastUsedElapsedTime = mElapsedDuration
186                + (elapsedRealtime - mElapsedSnapshot);
187        packageHistory.lastUsedScreenTime = getScreenOnTimeLocked(elapsedRealtime);
188        packageHistory.recent[HISTORY_SIZE - 1] = FLAG_LAST_STATE | FLAG_PARTIAL_ACTIVE;
189    }
190
191    public void setIdle(String packageName, int userId, long elapsedRealtime) {
192        ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId);
193        PackageHistory packageHistory = getPackageHistoryLocked(userHistory, packageName,
194                elapsedRealtime);
195
196        shiftHistoryToNow(userHistory, elapsedRealtime);
197
198        packageHistory.recent[HISTORY_SIZE - 1] &= ~FLAG_LAST_STATE;
199    }
200
201    private void shiftHistoryToNow(ArrayMap<String, PackageHistory> userHistory,
202            long elapsedRealtime) {
203        long thisPeriod = elapsedRealtime / PERIOD_DURATION;
204        // Has the period switched over? Slide all users' package histories
205        if (mLastPeriod != 0 && mLastPeriod < thisPeriod
206                && (thisPeriod - mLastPeriod) < HISTORY_SIZE - 1) {
207            int diff = (int) (thisPeriod - mLastPeriod);
208            final int NUSERS = mIdleHistory.size();
209            for (int u = 0; u < NUSERS; u++) {
210                userHistory = mIdleHistory.valueAt(u);
211                for (PackageHistory idleState : userHistory.values()) {
212                    // Shift left
213                    System.arraycopy(idleState.recent, diff, idleState.recent, 0,
214                            HISTORY_SIZE - diff);
215                    // Replicate last state across the diff
216                    for (int i = 0; i < diff; i++) {
217                        idleState.recent[HISTORY_SIZE - i - 1] =
218                            (byte) (idleState.recent[HISTORY_SIZE - diff - 1] & FLAG_LAST_STATE);
219                    }
220                }
221            }
222        }
223        mLastPeriod = thisPeriod;
224    }
225
226    private ArrayMap<String, PackageHistory> getUserHistoryLocked(int userId) {
227        ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId);
228        if (userHistory == null) {
229            userHistory = new ArrayMap<>();
230            mIdleHistory.put(userId, userHistory);
231            readAppIdleTimesLocked(userId, userHistory);
232        }
233        return userHistory;
234    }
235
236    private PackageHistory getPackageHistoryLocked(ArrayMap<String, PackageHistory> userHistory,
237            String packageName, long elapsedRealtime) {
238        PackageHistory packageHistory = userHistory.get(packageName);
239        if (packageHistory == null) {
240            packageHistory = new PackageHistory();
241            packageHistory.lastUsedElapsedTime = getElapsedTimeLocked(elapsedRealtime);
242            packageHistory.lastUsedScreenTime = getScreenOnTimeLocked(elapsedRealtime);
243            userHistory.put(packageName, packageHistory);
244        }
245        return packageHistory;
246    }
247
248    public void onUserRemoved(int userId) {
249        mIdleHistory.remove(userId);
250    }
251
252    public boolean isIdleLocked(String packageName, int userId, long elapsedRealtime) {
253        ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId);
254        PackageHistory packageHistory =
255                getPackageHistoryLocked(userHistory, packageName, elapsedRealtime);
256        if (packageHistory == null) {
257            return false; // Default to not idle
258        } else {
259            return hasPassedThresholdsLocked(packageHistory, elapsedRealtime);
260        }
261    }
262
263    private long getElapsedTimeLocked(long elapsedRealtime) {
264        return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration);
265    }
266
267    public void setIdleLocked(String packageName, int userId, boolean idle, long elapsedRealtime) {
268        ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId);
269        PackageHistory packageHistory = getPackageHistoryLocked(userHistory, packageName,
270                elapsedRealtime);
271        packageHistory.lastUsedElapsedTime = getElapsedTimeLocked(elapsedRealtime)
272                - mElapsedTimeThreshold;
273        packageHistory.lastUsedScreenTime = getScreenOnTimeLocked(elapsedRealtime)
274                - (idle ? mScreenOnTimeThreshold : 0) - 1000 /* just a second more */;
275    }
276
277    private boolean hasPassedThresholdsLocked(PackageHistory packageHistory, long elapsedRealtime) {
278        return (packageHistory.lastUsedScreenTime
279                    <= getScreenOnTimeLocked(elapsedRealtime) - mScreenOnTimeThreshold)
280                && (packageHistory.lastUsedElapsedTime
281                        <= getElapsedTimeLocked(elapsedRealtime) - mElapsedTimeThreshold);
282    }
283
284    private File getUserFile(int userId) {
285        return new File(new File(new File(mStorageDir, "users"),
286                Integer.toString(userId)), APP_IDLE_FILENAME);
287    }
288
289    private void readAppIdleTimesLocked(int userId, ArrayMap<String, PackageHistory> userHistory) {
290        FileInputStream fis = null;
291        try {
292            AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
293            fis = appIdleFile.openRead();
294            XmlPullParser parser = Xml.newPullParser();
295            parser.setInput(fis, StandardCharsets.UTF_8.name());
296
297            int type;
298            while ((type = parser.next()) != XmlPullParser.START_TAG
299                    && type != XmlPullParser.END_DOCUMENT) {
300                // Skip
301            }
302
303            if (type != XmlPullParser.START_TAG) {
304                Slog.e(TAG, "Unable to read app idle file for user " + userId);
305                return;
306            }
307            if (!parser.getName().equals(TAG_PACKAGES)) {
308                return;
309            }
310            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
311                if (type == XmlPullParser.START_TAG) {
312                    final String name = parser.getName();
313                    if (name.equals(TAG_PACKAGE)) {
314                        final String packageName = parser.getAttributeValue(null, ATTR_NAME);
315                        PackageHistory packageHistory = new PackageHistory();
316                        packageHistory.lastUsedElapsedTime =
317                                Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE));
318                        packageHistory.lastUsedScreenTime =
319                                Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE));
320                        userHistory.put(packageName, packageHistory);
321                    }
322                }
323            }
324        } catch (IOException | XmlPullParserException e) {
325            Slog.e(TAG, "Unable to read app idle file for user " + userId);
326        } finally {
327            IoUtils.closeQuietly(fis);
328        }
329    }
330
331    public void writeAppIdleTimesLocked(int userId) {
332        FileOutputStream fos = null;
333        AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
334        try {
335            fos = appIdleFile.startWrite();
336            final BufferedOutputStream bos = new BufferedOutputStream(fos);
337
338            FastXmlSerializer xml = new FastXmlSerializer();
339            xml.setOutput(bos, StandardCharsets.UTF_8.name());
340            xml.startDocument(null, true);
341            xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
342
343            xml.startTag(null, TAG_PACKAGES);
344
345            ArrayMap<String,PackageHistory> userHistory = getUserHistoryLocked(userId);
346            final int N = userHistory.size();
347            for (int i = 0; i < N; i++) {
348                String packageName = userHistory.keyAt(i);
349                PackageHistory history = userHistory.valueAt(i);
350                xml.startTag(null, TAG_PACKAGE);
351                xml.attribute(null, ATTR_NAME, packageName);
352                xml.attribute(null, ATTR_ELAPSED_IDLE,
353                        Long.toString(history.lastUsedElapsedTime));
354                xml.attribute(null, ATTR_SCREEN_IDLE,
355                        Long.toString(history.lastUsedScreenTime));
356                xml.endTag(null, TAG_PACKAGE);
357            }
358
359            xml.endTag(null, TAG_PACKAGES);
360            xml.endDocument();
361            appIdleFile.finishWrite(fos);
362        } catch (Exception e) {
363            appIdleFile.failWrite(fos);
364            Slog.e(TAG, "Error writing app idle file for user " + userId);
365        }
366    }
367
368    public void dump(IndentingPrintWriter idpw, int userId) {
369        idpw.println("Package idle stats:");
370        idpw.increaseIndent();
371        ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId);
372        final long elapsedRealtime = SystemClock.elapsedRealtime();
373        final long totalElapsedTime = getElapsedTimeLocked(elapsedRealtime);
374        final long screenOnTime = getScreenOnTimeLocked(elapsedRealtime);
375        if (userHistory == null) return;
376        final int P = userHistory.size();
377        for (int p = 0; p < P; p++) {
378            final String packageName = userHistory.keyAt(p);
379            final PackageHistory packageHistory = userHistory.valueAt(p);
380            idpw.print("package=" + packageName);
381            idpw.print(" lastUsedElapsed=");
382            TimeUtils.formatDuration(totalElapsedTime - packageHistory.lastUsedElapsedTime, idpw);
383            idpw.print(" lastUsedScreenOn=");
384            TimeUtils.formatDuration(screenOnTime - packageHistory.lastUsedScreenTime, idpw);
385            idpw.print(" idle=" + (isIdleLocked(packageName, userId, elapsedRealtime) ? "y" : "n"));
386            idpw.println();
387        }
388        idpw.println();
389        idpw.print("totalElapsedTime=");
390        TimeUtils.formatDuration(getElapsedTimeLocked(elapsedRealtime), idpw);
391        idpw.println();
392        idpw.print("totalScreenOnTime=");
393        TimeUtils.formatDuration(getScreenOnTimeLocked(elapsedRealtime), idpw);
394        idpw.println();
395        idpw.decreaseIndent();
396    }
397
398    public void dumpHistory(IndentingPrintWriter idpw, int userId) {
399        ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId);
400        final long elapsedRealtime = SystemClock.elapsedRealtime();
401        if (userHistory == null) return;
402        final int P = userHistory.size();
403        for (int p = 0; p < P; p++) {
404            final String packageName = userHistory.keyAt(p);
405            final byte[] history = userHistory.valueAt(p).recent;
406            for (int i = 0; i < HISTORY_SIZE; i++) {
407                idpw.print(history[i] == 0 ? '.' : 'A');
408            }
409            idpw.print(" idle=" + (isIdleLocked(packageName, userId, elapsedRealtime) ? "y" : "n"));
410            idpw.print("  " + packageName);
411            idpw.println();
412        }
413    }
414}
415