美文网首页Android进阶之路
Android 8.0 进程拉活 --- 蓝牙唤醒

Android 8.0 进程拉活 --- 蓝牙唤醒

作者: cff70524f5cf | 来源:发表于2019-06-05 21:47 被阅读19次

    前言:

    IPhone 可以通过 ibeacon 设备发出的蓝牙广播来唤醒应用,但android有没有类似的机制来进行唤醒app呢?

    很开心的告诉你,在 android 8.0(android 0) 以上的系统已经支持了!!!

    说明:

    在android 8.0 的 API中,蓝牙库中的android.bluetooth.le.BluetoothLeScanner类增加了一个新方法,看下图

    注:api level 26 即 android 8.0

    该方法是用于扫描手机周边的蓝牙设备。在8.0以前,google提供的蓝牙扫描方法都是需要app进程还活。但该方法只要调用成功,无论app进程是否还活着,系统都会在后台持续执行蓝牙扫描。如果手机靠近指定的蓝牙设备附近,app就能被唤醒接收蓝牙的扫描结果。

    用途:

    该机制虽然只能在特定的区域对app进行唤醒,但在很多业务场景上非常实用,举几个栗子:

    1.室内定位
    2.进入商场用户立马能收到商场的活动信息
    3.运动手环需要即时上报数据(如:电量不足等)

    ....

    开发:

    支持该机制的拉活,实际上就是让app去扫描一个特定的蓝牙广播,等待系统返回结果。相信有弄过蓝牙开发的小伙伴都知道蓝牙开发这个坑有多深,而下面是一个跳坑的教程。

    一.权限:

        <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
        <uses-permission android:name="android.permission.BLUETOOTH"/>
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    

    注:ACCESS_FINE_LOCATION在android 6.0以上需要运行获取(见6.0以上运行时权限申请)。

    app除获取了上述权限,还需要确保蓝牙开启,以下是代码开启蓝牙的方法:

    //判断如果蓝牙没有开启的话,则进行提示用户开启
    BluetoothManager bluetoothManager = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
    BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
    if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
         Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
         startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
    

    二.API调用

    @TargetApi(26)
        public void onOpen(View view){
            //BluetoothManager是向蓝牙设备通讯的入口
            BluetoothManager bluetoothManager = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
            BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
    
            //指定需要识别到的蓝牙设备
            List<ScanFilter> scanFilterList = new ArrayList<>();
            ScanFilter.Builder builder = new ScanFilter.Builder();
            builder.setServiceUuid(ParcelUuid.fromString(UUID_SERVICE));
            ScanFilter scanFilter = builder.build();
            scanFilterList.add(scanFilter);
    
            //指定蓝牙的方式,这里设置的ScanSettings.SCAN_MODE_LOW_LATENCY是比较高频率的扫描方式
            ScanSettings.Builder settingBuilder = new ScanSettings.Builder();
            settingBuilder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
            settingBuilder.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE);
            settingBuilder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
            settingBuilder.setLegacy(true);
            ScanSettings settings = settingBuilder.build();
    
            //指定扫描到蓝牙后是以什么方式通知到app端,这里将以可见服务的形式进行启动
            PendingIntent callbackIntent = PendingIntent.getForegroundService(
                    this,
                    1,
                    new Intent("com.hungrytree.receiver.BleService").setPackage(getPackageName()),
                    PendingIntent.FLAG_UPDATE_CURRENT );
    
            //启动蓝牙扫描
            bluetoothAdapter.getBluetoothLeScanner().startScan(scanFilterList,settings,callbackIntent);
        }
    

    这是开启蓝牙扫描的代码,此处不做过多的说明,有想对android蓝牙开发有更深入了解的小伙伴,可以前往这里的传送门。不过需要提醒一下大家要注意两个点:

    1.builder.setServiceUuid(ParcelUuid.fromString("填入您需要扫描的设备uuid")); 
    此处的uuid就相当于是你蓝牙设备的标志,是一定要与你的设备匹配上的,不能随便乱填。
    
    2.PendingIntent建议以前台服务(getForegroundService)方式create。经过测试,如果以getService,getBroadcast方法进行创建PendingIntent的话在拉活方面存在比较多的兼容问题.而getForegroundService方式在国内主流机型上可正常拉活进程(经过测试的机型有:华为,小米,oppo,vivo等)
    PendingIntent.getForegroundService(
                    this,
                    1,
                    new Intent("com.hungrytree.receiver.BleService").setPackage(getPackageName()),
                    PendingIntent.FLAG_UPDATE_CURRENT );
    

    上面代码已经实现蓝牙扫描触发,而下面是实现蓝牙广播的接收:

    //AndroidManifest中的服务声明
            <service android:name=".TestService">
                <intent-filter>
                    <action android:name="com.hungrytree.receiver.BleService"/>
                    <category android:name="android.intent.category.DEFAULT"/>
                </intent-filter>
            </service>
    
    @TargetApi(26)
    public class TestService extends Service {
        
        @Override
        public void onCreate() {
            super.onCreate();
            //以前台服务的方式启动,要调用startForeground,否则会出现arn异常
            Notification notification = new Notification.Builder(this, NotificationChannel.DEFAULT_CHANNEL_ID)
                    .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
                    .build();
            startForeground(110, notification);
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            if (intent.getAction() == null) {
                return super.onStartCommand(intent, flags, startId);
            }
    
            //获取返回的错误码
            int errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, -1);//ScanSettings.SCAN_FAILED_*
            //获取到的蓝牙设备的回调类型
            int callbackType = intent.getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, -1);//ScanSettings.CALLBACK_TYPE_*
            if (errorCode == -1) {
                //扫描到蓝牙设备信息
                List<ScanResult> scanResults = (List<ScanResult>) intent.getSerializableExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT);
                if (scanResults != null) {
                    for (ScanResult result : scanResults) {
                        //打印所有设备的地址
                        String address = result.getDevice().getAddress();
                        Log.i("haha", "device address " + address);
                    }
                }
            } else {
                //此处为扫描失败的错误处理
    
            }
            return super.onStartCommand(intent, flags, startId);
        }
    
    
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
        }
    }
    

    此处是一个的ForegroundService的创建,他和普通服务的创建没什么大区别,不同的地方是在服务创建后要即时调用 startForeground() 方法,否则会出现anr异常。正常调用了 startForeground() 方法,手机通知栏会出现相应的通知提示,但国内很多机型是不会显示通知栏的,所以按这种方式很多用户也看不到进程被拉活了( ̄▽ ̄)。

    通过intent回调过来的参数见下表:

    有启动扫描,当然有关闭方法,见以下代码:

    @TargetApi(26)
        public void onClose(View view){
            PendingIntent callbackIntent = PendingIntent.getBroadcast(
                    this,
                    1,
                    new Intent("com.hungrytree.receiver.BleReceiver")
                            .setPackage(getPackageName()),
                    PendingIntent.FLAG_UPDATE_CURRENT );
            BluetoothManager bluetoothManager = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
            BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
            bluetoothAdapter.getBluetoothLeScanner().stopScan(callbackIntent);
        }
    

    三、注意事项

    以上的api调用在主流机型亲测是没有问题,但如果遇到以下问题可偿试用以下方式解决

    1.蓝牙无法拉活

    大多数国内的手机里面都有一个很神奇的权限----自启动权限,它的意思不仅仅包含手机启动的时候启动app,同时还有其它应用(包含系统应用)能否拉活你的应用。简单的说,如果发现不能被拉活,那就可能是系统的限制,试一下打开app的自启动权限。

    2.无论怎么样都无法扫描到蓝牙

    在android 6.0以上的系统,部分手机如果想扫描到蓝牙设备,还要检查位置服务或定位服务是否开启!!!在位置服务打开之后是有其它选项要求的。一般有三种选项,分别是 1.高精确度(GPS+网络)2.低耗电量(网络) 3.权限设备(GPS)。需要用到蓝牙扫描的话就只能选1或者2,选3是没有用的。以下小米的位置服务为例,看下图。

    3.用户关闭蓝牙后再打开,会停止原先所有的蓝牙扫描,也意味着终止了app唤醒

    假设进程还活着的时候,监测蓝牙开关再触发蓝牙扫描是没有问题的。但如果进程死后,用户关闭蓝牙,那暂时还没找到合适的办法。

    参考资料

    结论:

    android 8.0以上也可以支持类似像ios ibeacon方式的唤醒,同时不会像苹果局限于只能用ibeacon,能给许多的业务带来更好的扩展。

    如果该拉活方式只是为了提供给用户在特定的场景有更好的体验,那我是建议的。但如果要用来让app持续性后台运行,这种方式就有点滥用了。哪天google或者国内的产商一不开心就......

    最后,感谢各位大牛的阅读,如果有不对的地方希望能帮忙提醒改正,在此先谢过。

    最后针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

    需要展开的架构学习笔记导图的加群免费获取 Android架构设计大群(185873940)

    PS:群内有许多技术大牛,高手如云,有任何问题,欢迎广大网友一起来交流,群内还不定期免费分享高阶Android学习视频资料和面试资料包~

    相关文章

      网友评论

        本文标题:Android 8.0 进程拉活 --- 蓝牙唤醒

        本文链接:https://www.haomeiwen.com/subject/uthexctx.html