大部分移動應用都是須要使用帳號登陸才能使用的。既然有帳號登陸操做,那麼相應地也有帳號登出操做。帳號登出包含如下兩種狀況:java
第一種狀況實現相對比較簡單,本文暫不涉及。
第二種狀況主要是因爲登陸憑證(AccessToken)過時失效等緣由,致使應用強制登出,而且要求用戶從新登陸。android
在文章開始以前,先看看Android Q的新特性:git
官方資料 - 1:Android Q 隱私權變動:針對後臺 Activity 啓動的限制github
Android Q 對應用可啓動 Activity 的時間施加了限制。此項行爲變動有助於最大限度地減小對用戶形成的中斷,而且可讓用戶更好地控制其屏幕上顯示的內容。具體而言,在 Android Q 上運行的應用只有在知足如下一個或多個條件時才能啓動 Activity:服務器
- 該應用具備可見窗口,例如在前臺運行的 Activity。
- 在前臺運行的另外一個應用會發送屬於該應用的PendingIntent。示例包括髮送菜單項待定 intent 的自定義標籤頁提供程序。
- 系統發送屬於該應用的PendingIntent,例如點按通知。只有應用應啓動界面的待定 intent 才能夠免除。
- 系統嚮應用發送廣播,例如SECRET_CODE_ACTION。只有應用應啓動界面的特定廣播才能夠免除。
注意:出於 Activity 啓動的目的,前臺服務不會將應用限定爲在前臺運行。ide
此項行爲變動適用於在 Android Q 上運行的全部應用,包括以 Android 9(API 級別 28)或更低版本爲目標平臺的應用。此外,即便您的應用以 Android 9 或更低版本爲目標平臺而且最初安裝在運行 Android 9 或更低版本的設備上,該行爲變動仍會在設備升級到 Android Q 後生效。函數
可是,只要您的應用啓動 Activity 是因用戶互動直接引起的,該應用就極有可能不會受到此項變動的影響。實際上,大多數應用都不會受到此項變動的影響。若是您發現本身的應用受到了影響,請向咱們發送反饋。ui
官方資料 - 2:Broadcasts overviewthis
Security considerations and best practices
Here are some security considerations and best practices for sending and receiving broadcasts:
...rest
- Do not start activities from broadcast receivers because the user experience is jarring; especially if there is more than one receiver. Instead, consider displaying a notification.
ActivityLifecycleCallbacks
BroadcastReceiver
PendingIntent
Intent.makeRestartActivityTask(ComponentName)
App
註冊「強制登出」的私有廣播,且將優先級設置爲最低-999;App
註冊ActivityLifecycleCallbacks
,監聽Activity
的生命週期;App#onActivityResumed(Activity)
被調用時,將Activity
註冊「強制登出」的私有廣播。此時,若是有「強制登出」的廣播發送,且應用在前臺運行,那麼則是前臺Activity
先接收到此廣播,而後重啓頁面;應用在後臺運行,那麼此廣播就會被App
接收到,而後下次打開應用時重啓頁面。注意:「強制登出」的廣播必需要定義爲私有廣播,只能在應用內發送接收。
1.YOUR_PACKAGE_NAME
:項目工程的PackageName包名,即AndroidManifest.xml
中的manifest-package值
:
<manifest package="${YOUR_PACKAGE_NAME}">
2.YOUR_LAUNCHER_ACTIVITY
:啓動頁面。須要注意的是重啓界面時,會傳遞EXTRA_FORCE_LOGOUT_INTENT
參數,告知啓動頁面進行必要的清理工做,如移除已登陸帳號信息等;
3.AnonymouslyAccessible
:接口,表示不要求登陸就能夠打開的頁面;
4.isServerSettingsAcquired()
:服務器配置已配置,若是服務器地址不是固定域名,則須要實現此函數;
5.hasAuthenticatedUser()
:是否用戶已登陸。
根據實際項目工程替換掉YOUR_PACKAGE_NAME
、YOUR_LAUNCHER_ACTIVITY
變量,並填充isServerSettingsAcquired()
、hasAuthenticatedUser()
public class App extends Application implements ActivityLifecycleCallbacks { public static String ACTION_FORCE_LOGOUT = "${YOUR_PACKAGE_NAME}.action.FORCE_LOGOUT"; public static String PERMISSION_PRIVATE = "${YOUR_PACKAGE_NAME}.permission.PRIVATE"; @Override public void onCreate() { super.onCreate(); registerActivityLifecycleCallbacks(this); IntentFilter intentFilter = new IntentFilter(ACTION_FORCE_LOGOUT); intentFilter.setPriority(-999); registerReceiver(mReceiver, intentFilter, PERMISSION_PRIVATE, null); } private boolean mWillForceLogout; private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { PendingIntent pendingIntent = getForceLogoutBroadcast(context, false); if (pendingIntent != null) { pendingIntent.cancel(); mWillForceLogout = true; } } }; private static PendingIntent getForceLogoutBroadcast(Context context, boolean createIfNotExist) { Intent intent = new Intent(ACTION_FORCE_LOGOUT); intent.setPackage(context.getPackageName()); int flag = createIfNotExist ? 0 : PendingIntent.FLAG_NO_CREATE; return PendingIntent.getBroadcast(context, 0, intent, flag); } @Override public void onActivityResumed(Activity activity) { if (!(activity instanceof AnonymouslyAccessible)) { IntentFilter intentFilter = new IntentFilter(ACTION_FORCE_LOGOUT); activity.registerReceiver(mActivityReceiver, intentFilter, PERMISSION_PRIVATE, null); } boolean willForceLogout = takeWillForceLogout(); if (willForceLogout || shouldStartLauncherActivity(activity)) { restartActivity(willForceLogout); } } protected void restartActivity(boolean willForceLogout) { ComponentName componentName = new ComponentName(this, ${YOUR_LAUNCHER_ACTIVITY}.class); Intent intent = Intent.makeRestartActivityTask(componentName); if (willForceLogout) { intent.putExtra(${YOUR_LAUNCHER_ACTIVITY}.EXTRA_FORCE_LOGOUT_INTENT, true); } startActivity(intent); } private BroadcastReceiver mActivityReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { restartActivity(true); PendingIntent pendingIntent = getForceLogoutBroadcast(context, false); if (pendingIntent != null) { pendingIntent.cancel(); } } }; private boolean takeWillForceLogout() { boolean willForceLogout = mWillForceLogout; mWillForceLogout = false; return willForceLogout; } private boolean shouldStartLauncherActivity(Activity activity) { if (activity instanceof AnonymouslyAccessible) { return false; } return !isServerSettingsAcquired() || !hasAuthenticatedUser(); } public boolean isServerSettingsAcquired() { // YOUR CODE ? } public boolean hasAuthenticatedUser() { // YOUR CODE ? } @Override public void onActivityPaused(Activity activity) { if (!(activity instanceof AnonymouslyAccessible)) { activity.unregisterReceiver(mActivityReceiver); } } @Override public void onTerminate() { unregisterReceiver(mReceiver); unregisterActivityLifecycleCallbacks(this); super.onTerminate(); } public static void forceLogout(Context context) { try { PendingIntent pendingIntent = getForceLogoutBroadcast(context, false); if (pendingIntent == null) { pendingIntent = getForceLogoutBroadcast(context, true); pendingIntent.send(context, 0, null, null, null, PERMISSION_PRIVATE); } } catch (CanceledException e) { e.printStackTrace(); } } }
<permission android:name="${YOUR_PACKAGE_NAME}.permission.PRIVATE" android:protectionLevel="signature" /> <uses-permission android:name="${YOUR_PACKAGE_NAME}.permission.PRIVATE" />
通常來講,客戶端收到登陸憑證(AccessToken)過時通知有兩種途徑:
客戶端收到上述通知後,調用如下代碼便可:
App.forceLogout(Context)
END. >> SEE MORE: http://erehmi.github.io/