$AppStart、$AppEnd全埋点方案
2019-05-10 本文已影响0人
Peakmain
参考书籍:《Android全埋点解决方案》
源码:https://github.com/Peakmain/baselibrary
背景
- 基于上篇$AppViewScreen全埋点事件
- $AppStar事件,应用程序处于前台
- $AppEnd事件,应用程序处于后台
原理
一、应用程序如果有多个进程该如何判断是处于前台还是后台
- 实际涉及到的是应用程序多进程之间的通信
- 目前采用的方案ContentProvider+SharedPreferences机制来解决
二、应用程序如果发生崩溃或者被强杀死该如何判断该应用处于前台还是后台
- 一个应用程序,它的页面退出了,如果在30s内没有新的页面打开,就认为这个应用程序处于后台
- 如果一个应用程序的一个新页面显示出来了,且与上一个页面的退出时间超过30s,就认为这个应用程序处于前台
思路
- Application.ActivityLifecycleCallbacks回掉,监听所有activity的生命周期
- 页面退出(onPause生命周期),启动一个30s的计时器,如果30s内没有新的页面进来(或显示),则触发$AppEnd
- 如果有新的页面进来(或显示),则存储一个标记位来标记已有新的页面进来。
- 若activity之间跨进程,利用ContentProvider+SharedPreferences存储,然后通过ContentObserver监听到新页面进来的标记位改变,并取消上个页面退出时启动的倒计时
- 页面启动的时候(onStart),判断与上个页面退出的时间间隔是否超过30s,如果没有超过30s,则触发$AppViewScreen事件
- 如果超过30s,判断是否触发了AppEnd事件,因为App可能崩溃了或者被强杀死了,没有触发AppEnd事件,若没有,先触发AppEnd事件,然后触发$AppViewScreen事件和AppStart事件
代码实现
AppViewScreenDataPrivate代码修改
首先我们需要一些变量。 时间:30s,倒计时:CountDownTimer,当前activity:WeakReference<Activity>,一个帮助类
private static SensorsDatabaseHelper mDatabaseHelper;
/**
* 当前activity的集合
*/
private static WeakReference<Activity> mCurrentActivity;
//倒计时
private static CountDownTimer countDownTimer;
//30s
private final static int SESSION_INTERVAL_TIME = 30 * 1000;
修改Application.ActivityLifecycleCallbacks
public static void registerActivityLifecycleCallbacks(Application application) {
mDatabaseHelper=new SensorsDatabaseHelper(application.getApplicationContext(),application.getPackageName());
//初始化30s,间隔1s
countDownTimer=new CountDownTimer(SESSION_INTERVAL_TIME,10*1000) {
@Override
public void onTick(long millisUntilFinished) {
}
@Override
public void onFinish() {
trackAppEnd(mCurrentActivity.get());
}
};
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
//添加标记,已经开始了
mDatabaseHelper.commitAppStart(true);
//获得间隔时间
double timeDiff = System.currentTimeMillis() - mDatabaseHelper.getAppPausedTime();
//如果间隔时间大于30s
if (timeDiff > SESSION_INTERVAL_TIME) {
//判断是否已经触发过了$AppEnd
if (!mDatabaseHelper.getAppEndEventState()) {
trackAppEnd(activity);
}
}
if (mDatabaseHelper.getAppEndEventState()) {
//设置未触发$AppEnd
mDatabaseHelper.commitAppEndEventState(false);
trackAppStart(activity);
}
}
@Override
public void onActivityResumed(Activity activity) {
trackAppViewScreen(activity);
}
@Override
public void onActivityPaused(Activity activity) {
mCurrentActivity = new WeakReference<>(activity);
countDownTimer.start();
mDatabaseHelper.commitAppPausedTime(System.currentTimeMillis());
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
});
}
AppStart和AppEnd事件
/**
* $AppStart事件
*/
private static void trackAppStart(Activity activity) {
try {
if (activity == null) {
return;
}
JSONObject properties = new JSONObject();
properties.put("$activity", activity.getClass().getCanonicalName());
properties.put("$title", getActivityTitle(activity));
AppViewScreenDataAPI.getInstance().track("$AppStart", properties);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* $AppEnd事件
*/
private static void trackAppEnd(Activity activity) {
try {
if(activity==null){
return;
}
JSONObject properties=new JSONObject();
properties.put("$activity",activity.getClass().getCanonicalName());
properties.put("$title",getActivityTitle(activity));
AppViewScreenDataAPI.getInstance().track("$AppEnd", properties);
mDatabaseHelper.commitAppEndEventState(true);
mCurrentActivity = null;
} catch (JSONException e) {
e.printStackTrace();
}
}
注册 AppStart 的监听:当appstart的时候,需要去取消上个页面退出时启动的倒计时
public static void registerActivityStateObserver(Application application) {
application.getContentResolver().registerContentObserver(mDatabaseHelper.getAppStartUri(),
false, new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange, Uri uri) {
if (mDatabaseHelper.getAppStartUri().equals(uri)) {
countDownTimer.cancel();
}
}
});
}
SensorsDatabaseHelper帮助类
public class SensorsDatabaseHelper {
private static final String SENSORSDATACONTENTPROVIDER = ".SensorsDataContentProvider/";
//内容解析者
private ContentResolver mContentResolver;
//uri
private Uri mAppStart;
private Uri mAppEndState;
private Uri mAppPausedTime;
public static final String APP_STARTED = "$app_started";
public static final String APP_END_STATE = "$app_end_state";
public static final String APP_PAUSED_TIME = "$app_paused_time";
public SensorsDatabaseHelper(Context context, String packageName) {
mContentResolver = context.getContentResolver();
mAppStart = Uri.parse("content://" + packageName + SENSORSDATACONTENTPROVIDER + SensorsDataTable.APP_STARTED.getName());
mAppEndState = Uri.parse("content://" + packageName + SENSORSDATACONTENTPROVIDER + SensorsDataTable.APP_END_STATE.getName());
mAppPausedTime = Uri.parse("content://" + packageName + SENSORSDATACONTENTPROVIDER + SensorsDataTable.APP_PAUSED_TIME.getName());
}
/**
* 添加标记已经开始了,添加到sharedpreferences
*
* @param appStart true 表示开始了
*/
public void commitAppStart(boolean appStart) {
ContentValues values=new ContentValues();
values.put(APP_STARTED,appStart);
mContentResolver.insert(mAppStart,values);
}
/**
* 将暂停时间 添加到sharedpreferences
*
* @param pausedTime 暂停时间
*/
public void commitAppPausedTime(long pausedTime) {
ContentValues contentValues = new ContentValues();
contentValues.put(APP_PAUSED_TIME, pausedTime);
mContentResolver.insert(mAppPausedTime, contentValues);
}
/**
* 获得 暂停时间
*/
public long getAppPausedTime() {
long pasuedTime = 0;
Cursor cursor = mContentResolver.query(mAppPausedTime, new String[]{APP_PAUSED_TIME}, null, null, null);
if (cursor != null && cursor.getCount() > 0) {
while (cursor.moveToNext()) {
pasuedTime = cursor.getLong(0);
}
}
if (cursor != null) {
cursor.close();
}
return pasuedTime;
}
/**
* 保存状态到sp
* @param appEndState
*/
public void commitAppEndEventState(boolean appEndState) {
ContentValues contentValues=new ContentValues();
contentValues.put(APP_END_STATE,appEndState);
mContentResolver.insert(mAppEndState,contentValues);
}
/**
* app是否已经触发过$AppEnd事件
*/
public boolean getAppEndEventState() {
boolean state = true;
Cursor cursor = mContentResolver.query(mAppEndState, new String[]{APP_END_STATE}, null, null, null);
if (cursor != null && cursor.getCount() > 0) {
while (cursor.moveToNext()) {
state = cursor.getInt(0) > 0;
}
}
if (cursor != null) {
cursor.close();
}
return state;
}
public Uri getAppStartUri() {
return mAppStart;
}
}
SensorsDataContentProvider
public class SensorsDataContentProvider extends ContentProvider{
//id
private final static int APP_START = 1;
private final static int APP_END_STATE = 2;
private final static int APP_PAUSED_TIME = 3;
private static SharedPreferences sharedPreferences;
private static SharedPreferences.Editor mEditor;
private static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private ContentResolver mContentResolver;
@Override
public boolean onCreate() {
if(getContext()!=null){
String packageName = getContext().getPackageName();
//添加
uriMatcher.addURI(packageName + ".SensorsDataContentProvider", SensorsDataTable.APP_STARTED.getName(), APP_START);
uriMatcher.addURI(packageName + ".SensorsDataContentProvider", SensorsDataTable.APP_END_STATE.getName(), APP_END_STATE);
uriMatcher.addURI(packageName + ".SensorsDataContentProvider", SensorsDataTable.APP_PAUSED_TIME.getName(), APP_PAUSED_TIME);
sharedPreferences = getContext().getSharedPreferences("com.peakmain.sdk.AppViewScreenDataAPI", Context.MODE_PRIVATE);
mEditor = sharedPreferences.edit();
mEditor.apply();
mContentResolver = getContext().getContentResolver();
}
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
int code=uriMatcher.match(uri);
//没有数据库使用MatrixCursor替代
MatrixCursor matrixCursor=null;
switch (code) {
case APP_START:
int appStart = sharedPreferences.getBoolean(SensorsDatabaseHelper.APP_STARTED, true) ? 1 : 0;
matrixCursor = new MatrixCursor(new String[]{SensorsDatabaseHelper.APP_STARTED});
matrixCursor.addRow(new Object[]{appStart});
break;
case APP_END_STATE:
int appEnd = sharedPreferences.getBoolean(SensorsDatabaseHelper.APP_END_STATE, true) ? 1 : 0;
matrixCursor = new MatrixCursor(new String[]{SensorsDatabaseHelper.APP_END_STATE});
matrixCursor.addRow(new Object[]{appEnd});
break;
case APP_PAUSED_TIME:
long pausedTime = sharedPreferences.getLong(SensorsDatabaseHelper.APP_PAUSED_TIME, 0);
matrixCursor = new MatrixCursor(new String[]{SensorsDatabaseHelper.APP_PAUSED_TIME});
matrixCursor.addRow(new Object[]{pausedTime});
break;
default:
break;
}
return matrixCursor;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
if (values == null) {
return uri;
}
int code = uriMatcher.match(uri);
switch (code) {
case APP_START:
boolean appStart = values.getAsBoolean(SensorsDatabaseHelper.APP_STARTED);
mEditor.putBoolean(SensorsDatabaseHelper.APP_STARTED, appStart);
mContentResolver.notifyChange(uri, null);
break;
case APP_END_STATE:
boolean appEnd = values.getAsBoolean(SensorsDatabaseHelper.APP_END_STATE);
mEditor.putBoolean(SensorsDatabaseHelper.APP_END_STATE, appEnd);
break;
case APP_PAUSED_TIME:
long pausedTime = values.getAsLong(SensorsDatabaseHelper.APP_PAUSED_TIME);
mEditor.putLong(SensorsDatabaseHelper.APP_PAUSED_TIME, pausedTime);
break;
default:
break;
}
mEditor.commit();
return uri;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
需要记得注册
<application>
<provider
android:name=".datacollection.appstartandend.SensorsDataContentProvider"
android:authorities="${applicationId}.SensorsDataContentProvider"
android:enabled="true"
android:exported="false" />
</application>