闹钟增加震动渐强功能——基于WooBox

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

先看一下安卓提供的 vibrateAPI

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

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

posted @ 2023-09-22 21:49  凡蝉  阅读(214)  评论(0)    收藏  举报