Toàn tập về AlarmManager trong Android

0

AlarmManager trong Android là một cầu nối giữa ứng dụng và alarm service của hệ thống Android. Nó có thể gửi một bản tin broadcast tới ứng dụng của bạn ở thời điểm đã được lên lịch trước đó. Sau đó ứng dụng của bạn có thể thực hiện bất kỳ tác vụ nào cần thiết ở thời điểm đó.

Để dễ hình khi nói tới AlarmManager, chúng ta nghĩ ngay tới ứng dụng báo thức. Đây chính là ví dụ điển hình, bạn đặt một lịch đến đúng 8h sáng thì chuông reo để còn đi làm. Dù ứng dụng của bạn đã bị bạn tắt, kill các kiểu nhưng đúng 8h sáng là nó lại “chồi lên” kêu ầm ĩ 🙂

Cùng với sự trợ giúp của PendingIntent và  Intent, một bundle đóng gói các thông tin cần thiết sẽ được gửi cùng broadcast hệ thống tới ứng dụng của bạn khi thời gian đặt lịch đến.

Tuy nhiên, từ phiên bản Android M thì Google đã có sự thay đổi với AlarmManager để hạn chế việc tiêu thụ pin quá mức. Alarm sẽ không còn được ưu tiên bật chính xác vào thời điểm đã đặt, mà tùy thuộc vào tình hình tài nguyên máy thời điểm đó mà thời gian bật alarm sẽ có sai lệch chút ít.

Có một vấn đề nhức đầu phổ biến của các nhà phát triển ứng dụng, đó là AlarmManager không hề lưu lại các lịch đã đặt sau khi thiết bị khởi động lại. Do vậy, việc quản lý các schedule sẽ cần phải làm thủ công bởi chính ứng dụng của bạn.

Chúng ta sẽ cùng nhau tìm hiểu kĩ hơn về AlarmManager Android trong bài viết này nhé.

Tìm hiểu AlarmManager trong Android

#Thiết lập AlarmManager Android trong ứng dụng

Có 2 bước để khởi tạo AlarmManager trong Android, đó là:

  • Tạo một BroadcastReceiver để nhận các bản tin broadcast từ hệ thống, tất nhiên bạn cần phải đăng ký trong xml
  • Đăng ký alarm với Alarm hệ thống ALARM_SERVICE thông qua PendingIntent kèm thời gian đặt trước.

Mình sẽ hướng dẫn các bạn thực hiện từng bước một.

Bước 1: Tạo BroadcastReceiver để nhận broadcast từ hệ thống

Đầu tiên, bạn tạo một class kế thừa từ BroadcastReceiver

class AlarmReceiver : BroadcastReceiver() {
  override fun onReceive(context: Context, intent: Intent) {
    // Is triggered when alarm goes off, i.e. receiving a system broadcast
    if (intent.action == "FOO_ACTION") {
      val fooString = intent.getStringExtra("KEY_FOO_STRING")
      Toast.makeText(context, fooString, Toast.LENGTH_LONG).show()
    }
  }
}

Sau đó đăng ký receiver trong AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest>
  <application>
    <receiver
        android:name=".AlarmReceiver"
        android:enabled="true"
        android:exported="true">
    </receiver>
  </application>
</manifest>

BroadcastReceiver là class để nhận và xử lý các bản tin broadcast được gửi tới ứng dụng của bạn. Có một hàm quan trọng nhất mà bạn phải overridden là onReceive(). Đây chính là hàm sẽ được gọi khi có broadcast gửi tới . Bạn có thể bóc tách dữ liệu từ bundle bằng các hàm getXXXExtra()

Ngoài ra, bạn cũng nên đặc biệt chú ý kiểm tra trường action trong intent, để đảm bảo nhận đúng broadcast mà bạn đã assign lúc lên lịch.

Bước 2: Tạo alarm với thời gian đặt trước

Dưới đây là đoạn code ví dụ để thiết lập trình schedule sẽ alarm sau 10s:

// Get AlarmManager instance
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager

