1、Register service(binder client)
ContextImpl
context.getSystemService(“usagestats”)
SystemServiceRegistry
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
if it is not initialized:
registerService(Context.USAGE_STATS_SERVICE, UsageStatsManager.class,
new CachedServiceFetcher<UsageStatsManager>() {
@Override
public UsageStatsManager createService(ContextImpl ctx) {
IBinder iBinder = ServiceManager.getService(Context.USAGE_STATS_SERVICE);
IUsageStatsManager service = IUsageStatsManager.Stub.asInterface(iBinder);
return new UsageStatsManager(ctx.getOuterContext(), service);
}});
2、SystemServer(binder server)
Device is booting with SystemServer.main()
being invoked:
- register
UsageStatsService
toServiceManager
:
private void startCoreServices() {
// Tracks the battery level. Requires LightService.
mSystemServiceManager.startService(BatteryService.class);
// Tracks application usage stats.
mSystemServiceManager.startService(UsageStatsService.class);
mActivityManagerService.setUsageStatsManager(
LocalServices.getService(UsageStatsManagerInternal.class));
// Tracks whether the updatable WebView is in a ready state and watches for update installs.
mWebViewUpdateService = mSystemServiceManager.startService(WebViewUpdateService.class);
}
mSystemServiceManager.startService(UsageStatsService.class);
is the core code.
it will invoke SystemServiceManager
class’s startService method,
//Having deleted some extra codes for easy reading
public <T extends SystemService> T startService(Class<T> serviceClass) {
try {
final String name = serviceClass.getName();
final T service;
Constructor<T> constructor = serviceClass.getConstructor(Context.class);
service = constructor.newInstance(mContext);
// Register it.
mServices.add(service);
// Start it.
try {
service.onStart();
} catch (RuntimeException ex) {
}
return service;
} finally {
}
}
service.onStart();
relates to a abstract class,SystemService
,and it is extended by other system services,
such as:
public class UsageStatsService extends SystemService implements
UserUsageStatsService.StatsUpdatedListener {}
in UsageStatsService
onStart method,it does some initial works:
- init the save-director for data :
File systemDataDir = new File(Environment.getDataDirectory(), "system");
mUsageStatsDir = new File(systemDataDir, "usagestats");
mUsageStatsDir.mkdirs();
- register service
publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService());
the codes relate to SystemService
:
protected final void publishBinderService(String name, IBinder service,
boolean allowIsolated) {
ServiceManager.addService(name, service, allowIsolated);
}
3、Get data example
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private ArrayList<AppLaunchInfoBean> getAppLaunchInfoBean(long start, long end) {
final UsageStatsManager usageStatsManager = (UsageStatsManager) mContext.getSystemService("usagestats");
UsageEvents usageEvents = usageStatsManager.queryEvents(start, end);
return getAppLaunchInfoBeanList(usageEvents, end);
}
4、Tracking source code
- In
UsageStatsManager
class trackqueryEvents
method:
public UsageEvents queryEvents(long beginTime, long endTime) {
try {
UsageEvents iter = mService.queryEvents(beginTime, endTime, mContext.getOpPackageName());
if (iter != null) {
return iter;
}
} catch (RemoteException e) {
// fallthrough and return null
}
return sEmptyResults;
}
UsageStatsService
mService
is referred to UsageStatsService
,this service is initialized in queryEvents
method in UsageStatsService
class,:
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);
}
}
getUserDataAndInitializeIfNeededLocked
method is here:
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;
}
the constructor and init
method accomplish some important work:
UserUsageStatsService constructor
UserUsageStatsService(Context context, int userId, File usageStatsDir,
StatsUpdatedListener listener) {
mContext = context;
mDailyExpiryDate = new UnixCalendar(0);
mDatabase = new UsageStatsDatabase(usageStatsDir);
mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
mListener = listener;
mLogPrefix = "User[" + Integer.toString(userId) + "] ";
mUserId = userId;
}
mDatabase = new UsageStatsDatabase(usageStatsDir);
it initializes file operator which can query data from disk file.
UsageStatsDatabase
public UsageStatsDatabase(File dir) {
mIntervalDirs = new File[] {
new File(dir, "daily"),
new File(dir, "weekly"),
new File(dir, "monthly"),
new File(dir, "yearly"),
};
mVersionFile = new File(dir, "version");
mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
mCal = new UnixCalendar(0);
}
it initialized all directors in mIntervalDirs
,and mSortedStatFiles
is a map which keys are the file’s name which exclude end of .bak and values are file,what’s more ,the file’s name is time.
UserUsageStatsService init()
method
init()
method contains mDatabase.init(currentTimeMillis);
method which updates current stats time and deletes some dead data which beyond today and it contains indexFilesLocked
method.
UsageStatsDatabase init()
private void indexFilesLocked() {
final FilenameFilter backupFileFilter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return !name.endsWith(BAK_SUFFIX);
}
};
// Index the available usage stat files on disk.
for (int i = 0; i < mSortedStatFiles.length; i++) {
if (mSortedStatFiles[i] == null) {
mSortedStatFiles[i] = new TimeSparseArray<>();
} else {
mSortedStatFiles[i].clear();
}
File[] files = mIntervalDirs[i].listFiles(backupFileFilter);
if (files != null) {
if (DEBUG) {
Slog.d(TAG, "Found " + files.length + " stat files for interval " + i);
}
for (File f : files) {
final AtomicFile af = new AtomicFile(f);
try {
mSortedStatFiles[i].put(UsageStatsXml.parseBeginTime(af), af);
} catch (IOException e) {
Slog.e(TAG, "failed to index file: " + f, e);
}
}
}
}
}
mSortedStatFiles
put all interval type of files in and its key are file’s name which is the begin-time and detail is in UsageStatsXml.parseBeginTime
:
public static long parseBeginTime(File file) throws IOException {
String name = file.getName();
while (name.endsWith(CHECKED_IN_SUFFIX)) {
name = name.substring(0, name.length() - CHECKED_IN_SUFFIX.length());
}
try {
return Long.parseLong(name);
} catch (NumberFormatException e) {
throw new IOException(e);
}
}
Back to UserUsageStatsService init()
method,it invokes loadActiveStats(currentTimeMillis);
which load all type interval data,updateRolloverDeadline();
build daily data while all of interval type are not exist.
loadActiveStats
method:
for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) {
final IntervalStats stats = mDatabase.getLatestUsageStats(intervalType);
if (stats != null && currentTimeMillis - 500 >= stats.endTime &&
currentTimeMillis < stats.beginTime + INTERVAL_LENGTH[intervalType]) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Loading existing stats @ " +
sDateFormat.format(stats.beginTime) + "(" + stats.beginTime +
") for interval " + intervalType);
}
mCurrentStats[intervalType] = stats;
} else {
// No good fit remains.
if (DEBUG) {
Slog.d(TAG, "Creating new stats @ " +
sDateFormat.format(currentTimeMillis) + "(" +
currentTimeMillis + ") for interval " + intervalType);
}
mCurrentStats[intervalType] = new IntervalStats();
mCurrentStats[intervalType].beginTime = currentTimeMillis;
mCurrentStats[intervalType].endTime = currentTimeMillis + 1;
}
}
mCurrentStats
is initialized in UserUsageStatsService
class constructor:
mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
and is filled with UsageStatsDatabase getLatestUsageStats
returned:
mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
UsageStatsDatabase getLatestUsageStats
:
try {
final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1);
IntervalStats stats = new IntervalStats();
UsageStatsXml.read(f, stats);
return stats;
} catch (IOException e) {
Slog.e(TAG, "Failed to read usage stats file", e);
}
the returned data is the latest file’s content.
Back to mService.queryEvents
:
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);
}
}
- And in
UserUsageStatsService
,methodqueryEvents
finally invokesqueryStats
:
UsageEvents queryEvents(final long beginTime, final long endTime) {
final ArraySet<String> names = new ArraySet<>();
List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY,
beginTime, endTime, new StatCombiner<UsageEvents.Event>() {
@Override
public void combine(IntervalStats stats, boolean mutable,
List<UsageEvents.Event> accumulatedResult) {
if (stats.events == null) {
return;
}
final int startIndex = stats.events.closestIndexOnOrAfter(beginTime);
if (startIndex < 0) {
return;
}
final int size = stats.events.size();
for (int i = startIndex; i < size; i++) {
if (stats.events.keyAt(i) >= endTime) {
return;
}
final UsageEvents.Event event = stats.events.valueAt(i);
names.add(event.mPackage);
if (event.mClass != null) {
names.add(event.mClass);
}
accumulatedResult.add(event);
}
}
});
if (results == null || results.isEmpty()) {
return null;
}
String[] table = names.toArray(new String[names.size()]);
Arrays.sort(table);
return new UsageEvents(results, table);
}
Generally,in queryStats
method,we find some vital codes:
// Truncate the endTime to just before the in-memory //stats. Then, we'll append the
// in-memory stats to the results (if necessary) so as to //avoid writing to disk too often.
final long truncatedEndTime = Math.min(currentStats.beginTime, endTime);
the code means some stats in memory that will not write to file immediately,and the truncatedEndTime make sure that the end time which will query in disk is less than the start time in current stats with interval type.
Firstly,it will search result in disk file:
List<T> results = mDatabase.queryUsageStats(intervalType, beginTime,truncatedEndTime, combiner);
UsageStatsDatabase queryUsageStats
final IntervalStats stats = new IntervalStats();
final ArrayList<T> results = new ArrayList<>();
for (int i = startIndex; i <= endIndex; i++) {
final AtomicFile f = intervalStats.valueAt(i);
if (DEBUG) {
Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath());
}
try {
UsageStatsXml.read(f, stats);
if (beginTime < stats.endTime) {
combiner.combine(stats, false, results);
}
} catch (IOException e) {
Slog.e(TAG, "Failed to read usage stats file", e);
// We continue so that we return results that are not
// corrupt.
}
}
return results;
UsageStatsXmlV1 read
UsageStatsXml.read(f, stats);
will finally invoke UsageStatsXmlV1
read
method,the file locates:/data/system/usagestats/0/,and with four directors,daily,monthly,weekly,yearly.File’s content is like this:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<usagestats version="1" endTime="86399999">
<packages>
<package lastTimeActive="-1552966614241" package="com.qiku.smart.components" timeActive="0" lastEvent="0" />
<package lastTimeActive="-1552966614241" package="com.android.providers.telephony" timeActive="0" lastEvent="0" />
<package lastTimeActive="-1552966614241" package="com.qiku.logsystem" timeActive="0" lastEvent="0" />
<package lastTimeActive="-1552966614241" package="com.android.providers.calendar" timeActive="0" lastEvent="0" />
</packages>
<configurations>
<config lastTimeActive="83403363" timeActive="48" count="2" fs="1065353216" locales="zh-CN" touch="3" key="1" keyHid="1" hardKeyHid="2" nav="1" navHid="2" ori="1" scrLay="268435810" clrMod="5" width="360" height="648" sw="360" density="480" app_bounds="0 0 1080 2016" />
<config lastTimeActive="83403339" timeActive="51" count="2" fs="1065353216" locales="zh-CN" touch="1" key="1" keyHid="1" hardKeyHid="2" nav="1" navHid="2" ori="1" scrLay="268435810" clrMod="5" width="360" height="720" sw="360" density="480" app_bounds="0 0 1080 2160" />
<config lastTimeActive="86399998" timeActive="74959721" count="2" active="true" fs="1065353216" locales="zh-CN" touch="3" key="1" keyHid="1" hardKeyHid="2" nav="1" navHid="2" ori="1" scrLay="268435810" clrMod="5" ui="17" width="360" height="648" sw="360" density="480" app_bounds="0 0 1080 2016" />
</configurations>
<event-log>
<event time="459" package="com.qiku.android.launcher3" class="com.qiku.android.launcher3.Launcher" flags="0" type="1" />
<event time="2181" package="com.qiku.android.launcher3" class="com.qiku.android.launcher3.Launcher" flags="0" type="2" />
</event-log>
</usagestats>
we can get end-time which is the latest stats’s begin-time adding time-attribute in xml file.
statsOut.endTime = statsOut.beginTime + XmlUtils.readLongAttribute(parser, END_TIME_ATTR);
And another data are filled below:
while ((eventCode = parser.next()) != XmlPullParser.END_DOCUMENT
&& (eventCode != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (eventCode != XmlPullParser.START_TAG) {
continue;
}
final String tag = parser.getName();
switch (tag) {
case PACKAGE_TAG:
loadUsageStats(parser, statsOut);
break;
case CONFIG_TAG:
loadConfigStats(parser, statsOut);
break;
case EVENT_TAG:
loadEvent(parser, statsOut);
break;
}
}
the data is wrapped into statsOut
object which is partial data,because entire data include that in memory,in UserUsageStatsService queryStats
:
if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Returning in-memory stats");
}
if (results == null) {
results = new ArrayList<>();
}
combiner.combine(currentStats, true, results);
}
All data are wrapped in UsageEvents
object to caller.