鎖屏界面開發中遇到的各類坑

鎖屏界面開發中遇到的各類坑

背景

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

填坑

1. 經過setAction startActivity 頁面起不來

以前由於頁面在上層,啓動操做放在sdk層,所以只能經過setAction的啓動方式,以前一直好好的而後到迴歸忽然發現頁面起不來了,通過大牛hook ActivtyThread代碼發現LAUNCH_ACTIVITY確實被調用了,可是頁面確實沒起來講明頁面沒找到。最後經過對intent setPackage進行限制,而後一切都正常了。app

2. 在某些4.x手機上會出現點Home鍵沒法回到主界面的問題

遇到這種問題頭皮都麻了,那就只能百度了。幸虧找到這麼個貼子,搞定。下面說下緣由:ide

KeyguardManager的內部類KeyguardLock,它有兩個方法用來禁用 disableKeyguard和啓用reenableKeyguard屏保ui

可是禁用disable方法並非解鎖屏幕,只是把鎖屏功能禁掉了,這也致使了今天要說的這個問題,在某些系統上鎖屏界面仍然存在並且並無解鎖,致使按Home鍵的時候Home的實際功能被鎖屏界面攔截而沒法進入主頁。並且調用完disable這個方法後,除非應用進程被殺死,不然按電源鍵只是黑屏,沒法鎖住屏幕的。this

其次,KeyguardLock對象必須是同一個才能在disable以後從新reenable,因此要使reenable生效的話要把調用disable的對象存起來便於再reenable,並且單純的調用reenable方法是沒有任何做用的,因此你鎖不了其餘程序打開的屏幕,有時候甚至鎖不了本身曾經打開的鎖(對象不是同一個的話)spa

因此說來,這個disableKeyguard——屏蔽屏保的方法仍是不能隨意亂用啊,因此乾脆把這部分代碼去掉,問題就完美解決了!

3. 鎖屏頁面閃爍問題

鎖屏頁面展現後,關閉電源鍵稍等一下,再次打開,頁面會發生閃爍現象。打印了一下LockScreenActivity的生命週期發現activty一遍不落的從onCreate到onDestroy執行了一遍,爲何會發生這種現象,這但是結合了衆多的帖子整出來的代碼,看着網易雲音樂不會出現這種狀況,好吧,那就開始一行行的代碼刪除吧,而後發現字段就出在noHistory上了,noHistory代表activty在用戶不可見的時候即會執行finish,在statck中不留歷史痕跡。通常用於空殼activty作跳轉使用。因此在這個熄屏的過程當中,頁面就這樣被銷燬了。關於noHistory能夠參考這個貼子

4. 鎖屏加載有時很緩慢

在本身的小米手機上和網易雲音樂作對比,大部分狀況都是網易雲先出現,而後本身的鎖屏頁面姍姍來遲,有時還出現出不來的狀況。另外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();
複製代碼

就這樣我作的鎖屏頁面勝過了網易雲音樂,其中還有兩點心得:

  1. APP啓動註冊廣播的前後順序會影響頁面展現的前後,仔細想下就知道,System拿着mListeners確定是按照誰先註冊就先通知誰
  1. 由於SCREEN_ON是後於SCREEN_OFF的,因此若是SCREEN_ON和SCREEN_OFF都啓動鎖屏頁面的話,只能按照SCREEN_ON來計算時間,由於SCREEN_OFF啓動的頁面會hold住,而SCREEN_ON啓動的頁面會隨後hold住同時將以前hold的索引刪除(也就是mPendingActivityLaunches)。若是要想APP鎖屏頁面啓動更快,就不能在SCREEN_ON中啓動activity
  1. 還有一種討巧的辦法不須要經歷hold過程,能夠參考咕咚app的鎖屏頁面。原理是監聽SCREEN_OFF,而後把主activity移到前臺,這樣的startActivity就不是後臺行爲了,不過這樣的用戶體驗會不好。

5. Android Q 鎖屏適配

在Android Q中,Google這樣解釋到:

Android Q 對應用可啓動 Activity的時間施加了限制。此項行爲變動有助於最大限度地減小對用戶形成的中斷,而且可讓用戶更好地控制其屏幕上顯示的內容。具體而言,在 Android Q 上運行的應用只有在知足如下一個或多個條件時才能啓動 Activity:

該應用具備可見窗口,例如在前臺運行的 Activity。

在前臺運行的另外一個應用會發送屬於該應用的 PendingIntent。示例包括髮送菜單項待定 intent 的自定義標籤頁提供程序。

系統發送屬於該應用的 PendingIntent,例如點按通知。只有應用應啓動界面的待定 intent 才能夠免除。

系統嚮應用發送廣播,例如 SECRET_CODE_ACTION。只有應用應啓動界面的特定廣播才能夠免除。

看到這些,不由仰天長嘆,贏了網易雲音樂又如何,卻輸給了這個時代啊。

結語

好了,就寫這麼多吧,我要去呼吸新鮮空氣了!

留下個人WX,歡迎各位大神點評。

參考資料

gityuan.com/2016/03/12/…

wossoneri.github.io/2018/06/03/…

blog.csdn.net/ixiaobu/art…

stackoverflow.com/questions/5…

developer.android.com/preview/pri…

shoewann0402.github.io/2019/03/16/…

相關文章
相關標籤/搜索