@file:Suppress("DEPRECATED_IDENTITY_EQUALS") package com.gh.common.util import android.annotation.SuppressLint import android.annotation.TargetApi import android.app.Activity import android.app.AppOpsManager import android.app.AppOpsManager.MODE_ALLOWED import android.app.AppOpsManager.OPSTR_GET_USAGE_STATS import android.app.usage.UsageEvents import android.app.usage.UsageStatsManager import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.os.Build import android.os.Process import android.preference.PreferenceManager import android.provider.Settings import androidx.annotation.RequiresApi import com.gh.common.util.UsageStatsHelper.getDataByUsageEvents import com.gh.common.util.UsageStatsHelper.getDataByUsageStats import com.gh.gamecenter.manager.UserManager import com.gh.gamecenter.retrofit.BiResponse import com.gh.gamecenter.retrofit.RetrofitManager import com.halo.assistant.HaloApp import com.lightgame.utils.Utils import okhttp3.MediaType import okhttp3.RequestBody import okhttp3.ResponseBody import org.json.JSONArray import org.json.JSONObject import java.util.* /** * 参考资料: https://developer.android.com/reference/android/app/usage/UsageStatsManager * * queryUsageStats 无法准确获取设定时间内的使用情况,所以以下统计有两个策略(但依然无法解决queryUsageStats初始取值问题) * * [getDataByUsageStats]:用于获取时间跨度较大的数据(至少跨度一天) * [getDataByUsageEvents]:用于获取时间跨度较小的数据(只用于获取当天数据) * * 详情可见:https://stackoverflow.com/questions/36238481/android-usagestatsmanager-not-returning-correct-daily-results * */ object UsageStatsHelper { const val USAGE_STATUS_REQUEST_CODE = 233 const val USAGE_STATUS_SP_KEY = "usage_status_sp_key" private val mApi = RetrofitManager.getInstance(HaloApp.getInstance().application).api private val mPreference = PreferenceManager.getDefaultSharedPreferences(HaloApp.getInstance().application) /** * 一般用于获取时间跨度较大的数据 * * 当天数据获取是有问题的 * 例如: 设置起始时间为上午10点,当时返回的数据还是包括更前的数据(例如9点) */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1) private fun getDataByUsageStats(lastPostTime: Long): JSONArray? { val usageStatsManager = HaloApp .getInstance().application .getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager val usageStatsMap = usageStatsManager.queryAndAggregateUsageStats( lastPostTime, System.currentTimeMillis()) ?: return null val postBody = JSONArray() for (entry in usageStatsMap) { if (entry.value.totalTimeInForeground != 0L) { val jsonObject = JSONObject() jsonObject.put("package", entry.key) jsonObject.put("duration", (entry.value.totalTimeInForeground / 1000)) postBody.put(jsonObject) debugOnly { Utils.log("UsageStats post log==>" + entry.key + ":" + (entry.value.totalTimeInForeground / 1000)) } } } return postBody } /** * 一般用于获取时间跨度较小的数据 * 在此功能只用来获取当天数据(queryEvents 获取的数据只保留几天内的数据) */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1) private fun getDataByUsageEvents(startTime: Long): JSONArray? { val mUsageStatsManager = HaloApp .getInstance() .application .getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager val usageEvents = mUsageStatsManager.queryEvents(startTime, System.currentTimeMillis()) val allEvents = ArrayList() var currentEvent: UsageEvents.Event while (usageEvents.hasNextEvent()) { currentEvent = UsageEvents.Event() usageEvents.getNextEvent(currentEvent) if (currentEvent.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND || currentEvent.eventType == UsageEvents.Event.MOVE_TO_BACKGROUND) { allEvents.add(currentEvent) } } val pakAndTime = HashMap() for (i in 0 until allEvents.size - 1) { val curEvent = allEvents[i] val nextEvent = allEvents[i + 1] if (curEvent.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND && nextEvent.eventType == UsageEvents.Event.MOVE_TO_BACKGROUND && curEvent.className == nextEvent.className) { val diff = nextEvent.timeStamp - curEvent.timeStamp if (pakAndTime[curEvent.packageName] == null) { pakAndTime[curEvent.packageName] = diff } else { pakAndTime[curEvent.packageName] = pakAndTime[curEvent.packageName]!! + diff } } } val postBody = JSONArray() for (pkg in pakAndTime.keys) { val totalTime = pakAndTime[pkg] ?: return null val jsonObject = JSONObject() jsonObject.put("package", pkg) jsonObject.put("duration", (totalTime / 1000)) postBody.put(jsonObject) Utils.log("UsageStats getDataByUsageEvents->" + pkg + " :" + (totalTime / 1000)) } return postBody } /** * 第一次统计默认起始时间 */ private fun getDefaultBeginTime(): Long { val calendar = Calendar.getInstance() calendar.add(Calendar.YEAR, -5) return calendar.timeInMillis } @SuppressLint("CheckResult") @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) private fun postUsageStats(beginTime: Long) { debugOnly { Utils.log("UsageStats->beginTime:$beginTime endTime:" + System.currentTimeMillis()) } val postBody = if (isSameDay(beginTime, System.currentTimeMillis())) { getDataByUsageEvents(beginTime) } else { getDataByUsageStats(beginTime) } if (postBody == null || postBody.length() == 0) { debugOnly { Utils.log("UsageStats: 没有可上传的数据") } return } val body = RequestBody.create(MediaType.parse("application/json"), postBody.toString()) mApi.postUsageStatus(body, UserManager.getInstance().userId) .subscribe(object : BiResponse() { override fun onSuccess(data: ResponseBody) { debugOnly { Utils.log("UsageStats: 数据上传成功") } } }) } @JvmStatic @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1) fun checkForPermission(): Boolean { val appOps = HaloApp .getInstance().application .getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager val mode = appOps.checkOpNoThrow( OPSTR_GET_USAGE_STATS, Process.myUid(), HaloApp.getInstance().application.packageName) return mode == MODE_ALLOWED } @JvmStatic @SuppressLint("CheckResult") fun checkAndPostUsageStats() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { debugOnly { Utils.log("UsageStats checkAndPostUsageStats Android 版本小于22") } return } if (!checkForPermission()) { debugOnly { Utils.log("UsageStats checkAndPostUsageStats 没有授予权限") } return } if (!mPreference.getBoolean(USAGE_STATUS_SP_KEY, true)) { debugOnly { Utils.log("UsageStats checkAndPostUsageStats 有权限,但用户在设置关闭统计") } return } mApi.getUsageStatusUpdateTime(HaloApp.getInstance().gid) .subscribe(object : BiResponse() { override fun onSuccess(data: ResponseBody) { val body = JSONObject(data.string()) val lastPostTime = body.getLong("update_time") * 1000 val beginTime = if (lastPostTime == 0L) { getDefaultBeginTime() } else { lastPostTime } postUsageStats(beginTime) } }) } private fun isSameDay(timeA: Long, timeB: Long): Boolean { if (timeA == timeB) { return true } val calendarB = Calendar.getInstance() calendarB.time = Date(timeB) val calendarA = Calendar.getInstance() calendarA.time = Date(timeA) return (calendarB.get(Calendar.YEAR) === calendarA.get(Calendar.YEAR) && calendarB.get(Calendar.MONTH) === calendarA.get(Calendar.MONTH) && calendarB.get(Calendar.DATE) === calendarA.get(Calendar.DATE)) } @JvmStatic fun skipToUsageStats(context: Context, requestCode: Int = -1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { try { if (context is Activity && requestCode != -1) { context.startActivityForResult(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS), requestCode) } else { context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)) } } catch (e: ActivityNotFoundException) { Utils.toast(context, "当前设备不支持直接跳转查看应用使用情况页面,请手动进入并打开") e.printStackTrace() } } else { Utils.toast(context, "当前设备不支持查看应用使用情况") } } }