├── .gitignore ├── LuckyMoney.iml ├── Readme.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── orz │ │ └── macrobull │ │ └── luckymoney │ │ ├── AService.java │ │ ├── MainActivity.java │ │ └── NLService.java │ └── res │ ├── layout-land │ └── activity_main.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── as_config.xml ├── build.gradle ├── gradle.properties ├── gradlew ├── gradlew.bat ├── local.properties ├── out ├── Screencast.mp4 ├── Screenshot.png └── app-debug.apk └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | 15 | .directory 16 | .gradle 17 | .idea/libraries 18 | app/build 19 | gradle -------------------------------------------------------------------------------- /LuckyMoney.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Wechat LuckyMoney 2 | ===== 3 | [下载](https://raw.githubusercontent.com/MidoriYakumo/LuckyMoney/master/out/app-debug.apk) 4 | 5 | 安卓4.4以上(4.3未测试),支持以下情形: 6 | 7 | 1. 处于本群聊天视图, 并位于最底部 8 | 9 | 2. 处于其他群聊天视图, 开启通知 10 | 11 | 3. 处于微信以外, 开启微信通知, 屏幕点亮 12 | 13 | 4. 无密码锁屏, 开启微信通知 14 | 15 | 还有很多意外情况,考虑到i18n的问题,懒得处理. 16 | 17 | ~~某些手机存在拆红包界面点不了的情况,勾选手动模式会震动提示(我选择狗带)~~ 18 | 19 | ![](out/Screenshot.png) 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | signingConfigs { 5 | } 6 | compileSdkVersion 23 7 | buildToolsVersion '23.0.1' 8 | defaultConfig { 9 | applicationId 'orz.macrobull.luckymoney' 10 | minSdkVersion 18 11 | targetSdkVersion 23 12 | versionCode 6 13 | versionName '1.1.0' 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled true 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | debuggable true 20 | jniDebuggable false 21 | } 22 | debug { 23 | versionNameSuffix '-Debug' 24 | } 25 | } 26 | productFlavors { 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_7 30 | targetCompatibility JavaVersion.VERSION_1_7 31 | } 32 | } 33 | 34 | dependencies { 35 | compile fileTree(include: ['*.jar'], dir: 'libs') 36 | testCompile 'junit:junit:4.12' 37 | compile 'com.android.support:appcompat-v7:23.1.0' 38 | } 39 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /opt/google/android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/orz/macrobull/luckymoney/AService.java: -------------------------------------------------------------------------------- 1 | package orz.macrobull.luckymoney; 2 | 3 | import android.accessibilityservice.AccessibilityService; 4 | import android.app.Service; 5 | import android.os.Vibrator; 6 | import android.util.Log; 7 | import android.view.accessibility.AccessibilityEvent; 8 | import android.view.accessibility.AccessibilityNodeInfo; 9 | import android.view.accessibility.AccessibilityRecord; 10 | import android.widget.Toast; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * Created by macrobull on 12/28/15. 16 | * 辅助点击服务 17 | */ 18 | public class AService extends AccessibilityService { 19 | 20 | static State state = State.CHAT_WALK; // 服务状态 21 | static boolean mutex = false; // 互斥锁 22 | static Integer lastNode = 0; // 简单记录上一红包节点hashcode去重复 23 | 24 | /* 25 | * 为了确保获得金额信息, 设置详情标志 26 | * 1: 红包有效 27 | * 2: 左上角"详情"出现 28 | * 4: 金额出现 29 | */ 30 | static Integer flags_detail = 0; 31 | static Boolean isFromNotification = false; 32 | 33 | static Integer size_open = 0; // 已点开的红包数 34 | static Integer size_new = 0; // 待处理的新红包数 35 | 36 | // 统计信息 37 | static Integer cnt_get = 0; // 点开的红包数 38 | static Integer cnt_open = 0; // 拆开的红包数 39 | static Integer cnt_detail = 0; // 进入详情数 40 | static Integer cnt_new = 0; // 捕获通知次数 41 | 42 | static Float amount_total = 0.0f; // 红包总金额 43 | static Float amount_success = 0.0f; // 成功抢到的红包总金额 44 | 45 | /** 46 | * 供主界面显示统计信息 47 | * 48 | * @return 统计信息 49 | */ 50 | public static String getStatistics() { 51 | return String.format( 52 | "点了%d个红包, 开了%d个\n抢到了%d个红包\n从通知抢了%d次" 53 | + "\n路过%.2f元, 抢到%.2f元" 54 | , cnt_get, cnt_detail, cnt_open, cnt_new, amount_total, amount_success); 55 | } 56 | 57 | /** 58 | * 监视UI变更事件 59 | * 60 | * @param event AccessibilityEvent 61 | */ 62 | @Override 63 | public void onAccessibilityEvent(AccessibilityEvent event) { 64 | if (mutex) { 65 | Log.w("onAccessibilityEvent", "MUTEX!"); 66 | return; 67 | } 68 | mutex = true; 69 | 70 | try { 71 | // Log.i("getPackageName", event.getPackageName().toString()); 72 | // Log.i("getRecord", (event.getRecordCount()>0)?event.getRecord(0).toString():"null"); 73 | // Log.i("getSource", (event.getSource() != null)?event.getSource().toString():"null"); 74 | // Log.i("getText[]", (!event.getText().isEmpty()) ? event.getText().toString() : "[]"); 75 | process(event); // 测试表明source和record有参考价值 76 | } finally { 77 | mutex = false; 78 | } 79 | 80 | } 81 | 82 | /** 83 | * 按要求重载 84 | */ 85 | @Override 86 | public void onInterrupt() { 87 | Log.d("onInterrupt", "!"); 88 | } 89 | 90 | /** 91 | * 搜索包含红包的UI节点, 点击所有 92 | * 93 | * @param root 根UI节点 94 | * @return 成功点击的红包数 95 | */ 96 | Integer getFromNode(AccessibilityNodeInfo root) { 97 | List mNodes = 98 | root.findAccessibilityNodeInfosByText(getResources().getString(R.string.chat_pattern)); 99 | 100 | for (AccessibilityNodeInfo node : mNodes) { 101 | Log.d("node", node.toString()); 102 | AccessibilityNodeInfo parent = node.getParent(); 103 | if (parent == null) { 104 | Log.d("node.parent", "null"); // 有时候没有父节点, 蜜汁bug 105 | } else { 106 | Log.d("click", "GET" + Integer.valueOf(node.hashCode()).toString()); 107 | parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); // TextView不能点, 点的是ListView, 详情查看clickable 108 | cnt_get += 1; 109 | lastNode = node.hashCode(); 110 | } 111 | } 112 | 113 | return mNodes.size(); // 即搜索结果数目 114 | } 115 | 116 | /** 117 | * 搜索包含红包的UI节点, 点击末几个 118 | * 119 | * @param root 根UI节点 120 | * @param size 点击最后size个 121 | * @param ignoreDup 是否无视重复检测 122 | * @return 成功点击的红包数 123 | */ 124 | Integer getFromLastNode(AccessibilityNodeInfo root, Integer size, boolean ignoreDup) { 125 | List mNodes = 126 | root.findAccessibilityNodeInfosByText(getResources().getString(R.string.chat_pattern)); 127 | 128 | size = Math.min(size, mNodes.size()); // 先设成功点击数为预计点击的红包数目 129 | for (Integer i = mNodes.size() - size; i < mNodes.size(); i++) { 130 | AccessibilityNodeInfo node = mNodes.get(i); 131 | Log.d("node", node.toString()); 132 | AccessibilityNodeInfo parent = node.getParent(); 133 | if (parent == null) { 134 | Log.d("node.parent", "null"); // 有时候没有父节点, 蜜汁bug 135 | } else { 136 | if (ignoreDup || (lastNode != node.hashCode())) { // 非重复红包, 点击 137 | Log.d("click", "GET" + Integer.valueOf(node.hashCode()).toString()); 138 | parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); 139 | cnt_get += 1; 140 | lastNode = node.hashCode(); 141 | } else { 142 | Log.d("node duplicate", Integer.valueOf(node.hashCode()).toString()); 143 | size -= 1; // 重复红包, 减少成功计数 144 | } 145 | } 146 | } 147 | 148 | return size; 149 | } 150 | 151 | void logState() { 152 | Log.d("state", state.toString() + "/" + size_open.toString() + "/" + size_new.toString()); 153 | } 154 | 155 | 156 | 157 | AccessibilityNodeInfo source; 158 | AccessibilityRecord record; 159 | 160 | AccessibilityNodeInfo getRoot(AccessibilityNodeInfo node){ 161 | AccessibilityNodeInfo parent; 162 | parent = node.getParent(); 163 | while (parent != null) { 164 | node = parent; 165 | parent = node.getParent(); 166 | } 167 | return node; 168 | } 169 | 170 | /** 171 | * 爬遍所有节点查找可点的按钮,用于解决Android5.1等组件层次分离的情况 172 | * @param root 界面根节点 173 | */ 174 | void crawlButton(AccessibilityNodeInfo root){ 175 | AccessibilityNodeInfo child; 176 | Integer size = root.getChildCount(); 177 | for (Integer i=0;i 0) { 208 | state = State.OPEN; 209 | } else { 210 | state = State.CHAT_IDLE; 211 | } 212 | 213 | break; 214 | case OPEN: // 已打开红包 215 | Log.d("open", source.toString()); 216 | 217 | // 寻找拆红包按钮 218 | AccessibilityNodeInfo root; 219 | root = super.getRootInActiveWindow(); 220 | if (root == null) root = getRoot(source); 221 | crawlButton(root); 222 | 223 | if (state != State.OPEN) break; 224 | 225 | // #TODO 处理没抢到的红包 226 | // 已拆的红包会进入详情界面 227 | 228 | case DETAIL: // 红包详情界面 229 | // Log.d("detail", source.toString()); 230 | // if (!source.getClassName().toString().equals("android.widget.LinearLayout")) { 231 | // 抓LinearLayout似乎不好用, 虽然无关语言 232 | if (source.getText() == null) break; //无视无文本的组件 233 | Log.d("detail text", source.getText().toString()); 234 | 235 | if (!(source.getText().toString().equals("Details") 236 | || source.getText().toString().equals("红包详情") 237 | )) flags_detail |= 2; // 抓左上角详情文本, #TODO i18n支持 238 | 239 | if (source.getText().toString().matches("\\d+\\.\\d\\d")) { // 抓金额, 采用第一个出现的值 240 | try { 241 | Log.d("amount", "got value:" + source.getText().toString()); 242 | amount_total += Float.valueOf(source.getText().toString()); 243 | if ((flags_detail & 1) > 0) 244 | amount_success += Float.valueOf(source.getText().toString()); 245 | flags_detail |= 4; 246 | } catch (Exception e) { // 潜在的转换异常 247 | Log.w("amount", e.getMessage()); 248 | } 249 | } 250 | // } 251 | 252 | if ((flags_detail & 6) != 6) return; // 等待抓到所有必需的UI 253 | flags_detail = 0; // 清除flag 254 | 255 | Log.d("click", "BACK"); 256 | performGlobalAction(GLOBAL_ACTION_BACK); 257 | // 点击返回, 虽然锁屏界面下辅助服务能够操作应用UI, 但是返回不可用!!! 258 | // #TODO 改用点击详情的左上角返回, 实现完全后台操作 259 | cnt_detail += 1; 260 | size_open -= 1; 261 | 262 | if (size_open > 0) { 263 | state = State.OPEN; 264 | } else { 265 | if (!isFromNotification) lastNode = 0; // 清除上一个节点hash, 因为潜在的节点重用? 266 | NLService.releaseLock(); // 结束抢红包后解除wakelock和恢复锁屏 267 | state = State.CHAT_IDLE; 268 | } 269 | 270 | break; 271 | case CHAT_IDLE: // 聊天界面 272 | if (NLService.catchTheGame()) { // 通知表明有新红包(后台或其他聊天界面有红包) 273 | isFromNotification = true; 274 | cnt_new += 1; 275 | size_new += 1; 276 | state = State.CHAT_NEW; 277 | } else { // 在更新的气泡里找新红包(当前聊天界面) 278 | if (event.getRecordCount() <= 0) return; 279 | record = event.getRecord(0); // 微信每次只增加一条record 280 | if (record.getText() == null) return; // 只关注有文本的UI 281 | 282 | Log.d("chat record", record.toString()); 283 | Log.d("chat source", source.toString()); 284 | 285 | boolean maybeMoney = false; 286 | 287 | if (record.getText().size() > 3) { // 典型的红包包含4段文本, 且关注点为chat_pattern 288 | for (CharSequence cText : record.getText()) { 289 | if (cText.toString().matches(getResources().getString(R.string.chat_pattern))) { 290 | maybeMoney = true; 291 | break; 292 | } 293 | } 294 | } 295 | 296 | if (record.getText().toString().matches("\\[\\d+\\]")) maybeMoney = true; 297 | // 有时只产生通知数UI更新, 也加以关注 298 | 299 | if (maybeMoney) { 300 | Log.d("source", source.toString()); 301 | isFromNotification = false; 302 | size_open += getFromLastNode(source, 1, false); // 只点最后一个红包, 并检测重复 303 | if (size_open > 0) state = State.OPEN; 304 | } 305 | 306 | } 307 | break; 308 | case CHAT_NEW: // 由通知进入聊天界面, 点最后size_new个红包 309 | Log.d("isFromNotification", isFromNotification.toString()); 310 | size_open += getFromLastNode(source, size_new, true); // 点最后size_new个红包, 不检测重复(UI节点重用情况) 311 | size_new -= size_open; 312 | 313 | if (size_open > 0) state = State.OPEN; 314 | break; 315 | } 316 | 317 | } 318 | 319 | enum State { 320 | CHAT_WALK, 321 | CHAT_IDLE, 322 | CHAT_NEW, 323 | OPEN, 324 | DETAIL, 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /app/src/main/java/orz/macrobull/luckymoney/MainActivity.java: -------------------------------------------------------------------------------- 1 | package orz.macrobull.luckymoney; 2 | 3 | import android.accessibilityservice.AccessibilityServiceInfo; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.provider.Settings; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.util.Log; 9 | import android.view.View; 10 | import android.view.accessibility.AccessibilityManager; 11 | import android.widget.Button; 12 | import android.widget.CheckBox; 13 | import android.widget.TextView; 14 | import android.widget.Toast; 15 | 16 | import java.util.List; 17 | 18 | /** 19 | * 主界面 20 | */ 21 | public class MainActivity extends AppCompatActivity { 22 | 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_main); 27 | 28 | // getSupportActionBar().setDisplayShowHomeEnabled(true); 29 | // getSupportActionBar().setIcon(R.mipmap.ic_launcher); 30 | 31 | updateStatus(); 32 | } 33 | 34 | @Override 35 | protected void onResume() { 36 | super.onResume(); 37 | 38 | updateStatus(); 39 | } 40 | 41 | /** 42 | * 更新信息 43 | */ 44 | private void updateStatus() { 45 | Boolean nl_status, as_status; 46 | as_status = false; 47 | 48 | nl_status = NLService.getBindStatus(); // 通知监听服务状态由服务绑定状态标识 49 | if (!nl_status) try { 50 | // startService(new Intent(this, NLService.class)); 51 | // nl_status = NLService.getBindStatus(); // #FIXME 怎样程序启动这个服务? 52 | Toast.makeText(this, "May recheck notification listener!", Toast.LENGTH_SHORT).show(); // 那只有手动启动啦 53 | // openNLSetting(null); 54 | } catch (Exception e) { 55 | Log.w("Start NLService fail:", e.getMessage()); 56 | } 57 | 58 | AccessibilityManager accessibilityManager = 59 | (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE); 60 | List accessibilityServices = 61 | accessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC); 62 | for (AccessibilityServiceInfo info : accessibilityServices) { 63 | if (info.getId().equals(getPackageName() + "/.AService")) { // 检索辅助服务, 确认运行状态 64 | as_status = true; 65 | break; 66 | } 67 | } 68 | 69 | Button b_nl = (Button) findViewById(R.id.b_nl); 70 | Button b_as = (Button) findViewById(R.id.b_as); 71 | // CheckBox cb_mode = (CheckBox) findViewById(R.id.cb_man); 72 | // 73 | // cb_mode.setChecked(AService.mode_man); 74 | 75 | b_nl.setText("通知监听服务: " + (nl_status ? "已启动" : "未启动或未启用")); 76 | b_as.setText("点击辅助服务: " + (as_status ? "已启动" : "未启用")); 77 | 78 | 79 | TextView t_stat = (TextView) findViewById(R.id.t_stat); 80 | t_stat.setText(AService.getStatistics()); // 显示统计数据 81 | } 82 | 83 | /** 84 | * 打开设置中的通知监听选项 85 | * 86 | * @param v View 87 | */ 88 | public void openNLSetting(View v) { 89 | // Intent intent=new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS); // Android Lint说API22+再使用这个 90 | Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); 91 | startActivity(intent); 92 | } 93 | 94 | /** 95 | * 打开设置中的辅助服务选项 96 | * 97 | * @param v View 98 | */ 99 | public void openASSetting(View v) { 100 | Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); 101 | startActivity(intent); 102 | } 103 | 104 | // /** 105 | // * 设置手动拆红包模式 106 | // * @param v CheckBox cb_man 107 | // */ 108 | // public void setModeMan(View v) { 109 | // AService.mode_man = ((CheckBox)v).isChecked(); 110 | // } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/orz/macrobull/luckymoney/NLService.java: -------------------------------------------------------------------------------- 1 | package orz.macrobull.luckymoney; 2 | 3 | import android.app.KeyguardManager; 4 | import android.app.Notification; 5 | import android.app.PendingIntent; 6 | import android.content.Intent; 7 | import android.os.IBinder; 8 | import android.os.PowerManager; 9 | import android.service.notification.NotificationListenerService; 10 | import android.service.notification.StatusBarNotification; 11 | import android.util.Log; 12 | 13 | /** 14 | * Created by macrobull on 12/28/15. 15 | * 通知监听服务 16 | */ 17 | 18 | public class NLService extends NotificationListenerService { 19 | 20 | static private boolean mBinding = false; //绑定状态 21 | static private boolean mInGame = false; //发现红包 22 | 23 | static private PowerManager powerMan; 24 | static private PowerManager.WakeLock wakeLock; 25 | static private KeyguardManager keyMan; 26 | static private KeyguardManager.KeyguardLock keyLock; 27 | 28 | static public boolean getBindStatus() { 29 | return mBinding; 30 | } 31 | 32 | /** 33 | * 是否在通知中发现红包 34 | * 35 | * @return 36 | */ 37 | static public boolean catchTheGame() { 38 | boolean ret = mInGame; 39 | mInGame = false; // 仅用一次, 清除标志 40 | return ret; 41 | } 42 | 43 | /** 44 | * 恢复屏幕关闭和锁屏 45 | */ 46 | static public void releaseLock() { 47 | Log.d("wakelock", String.valueOf(wakeLock.isHeld())); 48 | if (wakeLock.isHeld()) wakeLock.release(); //解除屏幕常亮 49 | keyLock.reenableKeyguard(); 50 | } 51 | 52 | @Override 53 | public IBinder onBind(Intent intent) { 54 | IBinder mIBinder = super.onBind(intent); 55 | mBinding = true; 56 | 57 | powerMan = (PowerManager) getSystemService(POWER_SERVICE); 58 | wakeLock = powerMan.newWakeLock( 59 | PowerManager.SCREEN_DIM_WAKE_LOCK 60 | | PowerManager.ACQUIRE_CAUSES_WAKEUP, "WakeLock"); 61 | 62 | keyMan = (KeyguardManager) getSystemService(KEYGUARD_SERVICE); 63 | keyLock = keyMan.newKeyguardLock("KeyLock"); 64 | 65 | // #TODO 换掉这些deprecated方法, 66 | 67 | return mIBinder; 68 | } 69 | 70 | @Override 71 | public boolean onUnbind(Intent intent) { 72 | boolean mOnUnbind = super.onUnbind(intent); 73 | mBinding = false; 74 | return mOnUnbind; 75 | } 76 | 77 | /** 78 | * 通知接收事件 79 | * 80 | * @param sbn StatusBarNotification 81 | */ 82 | @Override 83 | public void onNotificationPosted(StatusBarNotification sbn) { 84 | if (!(sbn.getPackageName().equals(getResources().getString(R.string.target_pname)) 85 | || sbn.getPackageName().equals(getResources().getString(R.string.target_pname_parallel)) 86 | )) 87 | return; // 过滤应用: 微信和双开/w\ 88 | 89 | Notification notification = sbn.getNotification(); 90 | String text; 91 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { 92 | text = notification.extras.getString("android.text"); // 全文本更靠谱 93 | } else { 94 | text = notification.tickerText.toString(); // API 19- 使用tickerText 95 | } 96 | 97 | if (text == null) return; 98 | Log.d("text", text); 99 | 100 | if (!text.matches(getResources().getString(R.string.notify_pattern))) return; // 过滤关键词 101 | 102 | Log.d("contentIntent", notification.contentIntent.toString()); 103 | try { 104 | sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); // 收起通知, 防止一些手机上展开通知妨碍操作的问题 105 | Log.d("wakelock", String.valueOf(wakeLock.isHeld())); 106 | // #FIXME bug??? 锁屏点击通知竟然进入聊天列表界面...狗带, 于是先解锁, 要增加PM和KM 107 | if (!wakeLock.isHeld()) { // 用wakelock判定屏幕是点亮还是锁屏 108 | keyLock.disableKeyguard(); // 解除锁屏 109 | wakeLock.acquire(); // 点亮屏幕 110 | try { // 等待解锁屏幕 111 | while (!powerMan.isScreenOn()) { // 好像能用 112 | Log.d("keyguard", String.valueOf(keyMan.inKeyguardRestrictedInputMode())); // #FIXME 并不能反映是否已解锁 113 | Log.d("keyguard", String.valueOf(powerMan.isScreenOn())); // #FIXME 并不能反映是否已解锁 114 | // Log.d("keyguard", String.valueOf(powerMan.isInteractive())); // isScreenOn是deprecated, 但是isInteractive是API20+... 115 | Log.d("keyguard", "locked"); 116 | Thread.sleep(250); // 极糟糕的workaround 117 | } 118 | } catch (Exception e) { 119 | // 120 | } 121 | } 122 | mInGame = true; // 标记:有红包 123 | notification.contentIntent.send(this, 0, new Intent()); // 点击通知 124 | } catch (PendingIntent.CanceledException e) { 125 | Log.w("pendingIntent", "Sending pendingIntent failed."); 126 | } 127 | } 128 | 129 | /** 130 | * 通知移除事件, 应Android Lint指示, API21- 必需重载 131 | * 132 | * @param sbn StatusBarNotification 133 | */ 134 | @Override 135 | public void onNotificationRemoved(StatusBarNotification sbn) { 136 | Log.d("onNotificationRemoved", "!"); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/res/layout-land/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 24 | 25 |