diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aa6329525a..56e37139b2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,9 @@ + + diff --git a/app/src/main/java/com/gh/common/util/DialogUtils.java b/app/src/main/java/com/gh/common/util/DialogUtils.java index bef388bdd5..fc8d2e5a8b 100644 --- a/app/src/main/java/com/gh/common/util/DialogUtils.java +++ b/app/src/main/java/com/gh/common/util/DialogUtils.java @@ -23,6 +23,10 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; + import com.gh.common.view.DrawableView; import com.gh.gamecenter.AboutActivity; import com.gh.gamecenter.R; @@ -33,10 +37,6 @@ import com.lightgame.utils.Utils; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; - public class DialogUtils { public static Dialog showWaitDialog(Context context, String msg) { @@ -788,7 +788,7 @@ public class DialogUtils { public static void showPrivacyPolicyDialog(Context context, String title, String content, EmptyCallback callback) { final Context activityContext = checkDialogContext(context); - + // 区分此 dialog 是点击 dialog 外部取消的还是点击返回取消的 AtomicBoolean isCanceledByClickOutsideOfDialog = new AtomicBoolean(true); @@ -804,7 +804,7 @@ public class DialogUtils { } final Dialog dialog = new Dialog(activityContext, R.style.GhAlertDialog); - + dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent); View contentView = LayoutInflater.from(activityContext).inflate(R.layout.dialog_privacy_policy, null); TextView contentTv = contentView.findViewById(R.id.dialog_content); @@ -844,13 +844,13 @@ public class DialogUtils { dialog.setOnDismissListener(d -> { callback.onCallback(); }); - + dialog.setOnCancelListener(cd -> { if (isCanceledByClickOutsideOfDialog.get()) { MtaHelper.onEvent("隐私政策弹窗", "隐私政策弹窗", "点击空白"); } }); - + dialog.setOnKeyListener((dialog1, keyCode, event) -> { if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { isCanceledByClickOutsideOfDialog.set(false); @@ -923,6 +923,48 @@ public class DialogUtils { return dialog; } + public static Dialog showUsageStatsDialog(Context context, final ConfirmListener cmListener, final CancelListener clListener) { + context = checkDialogContext(context); + + final Dialog dialog = new Dialog(context, R.style.GhAlertDialog); + + View contentView = LayoutInflater.from(context).inflate(R.layout.dialog_alert, null); + TextView contentTv = contentView.findViewById(R.id.dialog_content); + TextView titleTv = contentView.findViewById(R.id.dialog_title); + TextView negativeTv = contentView.findViewById(R.id.dialog_negative); + TextView positiveTv = contentView.findViewById(R.id.dialog_positive); + contentTv.setText("开启后可以为你记录每款游戏的游戏时长,你只需再系统【有权查看使用情况的应用】设置中开启即可"); + + titleTv.setText("游戏时长记录"); + negativeTv.setText("暂不"); + positiveTv.setText("去开启"); + + negativeTv.setOnClickListener(view -> { + if (clListener != null) { + clListener.onCancel(); + } + dialog.dismiss(); + }); + + positiveTv.setOnClickListener(view -> { + if (cmListener != null) { + cmListener.onConfirm(); + } + dialog.dismiss(); + }); + + dialog.setOnDismissListener(dialog1 -> { + if (clListener != null) { + clListener.onCancel(); + } + }); + + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.setContentView(contentView); + dialog.show(); + return dialog; + } + /** * @param context may be is application context * @return activity context diff --git a/app/src/main/java/com/gh/common/util/UsageStatsHelper.kt b/app/src/main/java/com/gh/common/util/UsageStatsHelper.kt new file mode 100644 index 0000000000..e8792664d6 --- /dev/null +++ b/app/src/main/java/com/gh/common/util/UsageStatsHelper.kt @@ -0,0 +1,140 @@ +package com.gh.common.util + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.app.AppOpsManager +import android.app.AppOpsManager.MODE_ALLOWED +import android.app.AppOpsManager.OPSTR_GET_USAGE_STATS +import android.app.usage.UsageStats +import android.app.usage.UsageStatsManager +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Process +import android.provider.Settings +import androidx.annotation.RequiresApi +import com.gh.gamecenter.retrofit.BiResponse +import com.gh.gamecenter.retrofit.RetrofitManager +import com.halo.assistant.HaloApp +import com.lightgame.utils.Utils +import io.reactivex.schedulers.Schedulers +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 + */ +object UsageStatsHelper { + + const val USAGE_STATUS_SP_KEY = "usage_status_sp_key" + + private var mUsageStatsManager: UsageStatsManager? = null + + private val mApi = RetrofitManager.getInstance(HaloApp.getInstance().application).api + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1) + private fun getUsageStatsList(intervalStyle: Int, beginTime: Long): List? { + if (mUsageStatsManager == null) { + mUsageStatsManager = HaloApp + .getInstance().application + .getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager + } + + val usageStatsList = mUsageStatsManager?.queryUsageStats( + intervalStyle, + beginTime, + System.currentTimeMillis()) + + return if (null == usageStatsList || usageStatsList.size == 0) { + null + } else usageStatsList + } + + /** + * 第一次统计默认起始时间 + */ + 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) { + val usageStatsList = getUsageStatsList(UsageStatsManager.INTERVAL_YEARLY, beginTime) + ?: return + + val pkgAndTime = HashMap() + for (usageStats in usageStatsList) { + val time = pkgAndTime[usageStats.packageName] + if (time == null) { + pkgAndTime[usageStats.packageName] = usageStats.totalTimeInForeground + } else { + pkgAndTime[usageStats.packageName] = usageStats.totalTimeInForeground + time + } + } + + val postBody = JSONArray() + for (entry in pkgAndTime) { + val jsonObject = JSONObject() + jsonObject.put("package", entry.key) + jsonObject.put("duration", entry.value) + postBody.put(jsonObject) + } + + val body = RequestBody.create(MediaType.parse("application/json"), postBody.toString()) + mApi.postUsageStatus(body) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: ResponseBody) { + + } + }) + } + + @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 readyPostUsageStats() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1 || !checkForPermission()) return + mApi.usageStatusUpdateTime + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: ResponseBody) { + val body = JSONObject(data.string()) + var beginTime = body.getLong("update_time") + if (beginTime == 0L) beginTime = getDefaultBeginTime() + postUsageStats(beginTime) + } + }) + } + + @JvmStatic + fun skipToUsageStats(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)) + } else { + Utils.toast(context, "当前设备不支持查看应用使用情况") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java b/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java index b3c9fad8b0..30c0084ac8 100644 --- a/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java +++ b/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java @@ -2148,4 +2148,16 @@ public interface ApiService { @GET("home/navbar") Single getHomeNavBar(@Query("channel") String channel); + /** + * 获取更新时间(上一次提交的时间) + */ + @GET("devices/{device_id}/played_update_time") + Single getUsageStatusUpdateTime(); + + /** + * 批量新增游戏时长 + */ + @POST("users/{user_id}/played_times:batch_create") + Single postUsageStatus(@Body RequestBody body); + } \ No newline at end of file diff --git a/app/src/main/java/com/halo/assistant/fragment/SettingsFragment.java b/app/src/main/java/com/halo/assistant/fragment/SettingsFragment.java index 6b400d1ba7..2196535787 100644 --- a/app/src/main/java/com/halo/assistant/fragment/SettingsFragment.java +++ b/app/src/main/java/com/halo/assistant/fragment/SettingsFragment.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.preference.PreferenceManager; @@ -30,6 +31,7 @@ import com.gh.common.util.MtaHelper; import com.gh.common.util.PackageUtils; import com.gh.common.util.PermissionHelper; import com.gh.common.util.StringUtils; +import com.gh.common.util.UsageStatsHelper; import com.gh.gamecenter.AboutActivity; import com.gh.gamecenter.BuildConfig; import com.gh.gamecenter.MainActivity; @@ -74,6 +76,8 @@ public class SettingsFragment extends NormalFragment { SwitchButton mSettingAutoinstallSb; @BindView(R.id.setting_sb_concerngame) SwitchButton mSettingConcerngameSb; + @BindView(R.id.setting_sb_usage_stats) + SwitchButton mSettingUsageStatsSb; @BindView(R.id.setting_tv_cache) TextView mSettingCacheTv; @BindView(R.id.setting_tv_size) @@ -144,6 +148,36 @@ public class SettingsFragment extends NormalFragment { fontTextSize(checkSizeIndex); initLoginStatus(); + + initUsageStats(); + } + + private void initUsageStats() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { + return; + } + + if (UsageStatsHelper.checkForPermission() && sp.getBoolean(UsageStatsHelper.USAGE_STATUS_SP_KEY, false)) { + mSettingUsageStatsSb.setChecked(true); + } + + mSettingUsageStatsSb.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + if (UsageStatsHelper.checkForPermission()) { + UsageStatsHelper.skipToUsageStats(requireContext()); + sp.edit().putBoolean(UsageStatsHelper.USAGE_STATUS_SP_KEY, true).apply(); + } else { + DialogUtils.showUsageStatsDialog(getContext(), + () -> { + UsageStatsHelper.skipToUsageStats(requireContext()); + mSettingUsageStatsSb.setChecked(false); + }, + () -> mSettingUsageStatsSb.setChecked(false)); + } + } else { + sp.edit().putBoolean(UsageStatsHelper.USAGE_STATUS_SP_KEY, false).apply(); + } + }); } private void initLoginStatus() { @@ -233,7 +267,7 @@ public class SettingsFragment extends NormalFragment { R.id.setting_rl_concerngame, R.id.setting_rl_about, R.id.setting_logout_rl, R.id.setting_network_diagnosis, R.id.setting_privacy_policy, R.id.setting_user_protocol, - R.id.setting_game_submission + R.id.setting_game_submission, R.id.setting_usage_stats }) @Override public void onClick(final View v) { @@ -313,7 +347,7 @@ public class SettingsFragment extends NormalFragment { MtaHelper.onEvent("我的光环_设置", "设置功能", "游戏投稿"); CheckLoginUtils.checkLogin(getContext(), "设置-游戏投稿-请先登录", () -> { PermissionHelper.checkReadPhoneStateAndStoragePermissionBeforeAction(getContext(), () -> { - startActivity(GameSubmissionActivity.getIntent(getContext(),mEntrance,"游戏上传")); + startActivity(GameSubmissionActivity.getIntent(getContext(), mEntrance, "游戏上传")); }); }); break; @@ -321,6 +355,9 @@ public class SettingsFragment extends NormalFragment { MtaHelper.onEvent("我的光环_设置", "设置功能", "用户协议"); startActivity(WebActivity.getWebIntent(getContext())); break; + case R.id.setting_usage_stats: + mSettingUsageStatsSb.performClick(); + break; default: break; } diff --git a/app/src/main/res/layout/fragment_setting.xml b/app/src/main/res/layout/fragment_setting.xml index 670e6fd14f..1280171a73 100644 --- a/app/src/main/res/layout/fragment_setting.xml +++ b/app/src/main/res/layout/fragment_setting.xml @@ -276,6 +276,44 @@ android:textColor="@color/title" android:textSize="15sp" /> + + + + + + + + + + 一键修复 下载完成自动安装游戏 安装完成自动关注游戏 + 统计游戏时长 + 开启后可以帮你记录玩过每款游戏的时长 安装完成自动删除安装包 …/GH-KC …/gh-download