闹钟增加震动渐强功能——基于WooBox
苗锦洲
新疆
1
24
654
0
对于不太喜欢开闹钟声音的我来说,正在熟睡时,突然被闹钟的震动震醒,这种体验实在是太震撼了,根本毫无防备,个人感觉体验极差,一点儿缓冲都没有,太刺激了;然而设置里只有响铃渐响功能,重新开发一个闹钟APP显然太小题大做了,XPosed插件很符合我的(奇葩)需求
友情提示:此篇文章大约需要阅读 1小时4分29秒
# 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. 使用体验
有了这个功能后,每天早上终于不用被“强制开机”了:),把人叫醒还是完全没问题的,能感觉到弱至强的震动,非常人性化!
1
评论
已自动恢复阅读位置、日/夜间模式参数