// Intent part
val intent = Intent(this, AlarmReceiver::class.java)
intent.action = "FOO_ACTION"
intent.putExtra("KEY_FOO_STRING", "Medium AlarmManager Demo")

val pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0)

// Alarm time
val ALARM_DELAY_IN_SECOND = 10
val alarmTimeAtUTC = System.currentTimeMillis() + ALARM_DELAY_IN_SECOND * 1_000L

// Set with system Alarm Service
// Other possible functions: setExact() / setRepeating() / setWindow(), etc
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, alarmTimeAtUTC, pendingIntent)

Trong đoạn code trên chắc cũng không có gì khó hiểu lắm. Việc tạo alarm về cơ bản chỉ có vậy.  Mình sẽ giải thích kỹ hơn về PendingIntent, thành phần quan trọng trong việc tạo Alarm.

#Tìm hiểu PendingIntent Android

Như trong đoạn code trên, chúng ta có sử dụng đến PendingIntent:

val pendingIntentRequestCode = 0
val flag = 0
val pendingIntent = PendingIntent.getBroadcast(this, pendingIntentRequestCode, intent, flag)

Bạn đã biết tới Intent rồi đúng không? Nếu chưa thì mời bạn đọc bài viết này: Tìm hiểu Intent trong Android.

Tuy nhiên, với alarm chúng ta còn dùng đến PendingIntent nữa. Đây là loại Intent đặc biệt, thay vì tác vụ đích cần thực hiện ngay Intent khi vừa gửi thì với PendingIntent, tác vụ sẽ thực hiện sau một khoảng thời gian nào đó. Bạn có thể hiểu nôm na như vậy.

Có tổng cộng 4 cách để tạo một PendingIntent, nhưng thường ứng dụng bạn chỉ sử dụng cách thứ 1.

  • PendingIntent.getBroadcast() — Applicable to AlarmManager
  • PendingIntent.getActivity()
  • PendingIntent.getActivities()
  • PendingIntent.getService()

Như đã đề cập ở trên, AlarmManager sẽ gửi một broadcast tới BroadcastReceiver (trong ví dụ bài viết này thì là class AlarmReceiver). Theo như tài liệu chính thức của Google có mô tả thì chỉ có hàm getBroadcast() là khả dụng cho AlarmManager. Tham khảo t ại đây: Google official documentation — PendingIntent.getBroadcast()

Tham số requestCode được coi là một unique ID để phân biệt các PendingIntent trong cùng một Intent.

Tham số cuối cùng là flag, hiểu nôm na là kiểu của PendingIntent.

  • FLAG_UPDATE_CURRENT
  • FLAG_CANCEL_CURRENT
  • FLAG_IMMUTABLE

#Xử lý sự kiện Alarm

AlarmManager cung cấp 2 cách để lắng nghe một alarm broadcast. Đó là:

  • Local listener (AlarmManager.OnAlarmListener)
  • BroadcastReceiver được quy định tại Intent bọc bên trong một PendingIntent

Việc triển khai AlarmManager.OnAlarmListener() tương tự như cách sử dụng PendingIntent. Đơn giản là yêu cầu một callback xử lý sự kiện alarm trong đó.

val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManagerv
val alarmTime = System.currentTimeMillis() + 4000
val tagStr = "TAG"
val handler = null // `null` means the callback will be run at the Main thread
alarmManager.setExact(AlarmManager.RTC_WAKEUP, alarmTime, tagStr, object : AlarmManager.OnAlarmListener {
  override fun onAlarm() {
    Toast.makeText(this, "AlarmManager.OnAlarmListener", Toast.LENGTH_LONG).show()
  }
}, null)

Có một hạn chế với cách sử dụng AlarmManager.OnAlarmListener() đó là nó không thể hoạt động nếu Activity/Fragment tương ứng bị destroy. Ngoài ra, dùng PendingIntent còn có ưu điểm là ứng dụng có thể nhận broadcast alarm ngay cả khi ứng dụng bị kill bởi người dùng trước đó.

#Những cách thiết lập thời gian schedule cho alarm manager Android

