Skip to content

Phong-Kaster/Lockscreen-Styled-Notification

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

10 Commits
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Lockscreen-styled Notification
Send a notification that looks like the lockscreen of device

Have you ever seen applications that can send their notification that overrides device's lookscreen?

Perhaps, you are thinking that it is complicated to impletement this function. Don't worry, because together, we will go into detail to build this function. Generally speaking, it is just send normal notification and the difference is content of notification. Here is an activity instead of a notification.

LOOK LIKE DEVICE'S LOCKSCREEN? ACTUALLY, IT IS A NOTIFICATION 😊

First of all, to send notifications, we need some run-time permissions to can access system and schedule what content and when we send notifications. The following permissions are all we need to do it

<!-- For firing notification on Android 13 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />

<!-- For showing Lockscreen Activity as device's lockscreen -->
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />

From Android 13 & higher, we need to define POST_NOTIFICATIONS permission because in Notification runtime permission , Google has highlighted

NOTE: We highly recommend that you target Android 13 or higher as soon as possible to benefit from the additional control and flexibility of this feature. If you continue to target 12L (API level 32) or lower, you lose some flexibility with requesting the permission in the context of your app's functionality.

To send notifications at 8 am every single day, for instance. I use Alarm Manager to schedule in Android system. So that, SCHEDULE_EXACT_ALARM & USE_EXACT_ALARM are necessary to use this Alarm Manager properly.

To show an activity that overrides device's lockscreen. Of course, we have to define activity in Android Manifest. However, this activiy is special when its background is current wallpaper of device that we need to add some attributes like the following example below

<activity
            android:name=".ui.activity.LockscreenActivity"
            android:excludeFromRecents="true"
            android:exported="false"
            android:launchMode="singleInstance"
            android:noHistory="true"
            android:showOnLockScreen="true"
            android:theme="@style/Theme.Lockscreen" />

In this activity tag, take a look at android:launchMode="singleInstance" because the system doesn't launch any other activities into the task holding the instance. The activity is always the single and only member of its task.

Last but not least, the theme below is important to apply to the activity. To define, go to res/values/themes.xml and copy paste into the themes.xml file:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="Theme.Lockscreen" 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>

</resources>
    /**
     * Note: Create the Notification-Channel, but only on API 26+ because the NotificationChannel
     * class is new and not in the support library
     */
    fun createNotificationChannel(context: Context) {
        //1. define variable
        val name: CharSequence = context.getString(R.string.app_name)
        val description = context.getString(R.string.app_name)
        val importance = NotificationManager.IMPORTANCE_HIGH
        val channel = NotificationChannel(LOCKSCREEN_CHANNEL_ID, name, importance)
        channel.description = description


        //2. Register the channel with the system; you can't change the importance or other notification behaviors after this
        val notificationManager = context.getSystemService(NotificationManager::class.java)
        notificationManager.createNotificationChannel(channel)
    }

We have to set importance equals NotificationManager.IMPORTANCE_HIGH or NotificationManager.IMPORTANCE_MAX to ensure that the lockscreen activity can override device's lockscreen.

If you want to send other normal notifications then create another notification channel. Never use the same channel with lockscreen-styled notifications.

You can define LOCKSCREEN_CHANNED_ID and NOTIFICATION_CHANNED_ID to send both normal notifications & lockscreen-styled notifications.

To send lockscreen-styled notification at 08h00 every day. Take the code below as sample

    fun setupDailyLockscreenNotification(context: Context) {
        //1. alarm manager sends a lockscreen-styled notification at 4 AM everyday
        val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val alarmTime = Calendar.getInstance()
        val now = Calendar.getInstance()


        alarmTime.timeInMillis = System.currentTimeMillis()
        alarmTime[Calendar.HOUR] = 8
        alarmTime[Calendar.MINUTE] = 0
        alarmTime[Calendar.SECOND] = 10
        if (now.after(alarmTime)) {
            alarmTime.add(Calendar.DATE, 1)
        }


        //2. set up notification at specific time
        val intent = Intent(context, LockscreenReceiver::class.java)
        val pendingIntent = PendingIntent.getBroadcast(context, 100, intent, PendingIntent.FLAG_IMMUTABLE)
        alarmManager.setExact(AlarmManager.RTC_WAKEUP, alarmTime.timeInMillis, pendingIntent)
    }

This lockscreen reveicer works like any Broadcast Receiver. Only one thing which we need to take note that we only send the lockscreen-styled notification when device sleeps or device has been locked.

If device is being used by users, we'll send a normal notification instead. Below is a good broadcast receiver that you can reference

