|
1 | 1 | package com.github.dhangofa.batteryremapper; |
2 | 2 |
|
| 3 | +import android.app.AlertDialog; |
| 4 | +import android.app.AndroidAppHelper; |
| 5 | +import android.content.Context; |
3 | 6 | import android.content.Intent; |
4 | 7 | import android.os.BatteryManager; |
| 8 | +import android.os.Bundle; |
| 9 | +import android.os.CountDownTimer; |
| 10 | +import android.os.Handler; |
| 11 | +import android.os.Looper; |
| 12 | +import android.view.WindowManager; |
5 | 13 | import de.robv.android.xposed.IXposedHookLoadPackage; |
6 | 14 | import de.robv.android.xposed.XC_MethodHook; |
7 | 15 | import de.robv.android.xposed.XposedBridge; |
|
10 | 18 |
|
11 | 19 | public class BatteryHook implements IXposedHookLoadPackage { |
12 | 20 |
|
| 21 | + // Keep track of state so we don't spawn 100 dialogs at once |
| 22 | + private static boolean isShuttingDown = false; |
| 23 | + private static AlertDialog shutdownDialog = null; |
| 24 | + private static CountDownTimer shutdownTimer = null; |
| 25 | + |
13 | 26 | @Override |
14 | 27 | public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable { |
15 | | - // We only want to trick the System UI into drawing the new number. |
16 | | - // We leave the rest of the OS alone so charging logic doesn't break. |
17 | 28 | if (!lpparam.packageName.equals("com.android.systemui")) { |
18 | 29 | return; |
19 | 30 | } |
20 | 31 |
|
21 | 32 | try { |
22 | | - // Hook the exact moment SystemUI reads an integer from ANY Intent |
23 | 33 | XposedHelpers.findAndHookMethod(Intent.class, "getIntExtra", String.class, int.class, new XC_MethodHook() { |
24 | 34 | @Override |
25 | 35 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { |
26 | 36 | String key = (String) param.args[0]; |
27 | 37 |
|
28 | | - // If SystemUI is asking for the Battery Level... |
29 | 38 | if (BatteryManager.EXTRA_LEVEL.equals(key)) { |
30 | | - // Get the real, physical battery level the system just read |
31 | 39 | int originalLevel = (Integer) param.getResult(); |
32 | 40 |
|
33 | | - // Run our math and overwrite the result being returned to crDroid |
| 41 | + Intent intent = (Intent) param.thisObject; |
| 42 | + Bundle extras = intent.getExtras(); |
| 43 | + |
| 44 | + // Check if phone is plugged in (0 means on battery) |
| 45 | + int plugged = extras != null ? extras.getInt(BatteryManager.EXTRA_PLUGGED, 0) : 0; |
| 46 | + |
| 47 | + // 1. SHUTDOWN LOGIC |
| 48 | + if (originalLevel <= 20) { |
| 49 | + if (plugged == 0) { |
| 50 | + // Hit 20% and not charging: Start the countdown once |
| 51 | + if (!isShuttingDown) { |
| 52 | + isShuttingDown = true; |
| 53 | + startCountdown(); |
| 54 | + } |
| 55 | + } else { |
| 56 | + // Hit 20% but charger is connected: Abort shutdown |
| 57 | + if (isShuttingDown) { |
| 58 | + cancelCountdown(); |
| 59 | + } |
| 60 | + } |
| 61 | + } else { |
| 62 | + // Battery is safely above 20%: Abort shutdown if running |
| 63 | + if (isShuttingDown) { |
| 64 | + cancelCountdown(); |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + // 2. UI LOGIC: Overwrite the result for the status bar |
34 | 69 | int displayedLevel = remapBattery(originalLevel); |
35 | 70 | param.setResult(displayedLevel); |
36 | 71 | } |
37 | 72 | } |
38 | 73 | }); |
39 | 74 |
|
40 | | - XposedBridge.log("BatteryRemapper: Successfully hooked Intent.getIntExtra for crDroid!"); |
| 75 | + XposedBridge.log("BatteryRemapper: Hook initialized with 30s Countdown Feature!"); |
41 | 76 |
|
42 | 77 | } catch (Throwable t) { |
43 | 78 | XposedBridge.log("BatteryRemapper Hook Critical Failure: " + t.getMessage()); |
44 | 79 | } |
45 | 80 | } |
46 | 81 |
|
| 82 | + private void startCountdown() { |
| 83 | + // UI elements MUST run on the main Android UI thread |
| 84 | + new Handler(Looper.getMainLooper()).post(new Runnable() { |
| 85 | + @Override |
| 86 | + public void run() { |
| 87 | + try { |
| 88 | + Context context = AndroidAppHelper.currentApplication(); |
| 89 | + if (context == null) return; |
| 90 | + |
| 91 | + // Build the Warning Box |
| 92 | + AlertDialog.Builder builder = new AlertDialog.Builder(context, android.R.style.Theme_DeviceDefault_Dialog_Alert); |
| 93 | + builder.setTitle("Battery Depleted"); |
| 94 | + builder.setMessage("Device will shut down in 30 seconds.\nPlug in charger to cancel."); |
| 95 | + builder.setCancelable(false); // Prevents dismissing by tapping outside |
| 96 | + |
| 97 | + shutdownDialog = builder.create(); |
| 98 | + // TYPE_SYSTEM_ERROR (2010) bypasses all screens to draw directly over everything |
| 99 | + shutdownDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR); |
| 100 | + shutdownDialog.show(); |
| 101 | + |
| 102 | + // Start the 30 second timer (30000ms total, updating every 1000ms) |
| 103 | + shutdownTimer = new CountDownTimer(30000, 1000) { |
| 104 | + @Override |
| 105 | + public void onTick(long millisUntilFinished) { |
| 106 | + if (shutdownDialog != null && shutdownDialog.isShowing()) { |
| 107 | + shutdownDialog.setMessage("Device will shut down in " + (millisUntilFinished / 1000) + " seconds.\nPlug in charger to cancel."); |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + @Override |
| 112 | + public void onFinish() { |
| 113 | + cancelCountdown(); // Cleanup the UI |
| 114 | + triggerShutdown(); // Kill the power |
| 115 | + } |
| 116 | + }.start(); |
| 117 | + |
| 118 | + XposedBridge.log("BatteryRemapper: 30-second shutdown countdown started."); |
| 119 | + } catch (Throwable t) { |
| 120 | + XposedBridge.log("BatteryRemapper UI Failure: " + t.getMessage()); |
| 121 | + // Failsafe: If the custom ROM blocks the dialog from drawing, shut down gracefully anyway |
| 122 | + triggerShutdown(); |
| 123 | + } |
| 124 | + } |
| 125 | + }); |
| 126 | + } |
| 127 | + |
| 128 | + private void cancelCountdown() { |
| 129 | + isShuttingDown = false; |
| 130 | + new Handler(Looper.getMainLooper()).post(new Runnable() { |
| 131 | + @Override |
| 132 | + public void run() { |
| 133 | + if (shutdownTimer != null) { |
| 134 | + shutdownTimer.cancel(); |
| 135 | + shutdownTimer = null; |
| 136 | + } |
| 137 | + if (shutdownDialog != null) { |
| 138 | + if (shutdownDialog.isShowing()) { |
| 139 | + shutdownDialog.dismiss(); |
| 140 | + } |
| 141 | + shutdownDialog = null; |
| 142 | + } |
| 143 | + XposedBridge.log("BatteryRemapper: Shutdown cancelled! Charger detected."); |
| 144 | + } |
| 145 | + }); |
| 146 | + } |
| 147 | + |
| 148 | + private void triggerShutdown() { |
| 149 | + try { |
| 150 | + Context context = AndroidAppHelper.currentApplication(); |
| 151 | + if (context != null) { |
| 152 | + Object powerManager = context.getSystemService(Context.POWER_SERVICE); |
| 153 | + if (powerManager != null) { |
| 154 | + // Call the hidden OS-level power off command |
| 155 | + XposedHelpers.callMethod(powerManager, "shutdown", false, "battery_remapper_empty", false); |
| 156 | + } |
| 157 | + } |
| 158 | + } catch (Throwable t) { |
| 159 | + XposedBridge.log("BatteryRemapper Shutdown Failure: " + t.getMessage()); |
| 160 | + isShuttingDown = false; |
| 161 | + } |
| 162 | + } |
| 163 | + |
47 | 164 | private int remapBattery(int physicalLevel) { |
48 | 165 | if (physicalLevel <= 20) return 0; |
49 | 166 | if (physicalLevel >= 80) return 100; |
|
0 commit comments