android自4.0版本,也就是API level 14開始,加入了鎖屏控制的功能,相關的類是RemoteControlClient,這個類在API level 21中被標記爲deprecated,被新的類MediaSession所替代。咱們的音樂App中最開始使用的是原生鎖屏控制API,說實話這個API很差用,遇到了一些小坑,最要命的是不一樣品牌的手機,鎖屏界面長的還不同,就連我本身都沒見過原生4.0的鎖屏控制界面是什麼樣的。國內的手機廠商都自覺得本身的審美很強,設計了千奇百怪的鎖屏控制界面,MIUI更奇怪,MIUI 6是在原生4.4.4的基礎上改的,居然有一段時間都沒有鎖屏控制界面,後來更新纔有。而原生Android在5.0時,將鎖屏和通知欄控制合併,整個邏輯很是混亂。咱們仍是決定像網易雲音樂/QQ音樂那樣,本身作一個鎖屏控制頁面。html
相似網易雲音樂和QQ音樂,通常是註冊一個廣播監聽ACTION_SCREEN_OFF/ACTION_SCREEN_ON操做,而後啓動一個activity。 基本代碼以下:android
private void addScreenChangeBroadCast() {
if(mScreenBroadcastReceiver == null){
mScreenBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
disableSystemLockScreen(context);
Logger.d(TAG, "Intent.ACTION_SCREEN_OFF");
Intent lockscreenIntent = new Intent();
lockscreenIntent.setAction(LOCKSCREEN_ACTION);
lockscreenIntent.setPackage(APP_PACKAGE);
lockscreenIntent.putExtra("INTENT_ACTION", action);
lockscreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(lockscreenIntent);
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
try {
registerReceiver(mScreenBroadcastReceiver, filter);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void removeScreenChangeBroadCast() {
if(mScreenBroadcastReceiver != null) {
try {
unregisterReceiver(mScreenBroadcastReceiver);
} catch (Exception e) {
e.printStackTrace();
}
mScreenBroadcastReceiver = null;
}
}
public static void disableSystemLockScreen(Context context) {
// 下面代碼會出現某些系統home鍵啓動後失效的問題
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
try {
KeyguardManager keyGuardService = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
KeyguardManager.KeyguardLock keyGuardLock = keyGuardService.newKeyguardLock("");
keyGuardLock.disableKeyguard();
} catch (Exception e) {
Logger.e(TAG, "disableSystemLockScreen exception, cause: " + e.getCause()
+ ", message: " + e.getMessage());
}
}
}
複製代碼
Manifest以下:git
<activity
android:name="com.activity.LockScreenActivity"
android:excludeFromRecents="true"
android:exported="false"
android:noHistory="true"
android:showOnLockScreen="true"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:taskAffinity="com.activity.LockScreenActivity"
android:hardwareAccelerated="true"
android:resizeableActivity="false"
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize"
android:theme="@style/LockScreenTheme">
<intent-filter>
<action android:name="com.android.lockscreen" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
複製代碼
Activity在onCreate中須要添加在鎖屏上顯示的flag,在onBackPress不響應Back按鍵:github
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
Logger.d(TAG, getClass().getSimpleName() + ": onCreate");
super.onCreate(savedInstanceState);
Window window = getWindow();
if (window != null) {
window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
fitStatusBar(window);
NavigationUtil.hideNavigationBar(window, true);
}
mSlideView = new SlideView(this, SlideView.TYPE_FRAMELAYOUT, R.color.framework_transparent);
mSlideView.setFullSlideAble(true);
LayoutInflater.from(this).inflate(R.layout.host_act_lockscreen, mSlideView.getContentView(), true);
setContentView(mSlideView);
initUi();
}
@Override
public void onBackPressed() {
// 鎖屏界面固然不響應Back按鍵, 只須要重寫Activity的onBackPressed方法便可
// super.onBackPressed();
}
複製代碼
而後這就開始了個人填坑之路,下面這些經驗不過是給大神取樂,給後來者拋磚引玉,不要在背後罵我就好。bash
以前由於頁面在上層,啓動操做放在sdk層,所以只能經過setAction的啓動方式,以前一直好好的而後到迴歸忽然發現頁面起不來了,通過大牛hook ActivtyThread代碼發現LAUNCH_ACTIVITY確實被調用了,可是頁面確實沒起來講明頁面沒找到。最後經過對intent setPackage進行限制,而後一切都正常了。app
遇到這種問題頭皮都麻了,那就只能百度了。幸虧找到這麼個貼子,搞定。下面說下緣由:ide
KeyguardManager的內部類KeyguardLock,它有兩個方法用來禁用 disableKeyguard
和啓用reenableKeyguard
屏保ui
可是禁用disable方法並非解鎖屏幕,只是把鎖屏功能禁掉了,這也致使了今天要說的這個問題,在某些系統上鎖屏界面仍然存在並且並無解鎖,致使按Home鍵的時候Home的實際功能被鎖屏界面攔截而沒法進入主頁。並且調用完disable這個方法後,除非應用進程被殺死,不然按電源鍵只是黑屏,沒法鎖住屏幕的。this
其次,KeyguardLock對象必須是同一個才能在disable以後從新reenable,因此要使reenable生效的話要把調用disable的對象存起來便於再reenable,並且單純的調用reenable方法是沒有任何做用的,因此你鎖不了其餘程序打開的屏幕,有時候甚至鎖不了本身曾經打開的鎖(對象不是同一個的話)spa
因此說來,這個disableKeyguard——屏蔽屏保的方法仍是不能隨意亂用啊,因此乾脆把這部分代碼去掉,問題就完美解決了!
鎖屏頁面展現後,關閉電源鍵稍等一下,再次打開,頁面會發生閃爍現象。打印了一下LockScreenActivity的生命週期發現activty一遍不落的從onCreate到onDestroy執行了一遍,爲何會發生這種現象,這但是結合了衆多的帖子整出來的代碼,看着網易雲音樂不會出現這種狀況,好吧,那就開始一行行的代碼刪除吧,而後發現字段就出在noHistory上了,noHistory代表activty在用戶不可見的時候即會執行finish,在statck中不留歷史痕跡。通常用於空殼activty作跳轉使用。因此在這個熄屏的過程當中,頁面就這樣被銷燬了。關於noHistory能夠參考這個貼子。
在本身的小米手機上和網易雲音樂作對比,大部分狀況都是網易雲先出現,而後本身的鎖屏頁面姍姍來遲,有時還出現出不來的狀況。另外app首次啓動,第一次鎖屏基本是起不來的,網易雲音樂也有這樣的狀況。
首先把界面換成只有TextView結果依舊,而後打印每部分代碼的運行時間,驚奇的發現startActivity每次都要大概3s左右才執行到onCreate,難道系統找個activity這麼慢嗎,結合1的問題,setPackage仍是無效,帖子裏有建議添加android:showWhenLocked="true"
加後發現確實變快了,但下午又變慢了。好吧,既然帖子沒用,那就只能回本溯源,看看startActivity源碼了,而後發現這麼代碼:
boolean checkAppSwitchAllowedLocked(int sourcePid, int sourceUid,
int callingPid, int callingUid, String name) {
if (mAppSwitchesAllowedTime < SystemClock.uptimeMillis()) {
return true;
}
int perm = checkComponentPermission(
android.Manifest.permission.STOP_APP_SWITCHES, sourcePid,
sourceUid, -1, true);
if (perm == PackageManager.PERMISSION_GRANTED) {
return true;
}
// If the actual IPC caller is different from the logical source, then
// also see if they are allowed to control app switches.
if (callingUid != -1 && callingUid != sourceUid) {
perm = checkComponentPermission(
android.Manifest.permission.STOP_APP_SWITCHES, callingPid,
callingUid, -1, true);
if (perm == PackageManager.PERMISSION_GRANTED) {
return true;
}
}
Slog.w(TAG, name + " request from " + sourceUid + " stopped");
return false;
}
複製代碼
@Override
public void stopAppSwitches() {
if (checkCallingPermission(android.Manifest.permission.STOP_APP_SWITCHES)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("viewquires permission "
+ android.Manifest.permission.STOP_APP_SWITCHES);
}
synchronized(this) {
// static final long APP_SWITCH_DELAY_TIME = 5*1000;
// 這裏設置的是5s 也就是在5s內是不容許app切換
mAppSwitchesAllowedTime = SystemClock.uptimeMillis()
+ APP_SWITCH_DELAY_TIME;
mDidAppSwitch = false;
mHandler.removeMessages(DO_PENDING_ACTIVITY_LAUNCHES_MSG);
Message msg = mHandler.obtainMessage(DO_PENDING_ACTIVITY_LAUNCHES_MSG);
mHandler.sendMessageDelayed(msg, APP_SWITCH_DELAY_TIME);
}
}
複製代碼
因此一切都清楚了,像來電顯示、鬧鐘這種系統應用是經過設置android.Manifest.permission.STOP_APP_SWITCHES
權限來響應後臺activity啓動,而普通應用只能耐心的等待了。把網易雲音樂的包反編譯看了下,普通的startActivity加上一堆flag,所有按照網易雲的設置整了個遍仍是那樣,感受多是插件包和生產包的差別緣由吧,由於線上的包感受速度還能夠,多是作過混淆的緣故吧,jekins打了若干次包仍是沒用。最後想到像QQ的通知界面是經過PendingIntent啓動展現的,在本身代碼裏試了下,問題就這樣解決了,繞開了activity使用PendingIntent。
Intent intent = new Intent(context, LockScreenActivity.class);
intent.setPackage(APP_PACKAGE);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_SINGLE_TOP
| Intent.FLAG_FROM_BACKGROUND
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
| Intent.FLAG_ACTIVITY_NO_ANIMATION
| Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
PendingIntent pendingIntent =
PendingIntent.getActivity(context, 0, intent, 0);
Logger.d(TAG, "pendingIntent.send() " + System.currentTimeMillis());
pendingIntent.send();
複製代碼
就這樣我作的鎖屏頁面勝過了網易雲音樂,其中還有兩點心得:
- APP啓動註冊廣播的前後順序會影響頁面展現的前後,仔細想下就知道,System拿着mListeners確定是按照誰先註冊就先通知誰
- 由於SCREEN_ON是後於SCREEN_OFF的,因此若是SCREEN_ON和SCREEN_OFF都啓動鎖屏頁面的話,只能按照SCREEN_ON來計算時間,由於SCREEN_OFF啓動的頁面會hold住,而SCREEN_ON啓動的頁面會隨後hold住同時將以前hold的索引刪除(也就是mPendingActivityLaunches)。若是要想APP鎖屏頁面啓動更快,就不能在SCREEN_ON中啓動activity
- 還有一種討巧的辦法不須要經歷hold過程,能夠參考咕咚app的鎖屏頁面。原理是監聽SCREEN_OFF,而後把主activity移到前臺,這樣的startActivity就不是後臺行爲了,不過這樣的用戶體驗會不好。
在Android Q中,Google這樣解釋到:
Android Q 對應用可啓動 Activity的時間施加了限制。此項行爲變動有助於最大限度地減小對用戶形成的中斷,而且可讓用戶更好地控制其屏幕上顯示的內容。具體而言,在 Android Q 上運行的應用只有在知足如下一個或多個條件時才能啓動 Activity:
該應用具備可見窗口,例如在前臺運行的 Activity。
在前臺運行的另外一個應用會發送屬於該應用的 PendingIntent。示例包括髮送菜單項待定 intent 的自定義標籤頁提供程序。
系統發送屬於該應用的 PendingIntent,例如點按通知。只有應用應啓動界面的待定 intent 才能夠免除。
系統嚮應用發送廣播,例如 SECRET_CODE_ACTION。只有應用應啓動界面的特定廣播才能夠免除。
看到這些,不由仰天長嘆,贏了網易雲音樂又如何,卻輸給了這個時代啊。
好了,就寫這麼多吧,我要去呼吸新鮮空氣了!
留下個人WX,歡迎各位大神點評。
wossoneri.github.io/2018/06/03/…
stackoverflow.com/questions/5…