class LockscreenReceiver: BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        /*0. Start repeating notification if the device was shut down and then reboot*/
        if (intent.action == "android.intent.action.BOOT_COMPLETED") {
            LockscreenManager.setupDailyLockscreenNotification(context)
        }


        AppUtil.logcat("BroadcastReceiver: Send lockscreen-styled notification !", tag = "Notification")

        val powerManager = context.getSystemService(PowerManager::class.java)
        val keyguardManager = context.getSystemService(KeyguardManager::class.java)

        if (!powerManager.isInteractive || keyguardManager.isKeyguardLocked) {
            AppUtil.logcat("BroadcastReceiver: popupLockscreenNotification !", tag = "Notification")
            popupLockscreenNotification(context)
        } else {
            AppUtil.logcat("BroadcastReceiver: popupNormalNotification !", tag = "Notification")
            popupNormalNotification(context)
        }


        //4. Set again this alarm manager
        LockscreenManager.setupDailyLockscreenNotification(context)
    }

    /**
     * send a lockscreen-styled notification
     */
    private fun popupLockscreenNotification(context: Context) {
        //1. Create pending
        val lockscreenIntent = Intent(context, LockscreenActivity::class.java)
        lockscreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        val lockscreenPendingIntent = PendingIntent.getActivity(context, 0, lockscreenIntent, PendingIntent.FLAG_IMMUTABLE)


        //2. Setup notification builder
        val builder: NotificationCompat.Builder =
            NotificationCompat.Builder(context, LOCKSCREEN_CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_nazi_symbol)
                .setStyle(NotificationCompat.DecoratedCustomViewStyle())
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setCategory(NotificationCompat.CATEGORY_CALL)
                .setFullScreenIntent(lockscreenPendingIntent, true)


        //3. Show notification with notificationId which is a unique int for each notification that you must define
        val notificationManager = NotificationManagerCompat.from(context)
        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
            return
        }
        notificationManager.cancel(DEFAULT_LOCKSCREEN_ID)
        notificationManager.notify(DEFAULT_LOCKSCREEN_ID, builder.build())
    }


    /**
     * send a normal notification instead of lockscreen-styled notification
     * */
    private fun popupNormalNotification(context: Context) {
        //2. Create an explicit intent for an Activity in your app
        val destinationIntent = Intent(context, MainActivity::class.java)
        destinationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
        val pendingIntent = PendingIntent.getActivity(
            context,
            1896,
            destinationIntent,
            PendingIntent.FLAG_IMMUTABLE
        )


        //3. define notification builder
        val builder: NotificationCompat.Builder =
            NotificationCompat.Builder(context, LOCKSCREEN_CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_nazi_symbol)
                .setContentTitle(context.getString(R.string.app_name))
                .setContentText(context.getString(R.string.fake_title))
                .setStyle(
                    NotificationCompat.BigTextStyle()
                        .bigText(context.getString(R.string.fake_message))
                )
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .setContentIntent(pendingIntent)
                .setAutoCancel(true)


        //4. Show notification with notificationId which is a unique int for each notification that you must define
        val notificationManager = NotificationManagerCompat.from(context)
        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
            return
        }
        notificationManager.notify(DEFAULT_LOCKSCREEN_ID, builder.build())
    }
}

Lastly, don't forget to define Lockscreen Receiver in Android Manifest

    <!-- For Lockscreen Receiver -->
    <receiver
        android:name=".notification.LockscreenReceiver"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
        </intent-filter>
    </receiver>

We need to ask users to give us notification run-time permission.

    private fun setupNotification() {
        //1. Request POST NOTIFICATION permission if device has Android OS from 13
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            val isAccessed: Boolean = PermissionUtil.isNotiEnabled(context = requireContext())
            if (!isAccessed) {
                permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
            }
        }


        //2. Create lockscreen-styled notification and send it every day
        LockscreenManager.createNotificationChannel(context = requireContext())
        LockscreenManager.setupDailyLockscreenNotification(context = requireContext())
    }

Before you do, define ActivityResultLauncher like below:

    @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
    private val permissionLauncher: ActivityResultLauncher<String> = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isAccessed ->
        if (isAccessed) {
            LockscreenManager.setupDailyLockscreenNotification(requireContext())
        } else {
            if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
                openRationaleDialog()
            } else {
                openSettingDialog()
            }
        }
    }

If users reject our request, we should show rationale dialog to expand them why we need these permissions. Both openRationaleDialog and openSettingDialog are used for this purpose. You can write yourself if you need, they are optional.

To set up our activity shown as device's lockscreen, we need run this function as soon as onCreate runs

     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableLockscreen()
    }

    private fun enableLockscreen() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
            setShowWhenLocked(true)
        } else {
            val window = window
            window.addFlags(WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
        }
    }

The enableLockscreen is answer for the issue that I mention above. Because, this is an activity that you can arrange any content you want.

A SAMPLE LAYOUT THAT WRITTEN BY JETPACK COMPOSE😊

I have pasted package named notification to help you do this function easier. You can open this package to see full code πŸ’•πŸ’•πŸ’•πŸ’•.

Probably we are not even living in the same country. We look different. We speak different languages. Maybe we are from entirely different generations. We are just complete strangers.

But there is something that connects us. We both have great taste in getting programming.

Thank you for being here. God bless you, whoever you are ✌️😎

About

Send a notification looks like the lockscreen of device

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages