DeskClock은 실행중에 wakelock을 요청하지 않는다. wakelock을 요청하는 부분은 알람이 울리는 경우에만 꺼지지 않고 알람을 울리도록 요청한다. 알람 매니저를 통해 브로드캐스트 리시버로 “times_up” 인텐트를 접수하면, TimerService를 실행하고 TimerModel.updateTimer()가 불린다. 여기서 doUpdateTimer()를 호출하는데, 여기서 updateRinger() 안에서 wakelock을 요청하고 해제한다.
...
AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext)
...
AlarmAlertWakeLock.releaseCpuLock()
타이머의 저장은 SharedPreference를 사용한다. 개인 핸드폰에서 사용하는 타이머 개수가 많지 않을거라고 가정한듯하고, 수시로 저장하고 가져오는데 적합하다고 판단한거라 추측한다. 저장 부분은 TimerDAO를 살펴보면 된다.
먼저 Timer 클래스를 보자. 타이머에 대한 정보와 계산등이 모두 이루어지는 곳이다. 기본적으로 SharedPreference에 저장되는 구조를 포함하고 있어 아이디, 상태, 설정시간, 남은시간등 여러 정보를 가지고 있다.
흥미로운 부분은 remainingTime 변수인데, 다음과 같이 구현되어 있다.
/**
* @return the total amount of time remaining up to this moment; expired and missed timers will
* return a negative amount
*/
val remainingTime: Long
get() {
if (state == State.PAUSED || state == State.RESET) {
return lastRemainingTime
}
// In practice, "now" can be any value due to device reboots. When the real-time clock
// is reset, there is no more guarantee that "now" falls after the last start time. To
// ensure the timer is monotonically decreasing, normalize negative time segments to 0,
val timeSinceStart = Utils.now() - lastStartTime
return lastRemainingTime - max(0, timeSinceStart)
}
PAUSED나 RESET 상태는 시간이 흐르는 상황이 아니므로, 저장된 가장 최근의 남은시간, lastRemainingTime 이 리턴된다. 그다음, 타이머가 시작된 이후로 흐른 시간을 계산하는데, now()로 현재시간에서 가장 최근 시작된 시간인 lastStartTime을 빼줘서 계산한다. now()를 한번 살펴보자.
fun now(): Long = DataModel.dataModel.elapsedRealtime()
...
fun elapsedRealtime(): Long = SystemClock.elapsedRealtime()
now()는 결국 SystemClock.elapsedRealtime()을 가져온다. 이게 무슨 값이냐면, 부팅 후 지금까지 흐른 시간으로 중간에 슬립상태에 들어간 시간도 포함한다. ( 안드로이드 레퍼런스 문서 참조 ) lastStartTime도 알아야 하는데, start()를 살펴보면,
fun start(): Timer {
return if (state == State.RUNNING || state == State.EXPIRED || state == State.MISSED) {
this
} else {
Timer(id, State.RUNNING, length, totalLength,
Utils.now(), Utils.wallClock(), lastRemainingTime, label, deleteAfterUse)
}
}
타이머를 시작할 때, Utils.now()로 elapsedRealtime()을 가져와 설정한다. 결국 처음으로 돌아가서 timeSinceStart는 현재 elapsedRealtime()에서 타이머 시작시점의 elapsedRealtime()을 빼주어 시작 후 흐른시간값을 갖는다.
lastRemainingTime이 언제 어떻게 설정되냐가 문제인데 일단, 처음 Timer 생성시 length로 설정된다.
fun addTimer(length: Long, label: String?, deleteAfterUse: Boolean): Timer {
// Create the timer instance.
var timer =
Timer(-1, Timer.State.RESET, length, length, Timer.UNUSED, Timer.UNUSED, length,
label, deleteAfterUse)
그 외에 1분씩 추가하는 경우나, 시간 길이가 변할 때, 추가된 시간을 포함하여 새로 설정된다. 또한, pause()함수에서 현재 remainingtime으로 설정하고 있다.
뭔가 복잡한데, 결국에 timer가 pause되지 않으면, 전체 시간이 lastRemainingTime이 된다(missed 나 expired 는 예외상황). 그래서 lastRemainingTime – max(0, timeSinceStart)가 남은 시간(remaining time)이 된다. max()를 사용하는 이유는, lastRemainingTime이 마이너스가 될 것을 생각해서 들어간거 같은데, 실제로 마이너스가 될 상황이 없어보이긴 한다.
예외상황으로 reboot이 일어났을 때, 시스템 시간이 변경됐을 때의 처리가 있다. AlarmInitReceiver에서 우선 시스템 변경 intent를 받는다.
if (ACTION_BOOT_COMPLETED == action) {
DataModel.dataModel.updateAfterReboot()
// Stopwatch and timer data need to be updated on time change so the reboot
// functionality works as expected.
} else if (Intent.ACTION_TIME_CHANGED == action) {
DataModel.dataModel.updateAfterTimeSet()
}
호출 체인을 따라가보면, Timer의 updateAfterReboot()과 updateAfterTimeSet()에 도달한다. 우선 rebooting시에는 wallClock, 실제로는 System.currentTimeMillis()를 가지고 마지막에 저장된 값과 현재값을 비교하여 리부팅 과정에서 흐른 시간 delta를 계산한다. 새로운 remaining time은 lastRemainingTime에서 delta를 빼주어 얻게된다. 이걸 새로운 타이머로 사용한다.
fun updateAfterReboot(): Timer {
if (state == State.RESET || state == State.PAUSED) {
return this
}
val timeSinceBoot = Utils.now()
val wallClockTime = Utils.wallClock()
// Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
// update the recorded times and proceed with no change in accumulated time.
val delta = max(0, wallClockTime - lastWallClockTime)
val remainingTime = lastRemainingTime - delta
return Timer(id, state, length, totalLength, timeSinceBoot, wallClockTime,
remainingTime, label, deleteAfterUse)
}
시간이 변경된 경우에는 elapsedRealtime을 가져와서 시작시 저장한 elapsedRealtime인 lastStartTime과의 차이를 가지고 계산한다.
fun updateAfterTimeSet(): Timer {
if (state == State.RESET || state == State.PAUSED) {
return this
}
val timeSinceBoot = Utils.now()
val wallClockTime = Utils.wallClock()
val delta = timeSinceBoot - lastStartTime
val remainingTime = lastRemainingTime - delta
return if (delta < 0) {
// Avoid negative time deltas. They typically happen following reboots when TIME_SET is
// broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
// updateAfterReboot() can successfully correct the data at a later time.
this
} else {
Timer(id, state, length, totalLength, timeSinceBoot, wallClockTime,
remainingTime, label, deleteAfterUse)
}
}
생각보다 뭔가 복잡하게 구현이 되어 있어 하나하나 이해하는데 자꾸 브레이크가 걸렸다. lastStartTime이 elapsedRealtime()으로 기록되고 lastRemaingTime이 시간의 흐름에 따라 계속 업데이트 되는게 아니라 pause같은 이벤트시에만 업데이트 되는게 혼란스러웠던거 같다. 전체적인 흐름과 개념만 잘 잡고 넘어가면 될거같다.
이로서 공식 Timer앱의 가장 큰 궁금증들이 풀렸다. 대충 다음과 같이 타이머 분석을 정리할 수 있겠다.
- 타이머들의 저장 및 관리? : SharedPreference로 이루어진다
- 타이머 실행중에 wakelock을 걸고 돌리는건지? : 알람 울릴 때 빼곤 필요없다. AlarmManager를 이용해 만기될 때 깨우면서 알람 울리고, 중간에 슬립 들어가면 elapsedRealTime()으로 흐른 시간을 계산, 업데이트해 사용한다.
- Service를 어떻게 사용하는지, Notification과 App간의 전환은? : Service는 onStartCommand()형태로 상주하는 서비스가 아니라 커맨드 처리기정도의 중계 역할만 한다. Notification은 메인 Activity인 DeskClock의 onStart(), onStop()에서 실행, 제거를 해준다.
타이머를 틱마다 계속 계산하는 개념이 아니고, UI업데이트시 남은시간을 계속 계산해서 보여주는 방식이다. 다만, UI가 보여지는 경우에는 UI를 틱마다 계속 업데이트 해준다.