Personal Homepage: Chengang. plus/

The article will be synchronized to personal wechat official account: Android blog

3. Health data recording project

The main problem this project encountered was the inaccurate usage duration and frequency of the application. The reason lies in the application’s business logic and source code.

Generally, we obtain details of application usage data by:

@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);
}
Copy the code

3.1 Service Logic

Use the above method to fetch usage data every time you open the application or every time you return to the home page from another page of the application, persisting the data to the local database.

This seems reasonable, but testers consistently report that the duration and frequency of use are inaccurate. Here is the need to find the reason from the source.

3.2 UsageStatsManager source traceability

We all know that Linux starts Zygote from the init.rc script. Zygote forks the system_server process. The integration belongs to the SystemServer class and starts a list of system services in its run method. We focus on when UsageStatsService starts.

SystemServer

private void run(a) {
    mSystemServiceManager = new SystemServiceManager(mSystemContext);
    LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);
    startCoreServices();
}

private void startCoreServices(a) {
    mSystemServiceManager.startService(UsageStatsService.class);
    mActivityManagerService.setUsageStatsManager(LocalServices.getService(UsageStatsManagerInternal.class));
}
Copy the code

The SystemServiceManager manages system services and assigns them to ActivityManagerService for scheduling.

SystemServiceManager

public <T extends SystemService> T startService(Class<T> serviceClass) {
    final String name = serviceClass.getName();
    final T service;
    Constructor<T> constructor = serviceClass.getConstructor(Context.class);
    service = constructor.newInstance(mContext);
    startService(service);
    return service;
}

public void startService(@NonNull final SystemService service) {
    // Register it.
    mServices.add(service);
    // Start it.
    long time = SystemClock.elapsedRealtime();
    try {
        service.onStart();
    } catch (RuntimeException ex) {
    }
    warnIfTooLong(SystemClock.elapsedRealtime() - time, service, "onStart");//50ms
}
Copy the code

The UsageStatsService constructor is called by reflection, and the startService method is used to start the service:

UsageStatsService

public class UsageStatsService extends SystemService implements UserUsageStatsService.StatsUpdatedListener {
    public UsageStatsService(Context context) {
        super(context); }}// The start method is long and only extracts important methods
@Override
public void onStart(a) {
    // The first part
    mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
    
    // Part 2
    mHandler = new H(BackgroundThread.get().getLooper());
    
    // Part 3
    File systemDataDir = new File(Environment.getDataDirectory(), "system");
    mUsageStatsDir = new File(systemDataDir, "usagestats");
    mUsageStatsDir.mkdirs();
    
    // Part 4
    publishLocalService(UsageStatsManagerInternal.class, new LocalService());
    publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService());
    
    // Part 5
    getUserDataAndInitializeIfNeededLocked(UserHandle.USER_SYSTEM, mSystemTimeSnapshot);
}

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;
}
Copy the code

The UsageStatsService constructor is relatively simple, focusing on the start method:

  • In part 1UserManagerThis is prepared for processing data in a multi-user situation
  • The second part creates a Handler for processing data, such as storing data to disk. Its looper actually comes from the HandlerThread, because the BackgroundThread inherits from the HandlerThread.
  • The third part is to create a usagestats folder under the /data/system/ directory to create files to store data.
  • In Part 4, you add the LocalService object to the collection of LocalServices, which is an internal class of UsageStatsService.publishBinderServiceWhat we’re doing is we’re going toBinderServiceAdded to theServiceManagerIn the.BinderServiceIs defined as:
private final class BinderService extends IUsageStatsManager.Stub {}
Copy the code

The iusAgestatsManager. Stub is the proxy object provided by the client, and the client obtains the object to operate on it. The specific operation function is defined in the method overridden by the BinderService.

  • Part five is intended to initialize oneUserUsageStatsServiceClass, which passes back the userId at initialization and creates folders to store data for different users based on this userId:

UserUsageStatsService

UserUsageStatsService(Context context, int userId, File usageStatsDir, StatsUpdatedListener listener) {
    mDatabase = new UsageStatsDatabase(usageStatsDir);
    mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
}
Copy the code

The UserUsageStatsService constructor creates a UsageStatsDatabase object and an array of type IntervalStats.

The former is mainly used to write data to XML files, while the latter is mainly used to process data at different time intervals.

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];
}
Copy the code

Here again, different time attributes create folders to store data.

The UserUsageStatsService finally calls the init method, whose purpose is to read the existing data and initialize the creation if there is no relevant data.

At this point the basic initialization is done.

3.3 Client to obtain data source tracing

The calling code for the client is:

usageStatsManager.queryEvents(start, end);
Copy the code

Trace the call stack for this code:

UsageStatsManager

public UsageEvents queryEvents(long beginTime, long endTime) {
    try {
        UsageEvents iter = mService.queryEvents(beginTime, endTime, mContext.getOpPackageName());
        if(iter ! =null) {
            returniter; }}catch (RemoteException e) {
    }
    return sEmptyResults;
}
Copy the code

Here mService is type IUsageStatsManager, is the operation object of the server, corresponding to the internal class BinderService of the server UsageStatsService, that is, the corresponding method to call it:

UsageStatsService.BinderService

@Override
public UsageEvents queryEvents(long beginTime, long endTime, String callingPackage) {
    if(! hasPermission(callingPackage)) {return null;
    }
    try {
        return UsageStatsService.this.queryEvents(userId, beginTime, endTime,
        obfuscateInstantApps);
    } finally{ Binder.restoreCallingIdentity(token); }}UsageEvents queryEvents(int userId, long beginTime, long endTime, boolean shouldObfuscateInstantApps) {
    final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow);
    return service.queryEvents(beginTime, endTime, shouldObfuscateInstantApps);
}
Copy the code

The queryEvents method of the external class is called, and the queryEvents method of UserUsageStatsService is finally called in this method:

UserUsageStatsService

UsageEvents queryEvents(final long beginTime, final long endTime, boolean obfuscateInstantApps) {
    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) {
            final int startIndex = stats.events.firstIndexOnOrAfter(beginTime);
            final int size = stats.events.size();
            for (int i = startIndex; i < size; i++) {
                UsageEvents.Event event = stats.events.get(i);
                names.add(event.mPackage);
                if(event.mClass ! =null) { names.add(event.mClass); } accumulatedResult.add(event); }}}); String[] table = names.toArray(new String[names.size()]);
    Arrays.sort(table);
    return new UsageEvents(results, table);
}
Copy the code

If the interval is not set, the default is INTERVAL_DAILY. Let’s look at the queryStats method:

private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime, StatCombiner<T> combiner) {
    // The first part
    final IntervalStats currentStats = mCurrentStats[intervalType];
    // Part 2
    List<T> results = mDatabase.queryUsageStats(intervalType, beginTime, truncatedEndTime, combiner);
    // Part 3
    if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
        combiner.combine(currentStats, true, results);
    }
    return results;
}
Copy the code
  • The first part is fetched from current memory, since INTERVAL_DAILY data is originally stored in mCurrentStats when reported.

MCurrentStats is the IntervalStats array type, and IntervalStats maintains an EventList object that holds an ArrayList<UsageEvents.Event> mEvents, Maintain application usage details.

  • The second part fetches data for the desired time interval from the XML file on the local disk. After fetching the data, we call the Combine method to store the data in a List:

UsageStatsDatabase

public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime, StatCombiner<T> combiner) {
    final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType];
    int startIndex = intervalStats.closestIndexOnOrBefore(beginTime);
    int endIndex = intervalStats.closestIndexOnOrBefore(endTime);
    final IntervalStats stats = new IntervalStats();
    final ArrayList<T> results = new ArrayList<>();
    for (int i = startIndex; i <= endIndex; i++) {
        final AtomicFile f = intervalStats.valueAt(i);
        UsageStatsXml.read(f, stats);
        if (beginTime < stats.endTime) {
            combiner.combine(stats, false, results); }}return results;
}
Copy the code
  • The third part is to merge the data from memory and disk.

