添加应用空转调试资源
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#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
|
stageCount=20
|
||||||
libraryProject=
|
libraryProject=
|
||||||
baseVersion=15.12
|
baseVersion=15.12
|
||||||
publishVersion=15.12.19
|
publishVersion=15.12.19
|
||||||
buildCount=0
|
buildCount=1
|
||||||
baseBetaVersion=15.12.20
|
baseBetaVersion=15.12.20
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import android.os.Bundle;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -21,10 +20,12 @@ import android.widget.HorizontalScrollView;
|
|||||||
import android.widget.ScrollView;
|
import android.widget.ScrollView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
@@ -42,46 +43,73 @@ import java.util.Date;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局Application应用入口类
|
||||||
|
* 功能概括:
|
||||||
|
* 1. 全局初始化管理、工具类框架初始化
|
||||||
|
* 2. 应用全局空转状态统一管理
|
||||||
|
* 3. IO读写通用工具封装
|
||||||
|
* 4. 全局崩溃捕获、崩溃日志本地保存、崩溃弹窗页面
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @CreateTime 2026/05/03 12:23:00
|
||||||
|
* @EditTime 2026/05/03 15:02:47
|
||||||
|
*/
|
||||||
public class App extends GlobalApplication {
|
public class App extends GlobalApplication {
|
||||||
|
|
||||||
|
//===================== 全局静态常量与变量 =====================
|
||||||
|
public static final String TAG = "App";
|
||||||
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
// ============ 新增:应用空转标记 ============
|
// 应用全局空转状态标记
|
||||||
// 是否正在进行应用空转
|
|
||||||
public static boolean isAppIdleRunning = false;
|
public static boolean isAppIdleRunning = false;
|
||||||
|
|
||||||
|
//===================== 空转状态对外方法 =====================
|
||||||
|
/**
|
||||||
|
* 获取当前应用空转运行状态
|
||||||
|
* @return 是否处于空转状态
|
||||||
|
*/
|
||||||
public static boolean isAppIdleRunning() {
|
public static boolean isAppIdleRunning() {
|
||||||
|
if (isAppIdleRunning) {
|
||||||
|
LogUtils.d(TAG, "isAppIdleRunning -> 当前应用处于空转运行状态");
|
||||||
|
}
|
||||||
return isAppIdleRunning;
|
return isAppIdleRunning;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置应用空转运行状态
|
||||||
|
* @param idleRunning 空转开关状态
|
||||||
|
*/
|
||||||
public static void setAppIdleRunning(boolean idleRunning) {
|
public static void setAppIdleRunning(boolean idleRunning) {
|
||||||
|
LogUtils.d(TAG, "setAppIdleRunning -> 传入参数 idleRunning = " + idleRunning);
|
||||||
if (isDebugging()) {
|
if (isDebugging()) {
|
||||||
isAppIdleRunning = idleRunning;
|
isAppIdleRunning = idleRunning;
|
||||||
|
LogUtils.i(TAG, "setAppIdleRunning -> 调试模式,空转状态设置生效");
|
||||||
} else {
|
} else {
|
||||||
LogUtils.i(TAG, "非调试状态,应用空转设置无意义。");
|
LogUtils.i(TAG, "setAppIdleRunning -> 非调试模式,空转设置无效");
|
||||||
LogUtils.i(TAG, "Non-debug state, app idle setting is meaningless.");
|
LogUtils.i(TAG, "Non-debug state, app idle setting is meaningless.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
|
//===================== 应用生命周期 =====================
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
LogUtils.i(TAG, "onCreate -> 全局Application初始化开始");
|
||||||
|
|
||||||
setIsDebugging(BuildConfig.DEBUG);
|
setIsDebugging(BuildConfig.DEBUG);
|
||||||
|
|
||||||
WinBoLLActivityManager.init(this);
|
WinBoLLActivityManager.init(this);
|
||||||
|
|
||||||
// 初始化 Toast 框架
|
|
||||||
ToastUtils.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);
|
LogUtils.i(TAG, "onCreate -> 全局组件初始化全部完成");
|
||||||
//CrashHandler.getInstance().registerPart(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//===================== IO通用工具方法 =====================
|
||||||
|
/**
|
||||||
|
* 流式读写输入输出流
|
||||||
|
* @param input 输入流
|
||||||
|
* @param output 输出流
|
||||||
|
* @throws IOException IO读写异常
|
||||||
|
*/
|
||||||
public static void write(InputStream input, OutputStream output) throws IOException {
|
public static void write(InputStream input, OutputStream output) throws IOException {
|
||||||
byte[] buf = new byte[1024 * 8];
|
byte[] buf = new byte[1024 * 8];
|
||||||
int len;
|
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 {
|
public static void write(File file, byte[] data) throws IOException {
|
||||||
File parent = file.getParentFile();
|
File parent = file.getParentFile();
|
||||||
if (parent != null && !parent.exists()) parent.mkdirs();
|
if (parent != null && !parent.exists()) {
|
||||||
|
parent.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
ByteArrayInputStream input = new ByteArrayInputStream(data);
|
ByteArrayInputStream input = new ByteArrayInputStream(data);
|
||||||
FileOutputStream output = new FileOutputStream(file);
|
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 {
|
public static String toString(InputStream input) throws IOException {
|
||||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
write(input, output);
|
write(input, output);
|
||||||
@@ -113,20 +155,27 @@ public class App extends GlobalApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量关闭IO流
|
||||||
|
* @param closeables 可关闭对象
|
||||||
|
*/
|
||||||
public static void closeIO(Closeable... closeables) {
|
public static void closeIO(Closeable... closeables) {
|
||||||
for (Closeable closeable : closeables) {
|
for (Closeable closeable : closeables) {
|
||||||
try {
|
try {
|
||||||
if (closeable != null) closeable.close();
|
if (closeable != null) {
|
||||||
} catch (IOException ignored) {}
|
closeable.close();
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//===================== 全局崩溃捕获管理类 =====================
|
||||||
public static class CrashHandler {
|
public static class CrashHandler {
|
||||||
|
|
||||||
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
|
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
|
||||||
|
|
||||||
private static CrashHandler sInstance;
|
private static CrashHandler sInstance;
|
||||||
|
|
||||||
private PartCrashHandler mPartCrashHandler;
|
private PartCrashHandler mPartCrashHandler;
|
||||||
|
|
||||||
public static CrashHandler getInstance() {
|
public static CrashHandler getInstance() {
|
||||||
@@ -136,35 +185,56 @@ public class App extends GlobalApplication {
|
|||||||
return sInstance;
|
return sInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册全局崩溃捕获
|
||||||
|
*/
|
||||||
public void registerGlobal(Context context) {
|
public void registerGlobal(Context context) {
|
||||||
registerGlobal(context, null);
|
registerGlobal(context, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册全局崩溃并自定义崩溃日志保存目录
|
||||||
|
* @param crashDir 崩溃日志路径
|
||||||
|
*/
|
||||||
public void registerGlobal(Context context, String crashDir) {
|
public void registerGlobal(Context context, String crashDir) {
|
||||||
|
LogUtils.d(TAG, "CrashHandler -> 注册全局异常捕获器");
|
||||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
|
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解除全局崩溃捕获
|
||||||
|
*/
|
||||||
public void unregister() {
|
public void unregister() {
|
||||||
|
LogUtils.d(TAG, "CrashHandler -> 解除全局异常捕获");
|
||||||
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
|
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册局部前台异常捕获
|
||||||
|
*/
|
||||||
public void registerPart(Context context) {
|
public void registerPart(Context context) {
|
||||||
unregisterPart(context);
|
unregisterPart(context);
|
||||||
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
|
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
|
||||||
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
|
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
|
||||||
|
LogUtils.d(TAG, "CrashHandler -> 局部异常监听已注册");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解除局部异常捕获
|
||||||
|
*/
|
||||||
public void unregisterPart(Context context) {
|
public void unregisterPart(Context context) {
|
||||||
if (mPartCrashHandler != null) {
|
if (mPartCrashHandler != null) {
|
||||||
mPartCrashHandler.isRunning.set(false);
|
mPartCrashHandler.isRunning.set(false);
|
||||||
mPartCrashHandler = null;
|
mPartCrashHandler = null;
|
||||||
|
LogUtils.d(TAG, "CrashHandler -> 局部异常监听已销毁");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 局部前台Looper异常捕获
|
||||||
|
*/
|
||||||
private static class PartCrashHandler implements Runnable {
|
private static class PartCrashHandler implements Runnable {
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
|
|
||||||
public AtomicBoolean isRunning = new AtomicBoolean(true);
|
public AtomicBoolean isRunning = new AtomicBoolean(true);
|
||||||
|
|
||||||
public PartCrashHandler(Context context) {
|
public PartCrashHandler(Context context) {
|
||||||
@@ -177,10 +247,8 @@ public class App extends GlobalApplication {
|
|||||||
try {
|
try {
|
||||||
Looper.loop();
|
Looper.loop();
|
||||||
} catch (final Throwable e) {
|
} catch (final Throwable e) {
|
||||||
e.printStackTrace();
|
|
||||||
if (isRunning.get()) {
|
if (isRunning.get()) {
|
||||||
MAIN_HANDLER.post(new Runnable() {
|
MAIN_HANDLER.post(new Runnable() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
|
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
|
||||||
@@ -198,12 +266,12 @@ public class App extends GlobalApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局未捕获异常处理实现
|
||||||
|
*/
|
||||||
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
|
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
|
||||||
|
|
||||||
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
|
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
|
|
||||||
private final File mCrashDir;
|
private final File mCrashDir;
|
||||||
|
|
||||||
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
|
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
|
||||||
@@ -214,39 +282,38 @@ public class App extends GlobalApplication {
|
|||||||
@Override
|
@Override
|
||||||
public void uncaughtException(Thread thread, Throwable throwable) {
|
public void uncaughtException(Thread thread, Throwable throwable) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
String log = buildLog(throwable);
|
String log = buildLog(throwable);
|
||||||
writeLog(log);
|
writeLog(log);
|
||||||
|
|
||||||
try {
|
|
||||||
Intent intent = new Intent(mContext, CrashActivity.class);
|
Intent intent = new Intent(mContext, CrashActivity.class);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, log);
|
intent.putExtra(Intent.EXTRA_TEXT, log);
|
||||||
mContext.startActivity(intent);
|
mContext.startActivity(intent);
|
||||||
} catch (Throwable e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
writeLog(e.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
throwable.printStackTrace();
|
|
||||||
android.os.Process.killProcess(android.os.Process.myPid());
|
android.os.Process.killProcess(android.os.Process.myPid());
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
|
|
||||||
} catch (Throwable e) {
|
} 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) {
|
private String buildLog(Throwable throwable) {
|
||||||
String time = DATE_FORMAT.format(new Date());
|
String time = DATE_FORMAT.format(new Date());
|
||||||
|
|
||||||
String versionName = "unknown";
|
String versionName = "unknown";
|
||||||
long versionCode = 0;
|
long versionCode = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
|
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
|
||||||
versionName = packageInfo.versionName;
|
versionName = packageInfo.versionName;
|
||||||
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
|
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
|
||||||
} catch (Throwable ignored) {}
|
} catch (Throwable ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
|
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
|
||||||
head.put("Time Of Crash", time);
|
head.put("Time Of Crash", time);
|
||||||
@@ -258,30 +325,37 @@ public class App extends GlobalApplication {
|
|||||||
head.put("Fingerprint", Build.FINGERPRINT);
|
head.put("Fingerprint", Build.FINGERPRINT);
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
for (String key : head.keySet()) {
|
for (String key : head.keySet()) {
|
||||||
if (builder.length() != 0) builder.append("\n");
|
if (builder.length() != 0) {
|
||||||
|
builder.append("\n");
|
||||||
|
}
|
||||||
builder.append(key);
|
builder.append(key);
|
||||||
builder.append(" : ");
|
builder.append(" : ");
|
||||||
builder.append(head.get(key));
|
builder.append(head.get(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.append("\n\n");
|
builder.append("\n\n");
|
||||||
builder.append(Log.getStackTraceString(throwable));
|
builder.append(throwable.toString());
|
||||||
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入崩溃日志到本地文件
|
||||||
|
*/
|
||||||
private void writeLog(String log) {
|
private void writeLog(String log) {
|
||||||
String time = DATE_FORMAT.format(new Date());
|
String time = DATE_FORMAT.format(new Date());
|
||||||
File file = new File(mCrashDir, "crash_" + time + ".txt");
|
File file = new File(mCrashDir, "crash_" + time + ".txt");
|
||||||
try {
|
try {
|
||||||
write(file, log.getBytes("UTF-8"));
|
write(file, log.getBytes("UTF-8"));
|
||||||
|
LogUtils.d(TAG, "崩溃日志已保存至本地文件");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
e.printStackTrace();
|
LogUtils.e(TAG, "崩溃日志写入失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统内核版本信息
|
||||||
|
*/
|
||||||
private static String getKernel() {
|
private static String getKernel() {
|
||||||
try {
|
try {
|
||||||
return App.toString(new FileInputStream("/proc/version")).trim();
|
return App.toString(new FileInputStream("/proc/version")).trim();
|
||||||
@@ -292,6 +366,7 @@ public class App extends GlobalApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//===================== 崩溃展示页面 =====================
|
||||||
public static final class CrashActivity extends Activity {
|
public static final class CrashActivity extends Activity {
|
||||||
|
|
||||||
private String mLog;
|
private String mLog;
|
||||||
@@ -299,15 +374,14 @@ public class App extends GlobalApplication {
|
|||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
LogUtils.i(TAG, "CrashActivity -> 崩溃展示页面已启动");
|
||||||
|
|
||||||
setTheme(android.R.style.Theme_DeviceDefault);
|
setTheme(android.R.style.Theme_DeviceDefault);
|
||||||
setTitle("App Crash");
|
setTitle("App Crash");
|
||||||
|
|
||||||
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||||
|
|
||||||
ScrollView contentView = new ScrollView(this);
|
ScrollView contentView = new ScrollView(this);
|
||||||
contentView.setFillViewport(true);
|
contentView.setFillViewport(true);
|
||||||
|
|
||||||
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
|
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
|
||||||
|
|
||||||
TextView textView = new TextView(this);
|
TextView textView = new TextView(this);
|
||||||
@@ -320,10 +394,12 @@ public class App extends GlobalApplication {
|
|||||||
|
|
||||||
horizontalScrollView.addView(textView);
|
horizontalScrollView.addView(textView);
|
||||||
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||||
|
|
||||||
setContentView(contentView);
|
setContentView(contentView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启当前应用
|
||||||
|
*/
|
||||||
private void restart() {
|
private void restart() {
|
||||||
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
@@ -335,6 +411,9 @@ public class App extends GlobalApplication {
|
|||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dp转px通用方法
|
||||||
|
*/
|
||||||
private static int dp2px(float dpValue) {
|
private static int dp2px(float dpValue) {
|
||||||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||||||
return (int) (dpValue * scale + 0.5f);
|
return (int) (dpValue * scale + 0.5f);
|
||||||
@@ -349,10 +428,10 @@ public class App extends GlobalApplication {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
if (item.getItemId() == android.R.id.copy) {
|
||||||
case android.R.id.copy:
|
|
||||||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
||||||
|
LogUtils.d(TAG, "CrashActivity -> 崩溃日志已复制到剪贴板");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
|
|||||||
@@ -14,209 +14,299 @@ import android.view.View;
|
|||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.CompoundButton;
|
import android.widget.CompoundButton;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
import android.widget.Switch;
|
import android.widget.Switch;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||||
import cc.winboll.studio.libaes.utils.DevelopUtils;
|
import cc.winboll.studio.libaes.utils.DevelopUtils;
|
||||||
import cc.winboll.studio.libaes.views.ADsBannerView;
|
import cc.winboll.studio.libaes.views.ADsBannerView;
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
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.AboutActivity;
|
||||||
import cc.winboll.studio.positions.activities.LocationActivity;
|
import cc.winboll.studio.positions.activities.LocationActivity;
|
||||||
import cc.winboll.studio.positions.activities.SettingsActivity;
|
import cc.winboll.studio.positions.activities.SettingsActivity;
|
||||||
import cc.winboll.studio.positions.activities.WinBoLLActivity;
|
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.AppConfigsUtil;
|
||||||
import cc.winboll.studio.positions.utils.ServiceUtil;
|
import cc.winboll.studio.positions.utils.ServiceUtil;
|
||||||
|
import cc.winboll.studio.positions.R;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主页面:仅负责
|
* 主页面控制器
|
||||||
* 1. 位置服务启动/停止(通过 Switch 开关控制)
|
* 功能简述:
|
||||||
* 2. 跳转至“位置管理页(LocationActivity)”和“日志页(LogActivity)”
|
* 1. 位置服务启停开关控制
|
||||||
* 3. Java 7 语法适配:无 Lambda、显式接口实现、兼容低版本
|
* 2. 页面菜单跳转与主题管理
|
||||||
|
* 3. 应用空转状态接收与日志实时输出展示
|
||||||
|
* 4. 全局权限申请与权限结果回调处理
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @CreateTime 2026/05/03 12:23:00
|
||||||
|
* @EditTime 2026/05/03 14:36:21
|
||||||
*/
|
*/
|
||||||
public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||||
|
|
||||||
|
// ===================== 常量定义 =====================
|
||||||
public static final String TAG = "MainActivity";
|
public static final String TAG = "MainActivity";
|
||||||
// 权限请求码(建议定义为类常量,避免魔法值)
|
|
||||||
private static final int REQUEST_LOCATION_PERMISSIONS = 1001;
|
private static final int REQUEST_LOCATION_PERMISSIONS = 1001;
|
||||||
private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002;
|
private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002;
|
||||||
|
|
||||||
// UI 控件:服务控制开关、顶部工具栏
|
// ===================== UI控件声明 =====================
|
||||||
|
private Toolbar mToolbar;
|
||||||
private Switch mServiceSwitch;
|
private Switch mServiceSwitch;
|
||||||
private Button mManagePositionsButton;
|
private Button mManagePositionsButton;
|
||||||
private Toolbar mToolbar;
|
private ADsBannerView mADsBannerView;
|
||||||
// 服务相关:服务实例、绑定状态标记
|
private TextView mTvIdleLog;
|
||||||
//private DistanceRefreshService mDistanceService;
|
private ScrollView mScrollIdleLog;
|
||||||
|
|
||||||
|
// ===================== 业务标记与回调 =====================
|
||||||
private boolean isServiceBound = false;
|
private boolean isServiceBound = false;
|
||||||
ADsBannerView mADsBannerView;
|
private OnAppIdleRunningListener mIdleRunningListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用空转状态回调内部接口
|
||||||
|
*/
|
||||||
|
public interface OnAppIdleRunningListener {
|
||||||
|
/**
|
||||||
|
* 接收空转开关状态变更
|
||||||
|
* @param isRunning 空转是否开启
|
||||||
|
*/
|
||||||
|
void onIdleStatusChange(boolean isRunning);
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public Activity getActivity() {
|
* 接收空转日志消息
|
||||||
return this;
|
* @param log 日志文本
|
||||||
|
*/
|
||||||
|
void onIdleLogReceive(String log);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
// ===================== 回调绑定方法 =====================
|
||||||
public String getTag() {
|
public void setOnAppIdleRunningListener(OnAppIdleRunningListener listener) {
|
||||||
return TAG;
|
this.mIdleRunningListener = listener;
|
||||||
|
LogUtils.i(TAG, "setOnAppIdleRunningListener -> 空转监听绑定完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------- 服务连接回调(仅用于获取服务状态,不依赖服务执行核心逻辑) ----------------------
|
public OnAppIdleRunningListener getOnAppIdleRunningListener() {
|
||||||
// private final ServiceConnection mServiceConn = new ServiceConnection() {
|
return mIdleRunningListener;
|
||||||
// /**
|
}
|
||||||
// * 服务绑定成功:获取服务实例,同步开关状态(以服务实际状态为准)
|
|
||||||
// */
|
|
||||||
// @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
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_main); // 关联主页面布局
|
setContentView(R.layout.activity_main);
|
||||||
|
LogUtils.i(TAG, "onCreate -> MainActivity页面创建完成");
|
||||||
|
|
||||||
// 1. 初始化顶部 Toolbar(保留原逻辑,设置页面标题)
|
|
||||||
initToolbar();
|
initToolbar();
|
||||||
// 2. 初始化其他控件
|
|
||||||
initViews();
|
initViews();
|
||||||
// 3. 检查并申请位置权限(含后台GPS权限,确保服务启动前权限就绪)
|
setLLMainBackgroundColor();
|
||||||
|
|
||||||
if (!checkLocationPermissions()) {
|
if (!checkLocationPermissions()) {
|
||||||
|
LogUtils.d(TAG, "onCreate -> 定位权限未授予,发起权限申请");
|
||||||
requestLocationPermissions();
|
requestLocationPermissions();
|
||||||
}
|
}
|
||||||
// 4. 绑定服务(仅用于获取服务实时状态,不影响服务独立运行)
|
|
||||||
//bindDistanceService();
|
|
||||||
|
|
||||||
mADsBannerView = findViewById(R.id.adsbanner);
|
mADsBannerView = findViewById(R.id.adsbanner);
|
||||||
|
initAppIdleHandler();
|
||||||
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
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
LogUtils.d(TAG, "onResume -> 页面恢复可见");
|
||||||
if (mADsBannerView != null) {
|
if (mADsBannerView != null) {
|
||||||
mADsBannerView.resumeADs(MainActivity.this);
|
mADsBannerView.resumeADs(MainActivity.this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
LogUtils.i(TAG, "onDestroy -> MainActivity页面销毁释放资源");
|
||||||
|
if (mADsBannerView != null) {
|
||||||
|
mADsBannerView.releaseAdResources();
|
||||||
|
}
|
||||||
|
mIdleRunningListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 初始化相关方法 =====================
|
||||||
// ---------------------- 核心功能1:初始化UI组件(Toolbar + 服务开关) ----------------------
|
|
||||||
/**
|
/**
|
||||||
* 初始化顶部 Toolbar,设置页面标题
|
* 初始化顶部导航栏
|
||||||
*/
|
*/
|
||||||
private void initToolbar() {
|
private void initToolbar() {
|
||||||
mToolbar = (Toolbar) findViewById(R.id.toolbar); // Java 7 显式 findViewById + 强转
|
mToolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(mToolbar);
|
setSupportActionBar(mToolbar);
|
||||||
// 给ActionBar设置标题(先判断非空,避免空指针异常)
|
|
||||||
if (getSupportActionBar() != null) {
|
if (getSupportActionBar() != null) {
|
||||||
getSupportActionBar().setTitle(getString(R.string.app_name));
|
getSupportActionBar().setTitle(getString(R.string.app_name));
|
||||||
}
|
}
|
||||||
|
LogUtils.d(TAG, "initToolbar -> 顶部工具栏初始化完毕");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化服务控制开关:读取SP状态、绑定点击事件(含权限检查)
|
* 初始化全部UI控件与绑定事件
|
||||||
*/
|
*/
|
||||||
private void initViews() {
|
private void initViews() {
|
||||||
mServiceSwitch = (Switch) findViewById(R.id.switch_service_control); // 显式强转
|
mTvIdleLog = (TextView) findViewById(R.id.tv_idle_log);
|
||||||
mServiceSwitch.setChecked(AppConfigsUtil.getInstance(this).isEnableMainService(true));
|
mScrollIdleLog = (ScrollView) findViewById(R.id.scroll_idle_log);
|
||||||
|
mServiceSwitch = (Switch) findViewById(R.id.switch_service_control);
|
||||||
mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions);
|
mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions);
|
||||||
mManagePositionsButton.setEnabled(mServiceSwitch.isChecked());
|
|
||||||
|
|
||||||
// Java 7 用匿名内部类实现 CompoundButton.OnCheckedChangeListener
|
boolean serviceEnable = AppConfigsUtil.getInstance(this).isEnableMainService(true);
|
||||||
|
mServiceSwitch.setChecked(serviceEnable);
|
||||||
|
mManagePositionsButton.setEnabled(serviceEnable);
|
||||||
|
|
||||||
mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||||
// 开关打开前先检查权限:无权限则终止操作、重置开关、引导申请
|
LogUtils.d(TAG, "onCheckedChanged -> 服务开关状态变更,isChecked = " + isChecked);
|
||||||
if (isChecked && !checkLocationPermissions()) {
|
if (isChecked && !checkLocationPermissions()) {
|
||||||
requestLocationPermissions();
|
requestLocationPermissions();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 权限就绪:执行服务启停逻辑
|
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
LogUtils.d(TAG, "设置启动服务");
|
|
||||||
ServiceUtil.startAutoService(MainActivity.this);
|
ServiceUtil.startAutoService(MainActivity.this);
|
||||||
} else {
|
} else {
|
||||||
LogUtils.d(TAG, "设置关闭服务");
|
|
||||||
|
|
||||||
ServiceUtil.stopAutoService(MainActivity.this);
|
ServiceUtil.stopAutoService(MainActivity.this);
|
||||||
}
|
}
|
||||||
|
|
||||||
mManagePositionsButton.setEnabled(isChecked);
|
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
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
// 主题菜单
|
|
||||||
AESThemeUtil.inflateMenu(this, menu);
|
AESThemeUtil.inflateMenu(this, menu);
|
||||||
// 调试工具菜单
|
|
||||||
if (App.isDebugging()) {
|
if (App.isDebugging()) {
|
||||||
DevelopUtils.inflateMenu(this, menu);
|
DevelopUtils.inflateMenu(this, menu);
|
||||||
}
|
}
|
||||||
// 应用其他菜单
|
|
||||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
int menuItemId = item.getItemId();
|
int menuItemId = item.getItemId();
|
||||||
|
LogUtils.d(TAG, "onOptionsItemSelected -> 点击菜单ID = " + menuItemId);
|
||||||
|
|
||||||
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
|
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
|
||||||
recreate();
|
recreate();
|
||||||
} if (DevelopUtils.onDevelopItemSelected(this, item)) {
|
} else if (DevelopUtils.onDevelopItemSelected(this, item)) {
|
||||||
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
|
LogUtils.d(TAG, "onOptionsItemSelected -> 进入开发工具菜单");
|
||||||
} else if (menuItemId == R.id.item_settings) {
|
} else if (menuItemId == R.id.item_settings) {
|
||||||
Intent intent = new Intent();
|
Intent intent = new Intent();
|
||||||
intent.setClass(this, SettingsActivity.class);
|
intent.setClass(this, SettingsActivity.class);
|
||||||
@@ -226,105 +316,28 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
|||||||
intent.setClass(this, AboutActivity.class);
|
intent.setClass(this, AboutActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
} else {
|
} else {
|
||||||
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
return true;
|
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) {
|
public void onPositions(View view) {
|
||||||
//ToastUtils.show("onPositions");
|
LogUtils.d(TAG, "onPositions -> 跳转位置任务管理页面");
|
||||||
// 服务已启动:跳转到位置管理页
|
|
||||||
startActivity(new Intent(MainActivity.this, LocationActivity.class));
|
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
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public Activity getActivity() {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
return this;
|
||||||
// 处理前台权限申请结果
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -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<zhangsken@qq.com>
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -57,6 +57,24 @@
|
|||||||
android:textColor="@drawable/btn_text_selector"
|
android:textColor="@drawable/btn_text_selector"
|
||||||
android:padding="12dp"/>
|
android:padding="12dp"/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/scroll_idle_log"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginLeft="50dp"
|
||||||
|
android:layout_marginRight="50dp"
|
||||||
|
android:layout_weight="1.0">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_idle_log"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textIsSelectable="true"
|
||||||
|
android:textColor="#FF000000"/>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<cc.winboll.studio.libaes.views.ADsBannerView
|
<cc.winboll.studio.libaes.views.ADsBannerView
|
||||||
|
|||||||
Reference in New Issue
Block a user