Creating Persistent Notification in Android Java

In this tutorial, we will write code to show unkillable/persistent notification in android application using Java.
You can use any latest version of Android Studio. This tutorial was written using Android Studio 4.1.

Creating Persistent Notification in Android Java

Because most mobile phones are battery optimized nowadays so all background processes of any app get shut down as soon as we clear it out of RAM. So, how to accomplish the task of running a continuous service like that of anti-virus in our mobile phone via our app?
Let’s find out:

Code:

Foreground permission is required when your app needs to perform a task that is noticeable by the user even when they’re not directly interacting with the app.
WakeLock permission is required to do things even when the device seems to be asleep.

So, firstly add these two permissions in the manifest.xml file:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

Then we create a Toggle button in our app:
So, write this in activity_main.xml file of your app:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <ToggleButton
        android:id="@+id/screen_tracking_toggle_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentBottom="true"
        android:layout_marginStart="141dp"
        android:layout_marginBottom="197dp"
        android:textOff="Scr trk off"
        android:textOn="Scr trk on"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.061"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.837" />


</androidx.constraintlayout.widget.ConstraintLayout>

 

Then, create the following files and packages as shown in the below picture:

Creating Persistent Notification in Android Java

Then, we create PreferencesManager.Java file to store info about app.
It’s like a small database using sharedPreferences.

So, write this code in the PreferencesManager.Java file:

package org.codespeedy.utils;


import android.content.Context;
import android.content.SharedPreferences;

public class PreferencesManager {

    public static final String PREF_SETTINGS_HIDE_SYSTEM_APPS = "hide_system_apps";
    public static final String PREF_SETTINGS_HIDE_UNINSTALL_APPS = "hide_uninstall_apps";
    public static final String PREF_LIST_SORT = "sort_list";
    public static final String FCM_ID = "fcm_id";
    private static final String PREF_NAME = "preference_application";

    private static PreferencesManager mManager;
    private static SharedPreferences mShare;

    private PreferencesManager() {
    }

    public static void init(Context context) {
        mManager = new PreferencesManager();
        mShare = context.getApplicationContext().getSharedPreferences(PREF_NAME, 0);
    }

    public static PreferencesManager getInstance() {
        return mManager;
    }

    public void putBoolean(String key, boolean value) {
        mShare.edit().putBoolean(key, value).apply();
    }

    public void putInt(String key, int value) {
        mShare.edit().putInt(key, value).apply();
    }

    public boolean getBoolean(String key) {
        return mShare.getBoolean(key, false);
    }

    public int getInt(String key) {
        return mShare.getInt(key, 0);
    }

    public boolean getUninstallSettings(String key) {
        return mShare.getBoolean(key, true);
    }

    public boolean getSystemSettings(String key) {
        return mShare.getBoolean(key, true);
    }

    public void putString(String key, String value) {
        mShare.edit().putString(key, value).apply();
    }

    public String getString(String key) {
        return mShare.getString(key, "");
    }

    public void putLong(String key, Long value){mShare.edit().putLong(key,value).apply();}

    public Long getlong(String key){return mShare.getLong(key,(0));}

}

 

Now, in the Restarter.java file we write a broadcastReceiver that receives Intent from some activities like SCREEN ON, SCREEN ON, POWER ON etc. and then performs some action:
So, write this code in the Restarter.java file:

package org.codespeedy.Service;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;

import org.codespeedy.utils.PreferencesManager;

public class Restarter extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.i("Broadcast Listened", "Service tried to stop");

        PreferencesManager.init(context);

        if(PreferencesManager.getInstance()==null)
        {
            return;
        }
        if (PreferencesManager.getInstance().getBoolean("track_screen")) {
            Toast.makeText(context, "Service restarted", Toast.LENGTH_SHORT).show();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                context.startForegroundService(new Intent(context, YourService.class));
            } else {
                context.startService(new Intent(context, YourService.class));
            }
        }
    }
}

In the above code, after receiving any broadcast from any event, a service called YourService is started

Now we write a Service that keeps running in background no matter what happens it is not destroyed when the app is removed from ram, its destroyed when we press clear RAM button / option in our mobile explicitly

So, now write the following code in YourService.java file:

package org.codespeedy.Service;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.IBinder;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;

import org.codespeedy.utils.PreferencesManager;

public class YourService extends Service {
    private static BroadcastReceiver m_ScreenOffReceiver = null;

    @Override
    public void onCreate() {
        super.onCreate();

        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O)
            startMyOwnForeground();
        else
            startForeground(1, new Notification());

        if (m_ScreenOffReceiver == null) {
            Toast.makeText(getApplicationContext(), "Registering Receiver",Toast.LENGTH_LONG).show();
        }
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private void startMyOwnForeground() {
        String NOTIFICATION_CHANNEL_ID = "example.permanence";
        String channelName = "Background Service";
        NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE);
        chan.setLightColor(Color.BLUE);

        NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        assert manager != null;
        manager.createNotificationChannel(chan);

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID);
        Notification notification = notificationBuilder.setOngoing(true)
                .setContentTitle("App is running in background")
                .setPriority(NotificationManager.IMPORTANCE_HIGH)
                .setCategory(Notification.CATEGORY_SERVICE)
                .build();
        startForeground(2, notification);
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);

        boolean shouldClose = intent.getBooleanExtra("close", false);
        if (shouldClose) {
            stopSelf();
        } else {
            // Continue to action here
        }

//        return START_STICKY;
        return START_REDELIVER_INTENT;
    }


    @Override
    public void onDestroy() {
        super.onDestroy();

        if(PreferencesManager.getInstance().getBoolean("track_screen"))
        {
            Intent broadcastIntent = new Intent();
            broadcastIntent.setAction("restartservice");
            broadcastIntent.setClass(this, Restarter.class);
            this.sendBroadcast(broadcastIntent);
        }
    }

    @Nullable
    public IBinder onBind(Intent intent) {
        return null;
    }

}

This service just broadcasts a message to Restarter.java when its getting destroyed and Restarter.java being a broadcast receiver restarts the service (YourService.java) and hence our service is still running.

Now coming to MainActivity.java write this simple code of toggle button:

package org.codespeedy;

import android.content.Intent;
import android.os.Bundle;
import android.widget.ToggleButton;

import androidx.appcompat.app.AppCompatActivity;

import org.codespeedy.Service.YourService;
import org.codespeedy.utils.PreferencesManager;

public class MainActivity extends AppCompatActivity {

    ToggleButton mScreenTrackingToggleButton;

    @Override
    public void onCreate(Bundle saved) {
        super.onCreate(saved);
        setContentView(R.layout.activity_main);

        PreferencesManager.init(getApplicationContext());

        if(PreferencesManager.getInstance().getBoolean("track_screen"))
        {
            this.startService(new Intent(this, YourService.class));
        }

        mScreenTrackingToggleButton = findViewById(R.id.screen_tracking_toggle_button);
        mScreenTrackingToggleButton.setOnCheckedChangeListener(
                ((compoundButton, b) -> {

                    if(b)
                    {
                        PreferencesManager.getInstance().putBoolean("track_screen",true);
                        Intent intent = new Intent(this, YourService.class);
                        startService(intent);
                    }
                    else
                    {
                        PreferencesManager.getInstance().putBoolean("track_screen",false);
                        Intent intent = new Intent(this, YourService.class);
                        intent.putExtra("close",true);
                        startService(intent);
                    }
                })
        );

    }
}

In the above code:

  1. First we check the value of the string “track_screen” in out SharedPreferences file using
  2. PreferencesManager object.
  3. Then, we set OnCheckedChangeListener() on the toggle button.
  4. Then we start the permanent notification service if the toggle button is checked else stop it.

Last but not the Least:
Don’t forget to register the BroadCastReceiver and Service in the manifest.xml file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.codespeedy">

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.CodeSpeedy">

        <service android:name=".Service.YourService" />
        <receiver
            android:name=".Service.Restarter"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.REBOOT" />
                <action android:name="android.intent.action.SCREEN_ON" />
                <action android:name="android.intent.action.SCREEN_OFF" />
                <action android:name="android.intent.action.USER_PRESENT" />
                <action android:name="android.intent.action.PACKAGE_DATA_CLEARED" />
                <action android:name="android.intent.action.PACKAGE_REPLACED" />
                <action android:name="android.intent.action.PACKAGE_RESTARTED" />
                <action android:name="android.intent.action.MY_PACKAGE_SUSPENDED" />

                <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
                <action android:name="android.intent.action.PACKAGE_REMOVED" />
                <action android:name="android.intent.action.MY_PACKAGE_SUSPENDED" />
                <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />

            </intent-filter>
        </receiver>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
    </application>

</manifest>

Screenshot of the running app:

Persistent_Notification_running

Persistent_Notification_restarted

How the whole thing works:

  • When you press the toggle button then our background notification service gets starter from MainActivity.java file and sharedPreferences is updated via PreferencesManager.java file object with value ‘true’ for variable track_screen
  • Then let’s say we remove the app from RAM, (don’t clear RAM) then too it keeps running, but why?
  • The reason is that when app is removed from RAM, onDestroy() of YourService file is called and this onDestroy sends a broadcast to Restarter and then this restarted initiates the service YourService again.
  • So the whole thing becomes like a recursion.

Then RAM is cleared background services get destroyed and then when mobile phone is closed and opened again, the Receiver which is Restarted.java here gets message as it listens to “SCREEN ON” Event and hence restarts Service-> YourService.java again.

Leave a Reply

Your email address will not be published. Required fields are marked *