Here we know, when the data is a collection of memory and disk data, so exactly how to get data to be more accurate? Look at how the system stores data.

3.4 System data source tracking

Remember that UsageStatsService sets one of its objects to AMS when the system is initialized. This is where the data store is triggered:

ActivityManagerService

void updateUsageStats(ActivityRecord component, boolean resumed) {
    if (resumed) {
        if(mUsageStatsService ! =null) { mUsageStatsService.reportEvent(component.realActivity, component.userId, UsageEvents.Event.MOVE_TO_FOREGROUND); }}else {
        if(mUsageStatsService ! =null) { mUsageStatsService.reportEvent(component.realActivity, component.userId, UsageEvents.Event.MOVE_TO_BACKGROUND); }}}Copy the code

The updateUsageStats method is called in three places:

  • ActivityStackSupervisor,reportResumedActivityLocked
  • ActivityStack,startPausingLocked
  • ActivityStack,removeHistoryRecordsForAppLocked

As you can see from these three method names, this method is usually triggered when the Activity switches from foreground to background, or from background to foreground.

MOVE_TO_FOREGROUND And MOVE_TO_BACKGROUND call reportEvent. MOVE_TO_FOREGROUND

Here is mUsageStatsService UsageStatsManagerInternal types, Remember that in the start method of UsageStatsService publishLocalService (UsageStatsManagerInternal. Class, new LocalService ()); UsageStatsManagerInternal method, here is the type, LocalService is corresponding service type, and the LocalService inherited from UsageStatsManagerInternal, Therefore, the specific operation is performed in the LocalService inner class of UsageStatsService.

UsageStatsService.LocalService

private final class BinderService extends IUsageStatsManager.Stub {
    @Override
    public void reportEvent(ComponentName component, int userId, int eventType) {
        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(); }}Copy the code

Create a new usageEvents. Event object, fill it with the package name, component name, time, and type, and process the message sequentially through the mHandler initialized in the UsageStatsService onStart method:

UsageStatsService

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; }}}Copy the code

Call reportEvent method of external class:

UsageStatsService

void reportEvent(UsageEvents.Event event, int userId) {
    final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow);
    service.reportEvent(event);
}
Copy the code

UserUsageStatsService

void reportEvent(UsageEvents.Event event) {
    final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY];
    
    // Add the event to the daily list.
    if (currentDailyStats.events == null) {
        currentDailyStats.events = new EventList();
    }
    if(event.mEventType ! = UsageEvents.Event.SYSTEM_INTERACTION) { currentDailyStats.events.insert(event); }for (IntervalStats stats : mCurrentStats) {
        switch (event.mEventType) {
            default: {
                stats.update(event.mPackage, event.mTimeStamp, event.mEventType);
                break;
            }
        }
    }
    notifyStatsChanged();
}
Copy the code
  • The first step is to put the data into the file that the Daily belongs to, that is, into memory.
  • The second step is to call the Update method of IntervalStats to perform the update. Take a look at this sequence of actions:

IntervalStats

void update(String packageName, long timeStamp, int eventType) {
    UsageStats usageStats = getOrCreateUsageStats(packageName);
    usageStats.mEndTimeStamp = timeStamp;

    if (eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
        usageStats.mLaunchCount += 1;
    }

    endTime = timeStamp;
}

UsageStats getOrCreateUsageStats(String packageName) {
    UsageStats usageStats = packageStats.get(packageName);
    if (usageStats == null) {
        usageStats = new UsageStats();
        usageStats.mPackageName = getCachedStringRef(packageName);
        usageStats.mBeginTimeStamp = beginTime;
        usageStats.mEndTimeStamp = endTime;
        packageStats.put(usageStats.mPackageName, usageStats);
    }
    return usageStats;
}
Copy the code

The IntervalStats object for each time type maintains a UsageStats object that contains the package name, start time, and end time data.

With the data ready, call notifyStatsChanged:

UserUsageStatsService