Như mình đã nói ở trên, do Google muốn hạn chế việc tiêu thụ pin quá đà nên việc thiết lập thời gian schedule bởi Alarm sẽ không chính xác.

Ví dụ: bạn thiết lập đúng 8h00 AM thì ứng dụng tự động bật một bài hát. Nhưng thực tế thì có thể 8h02 AM ứng dụng mới nhận broadcast alarm và mới bật được nhạc. Vẫn có những sai số về thời gian.

Bình thường bạn hay dùng tới 2 api:

  • set() – lên lịch thời gian nhưng cho phép Android linh động thời điểm kích hoạt
  • setExact()- yêu cầu Android kích hoạt chính xác thời điểm

Tuy nhiên, từ Android M, Google có thêm khái niệm Doze và App Standby. Hai chế độ này sẽ hủy các alarm được lên lịch trước đó khi thiết bị không được cắm sạc.

Khi người dùng tắt màn hình hoặc không cắm sạc, thiết bị sẽ chuyển sang chế độ Doze. Trong chế độ Doze, Android sẽ hạn chế thiết bị truy cập vào mạng và dịch vụ sử dụng nhiều CPU. Do đó, các alarm cũng sẽ bị hủy hoặc bị hoãn cho tới khi thiết bị sẵn sàng.

Android sẽ định kỳ thoát và vào lại chế độ Doze, gọi là khoảng thời gian bảo trì. Trong thời gian bảo trì này, tất cả các hạn chế trước đó sẽ được giải phóng. Tức là các yêu cầu truy cập mạng sẽ được phép, các alarm được kích hoạt (đó chính là lý do tại sao mình nói ở trên là thời gian kích hoạt alarm sẽ bị sai lệch).

Chế độ App Standby cũng tương tự chế độ Doze, chỉ khác là màn hình không bắt buộc phải tắt.

chế độ Doze trong Android

Để khắc phục việc app vào chế độ Doze, bạn có thể sử dụng 2 api bên dưới để tạo alarm.

setAndAllowWhileIdle(...)
setExactAndAllowWhileIdle(...)

#Cách hủy một AlarmManager trong Android

Để hủy một Alarm, đơn giản là bạn gọi hàm cancel()

val onAlarmListener = object : AlarmManager.OnAlarmListener {
  override fun onAlarm() {
     toast("Alarm goes off.")
  }
}
alarmManager.cancel(onAlarmListener)

Tuy nhiên, nếu bạn chỉ muốn hủy chính xác một alarm thì cần phải truyền chính xác requestCode mà bạn đã tạo trước đó.

val requestCode = 0
val alarmManager;

fun getAlarmIntent(): Intent { 
  val intent = Intent(this, AlarmReceiver::class.java)
  intent.action = ALARM_RECEIVER_ACTION
  return intent
}

fun createAlarm() {
  val sender = PendingIntent.getBroadcast(fooActivityContext, requestCode, getAlarmIntent(), 0)
  alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, alarmTimeAtUTC, pendingIntent)
  }

fun cancelAlarm(){
   val sender = PendingIntent.getBroadcast(fooActivityContext, requestCode, getAlarmIntent(), 0)
   alarmManager.cancel(sender)
}

#Tạm kết

Android cung cấp một alarm service để thông báo cho ứng dụng Android ở một thời điểm được thiết lập trước. Cùng với sự phát triển của Android, nó đã được thay đổi để điều khiển alarm service tiết kiệm pin hơn.

setExactAndAllowWhileIdle() chỉ nên được sử dụng khi ứng dụng của bạn cần alarm chính xác nhất.

Ngoài ra, bạn cũng cần phải quản lý các AlarmManager trong android một cách thủ công, để có thể đăng ký lại các alarm khi thiết bị khởi động lại.

Mình hi vọng qua bài viết này bạn sẽ hiểu rõ hơn và biết cách sử dụng Alarm trong Android. Đừng ngại để lại một bình luận nếu bài viết có ích.

Đọc thêm về lập trình Android:

Bình luận. Cùng nhau thảo luận nhé!

avatar
  Theo dõi bình luận  
Thông báo