记一次在广播(BroadcastReceiver)或服务(Service)里弹窗的“完美”实践

事情是这样的,目前在作一个医疗项目,须要定时在某个时间段好比午休时间和晚上让咱们的App休眠,那么这个时候在休眠时间段若是用户按了电源键点亮屏幕了,咱们就须要弹出一个全屏的窗口去作一我的性化的提示,“当前时间是休眠时间,请稍安勿躁...blabla”这样子。java

很显然,咱们须要一个BroadcastReceiver来监听系统的锁屏,亮屏,用户的解锁,息屏行为,在收到亮屏广播的时候弹窗。那么若是是你,会选择怎么样的方式去实现呢?android


  两种方案:api

  • Dialog弹窗,全屏
  • 启动一个Activity

一. Dialog

这里省去咱们项目里面的代码,以简单经常使用的AlertDialog为例app

正常弹出AlertDialog的流程以下:

new AlertDialog.Builder(context).setTitle("在BroadcastReceiver里弹出AlertDialog").show(); 

可是其实Dialog彷佛只能在activity中弹出,至于为何,网上已经有不少相关文章了。这里我随手用百度Google了两篇:ide

为了解决在BroadcastReceiver里弹出AlertDialog这个问题,咱们能够这样作:函数

  • 方案一

将Dialog的窗口类型设置为TYPE_SYSTEM_ALERT工具

AlertDialog alertDialog=new AlertDialog.Builder(context).create(); alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); alertDialog.show(); 

须要注意的是,最后还要在androidManifest.xml文件中加入如下两句话:学习

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW"/> 

事实上,若是你认真看了我给出的度娘到的两篇文章,你会发现这并非一个很好的方案。ui

  • 方案二

自定义Activity管理者或者说容器吧,经过它来获取当前界面的Activity做为Dialog的contextthis



public class MyActivityManager { private static MyActivityManager sInstance = new MyActivityManager(); private WeakReference<Activity> sCurrentActivityWeakRef; private List<Activity> activityList = new LinkedList<Activity>(); private MyActivityManager() { } public synchronized static MyActivityManager getInstance() { return sInstance; } public Activity getCurrentActivity() { Activity currentActivity = null; if (sCurrentActivityWeakRef != null) { currentActivity = sCurrentActivityWeakRef.get(); } return currentActivity; } public void setCurrentActivity(Activity activity) { sCurrentActivityWeakRef = new WeakReference<>(activity); } // add Activity public void addActivity(Activity activity) { if (!activityList.contains(activity)) activityList.add(activity); } // remove Activity public void removeActivity(Activity activity) { if (activityList.contains(activity)) activityList.remove(activity); } public void exitToHome() { try { for (Activity activity:activityList) { if (activity != null) { String className = activity.getClass().getSimpleName(); if (!className.equals("HomeActivity")) activity.finish(); } } } catch (Exception e) { e.printStackTrace(); } finally { } } //关闭每个list内的activity public void finishActivityList() { for (Activity activity : activityList) { activity.finish(); } } } 

在你的application里面

registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { MyActivityManager.getInstance().addActivity(activity); } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { MyActivityManager.getInstance().setCurrentActivity(activity); } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { MyActivityManager.getInstance().removeActivity(activity); } }); 

如写的鄙陋还请见谅, 固然了相似的工具类在网上也有不少。这里顺便再提一下

给dialog设置全屏的最简单的方法 ,在构造函数中
super(context,android.R.style.Theme);
setOwnerActivity((Activity)context);
若是该Dialog设置了自定义style,则在其初始化完view后,设置layout宽高
getWindow().setLayout(屏幕宽,屏幕高);

二. Activity

直接上代码:

Intent intent=new Intent(context,AnotherActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); 

注意必定要给Intent设置一个flag:FLAG_ACTIVITY_NEW_TASK,不写的话会抛异常:

* 可捕获异常信息:
* android.util.AndroidRuntimeException: 
* Calling startActivity() from outside of an Activity context   requires the FLAG_ACTIVITY_NEW_TASK flag. 
* Is this really what you want?

Why ?

