MyApplication.java 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. package com.YuyeTech.HeartRate;
  2. import android.app.Activity;
  3. import android.app.Application;
  4. import android.content.Context;
  5. import android.content.Intent;
  6. import android.content.IntentFilter;
  7. import android.os.Build;
  8. import android.os.Bundle;
  9. import android.provider.Settings;
  10. import android.util.Log;
  11. import android.view.KeyEvent;
  12. import android.view.MotionEvent;
  13. import android.view.View;
  14. import android.view.Window;
  15. import android.view.WindowManager;
  16. import android.view.inputmethod.InputMethodManager;
  17. import java.util.List;
  18. import androidx.annotation.NonNull;
  19. import androidx.annotation.Nullable;
  20. import com.ble.mylockview.BuildConfig;
  21. import com.ble.mylockview.admin.KioskManager;
  22. import com.ble.mylockview.config.LaunchConfig;
  23. import io.dcloud.PandoraEntry;
  24. import io.dcloud.PandoraEntryActivity;
  25. import io.dcloud.application.DCloudApplication;
  26. import io.dcloud.uniplugin.NativeLogBridgeHelper;
  27. /**
  28. * 自定义 Application,实现 Kiosk 模式自动初始化
  29. * Created on 2026/1/22.
  30. */
  31. public class MyApplication extends DCloudApplication {
  32. private static final String TAG = "MyApplication";
  33. // 屏幕状态广播接收器
  34. private ScreenStateReceiver screenStateReceiver;
  35. @Override
  36. public void onCreate() {
  37. super.onCreate();
  38. NativeLogBridgeHelper.init(this);
  39. if (BuildConfig.DEBUG) Log.d(TAG, "Application 初始化");
  40. // ✅ 尽早禁用锁屏(Device Owner):避免开机自启时仍在锁屏阶段导致 IME 无法显示/被遮挡
  41. KioskHelper.disableKeyguardIfOwner(this);
  42. // 启动时重置 WiFi 检测开关,避免上一次运行遗留为 true 导致一进来就弹框
  43. try {
  44. getSharedPreferences("kiosk_prefs", Context.MODE_PRIVATE)
  45. .edit()
  46. .putBoolean("wifi_check_enabled", false)
  47. .apply();
  48. if (BuildConfig.DEBUG) Log.d(TAG, "已在 Application 启动时将 wifi_check_enabled 重置为 false");
  49. } catch (Exception e) {
  50. Log.e(TAG, "重置 wifi_check_enabled 失败", e);
  51. }
  52. // ✅ 修复:首次安装后自启动时输入法无法调起的问题
  53. // 在应用启动时提前初始化输入法服务,确保输入法服务已就绪
  54. initializeInputMethodService();
  55. // ✅ 配置 Kiosk 启动 Activity - 直接启动主页面,跳过黑屏的 PandoraEntry
  56. LaunchConfig.setLaunchActivity(PandoraEntryActivity.class);
  57. // ✅ 启用 Debug 模式(发布时改为 false)
  58. // KioskManager.setDebug(true);
  59. KioskManager.setDebug(BuildConfig.DEBUG);
  60. // ✅ 注册屏幕状态广播接收器(监听电源键)
  61. registerScreenStateReceiver();
  62. // ✅ 注册 Activity 生命周期监听,自动绑定 KioskManager
  63. registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
  64. @Override
  65. public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
  66. // 只对主 Activity 进行 Kiosk 绑定
  67. if (activity instanceof io.dcloud.PandoraEntry ||
  68. activity instanceof io.dcloud.PandoraEntryActivity) {
  69. if (BuildConfig.DEBUG) Log.d(TAG, "✅ 绑定 Kiosk 到: " + activity.getClass().getSimpleName());
  70. // ✅ 先禁用锁屏(必须在 attach 之前)
  71. KioskHelper.disableLockScreen(activity);
  72. // 临时方案:不常亮,由系统休眠时间(如 50 分钟)控制;需在 attach 之前设置
  73. KioskManager.setAllowKeepScreenOn(false);
  74. //KioskManager.setDebug(BuildConfig.DEBUG);
  75. KioskManager.attach(activity);
  76. //KioskHelper.clearKeepScreenOn(activity);
  77. PowerSavingManager.ensureWakeLockReleased();
  78. // ✅ 拦截返回键,防止回到黑屏页面
  79. interceptBackPress(activity);
  80. // ✅ 设置当前 Activity 到 PowerSavingManager(用于省电模式)
  81. PowerSavingManager.setCurrentActivity(activity);
  82. // ✅ WiFi 检测:仍在 Activity 创建时注册延迟检测任务
  83. // 但实际是否执行检测由 DeviceStatusModule.startNetworkMonitor 设置的开关控制
  84. if (BuildConfig.DEBUG) Log.d(TAG, "🔍 Activity 创建完成,注册 WiFi 启动检测任务: " + activity.getClass().getSimpleName());
  85. WifiCheckHelper.checkWifiOnActivityStart(activity);
  86. }
  87. }
  88. @Override
  89. public void onActivityStarted(@NonNull Activity activity) {}
  90. @Override
  91. public void onActivityResumed(@NonNull Activity activity) {
  92. if (activity instanceof io.dcloud.PandoraEntry ||
  93. activity instanceof io.dcloud.PandoraEntryActivity) {
  94. KioskManager.onResume();
  95. // 临时方案:确保未加常亮(若 KioskManager.setAllowKeepScreenOn(false) 已设则此处为双保险)
  96. //KioskHelper.clearKeepScreenOn(activity);
  97. // ✅ 更新 PowerSavingManager 的 Activity 引用
  98. PowerSavingManager.setCurrentActivity(activity);
  99. // ✅ 在 onResume 时检测 WiFi(每次恢复都检测,包括从设置返回、开机启动等所有情况)
  100. if (BuildConfig.DEBUG) Log.d(TAG, "📱 " + activity.getClass().getSimpleName() + " onResume → 开始 WiFi 检测");
  101. WifiCheckHelper.checkWifiOnActivityResume(activity);
  102. }
  103. }
  104. @Override
  105. public void onActivityPaused(@NonNull Activity activity) {
  106. if (activity instanceof io.dcloud.PandoraEntry ||
  107. activity instanceof io.dcloud.PandoraEntryActivity) {
  108. KioskManager.onPause();
  109. }
  110. }
  111. @Override
  112. public void onActivityStopped(@NonNull Activity activity) {}
  113. @Override
  114. public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}
  115. @Override
  116. public void onActivityDestroyed(@NonNull Activity activity) {
  117. if (activity instanceof io.dcloud.PandoraEntry ||
  118. activity instanceof io.dcloud.PandoraEntryActivity) {
  119. KioskManager.onDestroy();
  120. // 清理 WiFi 检测资源
  121. WifiCheckHelper.cleanup(activity);
  122. // 清理 PowerSavingManager 的 Activity 引用
  123. PowerSavingManager.setCurrentActivity(null);
  124. }
  125. }
  126. });
  127. }
  128. /**
  129. * 初始化输入法服务
  130. * 通过多种方式触发输入法服务的初始化,确保输入法服务已就绪
  131. * 特别针对搜狗输入法等预装输入法进行优化
  132. * 解决首次安装后自启动时输入法无法调起的问题
  133. */
  134. private void initializeInputMethodService() {
  135. try {
  136. InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
  137. if (imm != null) {
  138. // 方法1: 获取输入法列表,触发系统初始化输入法服务
  139. List<android.view.inputmethod.InputMethodInfo> allInputMethods = imm.getInputMethodList();
  140. if (BuildConfig.DEBUG) Log.d(TAG, "所有输入法数量: " + (allInputMethods != null ? allInputMethods.size() : 0));
  141. // 方法2: 获取启用的输入法列表(特别重要,确保搜狗输入法已启用)
  142. try {
  143. List<android.view.inputmethod.InputMethodInfo> enabledInputMethods = imm.getEnabledInputMethodList();
  144. if (enabledInputMethods != null) {
  145. if (BuildConfig.DEBUG) Log.d(TAG, "已启用的输入法数量: " + enabledInputMethods.size());
  146. // 检查搜狗输入法是否在已启用列表中
  147. boolean hasSogou = false;
  148. for (android.view.inputmethod.InputMethodInfo info : enabledInputMethods) {
  149. String packageName = info.getPackageName();
  150. String serviceName = info.getServiceName();
  151. if (BuildConfig.DEBUG) Log.d(TAG, "已启用输入法: " + packageName + " / " + serviceName);
  152. // 检查是否是搜狗输入法(搜狗输入法的包名通常是 com.sohu.inputmethod.sogou)
  153. if (packageName != null && packageName.contains("sogou")) {
  154. hasSogou = true;
  155. if (BuildConfig.DEBUG) Log.d(TAG, "✅ 检测到搜狗输入法已启用: " + packageName);
  156. }
  157. }
  158. if (!hasSogou && allInputMethods != null) {
  159. // 检查搜狗输入法是否在全部列表中但未启用
  160. for (android.view.inputmethod.InputMethodInfo info : allInputMethods) {
  161. String packageName = info.getPackageName();
  162. if (packageName != null && packageName.contains("sogou")) {
  163. Log.w(TAG, "⚠️ 搜狗输入法存在但未启用: " + packageName);
  164. }
  165. }
  166. }
  167. }
  168. } catch (Exception e) {
  169. Log.w(TAG, "获取已启用输入法列表失败", e);
  170. }
  171. // 方法3: 获取当前输入法信息,进一步触发初始化
  172. try {
  173. String currentInputMethodId = Settings.Secure.getString(
  174. getContentResolver(),
  175. Settings.Secure.DEFAULT_INPUT_METHOD);
  176. if (currentInputMethodId != null) {
  177. if (BuildConfig.DEBUG) Log.d(TAG, "当前默认输入法: " + currentInputMethodId);
  178. if (currentInputMethodId.contains("sogou")) {
  179. if (BuildConfig.DEBUG) Log.d(TAG, "✅ 当前默认输入法是搜狗输入法");
  180. }
  181. }
  182. imm.getCurrentInputMethodSubtype();
  183. } catch (Exception e) {
  184. // Android 某些版本可能不支持,忽略
  185. Log.d(TAG, "获取当前输入法信息失败(可能不支持)", e);
  186. }
  187. // 方法4: 尝试获取输入法服务状态,进一步触发初始化
  188. // 注意:该方法在部分 Android 版本/ROM 上可能不存在(会抛 NoSuchMethodError),
  189. // 因此这里用反射探测存在性并吞掉所有 Throwable,避免在 Application.onCreate 阶段致命崩溃。
  190. try {
  191. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  192. java.lang.reflect.Method m =
  193. InputMethodManager.class.getMethod("isInputMethodSuppressingSpellChecker");
  194. Object r = m.invoke(imm);
  195. if (BuildConfig.DEBUG) Log.d(TAG, "IME spellChecker suppress: " + r);
  196. }
  197. } catch (Throwable t) {
  198. if (BuildConfig.DEBUG) Log.d(TAG, "IME 初始化探测方法不可用,已忽略", t);
  199. }
  200. if (BuildConfig.DEBUG) Log.d(TAG, "✅ 输入法服务已初始化(Application onCreate,已检查搜狗输入法)");
  201. } else {
  202. Log.w(TAG, "⚠️ 无法获取InputMethodManager");
  203. }
  204. } catch (Throwable t) {
  205. Log.w(TAG, "⚠️ 初始化输入法服务失败", t);
  206. }
  207. }
  208. /**
  209. * 注册屏幕状态广播接收器
  210. * 监听 ACTION_SCREEN_OFF 和 ACTION_SCREEN_ON 来实现电源键省电功能
  211. */
  212. private void registerScreenStateReceiver() {
  213. try {
  214. screenStateReceiver = new ScreenStateReceiver();
  215. IntentFilter filter = new IntentFilter();
  216. filter.addAction(Intent.ACTION_SCREEN_OFF);
  217. filter.addAction(Intent.ACTION_SCREEN_ON);
  218. filter.setPriority(1000); // 高优先级,确保能及时收到广播
  219. registerReceiver(screenStateReceiver, filter);
  220. if (BuildConfig.DEBUG) Log.d(TAG, "✅ 屏幕状态广播接收器已注册");
  221. } catch (Exception e) {
  222. Log.e(TAG, "注册屏幕状态广播接收器失败", e);
  223. }
  224. }
  225. /**
  226. * 注销屏幕状态广播接收器
  227. */
  228. private void unregisterScreenStateReceiver() {
  229. try {
  230. if (screenStateReceiver != null) {
  231. unregisterReceiver(screenStateReceiver);
  232. screenStateReceiver = null;
  233. if (BuildConfig.DEBUG) Log.d(TAG, "✅ 屏幕状态广播接收器已注销");
  234. }
  235. } catch (Exception e) {
  236. Log.e(TAG, "注销屏幕状态广播接收器失败", e);
  237. }
  238. }
  239. @Override
  240. public void onTerminate() {
  241. super.onTerminate();
  242. // 注销广播接收器
  243. unregisterScreenStateReceiver();
  244. }
  245. /**
  246. * 拦截 Activity 的返回键事件
  247. * 使用多种方式确保返回键被拦截
  248. * @param activity Activity 实例
  249. */
  250. private void interceptBackPress(Activity activity) {
  251. try {
  252. // 方法 1:使用 View.OnKeyListener
  253. activity.getWindow().getDecorView().setOnKeyListener((v, keyCode, event) -> {
  254. if (keyCode == KeyEvent.KEYCODE_BACK) {
  255. if (BuildConfig.DEBUG) Log.d(TAG, "🔙 [OnKeyListener] 检测到返回键: action=" + event.getAction());
  256. if (event.getAction() == KeyEvent.ACTION_UP) {
  257. return BackPressHandler.handleBackPress(activity);
  258. }
  259. return true; // 拦截 ACTION_DOWN
  260. }
  261. return false;
  262. });
  263. // 方法 2:设置 View 为可聚焦,确保能接收按键事件
  264. View decorView = activity.getWindow().getDecorView();
  265. decorView.setFocusable(true);
  266. decorView.setFocusableInTouchMode(true);
  267. decorView.requestFocus();
  268. // 方法 3:使用 Window.Callback 拦截(更底层)
  269. installWindowCallback(activity);
  270. if (BuildConfig.DEBUG) Log.d(TAG, "✅ 返回键拦截器已安装: " + activity.getClass().getSimpleName());
  271. } catch (Exception e) {
  272. Log.e(TAG, "安装返回键拦截器失败", e);
  273. }
  274. }
  275. /**
  276. * 安装 Window.Callback 拦截器(底层方法)
  277. * @param activity Activity 实例
  278. */
  279. private void installWindowCallback(Activity activity) {
  280. try {
  281. Window.Callback original = activity.getWindow().getCallback();
  282. activity.getWindow().setCallback(new Window.Callback() {
  283. @Override
  284. public boolean dispatchKeyEvent(KeyEvent event) {
  285. if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
  286. if (BuildConfig.DEBUG) Log.d(TAG, "🔙 [WindowCallback] 检测到返回键: action=" + event.getAction());
  287. if (event.getAction() == KeyEvent.ACTION_UP) {
  288. boolean handled = BackPressHandler.handleBackPress(activity);
  289. if (handled) {
  290. return true; // 已拦截
  291. }
  292. } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
  293. return true; // 拦截 DOWN 事件
  294. }
  295. }
  296. return original != null && original.dispatchKeyEvent(event);
  297. }
  298. // 委托其他方法给原始 Callback
  299. @Override public boolean dispatchKeyShortcutEvent(KeyEvent event) {
  300. return original != null && original.dispatchKeyShortcutEvent(event);
  301. }
  302. @Override public boolean dispatchTouchEvent(MotionEvent event) {
  303. return original != null && original.dispatchTouchEvent(event);
  304. }
  305. @Override public boolean dispatchTrackballEvent(MotionEvent event) {
  306. return original != null && original.dispatchTrackballEvent(event);
  307. }
  308. @Override public boolean dispatchGenericMotionEvent(MotionEvent event) {
  309. return original != null && original.dispatchGenericMotionEvent(event);
  310. }
  311. @Override public boolean dispatchPopulateAccessibilityEvent(android.view.accessibility.AccessibilityEvent event) {
  312. return original != null && original.dispatchPopulateAccessibilityEvent(event);
  313. }
  314. @Override public View onCreatePanelView(int featureId) {
  315. return original != null ? original.onCreatePanelView(featureId) : null;
  316. }
  317. @Override public boolean onCreatePanelMenu(int featureId, android.view.Menu menu) {
  318. return original != null && original.onCreatePanelMenu(featureId, menu);
  319. }
  320. @Override public boolean onPreparePanel(int featureId, View view, android.view.Menu menu) {
  321. return original != null && original.onPreparePanel(featureId, view, menu);
  322. }
  323. @Override public boolean onMenuOpened(int featureId, android.view.Menu menu) {
  324. return original != null && original.onMenuOpened(featureId, menu);
  325. }
  326. @Override public boolean onMenuItemSelected(int featureId, android.view.MenuItem item) {
  327. return original != null && original.onMenuItemSelected(featureId, item);
  328. }
  329. @Override public void onWindowAttributesChanged(WindowManager.LayoutParams attrs) {
  330. if (original != null) original.onWindowAttributesChanged(attrs);
  331. }
  332. @Override public void onContentChanged() {
  333. if (original != null) original.onContentChanged();
  334. }
  335. @Override public void onWindowFocusChanged(boolean hasFocus) {
  336. if (original != null) original.onWindowFocusChanged(hasFocus);
  337. }
  338. @Override public void onAttachedToWindow() {
  339. if (original != null) original.onAttachedToWindow();
  340. }
  341. @Override public void onDetachedFromWindow() {
  342. if (original != null) original.onDetachedFromWindow();
  343. }
  344. @Override public void onPanelClosed(int featureId, android.view.Menu menu) {
  345. if (original != null) original.onPanelClosed(featureId, menu);
  346. }
  347. @Override public boolean onSearchRequested() {
  348. return original != null && original.onSearchRequested();
  349. }
  350. @Override public boolean onSearchRequested(android.view.SearchEvent searchEvent) {
  351. return original != null && original.onSearchRequested(searchEvent);
  352. }
  353. @Override public android.view.ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback callback) {
  354. return original != null ? original.onWindowStartingActionMode(callback) : null;
  355. }
  356. @Override public android.view.ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback callback, int type) {
  357. return original != null ? original.onWindowStartingActionMode(callback, type) : null;
  358. }
  359. @Override public void onActionModeStarted(android.view.ActionMode mode) {
  360. if (original != null) original.onActionModeStarted(mode);
  361. }
  362. @Override public void onActionModeFinished(android.view.ActionMode mode) {
  363. if (original != null) original.onActionModeFinished(mode);
  364. }
  365. });
  366. if (BuildConfig.DEBUG) Log.d(TAG, "✅ Window.Callback 拦截器已安装");
  367. } catch (Exception e) {
  368. Log.e(TAG, "安装 Window.Callback 失败", e);
  369. }
  370. }
  371. }