Android Direct Boot
@irgaly
2019/07/11
HealthTech 0.5#3
@TIMESHARING
Summary
• About O:SLEEP
• O:SLEEP Alarm
•
• Android AlarmManager
• Android Doze Mode
• Alarm
• Alarm 1 ~ 3
• Android Direct Boot
•
• Foreground Service
• Direct Boot +
• Conclusion: Problems & Solutions
About O:SLEEP
•
• Android / iOS
•
•
https://play.google.com/store/apps/details?id=jp.oinc.osleep&hl=ja
O:SLEEP Alarm
• 2019/6/24 v1.3.0:
•
•
O:SLEEP Alarm
•
•
•
•
• AlarmManager Intent
•
AlarmManager
setAlarmClock()
PendingIntent Broadcast Intent
O:SLEEP
Notification

Manager

MediaPlayer

Vibrator
AlarmManager
• setExactAndAllowWhileIdle
• API 23 (Android 6.0)~
• “it will not dispatch these alarms more than about every minute (at which
point every such pending alarm is dispatched); when in low-power idle modes
this duration may be significantly longer, such as 15 minutes.”
• Exact 1 ( )
• → Doze Mode ( )
AlarmManagerCompat
AlarmManager
• setAlarmClock
• Doze Mode ( )
• API 21 (Android 5.0) ~
•
AlarmManagerCompat
Doze Mode
•
• Android 6.0 Doze
•
•
•
•
https://developer.android.com/training/monitoring-device-state/doze-
standby?hl=JA
Alarm Notification
Job
Alarm
•
•
•
1
•
•
setAlarmClock() setAlarmClock() setAlarmClock()
Alarm 1
•
setAlarmClock() setAlarmClock()
7:00
GMT+09:00 -> 2019/07/11 07:00 +09:00
GMT+08:00 -> 2019/07/11 07:00 +08:00 ←1
( )
<intent-filter>
<action android:name="android.intent.action.TIME_SET"/>
<action android:name=“android.intent.action.TIMEZONE_CHANGED"/>
…
Alarm 2
• AlarmManager
•
• System Locked Boot (Direct Boot, )
• System Boot (Locked Boot )
Locked Boot
setAlarmClock()
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
…
System Boot
setAlarmClock()
Alarm 3
•
• (Broadcast)
•
•
•
setAlarmClock()
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
…
setAlarmClock()
merge
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<action android:name="android.intent.action.PACKAGE_DATA_CLEARED"/>
<action android:name="android.intent.action.TIME_SET"/>
<action android:name="android.intent.action.TIMEZONE_CHANGED"/>
</intent-filter>
Locked Boot System Boot
Direct Boot
•
•
https://developer.android.com/training/articles/direct-boot
Direct Boot
• Context Context
• createDeviceProtectedStorageContext Context
Direct Boot
•
fun AlarmRoomDatabase(application: Application):
AlarmRoomDatabase {
// Android 7.0 Direct Boot Context
val context = ContextCompat
.createDeviceProtectedStorageContext(application)
?: application
return Room.databaseBuilder(
context,
AlarmRoomDatabase::class.java,
“AlarmRoomDatabase"
).build()
}
Direct Boot
• LOCKED_BOOT_COMPLETED BroadcastReceiver
• android:directBootAware=“true”: Direct Boot
BroadcastReceiver Service
<receiver
android:name=".app.receiver.SystemEventBroadcastReceiver"
android:directBootAware="true"
tools:targetApi="n">
<intent-filter>
<action
android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
…
</receiver>
<service
android:name=".app.service.AlarmService"
android:directBootAware="true"
tools:targetApi="n"/>
Direct Boot
• Context
Preferences Database
Direct Boot
Context
Context OK
•
•
AlarmSetting
Room SQLite Database
AlarmSchedule
Room SQLite Database
@Entity
data class AlarmSetting(
@PrimaryKey val id: String,
@Embedded(prefix = "time_") val time:
LocalTime,
val repeatDayOfWeek: Set<DayOfWeek>,
val enabled: Boolean,
…
@Entity
data class AlarmSchedule(
@PrimaryKey val id: String,
val alarmSettingId: String,
val time: DateTimeTz,
…
)
Foreground Service
• MediaPlayer
• Android 8.0
• Foreground Service
Android OS
BroadcastReceiverOSLEEP
ForegroundService
ForegroundNotification
Foreground Service
• Notification Channel
• ForegroundNotification
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channel: ChannelType) {
notificationManager.createNotificationChannel(
NotificationChannel(
"alarm",
" ",
NotificationManager.IMPORTANCE_MAX
).apply {
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
}
)
}
class AlarmForegroundService : DaggerService() {
…
override fun onCreate() {
super.onCreate()
startForeground(0, NotificationCompat.Builder(context, "")
.setOngoing(true) //
.setContentTitle(context.getString(R.string.title_alarm_notification))
.setContentText(context.getString(R.string.text_alarm_notification))
.setSmallIcon(R.drawable.ic_notification)
.setCategory(Notification.CATEGORY_ALARM)
.setPriority(PRIORITY_MAX)
.setVisibility(VISIBILITY_PUBLIC)
.setContentIntent(
PendingIntent.getBroadcast(…)
).build())
}
…
Foreground Service
• MediaPlayer
private var _player: MediaPlayer? = null
private val player: MediaPlayer get() {
return _player ?: MediaPlayer().apply {
setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
}.also {
_player = it
}
}
override suspend fun playAlarm(soundUrl: String, repeat: Boolean) =
suspendCoroutine<Unit> { continuation ->
player.apply {
reset()
setDataSource(context, Uri.parse(soundUrl))
isLooping = repeat
setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
prepare()
start()
}
continuation.resume(Unit)
}
Foreground Service
• Vibrator
private val vibrator by lazy {
context.getSystemService<Vibrator>() ?: throw
RuntimeException("Vibrator ")
}
override fun vibrateAlarmRepeat(vibrateTime: TimeSpan, pauseTime:
TimeSpan) {
// OFF 0, ON , OFF... list
val pattern = listOf(0L, vibrateTime.millisecondsLong,
pauseTime.millisecondsLong)
if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
vibrator.vibrate(
VibrationEffect.createWaveform(
pattern.toLongArray(),
0
),
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.build()
)
} else {
vibrator.vibrate(
pattern.toLongArray(),
0,
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.build()
)
}
}
TimeSpan https://github.com/korlibs/klock
: Direct Boot +
• Direct Boot Media Content Provider
• File Path
• MediaColumns.DATA File Path
private fun getSystemAlarms(): List<AlarmSound> {
return context.contentResolver.query(
MediaStore.Audio.Media.INTERNAL_CONTENT_URI,
null,
"${MediaStore.Audio.Media.IS_ALARM} != 0",
null,
"${MediaStore.Audio.Media.TITLE} ASC"
)?.use { cursor ->
cursor.asSequence().map {
AlarmSound(
it.getString(it.getColumnIndex(MediaStore.Audio.Media.TITLE)),
it.getString(it.getColumnIndex(MediaStore.MediaColumns.DATA))
)
}.toList()
} ?: emptyList()
}
fun Cursor.asSequence(): Sequence<Cursor> {
return generateSequence(seed = takeIf { it.moveToFirst() }) {
takeIf { it.moveToNext() }
}
}
Conclusion: Problems & Solutions
• AlarmManager
• →
• Doze Mode
• → setAlarmClock
• Direct Boot
• → createDeviceProtectedStorageContext
•
• → Foreground Service

