Экранное включение/выключение широковещательного прослушивателя для виджета на Android Oreo

У меня есть приложение Android-часов, которое теперь я пытаюсь обновить до требований API 26.

До сих пор я использовал фоновый сервис, который зарегистрировал при запуске в своем методе onCreate BroadcastReceiver для приема системных трансляций, таких как android.intent.action.SCREEN_ON, android.intent.action.SCREEN_OFF, android.intent.action.TIME_SET, android.intent.action.TIMEZONE_CHANGED. Эта услуга приостанавливала часы во время выключения экрана и пробуждения, когда экран снова включен, чтобы сохранить батарею.

В Oreo такая услуга вроде бы не является вариантом, поскольку она должна была бы работать на переднем плане с уведомлением, которое действительно не имеет значения для пользователя. Кроме того, насколько я видел в документации, JobScheduler не может помочь мне, поскольку я не нашел, что можно запланировать задание, когда экран JobScheduler.

Я попытался создать BroadcastReceiver в классе AppWidgetProvider и зарегистрировать его в AppWidgetProvider onUpdate для получения указанных системных передач. Это хорошо работает, и трансляции действительно получают, но только до тех пор, пока экран не погаснет в течение определенного периода времени; после этого кажется, что приложение каким-то образом убивается системой или иным образом перестает работать без какой-либо сообщенной ошибки или сбоя; однако, если я нажму на него, он откроет активность конфигурации как обычно.

Мои вопросы:

  1. Как правильно прослушивать трансляцию по экрану в API 26+, если я не хочу запускать службу переднего плана?

  2. Возможно ли прослушивание системных передач из самого класса AppWidgetProvider, путем регистрации в нем BroadcastReceiver или даже регистрации самого AppWidgetProvider для получения системных событий (в любом случае AppWidgetProvider является расширением BroadcastReceiver).

  3. Почему мой AppWidgetProvider видимому прекращает получать транслируемые системные намерения после некоторого периода сна?

РЕДАКТИРОВАТЬ:

В документации Android для метода registerReceiver я нашел следующее: это ответ на мои вопросы 2 и 3.

Примечание: этот метод нельзя вызвать из компонента BroadcastReceiver; то есть от BroadcastReceiver, объявленного в манифесте приложения. Однако можно назвать этот метод другим BroadcastReceiver, который сам был зарегистрирован во время выполнения с помощью registerReceiver (BroadcastReceiver, IntentFilter), так как время жизни такого зарегистрированного BroadcastReceiver привязано к объекту, который его зарегистрировал.

Я бы сделал вывод, что мое использование и регистрация BroadcastReceiver внутри AppWidgetProvider противоречили этой спецификации.

Я оставлю это сообщение открытым, потому что другие могут найти эту информацию полезной, и мой вопрос 1 все еще остается в силе.

Ответ 1

Вот что я делаю для прослушивания SCREEN_OFF и SCREEN_ON, транслируемых в Android API 26 (Oreo) и выше. Этот ответ не связан с виджетами, но может быть полезно найти некоторую часть рабочего процесса.

Я использую Планировщик заданий для реестров и реселлеров UnRegister Broadcast, которые прослушивают действия SCREEN_OFF и SCREEN_ON.

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 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 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 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);
    }
}

И мой класс JobCrator LockScreenJobCreator:

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 UnlockReceiver:

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
        }
    }

}

И добавление класса JobCreator в класс Application следующим образом:

public class AksApplication extends Application {

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

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

       //TODO: remaing code
    }

}

Не забудьте определить класс приложения в вашем AndroidManifest.xml

После этого я запускаю Job scheduler из моей Activity следующим образом:

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);
    }
}

Запустив Job Scheduler, я могу прослушивать действия SCREEN_OFF и SCREEN_ON без использования каких-либо функций переднего плана или фона. Я тестировал выше код API 26+, и он работает нормально.