타이머를 만들다가, 안드로이드의 오픈소스 앱인 DeskClock 소스를 좀 살펴봤다.
타이머 동작시, AlarmManager에 완료시간을 등록한다. 시간 변경시, AlarmManager에 등록한 알람을 업데이트 시킨다.
@JvmStatic
fun createTimerExpiredIntent(context: Context, timer: Timer?): Intent {
val timerId = timer?.id ?: -1
return Intent(context, TimerService::class.java)
.setAction(ACTION_TIMER_EXPIRED)
.putExtra(EXTRA_TIMER_ID, timerId)
}
private fun updateAlarmManager() {
// Locate the next firing timer if one exists.
var nextExpiringTimer: Timer? = null
for (timer in mutableTimers) {
if (timer.isRunning) {
if (nextExpiringTimer == null) {
nextExpiringTimer = timer
} else if (timer.expirationTime < nextExpiringTimer.expirationTime) {
nextExpiringTimer = timer
}
}
}
// Build the intent that signals the timer expiration.
val intent: Intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer)
if (nextExpiringTimer == null) {
// Cancel the existing timer expiration callback.
val pi: PendingIntent? = PendingIntent.getService(mContext,
0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_NO_CREATE)
if (pi != null) {
mAlarmManager.cancel(pi)
pi.cancel()
}
} else {
// Update the existing timer expiration callback.
val pi: PendingIntent = PendingIntent.getService(mContext,
0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
schedulePendingIntent(mAlarmManager, nextExpiringTimer.expirationTime, pi)
}
}
TimerService가 시작되면, onStartCommand()에서 다음과같이 expireTimer()를 불러준다.
...
ACTION_TIMER_EXPIRED -> {
Events.sendTimerEvent(R.string.action_fire, label)
DataModel.dataModel.expireTimer(this, timer)
}
...
TimerModel의 expreTimer()가 호출되고 실행중인 서비스가 저장된게 없다면, 넘겨받은 서비스를 실행중 서비스로 설정한다. 그리고 updateTimer()를 불러준다.
fun expireTimer(service: Service?, timer: Timer) {
if (mService == null) {
// If this is the first expired timer, retain the service that will be used to start
// the heads-up notification in the foreground.
mService = service
} else if (mService != service) {
// If this is not the first expired timer, the service should match the one given when
// the first timer expired.
LogUtils.wtf("Expected TimerServices to be identical")
}
updateTimer(timer.expire())
}
updateTimer()를 보면, 타이머가 expired됐을 때, updateHeadsUpNotification()을 불러준다.
fun updateTimer(timer: Timer) {
val before = doUpdateTimer(timer)
// Update the notification after updating the timer data.
updateNotification()
// If the timer started or stopped being expired, update the heads-up notification.
if (before.state != timer.state) {
if (before.isExpired || timer.isExpired) {
updateHeadsUpNotification()
}
}
}
updateHeadsUpNotification()에서는 서비스의 유무와 expired된 타이머의 유무를 체크하고 TimerNotificationBuilder의 buildHeadsUp()으로 알람용 Notification을 만든다. 그리고 service의 setForeground()로 foreground service로 Notification을 표시한다.
private fun updateHeadsUpNotification() {
// Nothing can be done with the heads-up notification without a valid service reference.
if (mService == null) {
return
}
val expired = expiredTimers
// If no expired timers exist, stop the service (which cancels the foreground notification).
if (expired.isEmpty()) {
mService!!.stopSelf()
mService = null
return
}
// Otherwise build and post a foreground notification reflecting the latest expired timers.
val notification: Notification = mNotificationBuilder.buildHeadsUp(mContext, expired)
val notificationId = mNotificationModel.expiredTimerNotificationId
mService!!.startForeground(notificationId, notification)
}
expired된 타이머 알람용 Notification은 TimerNotificationBuilder에서 다음과 같이 만들어 주고 있다.
// Content intent shows the timer full screen when clicked.
val content = Intent(context, ExpiredTimersActivity::class.java)
val contentIntent: PendingIntent = Utils.pendingActivityIntent(context, content)
// Full screen intent has flags so it is different than the content intent.
val fullScreen: Intent = Intent(context, ExpiredTimersActivity::class.java)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION)
val pendingFullScreen: PendingIntent = Utils.pendingActivityIntent(context, fullScreen)
val notification: Builder = Builder(
context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
.setOngoing(true)
.setLocalOnly(true)
.setShowWhen(false)
.setAutoCancel(false)
.setContentIntent(contentIntent)
.setPriority(NotificationManager.IMPORTANCE_HIGH)
.setDefaults(Notification.DEFAULT_LIGHTS)
.setSmallIcon(R.drawable.stat_notify_timer)
.setFullScreenIntent(pendingFullScreen, true)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setColor(ContextCompat.getColor(context, R.color.default_background))
완료된 타이머를 보여주기 위한 ExpiredTimersActivity를 화면에 띄우기 위해 다음과 같이 처리해주고 있다.
view.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
setTurnScreenOn(true)
setShowWhenLocked(true)
// Close dialogs and window shade, so this is fully visible
sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
// Honor rotation on tablets; fix the orientation on phones.
if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR)
}