1. 概况
? ? ? ? 在项目中,有需求要实现监测手机系统中设备事件和应用事件,然后每隔固定的周期上报给服务器,后台拿到这些数据后,挖掘分析统计出设备的销售数量,安装应用使用活跃度,用户使用时长等有用的价值信息,统计的设备事件:比如手机锁屏,手机解锁, 应用事件:比如应用启动,应用退出,应用安装卸载等数据。
? ? ? ? 在调研需求实现的过程中,通过查阅android源码,博客这方面的资料,得出Android在5.1版本之后就有搜集系统数据有自己的服务和API,那么我们就抽丝剥茧的去阅读理解源码,并运用到自己的项目中,?本篇文章讲解内容是基于Android10平台,UsageStatsService 后面简称为USS。
2.?UsageStatsService
2.1 定义
????????它是收集、聚合和保存应用程序使用数据的服务, 这些数据可以被 AppOps 授权的应用查询。源码路径 framework/base/services/usage/java/com/android/server/usage/下。
2.2 服务启动
? ? ? ? 我们来看看UsageStatsService启动的流程:
#我们只看关键代码和流程
#1 /frameworks/base/services/java/com/android/server/SystemServer.java
/**
* The main entry point from zygote.
*/
public static void main(String[] args) {
new SystemServer().run();
}
#2
private void run() {
''''''''''''
// Start services.
try {
traceBeginAndSlog("StartServices");
startBootstrapServices();
startCoreServices(); // 在这个方法中启动UsageStatsService服务
startOtherServices();
SystemServerInitThreadPool.shutdown();
} catch (Throwable ex) {
Slog.e("System", "******************************************");
Slog.e("System", "************ Failure starting system services", ex);
throw ex;
} finally {
traceEnd();
}
}
#3
private void startCoreServices() {
''''''''''''
// Tracks application usage stats.
traceBeginAndSlog("StartUsageService");
mSystemServiceManager.startService(UsageStatsService.class);
mActivityManagerService.setUsageStatsManager(
LocalServices.getService(UsageStatsManagerInternal.class));
traceEnd();
..........
}
从上可以看出它是由系统SystemServer进程启动的,UsageStatsService 又是继承系统基类SystemService服务,接下来,在看看onStart()生命周期方法中的代码
# framework/base/services/usage/java/com/android/server/usage/UsageStatsService.java
@Override
public void onStart() {
'''''''''''''
# 第一点
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());
}
# 第二点
publishLocalService(UsageStatsMMOVE_TO_BACKGROUNDanagerInternal.class, new LocalService());
publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService());
# 第三点
// Make sure we initialize the data, in case job scheduler needs it early.
getUserDataAndInitializeIfNeededLocked(UserHandle.USER_SYSTEM, mSystemTimeSnapshot);
''''''''''''
}
# /frameworks/base/services/core/java/com/android/server/SystemService.java
protected final void publishBinderService(String name, IBinder service,
boolean allowIsolated, int dumpPriority) {
ServiceManager.addService(name, service, allowIsolated, dumpPriority);
}
第一点: 创建 data/system/usagestats 文件夹,可以得知设备统计数据文件都是放到此目录下的
第二点:??publishBinderService这个方法,就是 USS 向??ServiceManager 注册自己,这样子客户端通过?ServiceManager.getService(Context.USAGE_STATS_SERVICE) 就可以获取USS服务了。?
第三点: 初始化UserUsageStatsService,每个用户(主用户,访客)的USS服务类,它是真正做事情的类,统计有Activity的启动和退出,? Service启动和停止, 分享,锁屏,解锁,设备启动和关机等事件。
private static String eventToString(int eventType) {
switch (eventType) {
case Event.NONE:
return "NONE";
case Event.ACTIVITY_PAUSED:
return "ACTIVITY_PAUSED";
case Event.ACTIVITY_RESUMED:
return "ACTIVITY_RESUMED";
case Event.FOREGROUND_SERVICE_START:
return "FOREGROUND_SERVICE_START";
case Event.FOREGROUND_SERVICE_STOP:
return "FOREGROUND_SERVICE_STOP";
case Event.ACTIVITY_STOPPED:
return "ACTIVITY_STOPPED";
case Event.END_OF_DAY:
return "END_OF_DAY";
case Event.ROLLOVER_FOREGROUND_SERVICE:
return "ROLLOVER_FOREGROUND_SERVICE";
case Event.CONTINUE_PREVIOUS_DAY:
return "CONTINUE_PREVIOUS_DAY";
case Event.CONTINUING_FOREGROUND_SERVICE:
return "CONTINUING_FOREGROUND_SERVICE";
case Event.CONFIGURATION_CHANGE:
return "CONFIGURATION_CHANGE";
case Event.SYSTEM_INTERACTION:
return "SYSTEM_INTERACTION";
case Event.USER_INTERACTION:
return "USER_INTERACTION";
case Event.SHORTCUT_INVOCATION:
return "SHORTCUT_INVOCATION";
case Event.CHOOSER_ACTION:
return "CHOOSER_ACTION";
case Event.NOTIFICATION_SEEN:
return "NOTIFICATION_SEEN";
case Event.STANDBY_BUCKET_CHANGED:
return "STANDBY_BUCKET_CHANGED";
case Event.NOTIFICATION_INTERRUPTION:
return "NOTIFICATION_INTERRUPTION";
case Event.SLICE_PINNED:
return "SLICE_PINNED";
case Event.SLICE_PINNED_PRIV:
return "SLICE_PINNED_PRIV";
case Event.SCREEN_INTERACTIVE:
return "SCREEN_INTERACTIVE";
case Event.SCREEN_NON_INTERACTIVE:
return "SCREEN_NON_INTERACTIVE";
case Event.KEYGUARD_SHOWN:
return "KEYGUARD_SHOWN";
case Event.KEYGUARD_HIDDEN:
return "KEYGUARD_HIDDEN";
case Event.DEVICE_SHUTDOWN:
return "DEVICE_SHUTDOWN";
case Event.DEVICE_STARTUP:
return "DEVICE_STARTUP";
default:
return "UNKNOWN_TYPE_" + eventType;
}
}
2.3 业务处理
# framework/base/services/usage/java/com/android/server/usage/UsageStatsService.java
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() {
mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL);
}
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;
?USS 每20分钟更新一次数据并存入到/data/system/usagestats/路径下的XML文件中,XML的所有操作,例如读,写等,都被封装在类UsageStatsXmlV1.java中,统一由UsageStatsDatabase去操作读写数据。
3.?UsageStatsManager
3.1 定义
提供对此设备的使用统计数据的访问。 使用数据的访问时间间隔可以用天、周、月和年。简单理解为客户端对象,用来和USS通信的对象,就好比 AcitivityManager 与 AMS 的关系。我们来看看类主要的方法:
方法 | 作用和用途 | queryAndAggregateUsageStats(long beginTime, long endTime) | 获取指定时间区间内使用统计数据,以应用包名为键值进行数据合并。 | queryConfigurations(int intervalType, long beginTime, long endTime) | 获取指定时间区间内硬件配置信息统计数据。 | queryEventStats(int intervalType, long beginTime, long endTime) | 获取指定时间区间内发生组件状态变化事件统计数据。 | queryEvents(long beginTime, long endTime) | 获取指定时间区间内组件状态变化事件 | queryEventsForSelf(long beginTime, long endTime) | 与queryEvents相似,获取指定时间区间内本应用的组件状态变化事件 | queryUsageStats(int intervalType, long beginTime, long endTime | 获取指定时间区间内应用使用统计数据。 |
queryEventStats和??queryUsageStats 这两个方法比较重要,与我们的需求强相关:
public List<UsageStats> queryUsageStats(int intervalType, long beginTime, long endTime) {
try {
@SuppressWarnings("unchecked")
ParceledListSlice<UsageStats> slice = mService.queryUsageStats(intervalType, beginTime,
endTime, mContext.getOpPackageName());
if (slice != null) {
return slice.getList();
}
} catch (RemoteException e) {
// fallthrough and return the empty list.
}
return Collections.emptyList();
}
public List<EventStats> queryEventStats(int intervalType, long beginTime, long endTime) {
try {
@SuppressWarnings("unchecked")
ParceledListSlice<EventStats> slice = mService.queryEventStats(intervalType, beginTime,
endTime, mContext.getOpPackageName());
if (slice != null) {
return slice.getList();
}
} catch (RemoteException e) {
// fallthrough and return the empty list.
}
return Collections.emptyList();
}
通过返回参数List<UsageStats>? ?List<EventStats>,我们继续来看看UsageStats?EventStats这两个Parcelable 序列化类
3.2??EventStats
它是在指定时间区间内某个类型事件统计数据的封装类,它有一个内部类Event类,这里面才是应用真正的事件统计信息.? 我们可以去看看定义哪些事件类型变量:
/**
* An event representing a state change for a component.
*/
public static final class Event {
//Activity Resume 和 pause 事件
public static final int ACTIVITY_RESUMED= 1;
public static final int ACTIVITY_PAUSED = 2;
public static final int END_OF_DAY = 3;
public static final int CONTINUE_PREVIOUS_DAY = 4;
//设备configuration改变事件
public static final int CONFIGURATION_CHANGE = 5;
//表示系统以某种方式与应用进行了交互的事件。
public static final int SYSTEM_INTERACTION = 6;
//表示用户以某种方式与应用进行了交互的事件。
public static final int USER_INTERACTION = 7;
//表示用户进行过快捷方式操作的事件
public static final int SHORTCUT_INVOCATION = 8;
//表示用户为 ChooserActivity 选择了一个应用事件
public static final int CHOOSER_ACTION = 9;
//用户查看了通知的事件类型
public static final int NOTIFICATION_SEEN = 10;
//
public static final int STANDBY_BUCKET_CHANGED = 11;
//表示应用发布中断通知的事件类型。
public static final int NOTIFICATION_INTERRUPTION = 12;
//
public static final int SLICE_PINNED_PRIV = 13;
public static final int SLICE_PINNED = 14;
//表示屏幕已进入交互状态的事件
public static final int SCREEN_INTERACTIVE = 15;
//表示屏幕处于非交互状态事件
public static final int SCREEN_NON_INTERACTIVE = 16;
//设备锁屏事件
public static final int KEYGUARD_SHOWN = 17;
//设备解锁事件
public static final int KEYGUARD_HIDDEN = 18;
//启动一个前台service事件
public static final int FOREGROUND_SERVICE_START = 19;
//停止一个前台service事件
public static final int FOREGROUND_SERVICE_STOP = 20;
//表示前台服务在时间间隔开始时处于启动状态。
public static final int CONTINUING_FOREGROUND_SERVICE = 21;
//表示当统计信息在时间间隔结束时翻转时前台服务处于启动状态。
public static final int ROLLOVER_FOREGROUND_SERVICE = 22;
//Activity处于stopped 和 Destoryed 状态
public static final int ACTIVITY_STOPPED = 23;
public static final int ACTIVITY_DESTROYED = 24;
//表示更新数据到磁盘事件
public static final int FLUSH_TO_DISK = 25;
// 设备启动和关机事件
public static final int DEVICE_SHUTDOWN = 26;
public static final int DEVICE_STARTUP = 27;
}
常用的共有方法如下:
方法 | 功能 | getEventType() | 获取事件类型 | getPackageName() | 和UsageStats类中的getPackageName()方法一样,获取应用的包名 | getTimeStamp() | 发生此事件的时间戳,以毫秒为单位 | getClassName() | 该方法的作用是获取类名,在UsageStats类中没有此方法,该方法配合getPackageName()方法可以精确的找到是那个应用的那个界面的统计信息 | getConfiguration() | 和ConfigurationStats类中的getConfiguration()方法一样,获取Configuration类的对象 以上是Event内部类中的主要方法和作用 | hasNextEvent() | 返回是否有更多要使用的事件读取,没有会返回false,有会返回true,一般在循环读取数据的时候使用 | getNextEvent(Event eventOut) | 获取应用事件的统计信息,会将数据保存到eventOut中,返回值为boolean型,true表示获取成功,false表示获取失败 |
3.3? UsageStats
这个类包含针对特定时间范围的应用程序包的使用情况统计信息,这里面的数据就是我们需要的应用使用情况的统计信息,该类里面主要包含了五个方法分别是:
方法 | 功能 | getPackageName() | 获取应用程序的包名 | getFirstTimeStamp() | 获取第一次运行的时间,以毫秒为单位 | getLastTimeStamp() | 获取最后一次运行的时间,以毫秒为单位 | getLastTimeUsed() | 获取上一次运行的时间,以毫秒为单位 | getTotalTimeInForeground() | 获取应用在前台的总时间,以毫秒为单位 | getLaunchCount() | 获取应用的启动次数 这个是通过反射的方式获取的,没有直接的API,如下: |
// 利用反射,获取UsageStats中统计的应用使用次数
public int getLaunchCount(UsageStats usageStats) throws IllegalAccessException {
Field field = null;
try {
field = usageStats.getClass().getDeclaredField("mLaunchCount");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return (int) field.get(usageStats);
}
4. 需求实现
把上面必备的知识了解清楚后,开始着手我们的需求实现, 通过上述API方法把需要的事件类型统计出来,然后上报给服务器端。实现大致逻辑:通过开机广播启动一个自定义service,此服务的作用整理搜集起来的数据,每隔一段时间上报给服务器。
4.1 定义权限
如果要使用这些API方法的话,首先您必须先在清单中声明 android.permission.PACKAGE_USAGE_STATS 权限,用户还必须通过 Settings > Security > Apps 为该应用启用访问使用情况的权限
public static void checkUsageStateAccessPermission(Context context) {
if(!AppUsageUtil.checkAppUsagePermission(context)) {
AppUsageUtil.requestAppUsagePermission(context);
}
}
public static boolean checkAppUsagePermission(Context context) {
UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
if(usageStatsManager == null) {
return false;
}
long currentTime = System.currentTimeMillis();
// try to get app usage state in last 1 min
List<UsageStats> stats = usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, currentTime - 60 * 1000, currentTime);
if (stats.size() == 0) {
return false;
}
return true;
}
public static void requestAppUsagePermission(Context context) {
Intent intent = new Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.i(TAG,"Start usage access settings activity fail!");
}
}
4.2 统计手机在一天中锁屏和解锁事件
这个需求的用途:可以用来衡量用户在一天中使用手机的频率,以这个小需求做为例子讲解
# 1.BootReceiver.java
#此类的作用就是接收开机广播,然后启动自己搜集数据的服务类。
public class BOOTReceiver extends BroadcastReceiver {
private static final String TAG = "BOOTReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (null == context) {
return;
}
Log.d(TAG, "BOOTReceiver");
try {
context.startService(new Intent(context, MyUsageStatsService.class));
} catch (Exception e) {
Log.e(TAG, " " + e.getMessage());
}
}
}
获取系统数据UsageEvents ,?UsageStats 工具类
/**
* 获取系统的数据,包括event和Usage
*/
public class EventUtils {
public static final String TAG = "EventUtils";
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("M-d-yyyy HH:mm:ss");
@SuppressWarnings("ResourceType")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static ArrayList<UsageEvents.Event> getEventList(Context context, long startTime, long endTime){
ArrayList<UsageEvents.Event> mEventList = new ArrayList<>();
UsageStatsManager mUsmManager = (UsageStatsManager) context.getSystemService("usagestats");
UsageEvents events = mUsmManager.queryEvents(startTime, endTime);
while (events.hasNextEvent()) {
UsageEvents.Event e = new UsageEvents.Event();
events.getNextEvent(e);
if (e != null) {
mEventList.add(e);
}
}
return mEventList;
}
@SuppressWarnings("ResourceType")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static ArrayList<UsageStats> getUsageList(Context context, long startTime, long endTime) {
ArrayList <UsageStats> list = new ArrayList<>();
UsageStatsManager mUsmManager = (UsageStatsManager) context.getSystemService("usagestats");
Map<String, UsageStats> map = mUsmManager.queryAndAggregateUsageStats(startTime, endTime);
for (Map.Entry<String, UsageStats> entry : map.entrySet()) {
UsageStats stats = entry.getValue();
list.add(stats);
}
return list;
}
}
下面这个类是搜集和封装数据类:
public class UseTimeDataManager {
//锁屏和解锁事件字段
public static final int KEYGUARD_SHOWN = 17;
public static final int KEYGUARD_HIDDEN = 18;
//上报给服务器的字段
public static final int KEYGUARD_SHOWN_REPORT = 13;
public static final int KEYGUARD_HIDDEN_REPORT = 14;
//记录从系统中读取的数据
private ArrayList<UsageEvents.Event> mEventList;
//设备锁屏数据
private ArrayList<UsageEvents.Event> mDeviceLockedChecked;
//设备锁屏 ArrayList上报数据集合
private ArrayList<DeviceEvent> mDeviceLockedObject = new ArrayList<>();
public ArrayList<DeviceEvent> getDeviceLockedObject() {
return mDeviceLockedObject;
}
//设备解锁数据
private ArrayList<UsageEvents.Event> mDeviceUnLockedChecked;
//设备解锁 ArrayList上报数据集合
private ArrayList<DeviceEvent> mDeviceUnLockedObject = new ArrayList<>();
public ArrayList<DeviceEvent> getDeviceUnLockedObject() {
return mDeviceUnLockedObject;
}
//从系统中获取event数据
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private ArrayList<UsageEvents.Event> getEventList(int dayNumber) {
ArrayList<UsageEvents.Event> mEventList = new ArrayList<>();
long endTime = 0, startTime = 0;
if (dayNumber == 0) {
endTime = System.currentTimeMillis();
startTime = DateTransUtils.getZeroClockTimestamp(endTime);
} else {
endTime = DateTransUtils.getZeroClockTimestamp(System.currentTimeMillis() - (dayNumber - 1) * DateTransUtils.DAY_IN_MILLIS) - 1;
startTime = endTime - DateTransUtils.DAY_IN_MILLIS + 1;
}
return EventUtils.getEventList(mContext, startTime, endTime);
}
//构造方法
public UseTimeDataManager() {
// 这个dayNumber变量,表示获取前xx天的数据
mEventList = getEventList(dayNumber);
mDeviceLockedChecked = getActionEventChecked(KEYGUARD_SHOWN);
generateDeviceLockedObject(mDeviceLockedChecked);
mDeviceUnLockedChecked = getActionEventChecked(KEYGUARD_HIDDEN);
generateDeviceUnLockedObject(mDeviceUnLockedChecked);
}
//根据事件类型来获取相应的数据
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private ArrayList<UsageEvents.Event> getActionEventChecked(int eventType) {
ArrayList<UsageEvents.Event> mList = new ArrayList<>();
for (int i = 0; i < mEventList.size(); i++) {
if (mEventList.get(i).getEventType() == eventType) {
mList.add(mEventList.get(i));
}
}
return mList;
}
//通过封装DeviceEvent这个javaBean对象,转换成json数据上报给服务器
private void generateDeviceUnLockedObject(ArrayList<UsageEvents.Event> deviceUnLockedChecked) {
for (int i=0; i<deviceUnLockedChecked.size(); i++) {
mDeviceUnLockedObject.add(new DeviceEvent(
mContext,
deviceUnLockedChecked.get(i).getTimeStamp(),
KEYGUARD_HIDDEN_REPORT
));
}
}
private void generateDeviceLockedObject(ArrayList<UsageEvents.Event> deviceLockedChecked) {
for (int i=0; i<deviceLockedChecked.size(); i++) {
mDeviceLockedObject.add(new DeviceEvent(
mContext,
deviceLockedChecked.get(i).getTimeStamp(),
KEYGUARD_SHOWN_REPORT
));
}
}
}
最后我们来看服务端MyUsageStatsService.java, 每隔1天上报一次数据,对时间间隔要求比较准备,所以我用的是AlarmManager + Broadcast 的组合,用AlarmManager 每隔24小时去启动一次广播,然后在广播里面在启动MyUsageStatsService, 因为MyUsageStatsService在第一次开机已经被启动过,当再次调用startService()方法时,不会走onCreate 只会走onStartCommand 方法。我们把上报数据的任务就放在onStartCommand生命周期方法中。
public class MyUsageStatsService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//通过AlamrManager + Broadcast 循环(每一天)上报数据的任务
Log.d(TAG, " MyUsageStatsService onStartCommand, Report datas once a day");
executeTasksLooper(mContext);
//每隔一天唤醒广播一次,广播里面在启动MyUsageStatsService
Intent intent1 = new Intent(this, WakeServiceReceiver.class);
generateCycleTask(intent1, WAKE_SERVICE_USAGE, AlarmManager.INTERVAL_DAY);
}
private void executeTasksLooper(Context mContext) {
''''''''''''''''
//这里是上报服务数据的地方
}
//每24小时循环唤醒请求任务
public void generateCycleTask(Intent intent, String action, long interval){
AlarmManager alarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
intent.setAction(action);
long intervalTime = SystemClock.elapsedRealtime() + interval;
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, intervalTime, pendingIntent);
}
}
此广播是专用来唤醒MyUsageStatsService服务类的
public class WakeServiceReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent i) {
if (null == context) {
return;
}
try {
context.startService(new Intent(context, MyUsageStatsService.class));
Log.d("UseTimeDataManager", "start MyUsageStatsService from WakeServiceReceiver : report data recyle task");
} catch (Exception e) {
Log.d("UseTimeDataManager", "WakeServiceReceiver start service failed :" + e.getMessage());
}
}
}
通过上述代码,就可以实现每隔24小时上报数据给服务器端,封装的DeviceEvent javaBean对象就是一些关于手机型号,内存大小,安卓版本号,制造商等信息,就不细列出来了。这里的循环任务就是通过AlarmManager 和 Broadcast 联合一起实现的。当然还有上报给服务器端的代码没有贴出来,由于是公司的项目,不好全部码出,只说明大致思路,供参考。
在UsageEvents.Event中定义的27种事件类型,参考上述代码,需要数据上报的话,都是可以实现的。还有需求是关于应用启动和退出的上报,这类数据可以用来衡量APP的活跃程度,我是参考USS获取系统数据的原理在Fwk层中实现的,应用启动依据这个app新创建task, 应用退出依据是这个task已销毁,如果想要了解具体实现的话,可以私我沟通讨论下。
另外,AlarmManager + Broadcast 循环启动任务的Demo已上传,供参考。AlarmManager+Broadcast循环启动任务-Android文档类资源-CSDN下载
|