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