1. 程式人生 > >記一次在廣播(BroadcastReceiver)或服務(Service)裏彈窗的“完美”實踐

記一次在廣播(BroadcastReceiver)或服務(Service)裏彈窗的“完美”實踐

dac target 百度 define key 捕獲 只有一個 show 一個

事情是這樣的,目前在做一個醫療項目,需要定時在某個時間段比如午休時間和晚上讓我們的App休眠,那麽這個時候在休眠時間段如果用戶按了電源鍵點亮屏幕了,我們就需要彈出一個全屏的窗口去做一個人性化的提示,“當前時間是休眠時間,請稍安勿躁...blabla”這樣子。

很顯然,我們需要一個BroadcastReceiver來監聽系統的鎖屏,亮屏,用戶的解鎖,息屏行為,在收到亮屏廣播的時候彈窗。那麽如果是你,會選擇怎麽樣的方式去實現呢?


  兩種方案:

  • Dialog彈窗,全屏
  • 啟動一個Activity

一. Dialog

這裏省去我們項目裏面的代碼,以簡單常用的AlertDialog為例

正常彈出AlertDialog的流程如下:

new AlertDialog.Builder(context).setTitle("在BroadcastReceiver裏彈出AlertDialog").show();

但是其實Dialog似乎只能在activity中彈出,至於為什麽,網上已經有很多相關文章了。這裏我隨手用百度Google了兩篇:

  • 為什麽Dialog不能用Application的Context
  • 創建Dialog所需的上下文為什麽必須是Activity?

為了解決在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"/>

事實上,如果你認真看了我給出的度娘到的兩篇文章,你會發現這並不是一個很好的方案。

  • 方案二

自定義Activity管理者或者說容器吧,通過它來獲取當前界面的Activity作為Dialog的context


技術分享
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打開C1、C3,則C2、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萌新、小白,一個小小的需求實現啰嗦這麽多,打我別打臉_

本文同步自我的個人小屋,歡迎來訪交流

記一次在廣播(BroadcastReceiver)或服務(Service)裏彈窗的“完美”實踐