Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Screen on/off broadcast listener for a widget on Android Oreo

I have a clock widget Android app which I am now trying to update to API 26 requirements.

Up to now I used a background service which registered upon start in its onCreate method a BroadcastReceiver to receive system broadcasts, such as android.intent.action.SCREEN_ON, android.intent.action.SCREEN_OFF, android.intent.action.TIME_SET, android.intent.action.TIMEZONE_CHANGED. This service was then pausing the clock while screen is off and waking it up when screen is back on to save the battery.

In Oreo a service of this kind does not seem to be an option, because it would have to run in the foreground with a notification which really has no significance for the user. Also, as far as I have seen in the documentation, JobScheduler cannot help me either as I have not found that it is possible to schedule a job to when the screen is on.

I tried creating a BroadcastReceiver within the AppWidgetProvider class, and registering it in the AppWidgetProvider's onUpdate method to receive the said system broadcasts. This works well and broadcasts do get received, but only until the screen remains off for a period of time; afterwards, it seems that the app gets killed by the system somehow, or otherwise stops working without any reported error or a crash; however still if I click it, it will open the configuration activity as normal.

My questions:

  1. How do I properly listen to screen on/off broadcasts on API 26+ if I do not want to run a foreground service?

  2. Is it possible to listen to system broadcasts from the AppWidgetProvider class itself, by registering a BroadcastReceiver within it, or by even registering AppWidgetProvider itself to receive system events (anyway AppWidgetProvider is an extension of BroadcastReceiver).

  3. Why does my AppWidgetProvider aparently stop receiving broadcasted system intents after some sleep period?

EDIT:

I found the following in the Android documentation for registerReceiver method which appears to be the answer to my questions 2 and 3.

Note: this method cannot be called from a BroadcastReceiver component; that is, from a BroadcastReceiver that is declared in an application's manifest. It is okay, however, to call this method from another BroadcastReceiver that has itself been registered at run time with registerReceiver(BroadcastReceiver, IntentFilter), since the lifetime of such a registered BroadcastReceiver is tied to the object that registered it.

I would conclude that my use and registration of a BroadcastReceiver inside the AppWidgetProvider was contrary to this specification.

I will leave this post open because others may find this information useful and my question 1 still remains valid.

like image 848
Elefterios Papalimani Avatar asked Feb 03 '18 14:02

Elefterios Papalimani


People also ask

What is broadcast receiver in an Android app?

Android BroadcastReceiver is a dormant component of android that listens to system-wide broadcast events or intents. When any of these events occur it brings the application into action by either creating a status bar notification or performing a task.

How do you stop broadcasting on Android?

CB stands for Cell Broadcast. To stop recieving CB messages, go to Messaging then tap the Menu key and select Settings. New menu appears then please, find CB activation and Uncheck it.

Which are the broadcast receivers are available in Android?

There are mainly two types of Broadcast Receivers: Static Broadcast Receivers: These types of Receivers are declared in the manifest file and works even if the app is closed. Dynamic Broadcast Receivers: These types of receivers work only if the app is active or minimized.

What is a broadcast listener app?

1.1. Definition. A broadcast receiver (receiver) is an Android component which allows you to register for system or application events. All registered receivers for an event are notified by the Android runtime once this event happens.


1 Answers

Here what I am doing for listen SCREEN_OFF and SCREEN_ON broadcast in Android API 26 (Oreo) and above. This answer is not related to widget but it may be helpful to find some work-around.

I am using Job Scheduler for register and unRegister Broadcast Receiver which listen SCREEN_OFF and SCREEN_ON action.

import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.PowerManager;
import android.support.annotation.NonNull;
import android.util.Log;

import com.evernote.android.job.Job;
import com.evernote.android.job.JobManager;
import com.evernote.android.job.JobRequest;

import java.util.concurrent.TimeUnit;


public class LockScreenJob extends Job {

    private static final String TAG = LockScreenJob.class.getSimpleName();

    public static final String TAG_P = "periodic_job_tag";
    public static final String TAG_I = "immediate_job_tag";

    //Used static refrence of broadcast receiver for ensuring if it's already register or not NULL
    // then first unregister it and set to null before registering it again.
    public static UnlockReceiver aks_Receiver = null;

    @Override
    @NonNull
    protected Result onRunJob(Params params) {
        // run your job here

        String jobTag = params.getTag();

        if (BuildConfig.DEBUG) {
            Log.i(TAG, "Job started! " + jobTag);
        }

        PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);

        boolean isInteractive = false;
        // Here we check current status of device screen, If it's Interactive then device screen is ON.
        if (Build.VERSION.SDK_INT >= 20) {
            isInteractive = pm.isInteractive();
        } else {
            isInteractive = pm.isScreenOn();
        }

