0. 相关链接
- LSPosed
- dex2jar
- apktool
- android-platform-tools
- jd-gui
- 反编译 dex 文件_dex 反编译_留仙洞的博客-CSDN 博客
- 了解 Activity 生命周期 | Android 开发者 | Android Developers (google.cn)
- EzXHelper (kyuubiran.github.io)
- Android 之 Xposed 框架完全使用指南 (taodudu.cc)
- 基于 xposed 框架 hook 使用_xposed hook_zhangjianming2018 的博客-CSDN 博客
- Mac 安装 adb (Android 调试桥)_大大大大大桃子的博客-CSDN 博客
- 开发者助手:酷安 @ 东芝酷安 @ 帝鲮
- RE 文件管理器
- ES 文件浏览器
- 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 项目简单分析
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 文件
3.3 部分逻辑分析
3.3.1 闹钟配置界面 UI 布局分析
使用工具开发者助手进行分析
考虑在震动开关下面增加震动渐强开关
可以发现该控件的 id,但经过编译后控件的 id 其实已经被替换了,Id-Hex=0x7F0A02D9
即为编译后的的定位符,猜测应该是为了加快查询速度,转为十进制为 2131362521
,再去 SetAlarmActivity
里面搜索,看看在哪儿初始化使用的
可以发现是在初始化其他设置方法里面使用的,是一个 LinearLayout
线性布局,由布局可以得知 orientation
布局方向是默认的水平方向,显然不能直接去添加新的控件,否则会破坏现有的布局,考虑后还是采用直接增加在最后一行的方案
于是考虑 scroll_holder
,但是根据 id 获取 view 并没有找到,id 可能是系统生成的
于是再考虑拿到震动开关的 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 保存闹钟代码逻辑分析
重点是能够区分出每个配置对应的是哪个闹钟,即找到闹钟的唯一标识符,通常是 id
、uuid
等
大概浏览了一下,直接看到了叫 saveAlarm
的方法
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
目录下
存放内容如下,与预期一致
顺便找到了 WooBoxForMIUI 的配置存放位置,怪不得之前直接去找没找到
3.3.3 闹钟生效逻辑分析
要找到控制震动的代码,倒推直接搜索
.vibrate(
即可,不过这里还是试一下正着找,这样比较有意思
对开发者来说,用户设置的闹钟时间以及重复规则是不确定的,每个人的使用场景都是不确定的,因此需要使应用程序能够按照用户设置的规则触发某段代码,通常实现方式有 Java 的 Timer 类以及 Android 的 Handler、Alarm 机制等;考虑到移动端的能耗等,一般都是通过注册 Service 使得应用即使没有被打开也可以运行一些代码
所以先来看看 AndroidManifest
,搜索一下 Servce
,果然有收获
可以看到一个 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
先来简单看一下类结构,直接找重写的 android.app.Service#onStartCommand
方法
可以很清楚的看到代码执行逻辑,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") }
测试后发现正确打印,那么就可以正式开始搞事情了
先看一下安卓提供的 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 删除闹钟逻辑分析
删除闹钟时应该同时删除增加的额外配置,否则会产生冗余的配置信息,文件虽然不大,但是这应该是个好习惯吧
先试着直接搜删除闹钟的方法,deletAlarm
,cancelAlarm
不太管用,那就还是从 UI 界面入手吧;删除闹钟操作为:进入时钟应用首页,在闹钟列表中长按某一个项目进入选择模式,然后再删除,那就先找一下控件,搜索 id 0x7F0A02B7
搜索 RecyclerViewAdapter 中标题控件的 id,定位到 AlarmAdapter
类
AlarmAdapter
类,可以明显看到 OnAlarmCheckedChangedListener
,OnLongClickListener
等点击 Listener
从监听闹钟选中状态改变的 OnAlarmCheckedChangedListener
继续搜索,终于又定位到了 AlarmClockFragment
先试着搜了一下这个 Listener,但是好像没有反编译成功
接着还是从 mAlarmAdapter
入手,发现了这么一段代码,UiUtil.updateActionModeDeleteBtn(param1ActionMode, bool);
,柳暗花明又一村
通过 MenuItem
的 id 终于找到了点击时删除执行的代码逻辑:如果选中的项目个数大于 0,通过 AlarmAdapter
拿到选中的闹钟 id 列表,然后调用删除方法 AlarmClockFragment.access$2902
那么我们就可以考虑 hook 执行删除震动渐强配置的逻辑了,虽然调用的具体删除方法看不到,不过我们也可以监听菜单按钮的点击,当点击删除按钮时,也通过 ,经测试无法 hook 接口实现类,尝试 hook 这个 AlarmAdapter
拿到会被删除的闹钟 Id 列表即可,注意 hook 在执行方法前,否则等退出选中模式后就拿不到了选择的 id 了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(",")}") }
删除部分的逻辑代码,完整代码将会发布到 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. 使用体验
有了这个功能后,每天早上终于不用被“强制开机”了:),把人叫醒还是完全没问题的,能感觉到弱至强的震动,非常人性化!