/** * Copyright (C) 2014 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.usage; import android.Manifest; import android.app.AppOpsManager; import android.app.usage.ConfigurationStats; import android.app.usage.IUsageStatsManager; import android.app.usage.UsageEvents; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManagerInternal; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.os.Binder; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; import com.android.internal.os.BackgroundThread; import com.android.internal.util.IndentingPrintWriter; import com.android.server.SystemService; import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Arrays; import java.util.List; /** * A service that collects, aggregates, and persists application usage data. * This data can be queried by apps that have been granted permission by AppOps. */ public class UsageStatsService extends SystemService implements UserUsageStatsService.StatsUpdatedListener { static final String TAG = "UsageStatsService"; static final boolean DEBUG = false; private static final long TEN_SECONDS = 10 * 1000; private static final long TWENTY_MINUTES = 20 * 60 * 1000; private static final long FLUSH_INTERVAL = DEBUG ? TEN_SECONDS : TWENTY_MINUTES; private static final long TIME_CHANGE_THRESHOLD_MILLIS = 2 * 1000; // Two seconds. // Handler message types. static final int MSG_REPORT_EVENT = 0; static final int MSG_FLUSH_TO_DISK = 1; static final int MSG_REMOVE_USER = 2; private final Object mLock = new Object(); Handler mHandler; AppOpsManager mAppOps; UserManager mUserManager; private final SparseArray mUserState = new SparseArray<>(); private File mUsageStatsDir; long mRealTimeSnapshot; long mSystemTimeSnapshot; public UsageStatsService(Context context) { super(context); } @Override public void onStart() { mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE); mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE); mHandler = new H(BackgroundThread.get().getLooper()); File systemDataDir = new File(Environment.getDataDirectory(), "system"); mUsageStatsDir = new File(systemDataDir, "usagestats"); mUsageStatsDir.mkdirs(); if (!mUsageStatsDir.exists()) { throw new IllegalStateException("Usage stats directory does not exist: " + mUsageStatsDir.getAbsolutePath()); } getContext().registerReceiver(new UserRemovedReceiver(), new IntentFilter(Intent.ACTION_USER_REMOVED)); synchronized (mLock) { cleanUpRemovedUsersLocked(); } mRealTimeSnapshot = SystemClock.elapsedRealtime(); mSystemTimeSnapshot = System.currentTimeMillis(); publishLocalService(UsageStatsManagerInternal.class, new LocalService()); publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService()); } private class UserRemovedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent != null && intent.getAction().equals(Intent.ACTION_USER_REMOVED)) { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); if (userId >= 0) { mHandler.obtainMessage(MSG_REMOVE_USER, userId, 0).sendToTarget(); } } } } @Override public void onStatsUpdated() { mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL); } private void cleanUpRemovedUsersLocked() { final List users = mUserManager.getUsers(true); if (users == null || users.size() == 0) { throw new IllegalStateException("There can't be no users"); } ArraySet toDelete = new ArraySet<>(); String[] fileNames = mUsageStatsDir.list(); if (fileNames == null) { // No users to delete. return; } toDelete.addAll(Arrays.asList(fileNames)); final int userCount = users.size(); for (int i = 0; i < userCount; i++) { final UserInfo userInfo = users.get(i); toDelete.remove(Integer.toString(userInfo.id)); } final int deleteCount = toDelete.size(); for (int i = 0; i < deleteCount; i++) { deleteRecursively(new File(mUsageStatsDir, toDelete.valueAt(i))); } } private static void deleteRecursively(File f) { File[] files = f.listFiles(); if (files != null) { for (File subFile : files) { deleteRecursively(subFile); } } if (!f.delete()) { Slog.e(TAG, "Failed to delete " + f); } } private UserUsageStatsService getUserDataAndInitializeIfNeededLocked(int userId, long currentTimeMillis) { UserUsageStatsService service = mUserState.get(userId); if (service == null) { service = new UserUsageStatsService(getContext(), userId, new File(mUsageStatsDir, Integer.toString(userId)), this); service.init(currentTimeMillis); mUserState.put(userId, service); } return service; } /** * This should be the only way to get the time from the system. */ private long checkAndGetTimeLocked() { final long actualSystemTime = System.currentTimeMillis(); final long actualRealtime = SystemClock.elapsedRealtime(); final long expectedSystemTime = (actualRealtime - mRealTimeSnapshot) + mSystemTimeSnapshot; if (Math.abs(actualSystemTime - expectedSystemTime) > TIME_CHANGE_THRESHOLD_MILLIS) { // The time has changed. final int userCount = mUserState.size(); for (int i = 0; i < userCount; i++) { final UserUsageStatsService service = mUserState.valueAt(i); service.onTimeChanged(expectedSystemTime, actualSystemTime); } mRealTimeSnapshot = actualRealtime; mSystemTimeSnapshot = actualSystemTime; } return actualSystemTime; } /** * Assuming the event's timestamp is measured in milliseconds since boot, * convert it to a system wall time. */ private void convertToSystemTimeLocked(UsageEvents.Event event) { event.mTimeStamp = Math.max(0, event.mTimeStamp - mRealTimeSnapshot) + mSystemTimeSnapshot; } /** * Called by the Binder stub */ void shutdown() { synchronized (mLock) { mHandler.removeMessages(MSG_REPORT_EVENT); flushToDiskLocked(); } } /** * Called by the Binder stub. */ void reportEvent(UsageEvents.Event event, int userId) { synchronized (mLock) { final long timeNow = checkAndGetTimeLocked(); convertToSystemTimeLocked(event); final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow); service.reportEvent(event); } } /** * Called by the Binder stub. */ void flushToDisk() { synchronized (mLock) { flushToDiskLocked(); } } /** * Called by the Binder stub. */ void removeUser(int userId) { synchronized (mLock) { Slog.i(TAG, "Removing user " + userId + " and all data."); mUserState.remove(userId); cleanUpRemovedUsersLocked(); } } /** * Called by the Binder stub. */ List queryUsageStats(int userId, int bucketType, long beginTime, long endTime) { synchronized (mLock) { final long timeNow = checkAndGetTimeLocked(); if (!validRange(timeNow, beginTime, endTime)) { return null; } final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow); return service.queryUsageStats(bucketType, beginTime, endTime); } } /** * Called by the Binder stub. */ List queryConfigurationStats(int userId, int bucketType, long beginTime, long endTime) { synchronized (mLock) { final long timeNow = checkAndGetTimeLocked(); if (!validRange(timeNow, beginTime, endTime)) { return null; } final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow); return service.queryConfigurationStats(bucketType, beginTime, endTime); } } /** * Called by the Binder stub. */ UsageEvents queryEvents(int userId, long beginTime, long endTime) { synchronized (mLock) { final long timeNow = checkAndGetTimeLocked(); if (!validRange(timeNow, beginTime, endTime)) { return null; } final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow); return service.queryEvents(beginTime, endTime); } } private static boolean validRange(long currentTime, long beginTime, long endTime) { return beginTime <= currentTime && beginTime < endTime; } private void flushToDiskLocked() { final int userCount = mUserState.size(); for (int i = 0; i < userCount; i++) { UserUsageStatsService service = mUserState.valueAt(i); service.persistActiveStats(); } mHandler.removeMessages(MSG_FLUSH_TO_DISK); } /** * Called by the Binder stub. */ void dump(String[] args, PrintWriter pw) { synchronized (mLock) { IndentingPrintWriter idpw = new IndentingPrintWriter(pw, " "); ArraySet argSet = new ArraySet<>(); argSet.addAll(Arrays.asList(args)); final int userCount = mUserState.size(); for (int i = 0; i < userCount; i++) { idpw.printPair("user", mUserState.keyAt(i)); idpw.println(); idpw.increaseIndent(); if (argSet.contains("--checkin")) { mUserState.valueAt(i).checkin(idpw); } else { mUserState.valueAt(i).dump(idpw); } idpw.decreaseIndent(); } } } class H extends Handler { public H(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_REPORT_EVENT: reportEvent((UsageEvents.Event) msg.obj, msg.arg1); break; case MSG_FLUSH_TO_DISK: flushToDisk(); break; case MSG_REMOVE_USER: removeUser(msg.arg1); break; default: super.handleMessage(msg); break; } } } private class BinderService extends IUsageStatsManager.Stub { private boolean hasPermission(String callingPackage) { final int callingUid = Binder.getCallingUid(); if (callingUid == Process.SYSTEM_UID) { return true; } final int mode = mAppOps.checkOp(AppOpsManager.OP_GET_USAGE_STATS, callingUid, callingPackage); if (mode == AppOpsManager.MODE_DEFAULT) { // The default behavior here is to check if PackageManager has given the app // permission. return getContext().checkCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS) == PackageManager.PERMISSION_GRANTED; } return mode == AppOpsManager.MODE_ALLOWED; } @Override public ParceledListSlice queryUsageStats(int bucketType, long beginTime, long endTime, String callingPackage) { if (!hasPermission(callingPackage)) { return null; } final int userId = UserHandle.getCallingUserId(); final long token = Binder.clearCallingIdentity(); try { final List results = UsageStatsService.this.queryUsageStats( userId, bucketType, beginTime, endTime); if (results != null) { return new ParceledListSlice<>(results); } } finally { Binder.restoreCallingIdentity(token); } return null; } @Override public ParceledListSlice queryConfigurationStats(int bucketType, long beginTime, long endTime, String callingPackage) throws RemoteException { if (!hasPermission(callingPackage)) { return null; } final int userId = UserHandle.getCallingUserId(); final long token = Binder.clearCallingIdentity(); try { final List results = UsageStatsService.this.queryConfigurationStats(userId, bucketType, beginTime, endTime); if (results != null) { return new ParceledListSlice<>(results); } } finally { Binder.restoreCallingIdentity(token); } return null; } @Override public UsageEvents queryEvents(long beginTime, long endTime, String callingPackage) { if (!hasPermission(callingPackage)) { return null; } final int userId = UserHandle.getCallingUserId(); final long token = Binder.clearCallingIdentity(); try { return UsageStatsService.this.queryEvents(userId, beginTime, endTime); } finally { Binder.restoreCallingIdentity(token); } } @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) { pw.println("Permission Denial: can't dump UsageStats from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() + " without permission " + android.Manifest.permission.DUMP); return; } UsageStatsService.this.dump(args, pw); } } /** * This local service implementation is primarily used by ActivityManagerService. * ActivityManagerService will call these methods holding the 'am' lock, which means we * shouldn't be doing any IO work or other long running tasks in these methods. */ private class LocalService extends UsageStatsManagerInternal { @Override public void reportEvent(ComponentName component, int userId, int eventType) { if (component == null) { Slog.w(TAG, "Event reported without a component name"); return; } UsageEvents.Event event = new UsageEvents.Event(); event.mPackage = component.getPackageName(); event.mClass = component.getClassName(); // This will later be converted to system time. event.mTimeStamp = SystemClock.elapsedRealtime(); event.mEventType = eventType; mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget(); } @Override public void reportConfigurationChange(Configuration config, int userId) { if (config == null) { Slog.w(TAG, "Configuration event reported with a null config"); return; } UsageEvents.Event event = new UsageEvents.Event(); event.mPackage = "android"; // This will later be converted to system time. event.mTimeStamp = SystemClock.elapsedRealtime(); event.mEventType = UsageEvents.Event.CONFIGURATION_CHANGE; event.mConfiguration = new Configuration(config); mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget(); } @Override public void prepareShutdown() { // This method *WILL* do IO work, but we must block until it is finished or else // we might not shutdown cleanly. This is ok to do with the 'am' lock held, because // we are shutting down. shutdown(); } } }