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é.
Nội dung chính của bài viết
#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 quaPendingIntent
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 AlarmManagerPendingIntent.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.
Để 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ài viết hay và dễ hiểu. Em đang muốn làm cái chạy đồng hồ nó update liên tục time với widget trên màn hình home. Không biết là nó dùng cái Alarm này để set thời gian thực được không.
Mình nghĩ không nên dùng nhé. Cái Alarm này mục đích chính là lên lịch, hẹn giờ. Nếu bạn cần làm đồng hồ update liên tục thì nên dùng một cái thread và cho nó chạy liên tục
E thấy thread với widget khi tắt điện thoại là nó dừng. Bật lên nó đứng hình tại lúc bị tắt. Có cách nào để khắc phục không a
Bạn có thể trigger event bật tắt màn hình để khởi động lại thread. Chứ khi người dùng đã tắt màn thì android nó cũng sẽ xem xét kill thread để tiết kiệm tài nguyên.
Mình làm một ứng dụng ghi âm bằng hẹn giờ, ở chế độ tắt màn hình thì service ghi âm chỉ ghi được một lúc rồi tự động dừng (cụ thể chỉ khoảng 69s là dừng). Cho mình hỏi đây là lỗi gì ak?
Chào bạn,
Đây không hẳn là lỗi bạn à. Nguyên nhân là do OS sẽ tự kill các task chạy ngầm để tiết kiệm pin. Bạn có thể tham khảo các giải pháp để chạy task dưới background như intent service…