OR博客
闹钟增加震动渐强功能——基于WooBox
苗锦洲
创建于:2023-06-10 23:37:38
更新于:2023-06-14 22:39:22
新疆
1
33
958
0
对于不太喜欢开闹钟声音的我来说,正在熟睡时,突然被闹钟的震动震醒,这种体验实在是太震撼了,根本毫无防备,个人感觉体验极差,一点儿缓冲都没有,太刺激了;然而设置里只有响铃渐响功能,重新开发一个闹钟APP显然太小题大做了,XPosed插件很符合我的(奇葩)需求

对于不太喜欢开闹钟声音的我来说,正在熟睡时,突然被闹钟的震动震醒,这种体验实在是太震撼了,根本毫无防备,个人感觉体验极差,一点儿缓冲都没有,太刺激了;然而设置里只有响铃渐响功能,重新开发一个闹钟 APP 显然太小题大做了,XPosed 插件很符合我的(奇葩)需求

0. 相关链接

  1. LSPosed
  2. dex2jar
  3. apktool
  4. android-platform-tools
  5. jd-gui
  6. 反编译 dex 文件_dex 反编译_留仙洞的博客-CSDN 博客
  7. 了解 Activity 生命周期 | Android 开发者 | Android Developers (google.cn)
  8. EzXHelper (kyuubiran.github.io)
  9. Android 之 Xposed 框架完全使用指南 (taodudu.cc)
  10. 基于 xposed 框架 hook 使用_xposed hook_zhangjianming2018 的博客-CSDN 博客
  11. Mac 安装 adb (Android 调试桥)_大大大大大桃子的博客-CSDN 博客
  12. 开发者助手:酷安 @ 东芝酷安 @ 帝鲮
  13. RE 文件管理器
  14. ES 文件浏览器
  15. https://github.com/1962247851/WooBoxForMIUI

1. 环境准备

  • 一部配置好 LSPosed 的 MIUI 安卓设备
  • 克隆 WooBoxForMIUI 项目到本地 AndroidStudio,完成依赖下载等,可以成功 Build 项目

可能涉及的 XPosed 相关 API 说明

// 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

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

d2j-dex2jar -f 时钟应用安装包全路径

配合 apktool 使用,直接 unzip 解压 apk 的话 XML 等文件的内容还是无法阅读

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

3.3 部分逻辑分析

3.3.1 闹钟配置界面 UI 布局分析

使用工具开发者助手进行分析
3311UI.png
考虑在震动开关下面增加震动渐强开关
3312.png
可以发现该控件的 id,但经过编译后控件的 id 其实已经被替换了,Id-Hex=0x7F0A02D9 即为编译后的的定位符,猜测应该是为了加快查询速度,转为十进制为 2131362521,再去 SetAlarmActivity 里面搜索,看看在哪儿初始化使用的
3313.png
可以发现是在初始化其他设置方法里面使用的,是一个 LinearLayout 线性布局,由布局可以得知 orientation 布局方向是默认的水平方向,显然不能直接去添加新的控件,否则会破坏现有的布局,考虑后还是采用直接增加在最后一行的方案
3314 震动设置行控件的本地变量.png
于是考虑 scroll_holder,但是根据 id 获取 view 并没有找到,id 可能是系统生成的
3315.png

3316.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,代码如下

// 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 保存闹钟代码逻辑分析

重点是能够区分出每个配置对应的是哪个闹钟,即找到闹钟的唯一标识符,通常是 iduuid
大概浏览了一下,直接看到了叫 saveAlarm 的方法

3321.png

saveAlarm 保存闹钟方法

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 初始化和响铃时配置读取,代码如下

// 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

存放内容如下,与预期一致

3323.jpg

顺便找到了 WooBoxForMIUI 的配置存放位置,怪不得之前直接去找没找到

3324WooBoxForMIUI.png

3.3.3 闹钟生效逻辑分析

