自定義一個漂亮實用的鎖屏app,若是能贏得用戶的承認,替換系統自帶的鎖屏,絕對是一個不小的日活入口。這段時間正好總結一下最近調研的Android平臺的鎖屏app開發中的難點。android
鎖屏的大概實現原理都很簡單。監聽系統的亮屏廣播,在亮屏的時候展現本身的鎖屏界面,用戶在鎖屏界面上進行一系列的動做才能解鎖。有的手機啓動鎖屏界面的過程會很卡,因此會明顯看到亮屏以後鎖屏界面的啓動有延時,所以也能夠選擇監聽系統滅屏的廣播,屏幕關掉的時候就將鎖屏界面準備好,直接亮屏展現(滅屏後你的app會比較容易被殺死,這點要注意作保活)。數據庫
還須要注意,亮屏和滅屏廣播,SCREEN_ON/SCREEN_OFF都是隻能動態監聽的,因此要另開一個Service來註冊,這個Service的自啓動和保活也要作好。api
基本的實現細節就很少講了,這篇文章只會講遇到的幾個難點。安全
既然是鎖屏界面,固然只能經過界面上的一些滑動或者輸入動做來解開鎖屏,不能簡單的直接被Home鍵一按,就解開了。從4.0開始,Home直接在framework層就被系統響應到,強退到桌面,第三方應用裏已經沒法再經過Activity.onKeyDown方法來監聽和攔截Home鍵,儘管還象徵性的保留了Home鍵的KeyCode來向前兼容,可是Home鍵按下去,並不會回調這個方法。併發
除了onKeyDown,有沒有其餘辦法監聽Home鍵,有的。前臺App退到後臺會有廣播ACTION_CLOSE_SYSTEM_DIALOGS,收到廣播攜帶的intent以後,解析裏面的"reason"參數,就能夠知道退出緣由是什麼了。home鍵按下後,reason是"homekey",最近任務鍵按下後,reason是"recentapps"。app
這固然不是最終方案,由於有些三星ROM裏並不會有這個廣播。並且廣播的意思只是通知你一下,人家framework層已經把你的應用退回桌面了,你能監聽home鍵,但沒有辦法攔截home鍵。也許想到了能夠監聽到home鍵的時候,立刻把本身的Activity又從新打開展現,我試了一下,home鍵按下後startActivity會有延時3秒左右,這應該是Google早就想到了咱們會這麼幹,作了這麼一個延時方案。ide
直接攔截行不通了,想一想別的路子。按Home鍵是讓系統退回到Launcher(即桌面啓動器),那麼若是咱們的鎖屏Activity自己就是Launcher的話,那按Home鍵不就等於回到咱們的鎖屏Activity,也就能夠阻止它把鎖屏Activity關掉了。ui
怎麼把本身的Activity聲明爲Launcher,在Activity中添加intent-filter:this
<intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.HOME" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter>
這樣,新安裝的app會是一個可以做爲launcher的app,因此首次按Home鍵的時候,就會有彈窗提示你選擇要進入哪一個launcher,選擇咱們本身的Activity,這樣home鍵就被咱們接管了。spa
不過這樣有一個很明顯的問題,若是不在咱們的鎖屏界面按Home鍵,一樣會進入到鎖屏Activity。固然,解決的方式也簡單,當咱們按Home時進入鎖屏Activity的onCreate裏作一個判斷,若是前一個前臺Activity是鎖屏Activity,那就不用對Home鍵處理,若是不是鎖屏Activity,那就要關閉鎖屏Activity,跳到用戶真正的桌面啓動器去了。真正的桌面啓動器是哪個,咱們能夠這樣來找:
List<String> pkgNamesT = new ArrayList<String>(); List<String> actNamesT = new ArrayList<String>(); List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); for (int i = 0; i < resolveInfos.size(); i++) { String string = resolveInfos.get(i).activityInfo.packageName; if (!string.equals(context.getPackageName())) {//本身的launcher不要 pkgNamesT.add(string); string = resolveInfos.get(i).activityInfo.name; actNamesT.add(string); } }
若是實際的launcher只有一個,那直接跳轉過去就能夠了:
ComponentName componentName = new ComponentName(pkgName, actName); Intent intent = new Intent(); intent.setComponent(componentName); context.startActivity(intent); ((Activity) context).finish();
若是手機安裝有多個launcher(如360桌面一類的app)就會麻煩一點,須要展現一個列表讓用戶來選取用哪一個launcher,這個在產品形態上可能會讓用戶以爲有點不解。
如今,若是在其餘APP裏按一下Home鍵,會跳到咱們的鎖屏Activity而後跳轉到真正的launcher。這裏可能會有Activity閃現一下的場景,影響用戶體驗。最優的辦法實際上是另外弄一個Activity來做爲Home鍵跳轉的Activity,這個Activity設爲透明的,就不會被用戶感知。如此,產品形態就變成了,鎖屏Activity中按Home鍵,跳轉到透明Activity,跳轉回鎖屏Activity,至關於Home鍵無效;其餘APP中按Home鍵,跳轉到透明Activity,跳轉到真正的桌面。
實現透明的Activity,只須要在xml中聲明
android:theme="@android:style/Theme.Translucent.NoTitleBar"
這樣的界面是透明的,實際上有佔位在屏幕的頂層,因此跳轉後記得要finish掉,否則會阻斷跳轉後的界面的交互。
另外,Theme.NoDisplay也能將Activity設置爲不可見,並且不佔位,可是筆者實現的時候發現,NoDisplay的Activity沒法被系統設置爲launcher(設置後會彈窗讓你從新設置,如此反覆)
因爲受Home鍵沒法直接攔截的限制,Activity實現的鎖屏會須要繞較多的路。因此有的鎖屏應用會使用懸浮窗來實現,懸浮窗可以無視Home鍵,在按下home鍵的時候不會退到後臺。因此不須要在home鍵的問題上糾結。懸浮窗統一由WindowManager來管理,具體的實現比較簡單,筆者就不贅述了,有個坑要注意,懸浮窗須要聲明權限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
有的手機設置裏,默認是不給應用受權懸浮窗使用權的,因此應用裏還要考慮引導用戶受權懸浮窗使用。
此外,有些應急解鎖的場景,好比來電接聽,鬧鈴處理,對於Activity實現的鎖屏界面,系統會自動把全部的前臺Activity隱藏,讓用戶直接去處理這些場景。可是懸浮窗會蓋住場景,因此遇到這些場景,懸浮窗實現的鎖屏界面要本身去處理這些特殊場景的自動解鎖。
有了本身的鎖屏界面,還須要禁用掉系統的鎖屏,以避免形成用戶須要解鎖兩次的局面。
首先咱們須要知道用戶是否設置了鎖屏,方法以下:
對於API Level 16及以上SDK,可使用以下方法判斷是否有鎖:
((KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE)).isKeyguardSecure()
對API Level 15及如下SDK,可使用反射來判斷:
try { Class<?> clazz = Class.forName("com.android.internal.widget.LockPatternUtils"); Constructor<?> constructor = clazz.getConstructor(Context.class); constructor.setAccessible(true); Object utils = constructor.newInstance(this); Method method = clazz.getMethod("isSecure"); return (Boolean) method.invoke(utils); }catch (Exception e){ e.printStackTrace(); }
好了,得知用戶設置了系統鎖屏,怎麼關掉呢?有前人建議了這種方法
KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); KeyguardManager.KeyguardLock keyguardLock = km.newKeyguardLock(""); keyguardLock.disableKeyguard();
須要權限
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
但經筆者測驗,這種方法只能禁用滑動鎖,若是用戶設置的是圖案或者PIN的鎖的話,是沒法直接取消的。
禁用掉密碼鎖或者圖案鎖是一個很危險的行爲,基於此,Google應該是不會把它開放給開發者的,因此如今的鎖屏應用的禁用鎖的辦法,都是直接跳到系統鎖屏設置界面,直接引導用戶去手動關閉。能夠經過以下代碼跳到用戶鎖屏設置界面:
Intent in = new Intent(Settings.ACTION_SECURITY_SETTINGS); startActivity(in);
這個也會有些許的兼容性問題,好比,360手機的ROM並無把設置系統鎖屏的功能放在安全設置中,因此打開安全設置的界面找不到取消系統鎖屏的地方,這個在一衆鎖屏應用中並無作兼容。
上面的功能都是直接針對鎖屏自己的實現來講的。鎖屏應用除了自己可以有「鎖住屏幕」的功能外,還應該有其餘一些漂亮又實用的功能,最起碼應該是儘可能往系統鎖屏的樣式上靠攏併發揮,才方便被用戶接受。
新的Notification到來時應該展現在鎖屏界面上,因此咱們須要對通知欄進行監聽。
從Android 4.3(api 18)開始,Google給咱們提供了一個NotificationListenerService類,第三方應用能夠更方便的得到通知欄使用權(Notification Access),固然,這麼敏感的權限得要應用本身聲明,同時還要引導用戶手動受權。以下,創建一個NotificationMonitor類繼承於NotificationListenerService,並聲明權限:
<service android:name=".NotificationMonitor" android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> <intent-filter> <action android:name="android.service.notification.NotificationListenerService" /> </intent-filter> </service>
而後同引導用戶關閉系統鎖屏同樣,要引導用戶來受權通知欄使用權:
startActivity(new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS));
能夠經過以下代碼檢查到通知欄使用權是否已經拿到:
private boolean isNotificationListenEnabled(){ String pkgName = getPackageName(); final String flat = Settings.Secure.getString(getContentResolver(), "enabled_notification_listeners"); if (!TextUtils.isEmpty(flat)) { final String[] names = flat.split(":"); for (int i = 0; i < names.length; i++) { final ComponentName cn = ComponentName.unflattenFromString(names[i]); if (cn != null) { if (TextUtils.equals(pkgName, cn.getPackageName())) { return true; } } } } return false; }
拿到通知欄使用權後,系統通知欄的變化就能夠在NotificationMonitor裏面監聽到了:
public class NotificationMonitor extends NotificationListenerService { @Override public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { return super.onStartCommand(intent,flags,startId); } //新的Notification到達 @Override public void onNotificationPosted(StatusBarNotification sbn) { super.onNotificationPosted(sbn); } //新的Notification到達,api 21新增 @Override public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { super.onNotificationPosted(sbn, rankingMap); } //Notification被移除 @Override public void onNotificationRemoved(StatusBarNotification sbn) { super.onNotificationRemoved(sbn); } //Notification被移除,api 21新增 @Override public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) { super.onNotificationRemoved(sbn, rankingMap); } //Notification排序變更,api 21新增 @Override public void onNotificationRankingUpdate(RankingMap rankingMap) { super.onNotificationRankingUpdate(rankingMap); } //Service與系統通知欄完成綁定時回調,綁定後才能收到通知欄回調,api 21新增 @Override public void onListenerConnected() { super.onListenerConnected(); } }
同時,NotificationListenerService還提供了cancelNotification和cancelAllNotification方法,用於移除通知欄的通知,能夠很方便的實如今自定義的鎖屏界面移除掉通知了。
筆者實現這個類的時候發現了一個坑,全部的代碼都是OK的,通知欄使用權也受權了,可是來通知時始終沒有回調onNotificationPosted,查問題查了好久,後來看到網上有人遇到一樣的問題,另外新建了一個類把代碼複製過去,就OK了,這樣看來應該是編譯器的問題。
獲取了通知欄使用權的Service自然就能被保活,若是被殺死,Android系統可以將它重啓。因此平時看到一些應用要求獲取通知欄使用權時,要注意這類應用會永久駐存後臺的。固然,若是這個Service所在進程崩潰達到必定次數的話,Android系統也會灰心,在下次關機重啓前不會再將Service重啓,因此,開發中最好能將這個Service放在一個輕量獨立的進程中。
桌面快捷方式分爲兩類,Desktop區,指隨着屏幕滾動的那部分,HotSeat區,指放置於桌面底部不隨屏幕滾動的部分。用戶自定義的HotSeat區裏的快捷方式屬於經常使用的應用。若是可以在鎖屏界面也添加這部分的快捷啓動,會是一個比較友好的功能。這個的主要問題是,怎麼獲取到HotSeat區的快捷方式呢。
系統快捷方式存儲在數據庫文件launcher.db中的favorites表中,如圖所示:
能夠看到有對應的快捷方式的id,title和intent,這個container屬性是用來指示所在文件夾的id,然而能夠看到有的container爲負數。這是爲何,筆者查看了一下Android Launcher相關的源碼,找到這麼兩句:
/** * The icon is a resource identified by a package name and an integer id. */ public static final int CONTAINER_DESKTOP = -100; public static final int CONTAINER_HOTSEAT = -101;
也就是說,container爲-100的是Desktop區的快捷方式,container爲-101的正是要找的HotSeat區的快捷方式。
如今知道了快捷方式的存儲方式,接下來的問題就是去找launcher.db文件的路徑。
在不一樣版本的Android原生api中,因爲默認使用的launcher啓動器的包名不同,launcher.db存儲的路徑也不同。
Android API 7及如下:/data/data/com.android.launcher/databases/laucher.db Android API 8~18:/data/data/com.android.launcher2/databases/laucher.db Android API 19及以上:/data/data/com.android.launcher3/databases/laucher.db
而對於各式各樣的第三方ROM,使用了千奇百怪的laucher包名,這個路徑就更亂了:
HTC: /data/data/com.htc.launcher/databases/laucher.db 360: /data/data/net.qihoo360.launcher/databases/laucher.db 華爲: /data/data/com.huawei.launcher3/databases/laucher.db 小米: /data/data/com.miui.mihome2/databases/laucher.db ...
固然,咱們不會經過直接讀取數據庫的方式來獲取快捷方式的信息,系統自帶的laucher會提供ContentProvider給外部讀取。避開了對數據庫路徑作兼容的大坑,轉眼就掉進了另外一個大坑,經過Provider來讀取快捷方式,所須要的權限和URI也須要作兼容。
從快捷方式的存儲可見,Android 的碎片化是多麼的嚴重,因此最後筆者決定再也不深刻去兼容實現,這是得不償失的行爲,有興趣實現的能夠看看這篇文章,判斷一個快捷方式是否存在是多麼的難:http://www.jianshu.com/p/dc3d...
鎖屏界面的背景和手機桌面壁紙保持一致,不至於讓用戶以爲突兀,這裏有兩種辦法實現獲取壁紙。
若是是Activity實現的鎖屏界面,能夠直接設置Activity的theme就能夠用壁紙作背景了。
android:theme="@android:style/Theme.Wallpaper.NoTitleBar"
懸浮窗模式的鎖屏界面沒法用theme,那麼能夠經過WallPaperManager來獲取壁紙。
// 獲取壁紙管理器 WallpaperManager wallpaperManager = WallpaperManager .getInstance(this); // 獲取當前壁紙 Drawable wallpaperDrawable = wallpaperManager.getDrawable(); // 將Drawable,轉成Bitmap Bitmap bm = ((BitmapDrawable) wallpaperDrawable).getBitmap(); mRootView.setBackgroundDrawable(new BitmapDrawable(bm));
這種方式在小米等仿iOS的一屏桌面上是OK的,可是在原生Android那樣的兩屏桌面(快捷方式與所有app分別在不一樣屏),快捷方式那屏獲取的壁紙是一整張大壁紙,而實際laucher顯示的是切割後的壁紙。因此以上方式會把尺寸不符的壁紙設爲了背景。須要本身去根據laucher的屏數和當前是第幾屏來進行切圖,laucher的總屏數能夠在上述launcher.db裏的workspaceScreens表裏找到,而具體當前在第幾屏是存在launcher app內存實例中的,沒法獲取。若是真要切的話,建議直接按照屏幕寬高切下整張壁紙的左邊一屏就行了。