Android Direct Boot となにがなんでも鳴るアラームアプリ開発

  • 1.
  • 2.
    Summary • About O:SLEEP •O:SLEEP Alarm • • Android AlarmManager • Android Doze Mode • Alarm • Alarm 1 ~ 3 • Android Direct Boot • • Foreground Service • Direct Boot + • Conclusion: Problems & Solutions
  • 3.
    About O:SLEEP • • Android/ iOS • • https://play.google.com/store/apps/details?id=jp.oinc.osleep&hl=ja
  • 4.
  • 5.
  • 6.
    • AlarmManager Intent • AlarmManager setAlarmClock() PendingIntentBroadcast Intent O:SLEEP Notification
 Manager
 MediaPlayer
 Vibrator
  • 7.
    AlarmManager • setExactAndAllowWhileIdle • API23 (Android 6.0)~ • “it will not dispatch these alarms more than about every minute (at which point every such pending alarm is dispatched); when in low-power idle modes this duration may be significantly longer, such as 15 minutes.” • Exact 1 ( ) • → Doze Mode ( ) AlarmManagerCompat
  • 8.
    AlarmManager • setAlarmClock • DozeMode ( ) • API 21 (Android 5.0) ~ • AlarmManagerCompat
  • 9.
    Doze Mode • • Android6.0 Doze • • • • https://developer.android.com/training/monitoring-device-state/doze- standby?hl=JA Alarm Notification Job
  • 10.
  • 11.
    Alarm 1 • setAlarmClock() setAlarmClock() 7:00 GMT+09:00-> 2019/07/11 07:00 +09:00 GMT+08:00 -> 2019/07/11 07:00 +08:00 ←1 ( ) <intent-filter> <action android:name="android.intent.action.TIME_SET"/> <action android:name=“android.intent.action.TIMEZONE_CHANGED"/> …
  • 12.
    Alarm 2 • AlarmManager • •System Locked Boot (Direct Boot, ) • System Boot (Locked Boot ) Locked Boot setAlarmClock() <intent-filter> <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/> … System Boot setAlarmClock()
  • 13.
    Alarm 3 • • (Broadcast) • • • setAlarmClock() <intent-filter> <actionandroid:name="android.intent.action.MY_PACKAGE_REPLACED"/> … setAlarmClock()
  • 14.
    merge <intent-filter> <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/> <actionandroid:name="android.intent.action.MY_PACKAGE_REPLACED"/> <action android:name="android.intent.action.PACKAGE_DATA_CLEARED"/> <action android:name="android.intent.action.TIME_SET"/> <action android:name="android.intent.action.TIMEZONE_CHANGED"/> </intent-filter> Locked Boot System Boot
  • 15.
  • 16.
    Direct Boot • ContextContext • createDeviceProtectedStorageContext Context Direct Boot • fun AlarmRoomDatabase(application: Application): AlarmRoomDatabase { // Android 7.0 Direct Boot Context val context = ContextCompat .createDeviceProtectedStorageContext(application) ?: application return Room.databaseBuilder( context, AlarmRoomDatabase::class.java, “AlarmRoomDatabase" ).build() }
  • 17.
    Direct Boot • LOCKED_BOOT_COMPLETEDBroadcastReceiver • android:directBootAware=“true”: Direct Boot BroadcastReceiver Service <receiver android:name=".app.receiver.SystemEventBroadcastReceiver" android:directBootAware="true" tools:targetApi="n"> <intent-filter> <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/> … </receiver> <service android:name=".app.service.AlarmService" android:directBootAware="true" tools:targetApi="n"/>
  • 18.
    Direct Boot • Context PreferencesDatabase Direct Boot Context Context OK
  • 19.
    • • AlarmSetting Room SQLite Database AlarmSchedule RoomSQLite Database @Entity data class AlarmSetting( @PrimaryKey val id: String, @Embedded(prefix = "time_") val time: LocalTime, val repeatDayOfWeek: Set<DayOfWeek>, val enabled: Boolean, … @Entity data class AlarmSchedule( @PrimaryKey val id: String, val alarmSettingId: String, val time: DateTimeTz, … )
  • 20.
    Foreground Service • MediaPlayer •Android 8.0 • Foreground Service Android OS BroadcastReceiverOSLEEP ForegroundService ForegroundNotification
  • 21.
    Foreground Service • NotificationChannel • ForegroundNotification @RequiresApi(Build.VERSION_CODES.O) private fun createNotificationChannel(channel: ChannelType) { notificationManager.createNotificationChannel( NotificationChannel( "alarm", " ", NotificationManager.IMPORTANCE_MAX ).apply { lockscreenVisibility = Notification.VISIBILITY_PUBLIC } ) } class AlarmForegroundService : DaggerService() { … override fun onCreate() { super.onCreate() startForeground(0, NotificationCompat.Builder(context, "") .setOngoing(true) // .setContentTitle(context.getString(R.string.title_alarm_notification)) .setContentText(context.getString(R.string.text_alarm_notification)) .setSmallIcon(R.drawable.ic_notification) .setCategory(Notification.CATEGORY_ALARM) .setPriority(PRIORITY_MAX) .setVisibility(VISIBILITY_PUBLIC) .setContentIntent( PendingIntent.getBroadcast(…) ).build()) } …
  • 22.
    Foreground Service • MediaPlayer privatevar _player: MediaPlayer? = null private val player: MediaPlayer get() { return _player ?: MediaPlayer().apply { setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK) }.also { _player = it } } override suspend fun playAlarm(soundUrl: String, repeat: Boolean) = suspendCoroutine<Unit> { continuation -> player.apply { reset() setDataSource(context, Uri.parse(soundUrl)) isLooping = repeat setAudioAttributes( AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build() ) prepare() start() } continuation.resume(Unit) }
  • 23.
    Foreground Service • Vibrator privateval vibrator by lazy { context.getSystemService<Vibrator>() ?: throw RuntimeException("Vibrator ") } override fun vibrateAlarmRepeat(vibrateTime: TimeSpan, pauseTime: TimeSpan) { // OFF 0, ON , OFF... list val pattern = listOf(0L, vibrateTime.millisecondsLong, pauseTime.millisecondsLong) if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) { vibrator.vibrate( VibrationEffect.createWaveform( pattern.toLongArray(), 0 ), AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM) .build() ) } else { vibrator.vibrate( pattern.toLongArray(), 0, AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM) .build() ) } } TimeSpan https://github.com/korlibs/klock
  • 24.
    : Direct Boot+ • Direct Boot Media Content Provider • File Path • MediaColumns.DATA File Path private fun getSystemAlarms(): List<AlarmSound> { return context.contentResolver.query( MediaStore.Audio.Media.INTERNAL_CONTENT_URI, null, "${MediaStore.Audio.Media.IS_ALARM} != 0", null, "${MediaStore.Audio.Media.TITLE} ASC" )?.use { cursor -> cursor.asSequence().map { AlarmSound( it.getString(it.getColumnIndex(MediaStore.Audio.Media.TITLE)), it.getString(it.getColumnIndex(MediaStore.MediaColumns.DATA)) ) }.toList() } ?: emptyList() } fun Cursor.asSequence(): Sequence<Cursor> { return generateSequence(seed = takeIf { it.moveToFirst() }) { takeIf { it.moveToNext() } } }
  • 25.
    Conclusion: Problems &Solutions • AlarmManager • → • Doze Mode • → setAlarmClock • Direct Boot • → createDeviceProtectedStorageContext • • → Foreground Service