1. 程式人生 > >使用AlarmManager實現Android應用每天定時執行任務

使用AlarmManager實現Android應用每天定時執行任務

介紹

android官方文件:AlarmManager
在Android平臺,除了使用AlarmManger外,還可以使用Timer或者Handler來實現定時任務,但這兩種方式定時並不會太準確;因此如果我們需要實現精準定時任務,使用AlarmManger更為合理。
AlarmManager類提供對系統鬧鐘服務(或稱為定時器服務)的訪問介面,使用它既可以指定單次執行的定時任務,也可以指定重複執行的任務。
當鬧鐘指定觸發時間到達時,實際上是系統發出為這個鬧鐘註冊的廣播,因此我們需要實現一個針對特定鬧鐘事件的廣播介面器。
從API 19開始,alarm的機制都是非準確傳遞,作業系統將會轉換鬧鐘,來最小化喚醒和電池使用。

Note: Beginning with API 19 (KITKAT) alarm delivery is inexact: the OS will shift alarms in order to minimize wakeups and battery use. There are new APIs to support applications which need strict delivery guarantees; see setWindow(int, long, long, PendingIntent) and setExact(int, long, PendingIntent). Applications whose targetSdkVersion is earlier than API 19 will continue to see the previous behavior in which all alarms are delivered exactly when requested.

說明

本文中的程式碼使用Kotlin來編寫,Kotlin是JVM上的一種程式語言,可以與Java無縫對接,由JetBrains公司研發,官方介紹:

Statically typed programming language for the JVM, Android and the browser
100% interoperable with Java™

剛實現定時任務我就寫了本文,因此對這方面的知識並沒有完全掌握,文中可能會有錯誤,有機會還會更新。
本文沒有將完整的示例程式碼貼出來,只是貼了關鍵程式碼,主要是鬧鐘的設定和取消,以及與Settings部分的互動。

定時任務實現

常量

首先定義幾個常量,包括用於Settings部分的key、用於PendingIntent的requestCode和用於Intent中的action,程式碼如下:

const val ACTION_ALARM_REPLENISH_STOCK = "witmoon.auto.replenish.stock.action"
const val ACTION_ALARM_SYNCHRONIZE = "witmoon.auto.synchronize.action"
const val ALARM_REPLENISH_STOCK_CODE = 11
const val ALARM_SYNCHRONIZE_CODE = 12

const val KEY_SETTING_AUTO_SYNCHRONIZE = "auto_synchronize_status"
const val KEY_SETTING_SYNCHRONIZE_TIME = "auto_synchronize_time"
const val KEY_SETTING_AUTO_SUBMIT = "auto_submit_status"
const val KEY_SETTING_AUTO_SUBMIT_TIME = "auto_submit_time"

BroadcastReceiver

接下來是鬧鐘事件廣播接收器,作為示例程式碼非常簡單,收到廣播事件後首先判斷action,根據不同的action使用Toast打印出不同的內容:

/**
 * 鬧鐘廣播接收器
 * Created by chuxin on 2016/5/4.
 */
class AlarmBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        var action = intent?.action
        when {
            ACTION_ALARM_SYNCHRONIZE.equals(action) -> doSynchronizeAction(context)
            ACTION_ALARM_REPLENISH_STOCK.equals(action) -> doReplenishStockAction(context)
        }
    }

    private fun doSynchronizeAction(context: Context?) {
        Toast.makeText(context!!, "同步", Toast.LENGTH_SHORT).show()
    }

    /**
     * 執行補充庫存動作, 即下單/定貨
     */
    private fun doReplenishStockAction(context: Context?) {
        Toast.makeText(context!!, "定貨", Toast.LENGTH_SHORT).show()
    }
}

不要忘記在AndroidManifest.xml檔案中註冊接收器:

<receiver android:name=".receiver.AlarmBroadcastReceiver">
      <intent-filter>
           <action android:name="witmoon.auto.replenish.stock.action"/>
           <action android:name="witmoon.auto.synchronize.action"/>
      </intent-filter>
</receiver>

設定鬧鐘

由於我需要在App啟動時就設定定時任務,因此我將程式碼放置到了Application中,並結合了Settings部分重新設定任務執行時間(每天一次),關於Settings部分後面說,此處只關心設定鬧鐘。

class HomeDoorApplication : Application() {

    companion object {
        // 自定義委託實現單例
        var instance: HomeDoorApplication by DelegatesExt.notNullSingleValue()
    }

    override fun onCreate() {
        super.onCreate()
        instance = this

        // 啟動定時服務
        startAlarm()
    }