要找到控制震动的代码,倒推直接搜索 .vibrate( 即可,不过这里还是试一下正着找,这样比较有意思

对开发者来说,用户设置的闹钟时间以及重复规则是不确定的,每个人的使用场景都是不确定的,因此需要使应用程序能够按照用户设置的规则触发某段代码,通常实现方式有 Java 的 Timer 类以及 Android 的 Handler、Alarm 机制等;考虑到移动端的能耗等,一般都是通过注册 Service 使得应用即使没有被打开也可以运行一些代码

所以先来看看 AndroidManifest,搜索一下 Servce,果然有收获

3331AndroidManifest.png

可以看到一个 com.android.deskclock.alarm.alert.AlarmService,还有其他的一些 Service,嫌疑最大的就是这个 AlarmService,就从他下手吧

<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

先来简单看一下类结构,直接找重写的 android.app.Service#onStartCommand 方法

3333AlarmService.png

可以很清楚的看到代码执行逻辑,handleAlarm 方法好像有点儿嫌疑

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 方法貌似就是要找的了

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);

private void play(Alarm paramAlarm) { Log.f("DC:AlarmService", "start AlarmService#play"); this.mAlarmKlaxon.start((Context)this, paramAlarm); registerTimeoutHandler(paramAlarm); }

AlarmKlaxon#start(Alarm 电喇叭 hhh)

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; }

注意这段代码,应该就是震动相关的代码了

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); }

终于找到头了

// 获取震动系统服务 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 测试一下,方法的全路径名为

com.android.deskclock.alarm.alert.AlarmKlaxon#vibrateLOrLater(Vibrator paramVibrator)

简单写一个打印日志的代码

Deskclock.CLZ_NAME_ALARM_KLAXON.hookAfterMethod("vibrateLOrLater", Vibrator::class.java) { Log.d(TAG, "after vibrateLOrLater") }

测试后发现正确打印,那么就可以正式开始搞事情了

3333hook.png

先看一下安卓提供的 vibrate API

/** * 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 根本没用到

// 震动模式 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;

来看看我的

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 删除闹钟逻辑分析

删除闹钟时应该同时删除增加的额外配置,否则会产生冗余的配置信息,文件虽然不大,但是这应该是个好习惯吧

先试着直接搜删除闹钟的方法,deletAlarmcancelAlarm 不太管用,那就还是从 UI 界面入手吧;删除闹钟操作为:进入时钟应用首页,在闹钟列表中长按某一个项目进入选择模式,然后再删除,那就先找一下控件,搜索 id 0x7F0A02B7
3341.png

搜索 RecyclerViewAdapter 中标题控件的 id,定位到 AlarmAdapter
3342id.png

AlarmAdapter 类,可以明显看到 OnAlarmCheckedChangedListenerOnLongClickListener 等点击 Listener
3343RecyclerViewAdapter.png

从监听闹钟选中状态改变的 OnAlarmCheckedChangedListener 继续搜索,终于又定位到了 AlarmClockFragment
3344Listener.png

先试着搜了一下这个 Listener,但是好像没有反编译成功
3345.png

接着还是从 mAlarmAdapter 入手,发现了这么一段代码,UiUtil.updateActionModeDeleteBtn(param1ActionMode, bool);,柳暗花明又一村

3346.png

3347id.png

通过 MenuItem 的 id 终于找到了点击时删除执行的代码逻辑:如果选中的项目个数大于 0,通过 AlarmAdapter 拿到选中的闹钟 id 列表,然后调用删除方法 AlarmClockFragment.access$2902

3348id.png

3349AlarmClockFragment.png

那么我们就可以考虑 hook 执行删除震动渐强配置的逻辑了,虽然调用的具体删除方法看不到,不过我们也可以监听菜单按钮的点击,当点击删除按钮时,也通过 AlarmAdapter 拿到会被删除的闹钟 Id 列表即可,注意 hook 在执行方法前,否则等退出选中模式后就拿不到了选择的 id 了,经测试无法 hook 接口实现类,尝试 hook 这个 access$2902 方法

public static interface MultiChoiceModeListener extends AbsListView.MultiChoiceModeListener { void onAllItemCheckedStateChanged(ActionMode param1ActionMode, boolean param1Boolean); }

通过测试后,只有发现当确认删除时,第二个参数才不为 null

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

删除部分的逻辑代码,完整代码将会发布到 fork 后的 WooBoxForMIUI 中:https://github.com/1962247851/WooBoxForMIUI

// 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. 使用体验

有了这个功能后,每天早上终于不用被“强制开机”了:),把人叫醒还是完全没问题的,能感觉到弱至强的震动,非常人性化!

评论