        try {
            if (aks_Receiver != null) {
                getContext().getApplicationContext().unregisterReceiver(aks_Receiver); //Use 'Application Context'.
            }
        } catch (Exception e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }
        } finally {
            aks_Receiver = null;
        }

        try {
            //Register receiver for listen "SCREEN_OFF" and "SCREEN_ON" action.

            IntentFilter filter = new IntentFilter("android.intent.action.SCREEN_OFF");
            filter.addAction("android.intent.action.SCREEN_ON");
            aks_Receiver = new UnlockReceiver();
            getContext().getApplicationContext().registerReceiver(aks_Receiver, filter); //use 'Application context' for listen brodcast in background while app is not running, otherwise it may throw an exception.
        } catch (Exception e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }
        }

        if (isInteractive)
        {
          //TODO:: Can perform required action based on current status of screen.
        }

        return Result.SUCCESS;
    }

    /**
     * scheduleJobPeriodic: Added a periodic Job scheduler which run on every 15 minute and register receiver if it's unregister. So by this hack broadcast receiver registered for almost every time w.o. running any foreground/ background service. 
     * @return
     */
    public static int scheduleJobPeriodic() {
        int jobId = new JobRequest.Builder(TAG_P)
                .setPeriodic(TimeUnit.MINUTES.toMillis(15), TimeUnit.MINUTES.toMillis(5))
                .setRequiredNetworkType(JobRequest.NetworkType.ANY)
                .build()
                .schedule();

        return jobId;
    }

    /**
     * runJobImmediately: run job scheduler immediately so that broadcasr receiver also register immediately
     * @return
     */
    public static int runJobImmediately() {
        int jobId = new JobRequest.Builder(TAG_I)
                .startNow()
                .build()
                .schedule();

        return jobId;
    }

    /**
     * cancelJob: used for cancel any running job by their jobId.
     * @param jobId
     */
    public static void cancelJob(int jobId) {
        JobManager.instance().cancel(jobId);
    }
}

And my JobCrator class LockScreenJobCreator is:

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.evernote.android.job.Job;
import com.evernote.android.job.JobCreator;

public class LockScreenJobCreator implements JobCreator {

    @Override
    @Nullable
    public Job create(@NonNull String tag) {
        switch (tag) {
            case LockScreenJob.TAG_I:
                return new LockScreenJob();
            case LockScreenJob.TAG_P:
                return new LockScreenJob();
            default:
                return null;
        }
    }
}

BroadcastReceiver class UnlockReceiver is :

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class UnlockReceiver extends BroadcastReceiver {

    private static final String TAG = UnlockReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context appContext, Intent intent) {

        if (BuildConfig.DEBUG) {
            Log.i(TAG, "onReceive: " + intent.getAction());
        }

        if (intent.getAction().equalsIgnoreCase(Intent.ACTION_SCREEN_OFF))
        {
          //TODO:: perform action for SCREEN_OFF
        } else if (intent.getAction().equalsIgnoreCase(Intent.ACTION_SCREEN_ON)) {
          //TODO:: perform action for SCREEN_ON
        }
    }

}

And adding JobCreator class to the Application class like this:

public class AksApplication extends Application {

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

       JobManager.create(this).addJobCreator(new LockScreenJobCreator());   

       //TODO: remaing code
    }

}

Don't forget to define application class in your AndroidManifest.xml

After all this I start Job scheduler from my Activity like this:

import android.support.v7.app.AppCompatActivity;

public class LockScreenActivity extends AppCompatActivity {

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

        runJobScheduler();

        //TODO: other code
    }

    @Override
    protected void onStop() {
      super.onStop();

      cancelImmediateJobScheduler();

      //TODO: other code
    }

    /**
     * runJobScheduler(): start immidiate job scheduler and pending job schedulaer from 
       your main Activity.
     */
    private void runJobScheduler() {
        Set<JobRequest> jobSets_I = null, jobSets_P = null;
        try {
            jobSets_I = JobManager.instance().getAllJobRequestsForTag(LockScreenJob.TAG_I);
            jobSets_P = JobManager.instance().getAllJobRequestsForTag(LockScreenJob.TAG_P);

            if (jobSets_I == null || jobSets_I.isEmpty()) {
                LockScreenJob.runJobImmediately();
            }
            if (jobSets_P == null || jobSets_P.isEmpty()) {
                LockScreenJob.scheduleJobPeriodic();
            }

            //Cancel pending job scheduler if mutiple instance are running.
            if (jobSets_P != null && jobSets_P.size() > 2) {
                JobManager.instance().cancelAllForTag(LockScreenJob.TAG_P);
            }
        } catch (Exception e) {
            if (Global_Var.isdebug) {
                e.printStackTrace();
            }
        } finally {
            if (jobSets_I != null) {
                jobSets_I.clear();
            }
            if (jobSets_P != null) {
                jobSets_P.clear();
            }
            jobSets_I = jobSets_P = null;
        }
    }


    /**
     * cancelImmediateJobScheduler: cancel all instance of running job scheduler by their 
      TAG name. 
     */
    private void cancelImmediateJobScheduler() {  
            JobManager.instance().cancelAllForTag(LockScreenJob.TAG_I);
    }
}

By running Job Scheduler like this I am able to listen SCREEN_OFF and SCREEN_ON action without running any foreground or background service. I tested above code on API 26+ and it's working fine.

like image 199
Ankit Kumar Singh Avatar answered Oct 28 '22 06:10

Ankit Kumar Singh