* 1 在普通状况下,必需要有前一个Activity的Context,才能启动后一个Activity
 * 2 可是在BroadcastReceiver里面是没有Activity的Context的
 * 3 对于startActivity()方法,源码中有这么一段描述:
 *   Note that if this method is being called from outside of an
 *   {@link android.app.Activity} Context, then the Intent must include
 *   the {@link Intent#FLAG_ACTIVITY_NEW_TASK} launch flag.  This is because,
 *   without being started from an existing Activity, there is no existing
 *   task in which to place the new activity and thus it needs to be placed
 *   in its own separate task.
 *   说白了就是若是不加这个flag就没有一个Task来存放新启动的Activity.
 *   
 * 4 其实该flag和设置Activity的LaunchMode为SingleTask的效果是同样的
 * 
 * 
 * 若有更加深刻的理解,请指点,多谢^_^

最后

我在项目里采用的是启动Activity的方法,just for easy ,比较符合需求场景,不用考虑全屏,Activity只作提示做用 基本没有什么代码

class DormancyReminderActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_dormancy_reminder) EventBus.getDefault().register(this) time.text = intent.getStringExtra("reminder") @Subscribe fun onScreenOnEvent(event: ScreenOnEvent) { Logger.d("get onScreenOnEvent") finish() } override fun onDestroy() { super.onDestroy() EventBus.getDefault().unregister(this) } override fun onBackPressed() { } } 

屏蔽返回键事件,EventBus注册接收到亮屏事件,在亮屏时finish,没啥好说的。值得注意的是考虑到在休眠的时候,用户按电源键 解锁,息屏的时候,会不断建立Activity加入到栈中,因此要在AndroidManifest文件中给Activity的启动模式设为singleInstance

<activity 
  android:name="com.hykd.model.compate.DormancyReminderActivity"
  android:launchMode="singleInstance"/>

鉴于我是一个Android萌新,这里又要回顾一下Activity的四种启动模式了,大神请略过_
容我简单说一下它们的使用场景:

Activity启动方式有四种,分别是:

  • standard
  • singleTop
  • singleTask
  • singleInstance

能够根据实际的需求为Activity设置对应的启动模式,从而能够避免建立大量重复的Activity等问题。

设置Activity的启动模式,只须要在AndroidManifest.xml里对应的<activity>标签设置android:launchMode属性,例如:
<activity
android:name=".A1"
android:launchMode="standard" />

下面是这四种模式的做用:

  • standard

默认模式,能够不用写配置。在这个模式下,都会默认建立一个新的实例。所以,在这种模式下,能够有多个相同的实例,也容许多个相同Activity叠加。

例如:
若我有一个Activity名为A1, 上面有一个按钮可跳转到A1。那么若是我点击按钮,便会新启一个Activity A1叠在刚才的A1之上,再点击,又会再新启一个在它之上……
点back键会依照栈顺序依次退出。

  • singleTop

能够有多个实例,可是不容许多个相同Activity叠加。即,若是Activity在栈顶的时候,启动相同的Activity,不会建立新的实例,而会调用其onNewIntent方法。

例如:
若我有两个Activity名为B1,B2,两个Activity内容功能彻底相同,都有两个按钮能够跳到B1或者B2,惟一不一样的是B1为standard,B2为singleTop。
若我意图打开的顺序为B1->B2->B2,则实际打开的顺序为B1->B2(后一次意图打开B2,实际只调用了前一个的onNewIntent方法)
若我意图打开的顺序为B1->B2->B1->B2,则实际打开的顺序与意图的一致,为B1->B2->B1->B2。

  • singleTask

只有一个实例。在同一个应用程序中启动他的时候,若Activity不存在,则会在当前task建立一个新的实例,若存在,则会把task中在其之上的其它Activity destory掉并调用它的onNewIntent方法。
若是是在别的应用程序中启动它,则会新建一个task,并在该task中启动这个Activity,singleTask容许别的Activity与其在一个task中共存,也就是说,若是我在这个singleTask的实例中再打开新的Activity,这个新的Activity仍是会在singleTask的实例的task中。

例如:
若个人应用程序中有三个Activity,C1,C2,C3,三个Activity可互相启动,其中C2为singleTask模式,那么,不管我在这个程序中如何点击启动,如:C1->C2->C3->C2->C3->C1-C2,C1,C3可能存在多个实例,可是C2只会存在一个,而且这三个Activity都在同一个task里面。
可是C1->C2->C3->C2->C3->C1-C2,这样的操做过程实际应该是以下这样的,由于singleTask会把task中在其之上的其它Activity destory掉。
操做:C1->C2 C1->C2->C3 C1->C2->C3->C2 C1->C2->C3->C2->C3->C1 C1->C2->C3->C2->C3->C1-C2
实际:C1->C2 C1->C2->C3 C1->C2 C1->C2->C3->C1 C1->C2