    /**
     * 啟動定時器(使用系統鬧鈴服務)
     */
    private fun startAlarm() {
        var preferences = PreferenceManager.getDefaultSharedPreferences(this)
        // 判斷是否需要啟動定時提交任務
        var isAutoSubmitEnable = preferences.getBoolean(KEY_SETTING_AUTO_SUBMIT, true)
        if (isAutoSubmitEnable) {
            var hourMinuteArray = preferences.getString(KEY_SETTING_AUTO_SUBMIT_TIME, "00:00").split(":")
            setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(), hourMinuteArray[1].toInt(),
                    AlarmManager.INTERVAL_DAY, ALARM_REPLENISH_STOCK_CODE, ACTION_ALARM_REPLENISH_STOCK)
        }
        // 判斷是否需要啟動定時同步任務
        var isAutoSynchronizeEnable = preferences.getBoolean(KEY_SETTING_AUTO_SYNCHRONIZE, true)
        if (isAutoSynchronizeEnable) {
            var hourMinuteArray = preferences.getString(KEY_SETTING_SYNCHRONIZE_TIME, "00:00").split(":")
            setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(), hourMinuteArray[1].toInt(),
                    AlarmManager.INTERVAL_DAY, ALARM_SYNCHRONIZE_CODE, ACTION_ALARM_SYNCHRONIZE)
        }
    }

    /**
     * 取消鬧鐘
     */
    fun cancelAlarm(requestCode: Int, action: String) {
        var alarmManager: AlarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
        alarmManager.cancel(
                PendingIntent.getBroadcast(this, requestCode, Intent(action), PendingIntent.FLAG_UPDATE_CURRENT))
    }

    fun setAlarm(type: Int, hour: Int, minute: Int, intervalMillis: Long, requestCode: Int, action: String) {
        var now = Calendar.getInstance()
        var targetTime = now.clone() as Calendar
        targetTime.set(Calendar.HOUR_OF_DAY, hour)
        targetTime.set(Calendar.MINUTE, minute)
        targetTime.set(Calendar.SECOND, 0)
        targetTime.set(Calendar.MILLISECOND, 0)
        if (targetTime.before(now))
            targetTime.add(Calendar.DATE, 1)
        // 方便測試,這裡指定即時啟動,每20秒執行一次
        setAlarm(type, 0, 20 * 1000, requestCode, action)
//        setAlarm(type, targetTime.timeInMillis, intervalMillis, requestCode, action)
    }

    /**
     * 設定鬧鐘
     */
    fun setAlarm(type: Int, triggerAtMillis: Long, intervalMillis: Long, requestCode: Int, action: String) {
        var alarmManager: AlarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
        alarmManager.setRepeating(type, triggerAtMillis, intervalMillis, PendingIntent.getBroadcast(this,
                requestCode, Intent(action), PendingIntent.FLAG_UPDATE_CURRENT))
    }
}

這段程式碼實際上是同時設定了兩個定時任務,自動提交訂單和自動同步,為了更容易讀懂程式碼,我們只看其中一個就可以。
在startAlarm方法中首先我讀取了Settings部分的設定,判斷是否需要啟動鬧鐘,如果設定值為true,則去讀取設定的任務執行時間;時間格式是每天的幾點幾分(如22:30表示每天晚上10點半執行)。
讀取時間後拆分為小時和分鐘,呼叫setAlarm方法,該方法引數包括鬧鐘型別、首次觸發時間、任務間隔、以及requestCode和Action;在該方法中我使用拆分出的小時和分鐘找到該時間點的毫秒數(如果時間在當前時間之前,即觸發時間已是過去,則到明天這個時間再觸發),呼叫setAlarm過載方法。
在setAlarm過載方法中,構建出PendingIntent物件並設定好鬧鐘。

設定

首先看一下最終的設定介面
這裡寫圖片描述
本文不介紹Settings部分內容,僅僅是記述重新設定任務執行時間的邏輯。
有關Android中Settings部分內容請參考官方文件:Settings

settings.xml

設定介面XML檔案,其中用到了一個自定義的Preference用於選擇時間,為了不顯得太亂這部分程式碼不再貼出。

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

    <PreferenceCategory android:title="同步">

        <SwitchPreference
            android:defaultValue="true"
            android:key="auto_synchronize_status"
            android:summary="自動與伺服器同步資料"
            android:title="自動同步"/>

        <com.witmoon.homedoor.ui.preference.TimePickPreference
            android:defaultValue="22:30"
            android:key="auto_synchronize_time"
            android:summary="每天22點30分"
            android:title="提交時間"/>
    </PreferenceCategory>

    <PreferenceCategory android:title="下單">

        <SwitchPreference
            android:defaultValue="true"
            android:key="auto_submit_status"
            android:summary="開啟後會在指定時間點自動提交缺貨商品訂單"
            android:title="自動提交訂單"/>

        <com.witmoon.homedoor.ui.preference.TimePickPreference
            android:defaultValue="23:30"
            android:key="auto_submit_time"
            android:summary="每天23點30分"
            android:title="提交時間"/>
    </PreferenceCategory>
</PreferenceScreen>

SettingFragment

設定部分使用PreferenceFragment,在3.0以後官方建議使用PreferenceFragment來代替PreferenceActivity,定義好SettingFragment後將其放置到一個普通的Activity即可。

