From 4a65e164276387c9e08d7be5a0435e5224a30d59 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Sun, 3 May 2026 13:16:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BA=94=E7=94=A8=E7=A9=BA?= =?UTF-8?q?=E8=BD=AC=E8=B0=83=E8=AF=95=E8=B5=84=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- positions/build.properties | 4 +- .../java/cc/winboll/studio/positions/App.java | 221 ++++--- .../studio/positions/MainActivity.java | 567 +++++++++--------- .../handlers/AppIdleRunningModeHandler.java | 143 +++++ .../src/main/res/layout/activity_main.xml | 18 + 5 files changed, 603 insertions(+), 350 deletions(-) create mode 100644 positions/src/main/java/cc/winboll/studio/positions/handlers/AppIdleRunningModeHandler.java diff --git a/positions/build.properties b/positions/build.properties index d7d387f..e9e8821 100644 --- a/positions/build.properties +++ b/positions/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat Apr 11 21:07:32 HKT 2026 +#Sun May 03 05:15:51 GMT 2026 stageCount=20 libraryProject= baseVersion=15.12 publishVersion=15.12.19 -buildCount=0 +buildCount=1 baseBetaVersion=15.12.20 diff --git a/positions/src/main/java/cc/winboll/studio/positions/App.java b/positions/src/main/java/cc/winboll/studio/positions/App.java index dea188b..fd5eade 100644 --- a/positions/src/main/java/cc/winboll/studio/positions/App.java +++ b/positions/src/main/java/cc/winboll/studio/positions/App.java @@ -13,7 +13,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.ViewGroup; @@ -21,10 +20,12 @@ import android.widget.HorizontalScrollView; import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; + import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; import cc.winboll.studio.libappbase.GlobalApplication; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; @@ -42,46 +43,73 @@ import java.util.Date; import java.util.LinkedHashMap; import java.util.concurrent.atomic.AtomicBoolean; +/** + * 全局Application应用入口类 + * 功能概括: + * 1. 全局初始化管理、工具类框架初始化 + * 2. 应用全局空转状态统一管理 + * 3. IO读写通用工具封装 + * 4. 全局崩溃捕获、崩溃日志本地保存、崩溃弹窗页面 + * @Author 豆包&ZhanGSKen + * @CreateTime 2026/05/03 12:23:00 + * @EditTime 2026/05/03 15:02:47 + */ public class App extends GlobalApplication { + //===================== 全局静态常量与变量 ===================== + public static final String TAG = "App"; private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper()); - // ============ 新增:应用空转标记 ============ - // 是否正在进行应用空转 + // 应用全局空转状态标记 public static boolean isAppIdleRunning = false; + //===================== 空转状态对外方法 ===================== + /** + * 获取当前应用空转运行状态 + * @return 是否处于空转状态 + */ public static boolean isAppIdleRunning() { + if (isAppIdleRunning) { + LogUtils.d(TAG, "isAppIdleRunning -> 当前应用处于空转运行状态"); + } return isAppIdleRunning; } + /** + * 设置应用空转运行状态 + * @param idleRunning 空转开关状态 + */ public static void setAppIdleRunning(boolean idleRunning) { - if (isDebugging()) { - isAppIdleRunning = idleRunning; - } else { - LogUtils.i(TAG, "非调试状态,应用空转设置无意义。"); - LogUtils.i(TAG, "Non-debug state, app idle setting is meaningless."); - } + LogUtils.d(TAG, "setAppIdleRunning -> 传入参数 idleRunning = " + idleRunning); + if (isDebugging()) { + isAppIdleRunning = idleRunning; + LogUtils.i(TAG, "setAppIdleRunning -> 调试模式,空转状态设置生效"); + } else { + LogUtils.i(TAG, "setAppIdleRunning -> 非调试模式,空转设置无效"); + LogUtils.i(TAG, "Non-debug state, app idle setting is meaningless."); + } } - // ========================================== + //===================== 应用生命周期 ===================== @Override public void onCreate() { super.onCreate(); - setIsDebugging(BuildConfig.DEBUG); + LogUtils.i(TAG, "onCreate -> 全局Application初始化开始"); - WinBoLLActivityManager.init(this); - - // 初始化 Toast 框架 + setIsDebugging(BuildConfig.DEBUG); + WinBoLLActivityManager.init(this); ToastUtils.init(this); - // 设置 Toast 布局样式 - //ToastUtils.setView(R.layout.view_toast); - //ToastUtils.setStyle(new WhiteToastStyle()); - //ToastUtils.setGravity(Gravity.BOTTOM, 0, 200); - //CrashHandler.getInstance().registerGlobal(this); - //CrashHandler.getInstance().registerPart(this); + LogUtils.i(TAG, "onCreate -> 全局组件初始化全部完成"); } + //===================== IO通用工具方法 ===================== + /** + * 流式读写输入输出流 + * @param input 输入流 + * @param output 输出流 + * @throws IOException IO读写异常 + */ public static void write(InputStream input, OutputStream output) throws IOException { byte[] buf = new byte[1024 * 8]; int len; @@ -90,9 +118,17 @@ public class App extends GlobalApplication { } } + /** + * 将字节数组写入指定文件 + * @param file 目标文件 + * @param data 字节数据 + * @throws IOException IO读写异常 + */ public static void write(File file, byte[] data) throws IOException { File parent = file.getParentFile(); - if (parent != null && !parent.exists()) parent.mkdirs(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } ByteArrayInputStream input = new ByteArrayInputStream(data); FileOutputStream output = new FileOutputStream(file); @@ -103,6 +139,12 @@ public class App extends GlobalApplication { } } + /** + * 输入流转为UTF-8字符串 + * @param input 输入流 + * @return 转换后的文本 + * @throws IOException IO读写异常 + */ public static String toString(InputStream input) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); write(input, output); @@ -113,20 +155,27 @@ public class App extends GlobalApplication { } } + /** + * 批量关闭IO流 + * @param closeables 可关闭对象 + */ public static void closeIO(Closeable... closeables) { for (Closeable closeable : closeables) { try { - if (closeable != null) closeable.close(); - } catch (IOException ignored) {} + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + + } } } + //===================== 全局崩溃捕获管理类 ===================== public static class CrashHandler { public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler(); - private static CrashHandler sInstance; - private PartCrashHandler mPartCrashHandler; public static CrashHandler getInstance() { @@ -136,35 +185,56 @@ public class App extends GlobalApplication { return sInstance; } + /** + * 注册全局崩溃捕获 + */ public void registerGlobal(Context context) { registerGlobal(context, null); } + /** + * 注册全局崩溃并自定义崩溃日志保存目录 + * @param crashDir 崩溃日志路径 + */ public void registerGlobal(Context context, String crashDir) { + LogUtils.d(TAG, "CrashHandler -> 注册全局异常捕获器"); Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir)); } + /** + * 解除全局崩溃捕获 + */ public void unregister() { + LogUtils.d(TAG, "CrashHandler -> 解除全局异常捕获"); Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER); } + /** + * 注册局部前台异常捕获 + */ public void registerPart(Context context) { unregisterPart(context); mPartCrashHandler = new PartCrashHandler(context.getApplicationContext()); MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler); + LogUtils.d(TAG, "CrashHandler -> 局部异常监听已注册"); } + /** + * 解除局部异常捕获 + */ public void unregisterPart(Context context) { if (mPartCrashHandler != null) { mPartCrashHandler.isRunning.set(false); mPartCrashHandler = null; + LogUtils.d(TAG, "CrashHandler -> 局部异常监听已销毁"); } } + /** + * 局部前台Looper异常捕获 + */ private static class PartCrashHandler implements Runnable { - private final Context mContext; - public AtomicBoolean isRunning = new AtomicBoolean(true); public PartCrashHandler(Context context) { @@ -177,18 +247,16 @@ public class App extends GlobalApplication { try { Looper.loop(); } catch (final Throwable e) { - e.printStackTrace(); if (isRunning.get()) { - MAIN_HANDLER.post(new Runnable(){ - - @Override - public void run() { - Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show(); - } - }); + MAIN_HANDLER.post(new Runnable() { + @Override + public void run() { + Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show(); + } + }); } else { if (e instanceof RuntimeException) { - throw (RuntimeException)e; + throw (RuntimeException) e; } else { throw new RuntimeException(e); } @@ -198,12 +266,12 @@ public class App extends GlobalApplication { } } + /** + * 全局未捕获异常处理实现 + */ private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler { - private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss"); - private final Context mContext; - private final File mCrashDir; public UncaughtExceptionHandlerImpl(Context context, String crashDir) { @@ -214,39 +282,38 @@ public class App extends GlobalApplication { @Override public void uncaughtException(Thread thread, Throwable throwable) { try { - String log = buildLog(throwable); writeLog(log); - try { - Intent intent = new Intent(mContext, CrashActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(Intent.EXTRA_TEXT, log); - mContext.startActivity(intent); - } catch (Throwable e) { - e.printStackTrace(); - writeLog(e.toString()); - } + Intent intent = new Intent(mContext, CrashActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Intent.EXTRA_TEXT, log); + mContext.startActivity(intent); - throwable.printStackTrace(); android.os.Process.killProcess(android.os.Process.myPid()); System.exit(0); - } catch (Throwable e) { - if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable); + if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) { + DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable); + } } } + /** + * 组装崩溃详细日志信息 + */ private String buildLog(Throwable throwable) { String time = DATE_FORMAT.format(new Date()); - String versionName = "unknown"; long versionCode = 0; + try { PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0); versionName = packageInfo.versionName; versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode; - } catch (Throwable ignored) {} + } catch (Throwable ignored) { + + } LinkedHashMap head = new LinkedHashMap(); head.put("Time Of Crash", time); @@ -254,34 +321,41 @@ public class App extends GlobalApplication { head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT)); head.put("App Version", String.format("%s (%d)", versionName, versionCode)); head.put("Kernel", getKernel()); - head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown"); + head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS) : "unknown"); head.put("Fingerprint", Build.FINGERPRINT); StringBuilder builder = new StringBuilder(); - for (String key : head.keySet()) { - if (builder.length() != 0) builder.append("\n"); + if (builder.length() != 0) { + builder.append("\n"); + } builder.append(key); builder.append(" : "); builder.append(head.get(key)); } - builder.append("\n\n"); - builder.append(Log.getStackTraceString(throwable)); + builder.append(throwable.toString()); - return builder.toString(); + return builder.toString(); } + /** + * 写入崩溃日志到本地文件 + */ private void writeLog(String log) { String time = DATE_FORMAT.format(new Date()); File file = new File(mCrashDir, "crash_" + time + ".txt"); try { write(file, log.getBytes("UTF-8")); + LogUtils.d(TAG, "崩溃日志已保存至本地文件"); } catch (Throwable e) { - e.printStackTrace(); - } + LogUtils.e(TAG, "崩溃日志写入失败"); + } } + /** + * 获取系统内核版本信息 + */ private static String getKernel() { try { return App.toString(new FileInputStream("/proc/version")).trim(); @@ -292,6 +366,7 @@ public class App extends GlobalApplication { } } + //===================== 崩溃展示页面 ===================== public static final class CrashActivity extends Activity { private String mLog; @@ -299,15 +374,14 @@ public class App extends GlobalApplication { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + LogUtils.i(TAG, "CrashActivity -> 崩溃展示页面已启动"); setTheme(android.R.style.Theme_DeviceDefault); setTitle("App Crash"); - mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT); ScrollView contentView = new ScrollView(this); contentView.setFillViewport(true); - HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this); TextView textView = new TextView(this); @@ -320,10 +394,12 @@ public class App extends GlobalApplication { horizontalScrollView.addView(textView); contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - setContentView(contentView); } + /** + * 重启当前应用 + */ private void restart() { Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName()); if (intent != null) { @@ -335,6 +411,9 @@ public class App extends GlobalApplication { System.exit(0); } + /** + * dp转px通用方法 + */ private static int dp2px(float dpValue) { final float scale = Resources.getSystem().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); @@ -343,17 +422,17 @@ public class App extends GlobalApplication { @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(0, android.R.id.copy, 0, android.R.string.copy) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.copy: - ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog)); - return true; + if (item.getItemId() == android.R.id.copy) { + ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog)); + LogUtils.d(TAG, "CrashActivity -> 崩溃日志已复制到剪贴板"); + return true; } return super.onOptionsItemSelected(item); } diff --git a/positions/src/main/java/cc/winboll/studio/positions/MainActivity.java b/positions/src/main/java/cc/winboll/studio/positions/MainActivity.java index c968c1b..12f48ac 100644 --- a/positions/src/main/java/cc/winboll/studio/positions/MainActivity.java +++ b/positions/src/main/java/cc/winboll/studio/positions/MainActivity.java @@ -14,46 +14,322 @@ import android.view.View; import android.widget.Button; import android.widget.CompoundButton; import android.widget.LinearLayout; +import android.widget.ScrollView; import android.widget.Switch; +import android.widget.TextView; import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; + import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; import cc.winboll.studio.libaes.utils.AESThemeUtil; import cc.winboll.studio.libaes.utils.DevelopUtils; import cc.winboll.studio.libaes.views.ADsBannerView; import cc.winboll.studio.libappbase.LogUtils; -import cc.winboll.studio.positions.R; import cc.winboll.studio.positions.activities.AboutActivity; import cc.winboll.studio.positions.activities.LocationActivity; import cc.winboll.studio.positions.activities.SettingsActivity; import cc.winboll.studio.positions.activities.WinBoLLActivity; +import cc.winboll.studio.positions.handlers.AppIdleRunningModeHandler; import cc.winboll.studio.positions.utils.AppConfigsUtil; import cc.winboll.studio.positions.utils.ServiceUtil; +import cc.winboll.studio.positions.R; /** - * 主页面:仅负责 - * 1. 位置服务启动/停止(通过 Switch 开关控制) - * 2. 跳转至“位置管理页(LocationActivity)”和“日志页(LogActivity)” - * 3. Java 7 语法适配:无 Lambda、显式接口实现、兼容低版本 + * 主页面控制器 + * 功能简述: + * 1. 位置服务启停开关控制 + * 2. 页面菜单跳转与主题管理 + * 3. 应用空转状态接收与日志实时输出展示 + * 4. 全局权限申请与权限结果回调处理 + * @Author 豆包&ZhanGSKen + * @CreateTime 2026/05/03 12:23:00 + * @EditTime 2026/05/03 14:36:21 */ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity { + + // ===================== 常量定义 ===================== public static final String TAG = "MainActivity"; - // 权限请求码(建议定义为类常量,避免魔法值) - private static final int REQUEST_LOCATION_PERMISSIONS = 1001; - private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002; + private static final int REQUEST_LOCATION_PERMISSIONS = 1001; + private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002; - // UI 控件:服务控制开关、顶部工具栏 - private Switch mServiceSwitch; - private Button mManagePositionsButton; + // ===================== UI控件声明 ===================== private Toolbar mToolbar; - // 服务相关:服务实例、绑定状态标记 - //private DistanceRefreshService mDistanceService; + private Switch mServiceSwitch; + private Button mManagePositionsButton; + private ADsBannerView mADsBannerView; + private TextView mTvIdleLog; + private ScrollView mScrollIdleLog; + + // ===================== 业务标记与回调 ===================== private boolean isServiceBound = false; - ADsBannerView mADsBannerView; + private OnAppIdleRunningListener mIdleRunningListener; + /** + * 应用空转状态回调内部接口 + */ + public interface OnAppIdleRunningListener { + /** + * 接收空转开关状态变更 + * @param isRunning 空转是否开启 + */ + void onIdleStatusChange(boolean isRunning); + /** + * 接收空转日志消息 + * @param log 日志文本 + */ + void onIdleLogReceive(String log); + } + + // ===================== 回调绑定方法 ===================== + public void setOnAppIdleRunningListener(OnAppIdleRunningListener listener) { + this.mIdleRunningListener = listener; + LogUtils.i(TAG, "setOnAppIdleRunningListener -> 空转监听绑定完成"); + } + + public OnAppIdleRunningListener getOnAppIdleRunningListener() { + return mIdleRunningListener; + } + + // ===================== 生命周期重写 ===================== + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + LogUtils.i(TAG, "onCreate -> MainActivity页面创建完成"); + + initToolbar(); + initViews(); + setLLMainBackgroundColor(); + + if (!checkLocationPermissions()) { + LogUtils.d(TAG, "onCreate -> 定位权限未授予,发起权限申请"); + requestLocationPermissions(); + } + + mADsBannerView = findViewById(R.id.adsbanner); + initAppIdleHandler(); + } + + @Override + protected void onResume() { + super.onResume(); + LogUtils.d(TAG, "onResume -> 页面恢复可见"); + if (mADsBannerView != null) { + mADsBannerView.resumeADs(MainActivity.this); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + LogUtils.i(TAG, "onDestroy -> MainActivity页面销毁释放资源"); + if (mADsBannerView != null) { + mADsBannerView.releaseAdResources(); + } + mIdleRunningListener = null; + } + + // ===================== 初始化相关方法 ===================== + /** + * 初始化顶部导航栏 + */ + private void initToolbar() { + mToolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(mToolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setTitle(getString(R.string.app_name)); + } + LogUtils.d(TAG, "initToolbar -> 顶部工具栏初始化完毕"); + } + + /** + * 初始化全部UI控件与绑定事件 + */ + private void initViews() { + mTvIdleLog = (TextView) findViewById(R.id.tv_idle_log); + mScrollIdleLog = (ScrollView) findViewById(R.id.scroll_idle_log); + mServiceSwitch = (Switch) findViewById(R.id.switch_service_control); + mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions); + + boolean serviceEnable = AppConfigsUtil.getInstance(this).isEnableMainService(true); + mServiceSwitch.setChecked(serviceEnable); + mManagePositionsButton.setEnabled(serviceEnable); + + mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + LogUtils.d(TAG, "onCheckedChanged -> 服务开关状态变更,isChecked = " + isChecked); + if (isChecked && !checkLocationPermissions()) { + requestLocationPermissions(); + return; + } + if (isChecked) { + ServiceUtil.startAutoService(MainActivity.this); + } else { + ServiceUtil.stopAutoService(MainActivity.this); + } + mManagePositionsButton.setEnabled(isChecked); + } + }); + LogUtils.i(TAG, "initViews -> 全部UI控件初始化绑定完成"); + } + + /** + * 初始化空转处理器并绑定监听回调 + */ + private void initAppIdleHandler() { + AppIdleRunningModeHandler.init(MainActivity.this); + setOnAppIdleRunningListener(new OnAppIdleRunningListener() { + @Override + public void onIdleStatusChange(boolean isRunning) { + appendIdleLog("IdleRunning Status : " + isRunning); + } + + @Override + public void onIdleLogReceive(String log) { + appendIdleLog(log); + } + }); + LogUtils.i(TAG, "initAppIdleHandler -> 空转处理器初始化与监听绑定完成"); + } + + /** + * 设置主布局主题背景色 + */ + private void setLLMainBackgroundColor() { + TypedArray typedArray = getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent}); + int colorAccent = typedArray.getColor(0, Color.GRAY); + typedArray.recycle(); + LinearLayout llmain = findViewById(R.id.llmain); + llmain.setBackgroundColor(colorAccent); + } + + // ===================== 空转日志输出工具 ===================== + /** + * 追加空转日志并自动滚动至底部 + * @param logText 需要追加的日志文本 + */ + private void appendIdleLog(final String logText) { + runOnUiThread(new Runnable() { + @Override + public void run() { + String allLog = mTvIdleLog.getText().toString(); + mTvIdleLog.setText(allLog + logText + "\n"); + mScrollIdleLog.post(new Runnable() { + @Override + public void run() { + mScrollIdleLog.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + } + }); + } + + // ===================== 权限处理相关 ===================== + /** + * 检查前台+后台定位权限 + * @return 权限是否全部通过 + */ + private boolean checkLocationPermissions() { + int foregroundPerm = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION); + boolean hasForegroundPerm = (foregroundPerm == PackageManager.PERMISSION_GRANTED); + boolean hasBackgroundPerm = true; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + int backgroundPerm = checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION); + hasBackgroundPerm = (backgroundPerm == PackageManager.PERMISSION_GRANTED); + } + return hasForegroundPerm && hasBackgroundPerm; + } + + /** + * 发起定位权限动态申请 + */ + private void requestLocationPermissions() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + String[] foregroundPermissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION}; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(foregroundPermissions, REQUEST_LOCATION_PERMISSIONS); + } + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, + REQUEST_BACKGROUND_LOCATION_PERMISSION); + } + } + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + LogUtils.d(TAG, "onRequestPermissionsResult -> 权限回调 requestCode = " + requestCode); + + if (requestCode == REQUEST_LOCATION_PERMISSIONS) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + requestLocationPermissions(); + } else { + Toast.makeText(this, "需要前台定位权限才能使用该功能", Toast.LENGTH_SHORT).show(); + } + } else if (requestCode == REQUEST_BACKGROUND_LOCATION_PERMISSION) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, "已获得后台定位权限", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "拒绝后台权限将无法在后台持续定位", Toast.LENGTH_SHORT).show(); + } + } + } + + // ===================== 菜单与页面跳转 ===================== + @Override + public boolean onCreateOptionsMenu(Menu menu) { + AESThemeUtil.inflateMenu(this, menu); + if (App.isDebugging()) { + DevelopUtils.inflateMenu(this, menu); + } + getMenuInflater().inflate(R.menu.toolbar_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int menuItemId = item.getItemId(); + LogUtils.d(TAG, "onOptionsItemSelected -> 点击菜单ID = " + menuItemId); + + if (AESThemeUtil.onAppThemeItemSelected(this, item)) { + recreate(); + } else if (DevelopUtils.onDevelopItemSelected(this, item)) { + LogUtils.d(TAG, "onOptionsItemSelected -> 进入开发工具菜单"); + } else if (menuItemId == R.id.item_settings) { + Intent intent = new Intent(); + intent.setClass(this, SettingsActivity.class); + startActivity(intent); + } else if (menuItemId == R.id.item_about) { + Intent intent = new Intent(); + intent.setClass(this, AboutActivity.class); + startActivity(intent); + } else { + return super.onOptionsItemSelected(item); + } + return true; + } + + /** + * 跳转位置管理页面 + */ + public void onPositions(View view) { + LogUtils.d(TAG, "onPositions -> 跳转位置任务管理页面"); + startActivity(new Intent(MainActivity.this, LocationActivity.class)); + } + + // ===================== 接口实现 ===================== @Override public Activity getActivity() { return this; @@ -63,268 +339,5 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity { public String getTag() { return TAG; } - - // ---------------------- 服务连接回调(仅用于获取服务状态,不依赖服务执行核心逻辑) ---------------------- -// private final ServiceConnection mServiceConn = new ServiceConnection() { -// /** -// * 服务绑定成功:获取服务实例,同步开关状态(以服务实际状态为准) -// */ -// @Override -// public void onServiceConnected(ComponentName name, IBinder service) { -// // Java 7 显式强转 Binder 实例(确保类型匹配,避免ClassCastException) -// DistanceRefreshService.DistanceBinder binder = (DistanceRefreshService.DistanceBinder) service; -// mDistanceService = binder.getService(); -// isServiceBound = true; -// } -// -// /** -// * 服务意外断开(如服务崩溃):重置服务实例和绑定状态 -// */ -// @Override -// public void onServiceDisconnected(ComponentName name) { -// mDistanceService = null; -// isServiceBound = false; -// } -// }; - - // ---------------------- Activity 生命周期(核心:初始化UI、申请权限、绑定服务、释放资源) ---------------------- - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); // 关联主页面布局 - - // 1. 初始化顶部 Toolbar(保留原逻辑,设置页面标题) - initToolbar(); - // 2. 初始化其他控件 - initViews(); - // 3. 检查并申请位置权限(含后台GPS权限,确保服务启动前权限就绪) - if (!checkLocationPermissions()) { - requestLocationPermissions(); - } - // 4. 绑定服务(仅用于获取服务实时状态,不影响服务独立运行) - //bindDistanceService(); - - mADsBannerView = findViewById(R.id.adsbanner); - - setLLMainBackgroundColor(); - } - - // 在 Activity 的 onCreate() 或需要获取颜色的方法中调用 - private void setLLMainBackgroundColor() { - // 1. 定义要解析的主题属性(这里是 colorAccent) - TypedArray a = getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent}); - // 2. 获取对应的颜色值(默认值可设为你需要的 fallback 颜色,如 Color.GRAY) - int colorAccent = a.getColor(0, Color.GRAY); - // 3. 必须回收,避免内存泄漏 - a.recycle(); - - LinearLayout llmain = findViewById(R.id.llmain); - llmain.setBackgroundColor(colorAccent); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (mADsBannerView != null) { - mADsBannerView.releaseAdResources(); - } - - // 页面销毁时解绑服务,避免Activity与服务相互引用导致内存泄漏 -// if (isServiceBound) { -// unbindService(mServiceConn); -// isServiceBound = false; -// mDistanceService = null; -// } - } - - @Override - protected void onResume() { - super.onResume(); - if (mADsBannerView != null) { - mADsBannerView.resumeADs(MainActivity.this); - } - } - - - - // ---------------------- 核心功能1:初始化UI组件(Toolbar + 服务开关) ---------------------- - /** - * 初始化顶部 Toolbar,设置页面标题 - */ - private void initToolbar() { - mToolbar = (Toolbar) findViewById(R.id.toolbar); // Java 7 显式 findViewById + 强转 - setSupportActionBar(mToolbar); - // 给ActionBar设置标题(先判断非空,避免空指针异常) - if (getSupportActionBar() != null) { - getSupportActionBar().setTitle(getString(R.string.app_name)); - } - } - - /** - * 初始化服务控制开关:读取SP状态、绑定点击事件(含权限检查) - */ - private void initViews() { - mServiceSwitch = (Switch) findViewById(R.id.switch_service_control); // 显式强转 - mServiceSwitch.setChecked(AppConfigsUtil.getInstance(this).isEnableMainService(true)); - - mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions); - mManagePositionsButton.setEnabled(mServiceSwitch.isChecked()); - - // Java 7 用匿名内部类实现 CompoundButton.OnCheckedChangeListener - mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - // 开关打开前先检查权限:无权限则终止操作、重置开关、引导申请 - if (isChecked && !checkLocationPermissions()) { - requestLocationPermissions(); - return; - } - - // 权限就绪:执行服务启停逻辑 - if (isChecked) { - LogUtils.d(TAG, "设置启动服务"); - ServiceUtil.startAutoService(MainActivity.this); - } else { - LogUtils.d(TAG, "设置关闭服务"); - - ServiceUtil.stopAutoService(MainActivity.this); - } - - mManagePositionsButton.setEnabled(isChecked); - } - }); - } - - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // 主题菜单 - AESThemeUtil.inflateMenu(this, menu); - // 调试工具菜单 - if (App.isDebugging()) { - DevelopUtils.inflateMenu(this, menu); - } - // 应用其他菜单 - getMenuInflater().inflate(R.menu.toolbar_main, menu); - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int menuItemId = item.getItemId(); - if (AESThemeUtil.onAppThemeItemSelected(this, item)) { - recreate(); - } if (DevelopUtils.onDevelopItemSelected(this, item)) { - LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId())); - } else if (menuItemId == R.id.item_settings) { - Intent intent = new Intent(); - intent.setClass(this, SettingsActivity.class); - startActivity(intent); - } else if (menuItemId == R.id.item_about) { - Intent intent = new Intent(); - intent.setClass(this, AboutActivity.class); - startActivity(intent); - } else { - // 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。 - return super.onOptionsItemSelected(item); - } - return true; - } - - - /** - * 绑定服务(仅用于获取服务状态,不启动服务) - */ -// private void bindDistanceService() { -// Intent serviceIntent = new Intent(this, MainService.class); -// // BIND_AUTO_CREATE:服务未启动则创建(仅为获取状态,启停由开关控制) -// bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); -// } - - // ---------------------- 核心功能3:页面跳转(位置管理页+日志页) ---------------------- - /** - * 跳转至“位置管理页(LocationActivity)”(按钮点击触发,需在布局中设置 android:onClick="onPositions") - * 服务未启动时提示,不允许跳转(避免LocationActivity无数据) - */ - public void onPositions(View view) { - //ToastUtils.show("onPositions"); - // 服务已启动:跳转到位置管理页 - startActivity(new Intent(MainActivity.this, LocationActivity.class)); - } - - // ---------------------- 新增:位置权限处理(适配Java7 + 后台GPS权限) ---------------------- - /** - * 检查是否拥有「前台+后台」位置权限(适配Android版本差异) - * Java7 特性:显式类型判断、无Lambda、兼容低版本API - */ - private boolean checkLocationPermissions() { - // 1. 检查前台精确定位权限(Android 6.0+ 必需,显式强转权限常量) - int foregroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION); - boolean hasForegroundPerm = (foregroundPermResult == PackageManager.PERMISSION_GRANTED); - - // 2. 检查后台定位权限(仅Android 10+ 需要,Java7 显式用Build.VERSION判断版本) - boolean hasBackgroundPerm = true; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - int backgroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION); - hasBackgroundPerm = (backgroundPermResult == PackageManager.PERMISSION_GRANTED); - } - - // 前台+后台权限均满足,才返回true - return hasForegroundPerm && hasBackgroundPerm; - } - - private void requestLocationPermissions() { - // 1. 先判断前台定位权限(ACCESS_FINE_LOCATION)是否已授予 - if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - // 1.1 未授予前台权限:先申请前台权限(API 30+ 后台权限依赖前台权限) - String[] foregroundPermissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION}; - // 对API 23+(Android 6.0)动态申请,低版本会直接授予(清单已声明前提下) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(foregroundPermissions, REQUEST_LOCATION_PERMISSIONS); - } - } else { - // 2. 已授予前台权限:判断是否需要申请后台权限(仅API 29+需要) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // 2.1 检查后台权限是否未授予 - if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - // 2.2 API 30+ 必须单独申请后台权限(不能和前台权限一起弹框) - requestPermissions( - new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, - REQUEST_BACKGROUND_LOCATION_PERMISSION - ); - } - } - // 3. 前台权限已授予(+ 后台权限按需授予):此处可执行定位相关逻辑 - // doLocationRelatedLogic(); - } - } - -// 【必须补充】权限申请结果回调(处理用户同意/拒绝逻辑) - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - // 处理前台权限申请结果 - if (requestCode == REQUEST_LOCATION_PERMISSIONS) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // 前台权限同意:自动尝试申请后台权限(如果是API 29+) - requestLocationPermissions(); - } else { - // 前台权限拒绝:提示用户(可选:引导跳转到应用设置页) - Toast.makeText(this, "需要前台定位权限才能使用该功能", Toast.LENGTH_SHORT).show(); - } - } else if (requestCode == REQUEST_BACKGROUND_LOCATION_PERMISSION) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // 后台权限同意:可执行后台定位逻辑 - Toast.makeText(this, "已获得后台定位权限", Toast.LENGTH_SHORT).show(); - } else { - // 后台权限拒绝:提示用户(可选:说明后台定位的用途,引导手动开启) - Toast.makeText(this, "拒绝后台权限将无法在后台持续定位", Toast.LENGTH_SHORT).show(); - } - } - } - } diff --git a/positions/src/main/java/cc/winboll/studio/positions/handlers/AppIdleRunningModeHandler.java b/positions/src/main/java/cc/winboll/studio/positions/handlers/AppIdleRunningModeHandler.java new file mode 100644 index 0000000..c00a7a3 --- /dev/null +++ b/positions/src/main/java/cc/winboll/studio/positions/handlers/AppIdleRunningModeHandler.java @@ -0,0 +1,143 @@ +package cc.winboll.studio.positions.handlers; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.positions.App; +import cc.winboll.studio.positions.MainActivity; + +/** + * 应用空转事务处理器 + * 作用:接收空转开关消息、空转日志消息,回调MainActivity内部接口实现UI联动 + * @Author 豆包&ZhanGSKen + * @CreateTime 2026/05/03 12:23:00 + * @EditTime 2026/05/03 14:42:18 + */ +public class AppIdleRunningModeHandler extends Handler { + + //===================== 常量标识 ===================== + public static final String TAG = "AppIdleRunningModeHandler"; + public static final int MSG_IDLE_MODE_SWITCH = 1001; + public static final int MSG_IDLE_LOG_PRINT = 1002; + + //===================== 成员变量 ===================== + private static AppIdleRunningModeHandler mHandler; + private MainActivity mMainActivity; + + //===================== 静态初始化 ===================== + static { + mHandler = new AppIdleRunningModeHandler(); + LogUtils.d(TAG, "静态代码块:默认主线程Looper完成初始化"); + } + + //===================== 构造方法 ===================== + /** + * 私有无参构造,禁止外部直接实例化 + */ + private AppIdleRunningModeHandler() { + super(Looper.getMainLooper()); + } + + /** + * 带MainActivity绑定构造 + * @param activity 主页面实例 + */ + public AppIdleRunningModeHandler(MainActivity activity) { + super(Looper.getMainLooper()); + this.mMainActivity = activity; + LogUtils.d(TAG, "构造方法:完成MainActivity绑定初始化"); + } + + //===================== 对外初始化与获取 ===================== + /** + * 全局静态初始化、重新绑定MainActivity + * @param activity 主页面实例 + */ + public static void init(MainActivity activity) { + if (mHandler == null) { + mHandler = new AppIdleRunningModeHandler(activity); + } else { + mHandler.mMainActivity = activity; + } + LogUtils.i(TAG, "init -> AppIdleRunningModeHandler初始化绑定成功"); + } + + /** + * 获取当前绑定的MainActivity实例 + * @return 已绑定的Activity + */ + public MainActivity getBindMainActivity() { + return mMainActivity; + } + + //===================== 对外静态发送方法 ===================== + /** + * 发送空转开关控制消息 + * @param isOpen 是否开启空转状态 + */ + public static void sendIdleSwitch(boolean isOpen) { + if (!App.isAppIdleRunning()) { + LogUtils.d(TAG, "sendIdleSwitch -> 当前非空转状态,函数执行无效"); + return; + } + LogUtils.d(TAG, "sendIdleSwitch -> 发送空转开关信号,参数isOpen = " + isOpen); + + Message message = Message.obtain(); + message.what = MSG_IDLE_MODE_SWITCH; + message.obj = isOpen; + mHandler.sendMessage(message); + } + + /** + * 发送空转日志打印消息 + * @param logText 待输出的日志内容 + */ + public static void sendIdleLog(String logText) { + if (!App.isAppIdleRunning()) { + LogUtils.d(TAG, "sendIdleLog -> 当前非空转状态,函数执行无效"); + return; + } + LogUtils.d(TAG, "sendIdleLog -> 发送空转日志消息"); + + Message message = Message.obtain(); + message.what = MSG_IDLE_LOG_PRINT; + message.obj = logText; + mHandler.sendMessage(message); + } + + //===================== 消息接收处理 ===================== + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + + // 全局状态校验 + if (!App.isAppIdleRunning()) { + return; + } + // 空指针安全防护 + if (mMainActivity == null || mMainActivity.getOnAppIdleRunningListener() == null) { + LogUtils.d(TAG, "handleMessage -> Activity或监听接口为空,终止回调"); + return; + } + + switch (msg.what) { + case MSG_IDLE_MODE_SWITCH: + boolean idleState = (boolean) msg.obj; + App.setAppIdleRunning(idleState); + LogUtils.i(TAG, "handleMessage -> 空转状态已变更:" + idleState); + // 回调主页面接口 + mMainActivity.getOnAppIdleRunningListener().onIdleStatusChange(idleState); + break; + + case MSG_IDLE_LOG_PRINT: + String logContent = (String) msg.obj; + LogUtils.i(TAG, logContent); + // 回调主页面日志接收接口 + mMainActivity.getOnAppIdleRunningListener().onIdleLogReceive(logContent); + break; + } + } +} + diff --git a/positions/src/main/res/layout/activity_main.xml b/positions/src/main/res/layout/activity_main.xml index 1635a0b..d73f257 100644 --- a/positions/src/main/res/layout/activity_main.xml +++ b/positions/src/main/res/layout/activity_main.xml @@ -57,6 +57,24 @@ android:textColor="@drawable/btn_text_selector" android:padding="12dp"/> + + + + + +