private void notifyStatsChanged(a) {
    if(! mStatsChanged) { mStatsChanged =true; mListener.onStatsUpdated(); }}Copy the code

The mListener is passed by UsageStatsService, and the corresponding onStatsUpdated is implemented in this class:

UsageStatsService

private static final long TEN_SECONDS = 10 * 1000;
private static final long TWENTY_MINUTES = 20 * 60 * 1000;
private static final long FLUSH_INTERVAL = COMPRESS_TIME ? TEN_SECONDS : TWENTY_MINUTES;

@Override
public void onStatsUpdated(a) {
    mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL);
}
Copy the code

FLUSH_INTERVAL = 20 minutes; FLUSH_INTERVAL = 20 minutes; FLUSH_INTERVAL = 20 minutes

UsageStatsService.H

class H extends Handler {
    public H(Looper looper) {
        super(looper);
    }
    
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_FLUSH_TO_DISK:
                flushToDisk();
                break; }}}Copy the code

UsageStatsService

void flushToDisk() {
    synchronized (mLock) {
        flushToDiskLocked();
    }
}

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);
}
Copy the code

Class UserUsageStatsService; store data for multiple users;

UserUsageStatsService

void persistActiveStats() {
    if (mStatsChanged) {
        try {
            for (int i = 0; i < mCurrentStats.length; i++) {
                mDatabase.putUsageStats(i, mCurrentStats[i]);
            }
            mStatsChanged = false;
        } catch (IOException e) {
        }
    }
}
Copy the code

Data will be written to files of each interval type. Next call the putUsageStats method in UsageStatsDatabase:

UsageStatsDatabase

public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
    synchronized (mLock) {
        // The first part
        AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime);
        if (f == null) {
            f = new AtomicFile(new File(mIntervalDirs[intervalType],
            Long.toString(stats.beginTime)));
            mSortedStatFiles[intervalType].put(stats.beginTime, f);
        }
        
        // Part 2UsageStatsXml.write(f, stats); stats.lastTimeSaved = f.getLastModifiedTime(); }}Copy the code

TimeSparseArray[] mSortedStatFiles, from LongSpareArray

  • In the first part, obtain whether the corresponding time file in mSortedStatFiles exists. If not, create a new one according to the corresponding time interval type. After creation, add time as key and file object as value to TimeSparseArray collection. This type is ordered and looks for the key through binary first, and overwrites the data if it exists.

  • The second part performs the write XML operation by calling the usagestatsxml. write method:

UsageStatsXml

private static final String USAGESTATS_TAG = "usagestats";

static void write(OutputStream out, IntervalStats stats) throws IOException {
    FastXmlSerializer xml = new FastXmlSerializer();
    xml.setOutput(out, "utf-8");
    xml.startDocument("utf-8".true);
    xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output".true);
    xml.startTag(null, USAGESTATS_TAG);
    xml.attribute(null, VERSION_ATTR, Integer.toString(CURRENT_VERSION));

    UsageStatsXmlV1.write(xml, stats);

    xml.endTag(null, USAGESTATS_TAG);
    xml.endDocument();
}
Copy the code

The start tag is USAGESTATS_TAG, which uses UsageStatsXmlV1 to write data:

UsageStatsXmlV1

public static void write(XmlSerializer xml, IntervalStats stats) throws IOException {
    xml.startTag(null, PACKAGES_TAG);
    final int statsCount = stats.packageStats.size();
    for (int i = 0; i < statsCount; i++) {
        writeUsageStats(xml, stats, stats.packageStats.valueAt(i));
    }
    xml.endTag(null, PACKAGES_TAG);
}

private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats, final UsageStats usageStats) throws IOException {
    xml.startTag(null, PACKAGE_TAG);

    // Write the time offset.
    XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR, usageStats.mLastTimeUsed - stats.beginTime);

    XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
    XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
    if (usageStats.mAppLaunchCount > 0) {
        XmlUtils.writeIntAttribute(xml, APP_LAUNCH_COUNT_ATTR, usageStats.mAppLaunchCount);
    }
    writeChooserCounts(xml, usageStats);
    xml.endTag(null, PACKAGE_TAG);
}
Copy the code

