Android UsageStatsManager get data

本文详细解析了 Android UsageStats 的工作原理,从客户端调用 getSystemService 到服务端启动 UsageStatsService,再到数据查询流程,包括文件存储结构及数据读取过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 to ServiceManager:
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 track queryEvents 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,method queryEvents finally invokes queryStats:
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.

ContextImpl(geSystemServSystemServerSystemServUsageStatsServiceServiceManagerinvokeif not initialized will invoke registerService method<<----binder clientbinder server---->>invoke main methodstartCoreServices invokedstartService(UsageStatsService.class) invokedonStart invokeUsageStatsServiceextends fromSystemServicepublishBinderService invokeaddServiceUsageStatsServicerigisteredContextImpl(geSystemServSystemServerSystemServUsageStatsServiceServiceManagerUsageStatsService Init
UsageStatsManagerUsageStatsServiceUserUsageSUsageStatUsageStatsXmlV1StatCombinerqueryEvents invoke to get data,request from client to sever by binderqueryEvents invokegetUserDataAndInitializeIfNeededLocked invokegetUserDataAndInitializeIfNeededLocked invoke UserUsageStatsService constructorUsageStatsDatabase constructor invokedgetUserDataAndInitializeIfNeededLocked invoke UserUsageStatsService init in constructorinit invoke in UserUsageStatsService init methodindexFilesLocked invoked to initialized mSortedStatFiles in initloadActiveStats invoked load all type interval datagetLatestUsageStats invoked in loadActiveStats methoddata returned to initialize mCurrentStats in loadActiveStats methodqueryEvents finally invoke queryStatsqueryUsageStats invokedread invokecombine invokecallback to queryEvents method with statsdata all wrapped in UsageEvents objectdata returned with binderUsageStatsManagerUsageStatsServiceUserUsageSUsageStatUsageStatsXmlV1StatCombinerGet Data series
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值