实现一个Android锁屏App的难点总结

自定义一个漂亮实用的锁屏app,若是能赢得用户的承认,替换系统自带的锁屏,绝对是一个不小的日活入口。这段时间正好总结一下最近调研的Android平台的锁屏app开发中的难点。android

1、前言

锁屏的大概实现原理都很简单。监听系统的亮屏广播,在亮屏的时候展现本身的锁屏界面,用户在锁屏界面上进行一系列的动做才能解锁。有的手机启动锁屏界面的过程会很卡,因此会明显看到亮屏以后锁屏界面的启动有延时,所以也能够选择监听系统灭屏的广播,屏幕关掉的时候就将锁屏界面准备好,直接亮屏展现(灭屏后你的app会比较容易被杀死,这点要注意作保活)。数据库

还须要注意,亮屏和灭屏广播,SCREEN_ON/SCREEN_OFF都是只能动态监听的,因此要另开一个Service来注册,这个Service的自启动和保活也要作好。api

基本的实现细节就很少讲了,这篇文章只会讲遇到的几个难点。安全

2、锁屏实现中的难点

1.屏蔽Home键

既然是锁屏界面,固然只能经过界面上的一些滑动或者输入动做来解开锁屏,不能简单的直接被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(设置后会弹窗让你从新设置,如此反复)

2.悬浮窗的实现方式

因为受Home键没法直接拦截的限制,Activity实现的锁屏会须要绕较多的路。因此有的锁屏应用会使用悬浮窗来实现,悬浮窗可以无视Home键,在按下home键的时候不会退到后台。因此不须要在home键的问题上纠结。悬浮窗统一由WindowManager来管理,具体的实现比较简单,笔者就不赘述了,有个坑要注意,悬浮窗须要声明权限:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

有的手机设置里,默认是不给应用受权悬浮窗使用权的,因此应用里还要考虑引导用户受权悬浮窗使用。

此外,有些应急解锁的场景,好比来电接听,闹铃处理,对于Activity实现的锁屏界面,系统会自动把全部的前台Activity隐藏,让用户直接去处理这些场景。可是悬浮窗会盖住场景,因此遇到这些场景,悬浮窗实现的锁屏界面要本身去处理这些特殊场景的自动解锁。

3.禁用系统锁屏

有了本身的锁屏界面,还须要禁用掉系统的锁屏,以避免形成用户须要解锁两次的局面。
首先咱们须要知道用户是否设置了锁屏,方法以下:
对于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并无把设置系统锁屏的功能放在安全设置中,因此打开安全设置的界面找不到取消系统锁屏的地方,这个在一众锁屏应用中并无作兼容。

3、附加功能中的难点

上面的功能都是直接针对锁屏自己的实现来讲的。锁屏应用除了自己可以有“锁住屏幕”的功能外,还应该有其余一些漂亮又实用的功能,最起码应该是尽可能往系统锁屏的样式上靠拢并发挥,才方便被用户接受。

1.获取通知

新的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放在一个轻量独立的进程中。

2.获取HotSeat区快捷方式

桌面快捷方式分为两类,Desktop区,指随着屏幕滚动的那部分,HotSeat区,指放置于桌面底部不随屏幕滚动的部分。用户自定义的HotSeat区里的快捷方式属于经常使用的应用。若是可以在锁屏界面也添加这部分的快捷启动,会是一个比较友好的功能。这个的主要问题是,怎么获取到HotSeat区的快捷方式呢。

系统快捷方式存储在数据库文件launcher.db中的favorites表中,如图所示:
launcher.db
能够看到有对应的快捷方式的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...

3.获取壁纸

锁屏界面的背景和手机桌面壁纸保持一致,不至于让用户以为突兀,这里有两种办法实现获取壁纸。

Activity Style模式

若是是Activity实现的锁屏界面,能够直接设置Activity的theme就能够用壁纸作背景了。

android:theme="@android:style/Theme.Wallpaper.NoTitleBar"

WallPaperManager模式

悬浮窗模式的锁屏界面没法用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内存实例中的,没法获取。若是真要切的话,建议直接按照屏幕宽高切下整张壁纸的左边一屏就行了。

相关文章
相关标签/搜索