class SettingFragment : PreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        addPreferencesFromResource(R.xml.setting)
    }

    override fun onResume() {
        super.onResume()
        preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
    }

    override fun onPause() {
        super.onPause()
        preferenceScreen.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
    }

    // 設定改變響應
    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String) {
        when {
            KEY_SETTING_AUTO_SYNCHRONIZE.equals(key) -> onAutoSynchronizeChanged(sharedPreferences, key)
            KEY_SETTING_SYNCHRONIZE_TIME.equals(key) -> onSynchronizeTimeChanged(sharedPreferences, key)
            KEY_SETTING_AUTO_SUBMIT.equals(key) -> onAutoSubmitChanged(sharedPreferences, key)
            KEY_SETTING_AUTO_SUBMIT_TIME.equals(key) -> onSubmitTimeChanged(sharedPreferences, key)
        }
    }

    private fun onSubmitTimeChanged(preferences: SharedPreferences?, key: String) {
        // 取消定時任務
        HomeDoorApplication.instance.cancelAlarm(ALARM_REPLENISH_STOCK_CODE, ACTION_ALARM_REPLENISH_STOCK)
        var hourMinuteArray = preferences?.getString(KEY_SETTING_AUTO_SUBMIT_TIME, "00:00")!!.split(":")
        HomeDoorApplication.instance.setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(),
                hourMinuteArray[1].toInt(), AlarmManager.INTERVAL_DAY, ALARM_REPLENISH_STOCK_CODE,
                ACTION_ALARM_REPLENISH_STOCK)
    }

    private fun onSynchronizeTimeChanged(preferences: SharedPreferences?, key: String) {
        // 取消定時任務
        HomeDoorApplication.instance.cancelAlarm(ALARM_SYNCHRONIZE_CODE, ACTION_ALARM_SYNCHRONIZE)
        // 設定定時任務
        var hourMinuteArray = preferences?.getString(KEY_SETTING_SYNCHRONIZE_TIME, "00:00")!!.split(":")
        HomeDoorApplication.instance.setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(),
                hourMinuteArray[1].toInt(), AlarmManager.INTERVAL_DAY, ALARM_SYNCHRONIZE_CODE,
                ACTION_ALARM_SYNCHRONIZE)
    }

    // 自動提交訂單設定有變化,啟用則繫結鬧鐘,禁用則取消鬧鐘
    private fun onAutoSubmitChanged(preferences: SharedPreferences?, key: String) {
        var isAutoSubmit = preferences?.getBoolean(key, true)!!
        if (isAutoSubmit) {
            preferenceScreen.findPreference(KEY_SETTING_AUTO_SUBMIT_TIME).isEnabled = true
            var hourMinuteArray = preferences?.getString(KEY_SETTING_AUTO_SUBMIT_TIME, "00:00")!!.split(":")
            HomeDoorApplication.instance.setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(),
                    hourMinuteArray[1].toInt(), AlarmManager.INTERVAL_DAY, ALARM_REPLENISH_STOCK_CODE,
                    ACTION_ALARM_REPLENISH_STOCK)
        } else {
            preferenceScreen.findPreference(KEY_SETTING_AUTO_SUBMIT_TIME).isEnabled = false
            // 取消定時任務
            HomeDoorApplication.instance.cancelAlarm(ALARM_REPLENISH_STOCK_CODE, ACTION_ALARM_REPLENISH_STOCK)
        }
    }

    private fun onAutoSynchronizeChanged(preferences: SharedPreferences?, key: String) {
        var isAutoSynchronize = preferences?.getBoolean(key, true)!!
        if (isAutoSynchronize) {
            preferenceScreen.findPreference(KEY_SETTING_SYNCHRONIZE_TIME).isEnabled = true
            // 設定定時任務
            var hourMinuteArray = preferences?.getString(KEY_SETTING_SYNCHRONIZE_TIME, "00:00")!!.split(":")
            HomeDoorApplication.instance.setAlarm(AlarmManager.RTC_WAKEUP, hourMinuteArray[0].toInt(),
                    hourMinuteArray[1].toInt(), AlarmManager.INTERVAL_DAY, ALARM_SYNCHRONIZE_CODE,
                    ACTION_ALARM_SYNCHRONIZE)
        } else {
            preferenceScreen.findPreference(KEY_SETTING_SYNCHRONIZE_TIME).isEnabled = false
            // 取消定時任務
            HomeDoorApplication.instance.cancelAlarm(ALARM_SYNCHRONIZE_CODE, ACTION_ALARM_SYNCHRONIZE)
        }
    }
}

SharedPreferences.OnSharedPreferenceChangeListener介面用於監聽設定項的改變,從程式碼中可以看出,如果是關閉自動任務,則直接呼叫取消鬧鐘;如果是重新設定了任務執行時間,則先取消鬧鐘,再用新的時間重新設定鬧鐘。取消和設定都在前面的Application中定義了,這裡只是簡單地呼叫即可。