At this point we find that the package name, duration, number of uses, mLastEvent are all written to disk.

MLastEvent corresponds to a foreground or background event, which is of type int. The foreground is 1, the background is 2, and the end of the day event is 3.

3.5 Review of project problems

  • Analyze problems with source code

Android9.0 writes most of the app usage details to disk, but Android9.0 does not write app usage times to disk. There is also a delay of 20 minutes in writing to the disk. If data is fetched from the disk every time, the number of reads in Android versions below 9.0 must be inaccurate.

The related version differences are as follows:

/ / the Android 7.1
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
        final UsageStats usageStats) throws IOException {
    xml.startTag(null, PACKAGE_TAG);
    // Write the time offset.
    XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
            usageStats.mLastTimeUsed - stats.beginTime);
    XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
    XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
    xml.endTag(null, PACKAGE_TAG);
}
/ / the Android 8.1
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
        final UsageStats usageStats) throws IOException {
    xml.startTag(null, PACKAGE_TAG);
    // Write the time offset.
    XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
            usageStats.mLastTimeUsed - stats.beginTime);
    XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
    XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
    writeChooserCounts(xml, usageStats);
    xml.endTag(null, PACKAGE_TAG);
}

/ / the Android 9.0
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
        final UsageStats usageStats) throws IOException {
    xml.startTag(null, PACKAGE_TAG);
    // Write the time offset.
    XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
            usageStats.mLastTimeUsed - stats.beginTime);
    XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
    XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
    if (usageStats.mAppLaunchCount > 0) {
        XmlUtils.writeIntAttribute(xml, APP_LAUNCH_COUNT_ATTR, usageStats.mAppLaunchCount);
    }
    writeChooserCounts(xml, usageStats);
    xml.endTag(null, PACKAGE_TAG);
}
/ / the Android 10.0
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
        final UsageStats usageStats) throws IOException {
    xml.startTag(null, PACKAGE_TAG);
    // Write the time offset.
    XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
            usageStats.mLastTimeUsed - stats.beginTime);
    XmlUtils.writeLongAttribute(xml, LAST_TIME_VISIBLE_ATTR,
            usageStats.mLastTimeVisible - stats.beginTime);
    XmlUtils.writeLongAttribute(xml, LAST_TIME_SERVICE_USED_ATTR,
            usageStats.mLastTimeForegroundServiceUsed - stats.beginTime);
    XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_VISIBLE_ATTR, usageStats.mTotalTimeVisible);
    XmlUtils.writeLongAttribute(xml, TOTAL_TIME_SERVICE_USED_ATTR,
            usageStats.mTotalTimeForegroundServiceUsed);
    XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
    if (usageStats.mAppLaunchCount > 0) {
        XmlUtils.writeIntAttribute(xml, APP_LAUNCH_COUNT_ATTR, usageStats.mAppLaunchCount);
    }
    writeChooserCounts(xml, usageStats);
    xml.endTag(null, PACKAGE_TAG);
}
Copy the code

Later, the granularity of written data becomes smaller and smaller. For example, the duration of application visibility and foreground service are written to disks. This is because later Android also does the application usage details function in the Settings, if these data are not written, the data will be inconsistent.

3.5.1 Solution

In the early stage of the project, this App was a system-level App. We could monitor the broadcast of screen going off and immediately obtain the application usage data from the last screen going off to the current screen going off. Although the interval would be more than 20 minutes, the latest data would first be written into the memory after screen going off. Data will be written into the disk within 20 minutes, causing data loss for a certain number of times. However, the probability of data loss is low and acceptable.

In the late stage of the project, the system-level attributes of the App were removed and it could only be developed as a common App. On the one hand, the framework was modified and the application usage times were persisted to the disk. If the patch of the framework is not integrated, it can realize the data saving logic of the App of the earlier project in another system-level service, save the data to the local database immediately, provide data interface externally, and strengthen the judgment of permissions to avoid misuse. So the App can get the latest data.