OR博客
闹钟增加震动渐强功能——基于WooBox
OrdinaryRoad
创建于:2023-06-10 23:37:38
更新于:2023-06-14 22:39:22
新疆
1
13
126
0
对于不太喜欢开闹钟声音的我来说,正在熟睡时,突然被闹钟的震动震醒,这种体验实在是太震撼了,根本毫无防备,个人感觉体验极差,一点儿缓冲都没有,太刺激了;然而设置里只有响铃渐响功能,重新开发一个闹钟APP显然太小题大做了,XPosed插件很符合我的(奇葩)需求
# 0. 相关链接 1. [LSPosed](http://lsposed.org) 2. [dex2jar](https://github.com/pxb1988/dex2jar) 3. [apktool](https://formulae.brew.sh/formula/apktool#default) 4. [android-platform-tools](https://formulae.brew.sh/cask/android-platform-tools#default) 5. [jd-gui](https://github.com/java-decompiler/jd-gui) 6. [反编译dex文件_dex反编译_留仙洞的博客-CSDN博客](https://blog.csdn.net/x_xingduo_2315/article/details/128810106) 7. [了解 Activity 生命周期 | Android 开发者 | Android Developers (google.cn)](https://developer.android.google.cn/guide/components/activities/activity-lifecycle?hl=zh_cn) 8. [EzXHelper (kyuubiran.github.io)](https://kyuubiran.github.io/EzXHelper/) 9. [Android之Xposed框架完全使用指南 (taodudu.cc)](http://www.taodudu.cc/news/show-525866.html?action=onClick) 10. [基于xposed框架hook使用_xposed hook_zhangjianming2018的博客-CSDN博客](https://blog.csdn.net/zhangjianming2018/article/details/125307350) 11. [Mac 安装 adb (Android调试桥)_大大大大大桃子的博客-CSDN博客](https://blog.csdn.net/soindy/article/details/71700745) 12. 开发者助手:[酷安@东芝](https://www.coolapk.com/u/466688)[酷安@帝鲮](http://www.coolapk.com/u/651863) 13. [RE文件管理器](https://baike.baidu.com/item/re文件管理器/10651785?fr=aladdin) 14. [ES文件浏览器](http://www.estrongs.com/) 15. [https://github.com/1962247851/WooBoxForMIUI](https://github.com/1962247851/WooBoxForMIUI) # 1. 环境准备 - 一部配置好LSPosed的MIUI安卓设备 - 克隆[WooBoxForMIUI](https://github.com/Simplicity-Team/WooBoxForMIUI/)项目到本地AndroidStudio,完成依赖下载等,可以成功Build项目 > 可能涉及的XPosed相关API说明 ```java // XposedHelpers de.robv.android.xposed.XposedHelpers // 获取Object的某个属性 de.robv.android.xposed.XposedHelpers#getObjectField // SharedPreferences工具 de.robv.android.xposed.XSharedPreferences // 访问类this对象 de.robv.android.xposed.XC_MethodHook.MethodHookParam#thisObject // EzXHelper com.github.kyuubiran.ezxhelper.utils // 找到某个类的方法 com.github.kyuubiran.ezxhelper.utils.findMethod // 方法执行后hook com.lt2333.simplicitytools.utils.KotlinXposedHelperKt#hookAfterMethod(java.lang.String, java.lang.String, java.lang.Object[], kotlin.jvm.functions.Function1<? super de.robv.android.xposed.XC_MethodHook.MethodHookParam,kotlin.Unit>) ``` # 2. WooBox项目简单分析 ![21WooBox.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/d4429d9ef59f49dda73f737d8e3a0f85.png) MainHook,WooBoxForMIUI是否启用? 是:EzXHelperInit注册各个应用(AppRegister) AppRegister:handleLoadPackage根据安卓版本加载HookRegister HookRegister:init,先调用hasEnable方法读取SharedPreference的配置判断是否启用,然后当hook点出现时就会触发具体逻辑代码 # 3. MIUI时钟应用程序分析 ## 3.1 获取安装包 我使用的是ES文件浏览器备份功能,其他还可以使用adb(Android Debug Bridge)命令导出 ## 3.2 反编译安装包 ### 3.2.1 apk2jar ```bash d2j-dex2jar -f 时钟应用安装包全路径 ``` 配合apktool使用,直接unzip解压apk的话xml等文件的内容还是无法阅读 ```shell apktool d com.android.deskclock.apk I: Using Apktool 2.7.0 on com.android.deskclock.apk I: Loading resource table... I: Decoding Shared Library (miui), pkgId: 16 I: Decoding Shared Library (miui.system), pkgId: 18 I: Decoding AndroidManifest.xml with resources... I: Loading resource table from file: /Users/ordinaryroad/Library/apktool/framework/1.apk I: Regular manifest package... I: Decoding file-resources... I: Decoding values */* XMLs... I: Baksmaling classes.dex... I: Baksmaling classes2.dex... I: Copying assets and libs... I: Copying unknown files... I: Copying original files... ``` ### 3.2.2 jd-gui 然后将jar包拖入jd-gui,即可看到反编译后的class文件 ![3221jdguiclass.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/8d4d2d9519044babb96398734a785017.png) ## 3.3 部分逻辑分析 ### 3.3.1 闹钟配置界面UI布局分析 使用工具开发者助手进行分析 ![3311UI.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/6f54db0a5406499faf4dcc9c9ade05bb.png) 考虑在震动开关下面增加震动渐强开关 ![3312.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/6f087ebae46b48efad2fcee4df63b0bf.png) 可以发现该控件的id,但经过编译后控件的id其实已经被替换了,`Id-Hex=0x7F0A02D9`即为编译后的的定位符,猜测应该是为了加快查询速度,转为十进制为 `2131362521`,再去 `SetAlarmActivity`里面搜索,看看在哪儿初始化使用的 ![3313.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/af58dd33537845b9a0cfb0c46d21c623.png) 可以发现是在初始化其他设置方法里面使用的,是一个 `LinearLayout`线性布局,由布局可以得知 `orientation`布局方向是默认的水平方向,显然不能直接去添加新的控件,否则会破坏现有的布局,考虑后还是采用直接增加在最后一行的方案 ![3314震动设置行控件的本地变量.png](https://ordinaryroad.xyz:444/api/upms/file/download/ordinaryroad-blog/2023-06-10/cb618622970f43acb2ec3ce3aa95d152.png) 于是考虑 `scroll_holder`,但是根据id获取view并没有找到,id可能是系统生成的 ![3315.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/269a4ba06a5b4770b6fd2fa892e471f0.png) ![3316.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/266cb9276a0c46138261e451d9020a42.png) 于是再考虑拿到震动开关的View,找到他的ParentView,就是这个ScrollView了 注意ScrollView只能有一个子View,所以还得拿到ScrollView的第一个childView,然后就可以添加其他View到这个ScrollView里面了 经过日志打印调试,当执行 `initAll`方法后几个id的值如下 | | mId | mOriginalAlarm.id | mModifiedAlarm.id | | -------- | --- | ----------------- | ----------------- | | 新增闹钟 | 0 | -1 | null | | 更新闹钟 | 0 | 更新Alarm的id | 更新Alarm的id | 因此只需要关注 `mOriginalAlarm.Id`即可,读取是否开启震动渐强,初始化 `Switch`,代码如下 ```java // 1. 修改UI界面,增加选项 Deskclock.CLZ_NAME_SET_ALARM_ACTIVITY.hookAfterMethod("initAll", Bundle::class.java) { // Log.d(TAG, "after initAll") val activity = it.thisObject as Activity val mId = activity.getObjectField("mId")!! // mOriginalAlarmId,创建时为-1 val mOriginalAlarmId = activity.getObjectField("mOriginalAlarm")!!.getObjectField("id")!! val mAlarmChangedId = activity.getObjectField("mAlarmChanged")?.getObjectField("id") Log.d( TAG, "mId${mId}, mOriginalAlarmId${mOriginalAlarmId}, mAlarmChangedId:${mAlarmChangedId}" ) sp = activity.getSharedPreferences( "_vibration_gradually_stronger_config", Context.MODE_PRIVATE ) val scrollHolderLayoutID = activity.resources.getIdentifier("scroll_holder", "id", activity.packageName) // Log.d(TAG, "scrollHolderLayoutID=${scrollHolderLayoutID}") activity.runOnUiThread { val scrollHolder = activity.findViewById<ScrollView>(scrollHolderLayoutID) val linearLayout = scrollHolder.getChildAt(0) as LinearLayout linearLayout.apply { addView( Switch(context).apply { setText("震动渐强") setTextAppearance( android.R.style.TextAppearance_Material_Title ) setTextSize(18F) layoutParams.apply { setPadding( dp2px(context, 30F), dp2px(context, 15F), dp2px(context, 30F), dp2px(context, 15F) ) } isChecked = sp.getBoolean("$mOriginalAlarmId", false) setOnCheckedChangeListener { _, isChecked -> alarmEnabled = isChecked } } ) } }} ``` ### 3.3.2 保存闹钟代码逻辑分析 重点是能够区分出每个配置对应的是哪个闹钟,即找到闹钟的唯一标识符,通常是 `id`、`uuid`等 大概浏览了一下,直接看到了叫 `saveAlarm`的方法 ![3321.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/dcf14a6135464f829459bbee23545139.png) `saveAlarm`保存闹钟方法 ```java private long saveAlarm(Alarm paramAlarm) { boolean bool; long l; if (paramAlarm == null) { bool = true; } else { bool = false; } Alarm alarm = paramAlarm; if (paramAlarm == null) { alarm = buildAlarmFromUi(); alarm.skipTime = 0L; alarm.enabled = true; if (!alarm.daysOfWeek.isRepeatSet()) { alarm.deleteAfterUse = this.mOneShotValueCb.isChecked(); } else { alarm.deleteAfterUse = false; } } if (alarm.id == -1) { long l1 = AlarmHelper.addAlarm((Context)this, alarm); this.mId = alarm.id; handlerXiaoAiRingtone(this.mId); AlarmHelper.setNextAlert((Context)this); if (RingtoneManager.getDefaultUri(4).equals(alarm.alert)) { StatHelper.deskclockEvent("new_alarm_default_ringtone"); OneTrackStatHelper.trackBoolEvent(false, "479.1.5.1.11745"); } else { StatHelper.deskclockEvent("new_alarm_edit_ringtone"); OneTrackStatHelper.trackBoolEvent(true, "479.1.5.1.11745"); } StatHelper.recordAlarmAction((Context)this, "alarm_add", alarm); OneTrackStatHelper.recordAlarmAction((Context)this, alarm); TimePicker timePicker = this.mTimePicker; l = l1; if (timePicker != null) { StatHelper.updateAlarmProperties("new_alarm_hour_picker_slide_times", timePicker.getHourSlideTimes()); StatHelper.updateAlarmProperties("new_alarm_min_picker_slide_times", this.mTimePicker.getMinSlideTimes()); OneTrackStatHelper.trackNumEvent(this.mTimePicker.getHourSlideTimes(), "479.1.5.1.11814"); OneTrackStatHelper.trackNumEvent(this.mTimePicker.getMinSlideTimes(), "479.1.5.1.11816"); l = l1; } } else { handlerXiaoAiRingtone(alarm.id); l = AlarmHelper.setAlarm((Context)this, alarm); if (bool && isModified()) if (this.mOriginalAlarm.alert == null) { if (alarm.alert == null) { StatHelper.deskclockEvent("edit_alarm_not_change_ringtone"); OneTrackStatHelper.trackBoolEvent(false, "479.1.5.1.11746"); } else { StatHelper.deskclockEvent("edit_alarm_change_ringtone"); OneTrackStatHelper.trackBoolEvent(true, "479.1.5.1.11746"); } } else if (this.mOriginalAlarm.alert.equals(alarm.alert)) { StatHelper.deskclockEvent("edit_alarm_not_change_ringtone"); OneTrackStatHelper.trackBoolEvent(false, "479.1.5.1.11746"); } else { StatHelper.deskclockEvent("edit_alarm_change_ringtone"); OneTrackStatHelper.trackBoolEvent(true, "479.1.5.1.11746"); } StatHelper.recordAlarmAction((Context)this, "alarm_edit", alarm); OneTrackStatHelper.recordAlarmAction((Context)this, alarm); TimePicker timePicker = this.mTimePicker; if (timePicker != null) { StatHelper.updateAlarmProperties("edit_alarm_hour_picker_slide_times", timePicker.getHourSlideTimes()); StatHelper.updateAlarmProperties("edit_alarm_min_picker_slide_times", this.mTimePicker.getMinSlideTimes()); OneTrackStatHelper.trackNumEvent(this.mTimePicker.getHourSlideTimes(), "479.1.5.1.11815"); OneTrackStatHelper.trackNumEvent(this.mTimePicker.getMinSlideTimes(), "479.1.5.1.11817"); } } if (WeatherRingtoneHelper.isWeatherRingtone(this.mAlert)) { StatHelper.trackEvent("category_deskclock_common", "set_alarm_ringtone", "WEATHER"); } else if (WeekRingtoneHelper.isWeekRingtone(this.mAlert)) { StatHelper.trackEvent("category_deskclock_common", "set_alarm_ringtone", "WEEK"); } else { StatHelper.trackEvent("category_deskclock_common", "set_alarm_ringtone", "OTHER"); } StatHelper.trackEvent("set_alarm_time", TimeUtil.composeTime(alarm.hour, alarm.minutes)); OneTrackStatHelper.trackNumEvent((this.mHour * 60 + this.mMinute), ""); return l; } ``` 经过日志打印调试,当执行 `saveAlarm`方法后几个id的值如下 | | mId | mOriginalAlarm.id | mModifiedAlarm.id | | -------- | ------------- | ----------------- | ----------------- | | 新增闹钟 | 新Alarm的id | -1 | null | | 更新闹钟 | 更新Alarm的id | 更新Alarm的id | 更新Alarm的id | 因此,只需要关注mId即可,`saveAlarm`执行后,保存自定义配置,将震动增强 `Switch`的状态存入 `SharedPreference`,用于UI初始化和响铃时配置读取,代码如下 ```java // 2. 保存闹钟后保存自定义配置 findMethod(Deskclock.CLZ_NAME_SET_ALARM_ACTIVITY) { name == "saveAlarm" && parameterCount == 1 }.hookAfter { // Log.i(TAG, "after saveAlarm") val activity = it.thisObject val mId = activity.getObjectField("mId")!! val mOriginalAlarmId = activity.getObjectField("mOriginalAlarm")!!.getObjectField("id")!! val mAlarmChangedId = activity.getObjectField("mAlarmChanged")?.getObjectField("id") Log.i( TAG, "mId${mId}, mOriginalAlarmId${mOriginalAlarmId}, mAlarmChangedId:${mAlarmChangedId}" ) // mId sp.edit().putBoolean("$mId", alarmEnabled).apply() // Log.i(TAG, "sp路径${(XSPUtils.findFieldObject { name == "prefs" } as XSharedPreferences).file.absolutePath}") } ``` 因为用的是应用自身的 `Context`上下文,所以实际存放位置自然是在应用的 `SharedPreference`目录下 ![3322.jpg](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/15860d7ba9154dfc9e0024eb2e068f3c.jpg) 存放内容如下,与预期一致 ![3323.jpg](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/2ef5287a24384834810c9c132efd81f7.jpg) 顺便找到了WooBoxForMIUI的配置存放位置,怪不得之前直接去找没找到 ![3324WooBoxForMIUI.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/4ee1b2cc646d4e0882f554fef84935b4.png) ### 3.3.3 闹钟生效逻辑分析 > 要找到控制震动的代码,倒推直接搜索 `.vibrate(`即可,不过这里还是试一下正着找,这样比较有意思 对开发者来说,用户设置的闹钟时间以及重复规则是不确定的,每个人的使用场景都是不确定的,因此需要使应用程序能够按照用户设置的规则触发某段代码,通常实现方式有JAVA的Timer类以及Android的Handler、Alarm机制等;考虑到移动端的能耗等,一般都是通过注册Service使得应用即使没有被打开也可以运行一些代码 所以先来看看 `AndroidManifest`,搜索一下 `Servce`,果然有收获 ![3331AndroidManifest.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/8e4a0d384bdc4e978b0f07b8e24a8f5a.png) 可以看到一个 `com.android.deskclock.alarm.alert.AlarmService`,还有其他的一些Service,嫌疑最大的就是这个 `AlarmService`,就从他下手吧 ```xml <service android:name="com.android.deskclock.alarm.alert.AlarmService" android:exported="false" android:description="@ref/0x7f110035" android:directBootAware="true"> <intent-filter> <action android:name="com.android.deskclock.ALARM_ALERT" /> </intent-filter></service> <service android:name="com.android.deskclock.alarm.lifepost.RecommendIntentService" android:exported="false" android:directBootAware="true" /> <service android:name="com.android.deskclock.settings.RingtonePlayService" android:exported="false" android:directBootAware="true" /> <service android:name="com.android.deskclock.addition.resource.ResourceLoadService" android:exported="false" android:directBootAware="true" /> <service android:name="com.android.deskclock.addition.monitor.MonitorJobScheduler" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false" android:directBootAware="true" /> <service android:name="com.android.deskclock.addition.backup.ClockBackupService" android:permission="com.xiaomi.permission.CLOUD_MANAGER" android:exported="true" android:directBootAware="true"> <intent-filter> <action android:name="miui.action.CLOUD_BACKUP_SETTINGS" /> <action android:name="miui.action.CLOUD_RESTORE_SETTINGS" /> </intent-filter></service> <service android:name="com.android.deskclock.KeepLiveService" android:exported="false" android:directBootAware="true" /> <service android:name="com.android.deskclock.timer.TimerService" android:exported="false" android:directBootAware="true" /> <service android:name="com.android.deskclock.JobSchedulerService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false" android:directBootAware="true" /> ``` `com.android.deskclock.alarm.alert.AlarmService` ![3332AlarmService.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/4e0b11364925458cbd754e225dcfaaca.png) 先来简单看一下类结构,直接找重写的 `android.app.Service#onStartCommand`方法 ![3333AlarmService.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/bb759f198874452bad2851f35fe2117e.png) 可以很清楚的看到代码执行逻辑,`handleAlarm`方法好像有点儿嫌疑 ```java public int onStartCommand(Intent paramIntent, int paramInt1, int paramInt2) { StringBuilder stringBuilder1; Log.f("DC:AlarmService", "onStartCommand triggered"); if (paramIntent == null || paramIntent.getAction() == null) { Log.f("DC:AlarmService", "onStartCommand stopped: intent or intent action is null"); handleInvalidData(); return 2; } String str = paramIntent.getAction(); StringBuilder stringBuilder2 = new StringBuilder(); stringBuilder2.append("action: "); stringBuilder2.append(str); Log.f("DC:AlarmService", stringBuilder2.toString()); if ("com.android.deskclock.ALARM_ALERT".equals(str)) { Alarm alarm = AlarmHelper.parseAlarmFromRawDataIntent(paramIntent); if (alarm != null) { stringBuilder1 = new StringBuilder(); stringBuilder1.append("coming alarm: "); stringBuilder1.append(alarm.toString()); Log.f("DC:AlarmService", stringBuilder1.toString()); handleAlarm(alarm); } else { Log.f("DC:AlarmService", "onStartCommand stopped: alarm is null"); handleInvalidData(); } } else if ("com.android.deskclock.TIMER_ALERT".equals(str)) { Log.f("DC:AlarmService", "coming timer"); Alarm alarm = new Alarm(); alarm.id = -2; alarm.vibrate = false; alarm.alert = TimerDao.getTimerRingtone(); if (stringBuilder1.hasExtra("action.timer_name")) alarm.label = stringBuilder1.getStringExtra("action.timer_name"); handleTimer(alarm); } else { Log.f("DC:AlarmService", "onStartCommand stopped: not alarm/timer alert action, ignore"); handleInvalidData(); } return 2; } ``` `AlarmService#handleAlarm`方法,`play`方法貌似就是要找的了 ```java private void handleAlarm(Alarm paramAlarm) { long l = System.currentTimeMillis(); if (l > paramAlarm.time + 1800000L) { Log.f("DC:AlarmService", "trigger alarm 30 minutes overtime, ignore"); handleInvalidData(); return; } showForegroundNotification(paramAlarm); mCurrentAlarm = paramAlarm; play(mCurrentAlarm); PrefUtil.setRecentAlarmAlertTime(l); boolean bool = ((KeyguardManager)getSystemService("keyguard")).inKeyguardRestrictedInputMode(); recordAlarmTime(paramAlarm.id, paramAlarm.time, l, bool); AlarmHelper.disableSnoozeAlert((Context)this, paramAlarm.id); if (!paramAlarm.daysOfWeek.isRepeatSet()) { AlarmHelper.enableAlarm((Context)this, paramAlarm.id, false); } else { AlarmHelper.setNextAlert((Context)this); } if (paramAlarm.id == Integer.MIN_VALUE) BedtimeUtil.doInWakeTime((Context)this); mMiWearableExist = AdditionUtil.isMiWearableSupport(); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("mi wearable exist: "); stringBuilder.append(mMiWearableExist); Log.f("DC:AlarmService", stringBuilder.toString()); if (mMiWearableExist) bindMiWearableService(); } ``` `AlarmService#play`方法,还有调用 `this.mAlarmKlaxon.start((Context)this, paramAlarm);` ```java private void play(Alarm paramAlarm) { Log.f("DC:AlarmService", "start AlarmService#play"); this.mAlarmKlaxon.start((Context)this, paramAlarm); registerTimeoutHandler(paramAlarm); } ``` `AlarmKlaxon#start`(Alarm电喇叭hhh) ```java public void start(Context paramContext, Alarm paramAlarm) { Log.v("DC:AlarmKlaxon", "AlarmKlaxon.start()"); if (paramAlarm.vibrate) { Log.f("DC:AlarmKlaxon", "start vibrator"); vibrateLOrLater(getVibrator(paramContext)); Log.i("DC:AlarmKlaxon", "vibrate mi bracelet"); BleUtil.vibrateMiBracelet(paramContext); } else { Log.f("DC:AlarmKlaxon", "cancel vibrator for alarm setting"); stopVibrator(paramContext); } stop(paramContext); int i = paramAlarm.id; Uri uri = prepareRingtone(paramContext, paramAlarm); boolean bool = false; if (XiaoAiRingtoneHelper.isXiaoAiAlarm(paramContext, i) || XiaoAiRingtoneHelper.handleNotSureAlarm()) { uri = XiaoAiRingtoneHelper.getRingtoneUri(); bool = true; } doRingtoneStat(paramContext, uri, bool); if (uri != null) { if (i == -2) { Log.f("DC:AlarmKlaxon", "play timer ringtone"); getAsyncRingtonePlayer(paramContext).setPlaybackDelegate((AsyncRingtonePlayer.PlaybackDelegate)getTimerPlaybackDelegate(paramContext)); } else if (bool) { getAsyncRingtonePlayer(paramContext).setPlaybackDelegate(getXiaoAiPlaybackDelegate(paramContext)); } else if (WeatherRingtoneHelper.isWeatherRingtone(uri)) { Log.f("DC:AlarmKlaxon", "play weather ringtone"); getAsyncRingtonePlayer(paramContext).setPlaybackDelegate(getWeatherPlaybackDelegate(paramContext)); } else if (WeekRingtoneHelper.isWeekRingtone(uri)) { String str = WeekRingtoneHelper.getWeekRingtoneBackground(Calendar.getInstance()); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("play week ringtone, path: "); stringBuilder.append(str); Log.f("DC:AlarmKlaxon", stringBuilder.toString()); if (str != null) { uri = Uri.parse(str); } else { Log.e("DC:AlarmKlaxon", "get week ringtone failed, play audition ringtone"); } getAsyncRingtonePlayer(paramContext).setPlaybackDelegate(getDefaultPlaybackDelegate(paramContext)); } else { Log.f("DC:AlarmKlaxon", "play normal ringtone"); getAsyncRingtonePlayer(paramContext).setPlaybackDelegate(getDefaultPlaybackDelegate(paramContext)); } getAsyncRingtonePlayer(paramContext).play(uri, paramAlarm); } else { Log.f("DC:AlarmKlaxon", "play silent ringtone"); } this.mAudioStarted = true; } ``` 注意这段代码,应该就是震动相关的代码了 ```java if (paramAlarm.vibrate) { Log.f("DC:AlarmKlaxon", "start vibrator"); vibrateLOrLater(getVibrator(paramContext)); Log.i("DC:AlarmKlaxon", "vibrate mi bracelet"); // 小米手环震动 BleUtil.vibrateMiBracelet(paramContext); } else { Log.f("DC:AlarmKlaxon", "cancel vibrator for alarm setting"); stopVibrator(paramContext); } ``` 终于找到头了 ```java // 获取震动系统服务 private Vibrator getVibrator(Context paramContext) { return (Vibrator)paramContext.getSystemService("vibrator"); } // 开始震动 private void vibrateLOrLater(Vibrator paramVibrator) { paramVibrator.vibrate(VIBRATE_PATTERN, 0, (new AudioAttributes.Builder()).setUsage(4).setContentType(4).build()); } ``` 接下来可以编写hook测试一下,方法的全路径名为 ```java com.android.deskclock.alarm.alert.AlarmKlaxon#vibrateLOrLater(Vibrator paramVibrator) ``` 简单写一个打印日志的代码 ```java Deskclock.CLZ_NAME_ALARM_KLAXON.hookAfterMethod("vibrateLOrLater", Vibrator::class.java) { Log.d(TAG, "after vibrateLOrLater") } ``` 测试后发现正确打印,那么就可以正式开始搞事情了 ![3333hook.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-10/5463539ad6b24e19ab447a3f75e532c0.png) 先看一下安卓提供的 `vibrate`API ```java /** * Vibrate with a given pattern. * * <p> * Pass in an array of ints that are the durations for which to turn on or off * the vibrator in milliseconds. The first value indicates the number of milliseconds * to wait before turning the vibrator on. The next value indicates the number of milliseconds * for which to keep the vibrator on before turning it off. Subsequent values alternate * between durations in milliseconds to turn the vibrator off or to turn the vibrator on. * </p><p> * To cause the pattern to repeat, pass the index into the pattern array at which * to start the repeat, or -1 to disable repeating. * </p> * * <p>The app should be in the foreground for the vibration to happen. Background apps should * specify a ringtone, notification or alarm usage in order to vibrate.</p> * * @param pattern an array of longs of times for which to turn the vibrator on or off. * @param repeat the index into pattern at which to repeat, or -1 if * you don't want to repeat. * @param attributes {@link AudioAttributes} corresponding to the vibration. For example, * specify {@link AudioAttributes#USAGE_ALARM} for alarm vibrations or * {@link AudioAttributes#USAGE_NOTIFICATION_RINGTONE} for * vibrations associated with incoming calls. * @deprecated Use {@link #vibrate(VibrationEffect, VibrationAttributes)} instead. */ @Deprecated @RequiresPermission(android.Manifest.permission.VIBRATE) public void vibrate(long[] pattern, int repeat, AudioAttributes attributes) { // This call needs to continue throwing ArrayIndexOutOfBoundsException but ignore all other // exceptions for compatibility purposes if (repeat < -1 || repeat >= pattern.length) { Log.e(TAG, "vibrate called with repeat index out of bounds" + " (pattern.length=" + pattern.length + ", index=" + repeat + ")"); throw new ArrayIndexOutOfBoundsException(); } try { vibrate(VibrationEffect.createWaveform(pattern, repeat), attributes); } catch (IllegalArgumentException iae) { Log.e(TAG, "Failed to create VibrationEffect", iae); } } ``` 再看一下MIUI时钟调用 `vibrate`方法所传的参数,震动模式 `{ 500L, 500L }`,重复 `0`,效果为500ms后开始震动,500ms后关闭震动,一直循环这种模式,震动强度相关的API根本没用到 ```java // 震动模式 private static final long[] VIBRATE_PATTERN = new long[] { 500L, 500L }; /** * Usage value to use when the usage is an alarm (e.g. wake-up alarm). */ public final static int USAGE_ALARM = 4; /** * Content type value to use when the content type is a sound used to accompany a user * action, such as a beep or sound effect expressing a key click, or event, such as the * type of a sound for a bonus being received in a game. These sounds are mostly synthesized * or short Foley sounds. */ public final static int CONTENT_TYPE_SONIFICATION = 4; ``` 来看看我的 ```java Deskclock.CLZ_NAME_ALARM_KLAXON.hookAfterMethod("vibrateLOrLater", Vibrator::class.java) { Log.d(TAG, "after vibrateLOrLater") val vibrator = it.args[0] as Vibrator vibrator.cancel() // 照搬原来的 val audioAttributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build() // 重点是这里 val vibratorEffect = VibrationEffect.createWaveform( LongArray(100) { index -> // 等待100ms后,震动100ms,周期200ms 100L }, IntArray(100) { index -> // 震动由弱至强,共255(1-255)个等级 (index + 1) * 2 }, // 到最强后从最弱重复 0 ) vibrator.vibrate(vibratorEffect,audioAttributes) } ``` 经真机运行测试,完全ok,剩下的就是震动模式的玩法了 // TODO 挖坑1 - 提供预设的震动模式,下拉框选择 - 设置震动模式时可以实时测试 - 高级模式,直接输入模式、强度数组 ### 3.3.4 删除闹钟逻辑分析 删除闹钟时应该同时删除增加的额外配置,否则会产生冗余的配置信息,文件虽然不大,但是这应该是个好习惯吧 先试着直接搜删除闹钟的方法,`deletAlarm`,`cancelAlarm`不太管用,那就还是从UI界面入手吧;删除闹钟操作为:进入时钟应用首页,在闹钟列表中长按某一个项目进入选择模式,然后再删除,那就先找一下控件,搜索id `0x7F0A02B7` ![3341.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-14/678302f2d7bc4ba789c3f73adc562bdc.png) 搜索RecyclerViewAdapter中标题控件的id,定位到 `AlarmAdapter`类 ![3342id.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-14/a220b411e7634fbf905ff3d20e2eef05.png) `AlarmAdapter`类,可以明显看到 `OnAlarmCheckedChangedListener`,`OnLongClickListener`等点击Listener ![3343RecyclerViewAdapter.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-14/afab41eba691400ea1c41394dcf25147.png) 从监听闹钟选中状态改变的 `OnAlarmCheckedChangedListener`继续搜索,终于又定位到了 `AlarmClockFragment` ![3344Listener.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-14/ee98b217b2124843a6fae519a867db5a.png) 先试着搜了一下这个Listener,但是好像没有反编译成功 ![3345.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-14/3545039a37b54c7e8a53f00308dff999.png) 接着还是从 `mAlarmAdapter`入手,发现了这么一段代码,`UiUtil.updateActionModeDeleteBtn(param1ActionMode, bool);`,柳暗花明又一村 ![3346.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-14/3f7090b4699a4d8590c5bccc3b61efa5.png) ![3347id.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-14/34198a82bf234678b82f250f2a43aa23.png) 通过 `MenuItem`的id终于找到了点击时删除执行的代码逻辑:如果选中的项目个数大于0,通过 `AlarmAdapter`拿到选中的闹钟id列表,然后调用删除方法 `AlarmClockFragment.access$2902` ![3348id.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-14/ab81ff66b44a4ab09f455f9ccf1a9a79.png) ![3349AlarmClockFragment.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-14/5d87a04551b349f9a054276cb49dd621.png) 那么我们就可以考虑hook执行删除震动渐强配置的逻辑了,虽然调用的具体删除方法看不到,~~不过我们也可以监听菜单按钮的点击,当点击删除按钮时,也通过 `AlarmAdapter`拿到会被删除的闹钟Id列表即可,注意hook在执行方法前,否则等退出选中模式后就拿不到了选择的id了~~,经测试无法hook接口实现类,尝试hook这个 `access$2902`方法 ```java public static interface MultiChoiceModeListener extends AbsListView.MultiChoiceModeListener { void onAllItemCheckedStateChanged(ActionMode param1ActionMode, boolean param1Boolean); } ``` 通过测试后,只有发现当确认删除时,第二个参数才不为null ```kotlin findMethod("com.android.deskclock.alarm.AlarmClockFragment") { name == "access\$2902" && parameterCount == 2 }.hookAfter { Log.d(TAG, "after access\$2902") Log.i(TAG,"checkedItems: ${(it.args[1] as IntArray?)?.joinToString(",")}") } ``` ![33410hook.png](https://api.ordinaryroad.tech/upms/file/download/ordinaryroad-blog/2023-06-14/a2d2044d63374677942367a0f3a1a486.png) 删除部分的逻辑代码,完整代码将会发布到fork后的WooBoxForMIUI中:[https://github.com/1962247851/WooBoxForMIUI](https://github.com/1962247851/WooBoxForMIUI) ```kotlin // 4 删除闹钟时删除对应的配置 findMethod("com.android.deskclock.alarm.AlarmClockFragment") { name == "access\$2902" && parameterCount == 2 }.hookAfter { Log.d(TAG, "after access\$2902") Log.i(TAG, "checkedItems: ${(it.args[1] as IntArray?)?.joinToString(",")}") // 删除闹钟对应的震动渐强配置 (it.args[1] as IntArray?)?.let { checkedItems -> (it.args[0].invokeMethod("getContext") as Context) .getSharedPreferences(SP_NAME, Context.MODE_PRIVATE) .edit().apply { for (id in checkedItems) { this.remove("$id") } } .apply() } } ``` # 4. 使用体验 有了这个功能后,每天早上终于不用被“强制开机”了:),把人叫醒还是完全没问题的,能感觉到弱至强的震动,非常人性化!
评论