Android Oreo上用于小部件的屏幕开/关广播监听器

17

我有一个时钟小部件的安卓应用程序,现在需要更新到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也不能帮助我,因为我没有发现可以在屏幕打开时调度作业的方式。

我尝试在AppWidgetProvider类中创建一个BroadcastReceiver,并在AppWidgetProvideronUpdate方法中注册它来接收这些系统广播。这很有效,并且确实可以接收广播,但只有在屏幕关闭了一段时间之内才有效; 之后,似乎该应用程序被系统终止了,或者以其他方式停止工作,并且没有报告任何错误或崩溃;但是,如果我单击它,它仍然会像平常一样打开配置活动。

我的问题:

  1. 如果我不想运行前台服务,如何正确监听API 26+上的屏幕开/关广播?

  2. 是否可能通过在AppWidgetProvider类本身中注册一个BroadcastReceiver或甚至将AppWidgetProvider本身注册为接收系统事件来监听系统广播(无论如何,AppWidgetProviderBroadcastReceiver的扩展)?

  3. 为什么我的AppWidgetProvider在经过一段时间后似乎停止接收广播的系统意图?

编辑:

我在Android的registerReceiver方法文档中找到了下面的内容,这似乎是我问题2和3的答案。

注意:此方法不能从BroadcastReceiver组件调用;也就是说,不能从声明在应用程序清单中的BroadcastReceiver中调用。但是,可以从另一个已经使用registerReceiver(BroadcastReceiver、IntentFilter)在运行时注册的BroadcastReceiver中调用此方法,因为这种注册的BroadcastReceiver的生命周期与注册它的对象相关联。

我得出结论,我的在AppWidgetProvider内部使用和注册BroadcastReceiver是与此规范相反的。

我将让这篇文章保持开放,因为其他人可能会发现这些信息有用,而我的问题1仍然有效。


你找到了问题1的解决方案了吗?即在API 26+中监听屏幕开/关广播。 - Ankit Kumar Singh
1
@Ankit Kumar Singh 不,我发现唯一的方法是使用前台服务并向用户发送通知。作为用户界面设计,这对我来说是不可接受的,因此我的应用程序仍然针对API 25进行目标设置。至少在11月1日之前是这样。 - Elefterios Papalimani
我有类似的问题。我用AlarmManager“解决”了它(感觉非常hacky),它每分钟触发一次(仅在设备未进入待机状态时触发)。在接收器中,我使用PowerManager检查屏幕是否亮着,如果是,则执行逻辑。否则,重新安排闹钟(我注意到,如果我将其一次性地安排为重复的,则不可靠),它可以工作,但小部件刷新可能会在最坏的情况下延迟1分钟后用户解锁屏幕。 - c0dehunter
1
@Primož Kralj 谢谢。这对某些目的可能有效,但对于时钟来说最糟糕的事情就是显示错误的时间,即在第一分钟内。我尝试了类似但可能更好的方法:在时钟滴答声(或您的闹钟)上,我使用PowerManager检查屏幕是否开启,然后如果它关闭,我会启动一个前台服务,并注册监听SCREEN_ON广播的状态栏通知。当服务接收到它时,它启动时钟并退出。这样,您只在屏幕关闭时获得丑陋的通知,但仍然可以在唤醒时注意到它。这可能是某人的解决方案。 - Elefterios Papalimani
@ElefteriosPapalimani,请查看我的答案,它可能解决了你的问题/第一问。 - Ankit Kumar Singh
1个回答

8
这里是我在Android API 26(Oreo)及以上版本中监听SCREEN_OFF和SCREEN_ON广播所做的事情。这个答案与小部件无关,但可能有助于找到一些解决方法。
我正在使用作业调度器来注册和取消注册广播接收器,以便监听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'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);
    }
}

我的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;
        }
    }
}

广播接收器类 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 中定义应用程序类。
之后,我会像这样从我的 Activity 中启动 Job 调度器:
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);
    }
}

通过这样运行作业调度器,我可以在不运行任何前台或后台服务的情况下监听SCREEN_OFF和SCREEN_ON操作。我在API 26+上测试了上述代码,它可以正常工作。

1
谢谢,我已经完成了类似的黑客(注册一个“BroadcastReceiver”,然后在我的时钟跳动时刷新其注册),它似乎也起作用了。但是我发现魔鬼在细节中。在我的测试中,被动的“BroadcastReceiver”经常会在深度睡眠时被垃圾回收,因此后来无法检测到SCREEN_ON。我建议你也在你的解决方案中对该部分进行测试。服务非常方便,因为它可以可靠地持久存在于内存中。 - Elefterios Papalimani
1
此外,如果应用程序因低内存、备份操作或崩溃而在后台中止,服务始终会由系统重新启动;迄今为止,小部件没有其他可靠创建的入口点。 - Elefterios Papalimani
当应用程序被杀死时,onReceive没有被调用。我使用了你的方法。 - Ananth
@AnanthRajSingh 是的,如果应用程序由于任何原因强制停止,则广播接收器也会停止,我没有找到任何解决此问题的方法。目前,您/用户必须重新启动应用程序。 - Ankit Kumar Singh
我认为@Ananth也提到了同样的事情,当应用程序处于空闲状态(在关闭屏幕约5分钟后),它不会捕获任何东西。 - Mustafa Güven

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接