欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-03-01 12:33:16
自定义一个漂亮实用的锁屏app,如果能赢得用户的认可,替换系统自带的锁屏,绝对是一个不小的日活入口。这段时间正好总结一下最近调研的android平台的锁屏app开发中的难点...

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

一、前言

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

还需要注意,亮屏和灭屏广播,screen_on/screen_off都是只能动态监听的,所以要另开一个service来注册,这个service的自启动和保活也要做好。

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

二、锁屏实现中的难点

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"。

这当然不是最终方案,因为有些三星rom里并不会有这个广播。而且广播的意思只是通知你一下,人家framework层已经把你的应用退回桌面了,你能监听home键,但没有办法拦截home键。也许想到了可以监听到home键的时候,马上把自己的activity又重新打开展示,我试了一下,home键按下后startactivity会有延时3秒左右,这应该是google早就想到了我们会这么干,做了这么一个延时方案。

直接拦截行不通了,想想别的路子。按home键是让系统退回到launcher(即桌面启动器),那么如果我们的锁屏activity本身就是launcher的话,那按home键不就等于回到我们的锁屏activity,也就可以阻止它把锁屏activity关掉了。

怎么把自己的activity声明为launcher,在activity中添加intent-filter:

<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键就被我们接管了。

不过这样有一个很明显的问题,如果不在我们的锁屏界面按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并没有把设置系统锁屏的功能放在安全设置中,所以打开安全设置的界面找不到取消系统锁屏的地方,这个在一众锁屏应用中并没有做兼容。

三、附加功能中的难点

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

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表中,如图所示:实现一个Android锁屏App功能的难点总结
可以看到有对应的快捷方式的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 的碎片化是多么的严重,所以最后笔者决定不再深入去兼容实现,这是得不偿失的行为,有兴趣实现的可以看看这篇文章,判断一个快捷方式是否存在是多么的难:...

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内存实例中的,无法获取。如果真要切的话,建议直接按照屏幕宽高切下整张壁纸的左边一屏就好了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。