The widget we are showing on lockscreen isn't an actual widget, it's our Activity that look like a lockscreen and we put a View on it.
In order start an activity when there is no foreground component, we have to attach it to a notification by calling "setFullscreenIntent" when building the notification and allow the activity to show on lockscreen in the activity.
- Only send notification if the device screen is off or at lockscreen. AppNotificationManager.kt:
val powerManager = context.getSystemService(PowerManager::class.java)
val keyguardManager = context.getSystemService(KeyguardManager::class.java)
// Only send the notification when screen if off or screen is on but in lockscreen
if (!powerManager.isInteractive || (powerManager.isInteractive && keyguardManager.isKeyguardLocked)) {
// Send notification here
}
- Cancel the notification right after the widget activity shows to avoid user seeing or clicking on the notification. This should be call right in onCreate of the widget activity
AppNotificationManager.cancelNotification(this, AppNotificationManager.LockscreenWidgetNotificationId)
- Add dismiss keyguard function to prompt user to unlock screen after clicking on the widget. Call this in onClick listener.
private fun dismissKeyguard() {
with(getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
requestDismissKeyguard(this@LockscreenWidgetActivity, null)
}
}
}
- First we need some permissions:
<manifest>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />~~~~
</manifest>
- Now we create an activity that looks like lockscreen with our desired view on it. Take a look at my sample "LockscreenWidgetActivity".
- Put the follow block of code into the activity and call it in onCreate to allow the activity to show on lockscreen.
private fun showOnLockscreen() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
}
}
- To make the activity looks like lockscreen, we have to put some attributes into activity's theme:
<style name="FullscreenReminderTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowShowWallpaper">true</item>
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:windowTranslucentStatus">true</item>
</style>
- And we also want to exclude the Activity from recent applications. Put these into Activity's manifest:
<activity android:name=".LockscreenWidgetActivity" android:exported="false" android:excludeFromRecents="true" android:launchMode="singleInstance"
android:noHistory="true" android:theme="@style/FullscreenReminderTheme" />
- Now let's build the notification and put the intent to launch activity into it
fun sendLockscreenWidgetNotification(context: Context) {
val fullScreenIntent = Intent(context, LockscreenWidgetActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val fullScreenPendingIntent = PendingIntent.getActivity(
context, 0,
fullScreenIntent, PendingIntent.FLAG_IMMUTABLE
)
val notificationBuilder =
NotificationCompat.Builder(context, DefaultNotificationChannelId)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(context.getString(R.string.app_name))
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setFullScreenIntent(fullScreenPendingIntent, true)
// Cancel old notification
cancelNotification(context, LockscreenWidgetNotificationId)
// Send new notification
sendNotification(context, LockscreenWidgetNotificationId, notificationBuilder.build())
}
- When the notification is fired, there are two situations:
- If device is in lockscreen (regardless the screen is on or not): the activity will show over the lockscreen.
- If device is not in lockscreen: a notification is shown instead.
- In demo code, when the button is clicked, I set a delay of 5 seconds before the notification is fired.
- In real situation, you should schedule to fire the notification using schedule mechanics. Currently I'm using AlarmManager.
<manifest>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
</manifest>
- We need SCHEDULE_EXACT_ALARM so that we can schedule with AlarmManager
- We need RECEIVE_BOOT_COMPLETED in order to reschedule alarm after boot because all alarms will be gone if device resets.
private fun scheduleLockscreenWidget(context: Context, requestCode: Int, atHour: Int, atMinute: Int = 0) {
Log.d(TAG, "scheduleLockscreenWidget")
val alarmIntent = Intent(context, AlarmReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context, requestCode, alarmIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
// Set time to show widget
val currentTimeMillis = System.currentTimeMillis()
val calendar: Calendar = Calendar.getInstance().apply {
timeInMillis = currentTimeMillis
set(Calendar.HOUR_OF_DAY, atHour)
set(Calendar.MINUTE, atMinute)
set(Calendar.SECOND, 0)
// If current time is after set time, push set time back one day
if (timeInMillis <= currentTimeMillis) add(Calendar.HOUR_OF_DAY, 24)
}
// Schedule alarm
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, AlarmManager.INTERVAL_DAY, pendingIntent)
Log.d(TAG, "scheduleFullscreenReminder: alarm scheduled at ${calendar.time}")
}
Then we simple call this function in our code depends on where we want to call in
To cancel this alarm, declare a similar PendingIntent with the same Intent and request code then call cancel on it:
fun cancelLockscreenWidgets(context: Context) {
// Create a pending intent with the same intent and request code
val pendingIntent = PendingIntent.getBroadcast(
context, lockscreenWidgetRequestCode,
Intent(context, AlarmReceiver::class.java), PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_NO_CREATE
)
// Cancel it with alarm manager
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.cancel(pendingIntent)
}