如果别的应用程序打开C2,则会新启一个task。
如别的应用Other中有一个activity,taskId为200,从它打开C2,则C2的taskIdI不会为200,例如C2的taskId为201,那么再从C2打开C一、C3,则C二、C3的taskId仍为201。
注意:若是此时你点击home,而后再打开Other,发现这时显示的确定会是Other应用中的内容,而不会是咱们应用中的C1 C2 C3中的其中一个。

  • singleInstance

只有一个实例,而且这个实例独立运行在一个task中,这个task只有这个实例,不容许有别的Activity存在。

例如:
程序有三个ActivityD1,D2,D3,三个Activity可互相启动,其中D2为singleInstance模式。那么程序从D1开始运行,假设D1的taskId为200,那么从D1启动D2时,D2会新启动一个task,即D2与D1不在一个task中运行。假设D2的taskId为201,再从D2启动D3时,D3的taskId为200,也就是说它被压到了D1启动的任务栈中。

如果在别的应用程序打开D2,假设Other的taskId为200,打开D2,D2会新建一个task运行,假设它的taskId为201,那么若是这时再从D2启动D1或者D3,则又会再建立一个task,所以,若操做步骤为other->D2->D1,这过程就涉及到了3个task了。

插曲

至此本次需求就已经完美实现了,细心的你可能发现了个人标题完美是打引号的,那么又有怎样的插曲呢 哎😔

由于今天是我学习kotlin的第一天,也是第一次尝试,当我加载Activity界面的时候,打出onCreate随手回车,系统自动给我提供了这么一个onCreate():

override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { super.onCreate(savedInstanceState, persistentState) } 

Java代码:

@Override public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) { super.onCreate(savedInstanceState, persistentState); } 

然而我这小白并无发现,致使个人休眠提醒界面,setContentView以后却始终显示一片白,找遍一切可能出错的地方,属实浪费很多时间,最后在这个onCreate方法上面发现了猫腻(在这个onCreate方法里写了一个输出,发现根本没走这个方法!!!)。

第一反应,我并不认识这是一个什么玩意。打开陈旧的api文档,也没有发现PersistableBundle这个类,因而只能求助百度,Google。原来是Api21新加的特性,上一下google,找一下最新api。咱们先来看一下PersistableBundle是什么东西。

A mapping from String values to various types that can be saved to persistent and later restored.

显然,这是一个和Bundle差很少的东西,Bundle咱们就比较熟悉了。他两都是一个键值对,前者多了这么一段话,can be saved to persistent and later restored,能够持久化保存而且能够恢复。咱们再看一下新的onCreate()方法的源码。

/** * Same as {@link #onCreate(android.os.Bundle)} but called for those activities created with * the attribute {@link android.R.attr#persistableMode} set to * <code>persistAcrossReboots</code>. * * @param savedInstanceState if the activity is being re-initialized after * previously being shut down then this Bundle contains the data it most * recently supplied in {@link #onSaveInstanceState}. * <b><i>Note: Otherwise it is null.</i></b> * @param persistentState if the activity is being re-initialized after * previously being shut down or powered off then this Bundle contains the data it most * recently supplied to outPersistentState in {@link #onSaveInstanceState}. * <b><i>Note: Otherwise it is null.</i></b> public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { onCreate(savedInstanceState); } 

从源码中能够看到,依然是调用了原始的onCreate()方法,结合如下两个方法,

@Override public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { super.onSaveInstanceState(outState, outPersistentState); } @Override public void onRestoreInstanceState(Bundle savedInstanceState, PersistableBundle persistentState) { super.onRestoreInstanceState(savedInstanceState, persistentState); } 

最后记得在配置文件中注册当前Activity的时候加上这个属性,android:persistableMode="persistAcrossReboots",这样就能够给你的Activity存储一些持久化数据。当你的手机重启或者发生其余意外状况的时候,也能够给你的页面获取到相关数据。

结尾

再次请求原谅我是一只Android萌新、小白,一个小小的需求实现啰嗦这么多,打我别打脸_

 

本文同步自个人我的小屋,欢迎来访交流

相关文章
相关标签/搜索