Android 二维码登录轮询机制:从扫码到登录的完整客户端实现

现在很多登录功能都使用二维码登录,那么我怎么知道用户已经扫码登录了呢?

可以使用轮询机制,

轮询的退出条件 :代码里通过 count < MaxCount 控制最多 15 次(约 45 秒),超时后自动停止。

生命周期绑定 :当前 IntervalCheckToken 是单例且持有 Disposable,如果用户按返回键退出登录页,轮询不会自动停止。

markdown 复制代码
用户进入登录页
↓ 
生成二维码业务URL 
↓ 
Zxing 生成二维码Bitmap → 缩放/裁剪处理 → 页面展示二维码
↓
开启 RxJava 定时轮询(3秒间隔、限制最大轮询次数)
↓ 
客户端携带 deviceId + cacheKey 轮询请求后端接口
↓ 
后端返回三种状态: 1. 未扫码/未确认 → 继续轮询 
                 2. 已扫码确认登录 → 停止轮询、本地保存Token、发送登录事件跳转主页
                 3. 二维码过期 → 停止轮询/刷新新二维码 4. 账号已下线 → 清空登录状态、退出登录 
↓ 
接收EventBus登录事件 → 页面刷新UI、跳转主界面

1、展示二维码

typescript 复制代码
private void showQrCode(String url){
        TaskManager.runInConcurrentTaskManger(LoginActivity.this, url, new TaskManager.TaskRunnable<String>() {
            Bitmap bitmap;

            @Override
            public void success(String data) {
                if (bitmap != null) {
                    mBinding.deviceCode.setDeviceCode(bitmap);
                    IntervalCheckToken.getInstance().checkTokenInterval(15); //开启轮询
                }
            }

            @Override
            public void run(String data) throws AbsException { //将二维码url转为bitmap 
                bitmap = BitmapUtils.encodeAsBitmap(url, getResources().getDimensionPixelOffset(R.dimen.x220), getResources().getDimensionPixelOffset(R.dimen.x220));
            }

            @Override
            public void fail(String data, AbsException exception) { }
        });
    }

bitmap工具类

ini 复制代码
public class BitmapUtils {

    public static void recycle(Bitmap bitmap) {
        if (isCanRecycled(bitmap)) {
            bitmap.recycle();
        }
    }

    public static boolean isCanRecycled(Bitmap bitmap) {
        return bitmap != null && !bitmap.isRecycled();
    }

    public static Bitmap cropBitmap(Bitmap target, int width, int height, @BitmapCutEnum int type) {
        Bitmap bitmap = scaleBitmap(target, width, height);
        switch (type) {
            case BitmapCutEnum.CENTER:
                return centerCrop(bitmap, width, height);
            default:
                return target;
        }
    }

    public static Bitmap scaleBitmap(Bitmap target, int width, int height) {
        int targetWidth = target.getWidth();
        int targetHeight = target.getHeight();
        float scaleWidth = width * 1f / targetWidth;
        float scaleHeight = height * 1f / targetHeight;
        float scale = Math.max(scaleWidth, scaleHeight);
        Matrix matrix = new Matrix();
        matrix.postScale(scale, scale);
        return Bitmap.createBitmap(target, 0, 0, targetWidth, targetHeight, matrix, true);
    }

    private static Bitmap centerCrop(Bitmap target, int width, int height) {
        Bitmap scaleBitmap = scaleBitmap(target, width, height);
        Bitmap destBitmap = scaleBitmap;
        int scaleWidth = scaleBitmap.getWidth();
        int scaleHeight = scaleBitmap.getHeight();
        if (scaleWidth > width) {
            destBitmap = Bitmap.createBitmap(scaleBitmap, (scaleWidth - width) / 2, 0, width, height);
        } else if (scaleHeight > height) {
            destBitmap = Bitmap.createBitmap(scaleBitmap, 0, (scaleHeight - height) / 2, width, height);
        }
        return destBitmap;
    }

    public static Bitmap getBitmapHttp(String http) {
        try {
            URL url = new URL(http);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setDoInput(true);
            connection.connect();
            InputStream is = connection.getInputStream();
            Bitmap bitmap = BitmapFactory.decodeStream(is);
            is.close();
            return bitmap;
        } catch (Exception exception) {

        }

        return null;
    }

    /**
     * 创建黑白二维码
     *
     * @param contents
     * @return
     * @throws WriterException
     */
    public static Bitmap encodeAsBitmap(String contents, int width, int height) {
        return encodeAsBitmap(contents, width, height, 0, ErrorCorrectionLevel.Q);
    }

    /**
     * 创建黑白二维码
     *
     * @param contents
     * @return
     * @throws WriterException
     */
    public static Bitmap encodeAsBitmap(String contents, int width, int height, int margin, ErrorCorrectionLevel level) {
        MultiFormatWriter barcodeWriter = new MultiFormatWriter();
        //com.google.zxing.EncodeHintType:编码提示类型,枚举类型
        Map<EncodeHintType, Object> hints = new HashMap();
        //EncodeHintType.MARGIN:设置二维码边距,单位像素,值越小,二维码距离四周越近
        hints.put(EncodeHintType.MARGIN, margin);
        /*设置字符编码类型*/
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        /*设置误差校正*/
        hints.put(EncodeHintType.ERROR_CORRECTION, level);
        BitMatrix matrix = null;
        try {
            matrix = barcodeWriter.encode(contents, BarcodeFormat.QR_CODE, width, height, hints);
        } catch (WriterException e) {
            throw new RuntimeException(e);
        }
        int[] pixels = new int[width * height];
        for (int y = 0; y < height; y++) {
            int offset = y * width;
            for (int x = 0; x < width; x++) {
                pixels[offset + x] = matrix.get(x, y) ? BLACK : WHITE;
            }
        }
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
        return bitmap;
    }

    public static Bitmap base64ToBitmap(String base64Data) {
        byte[] bytes = Base64.decode(base64Data, Base64.DEFAULT);
        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    }
}

2、开启轮询

typescript 复制代码
public class IntervalCheckToken {
    private static final String TAG = IntervalCheckToken.class.getSimpleName();
    private static volatile IntervalCheckToken instance = null;
    private Disposable disposable;

    public static IntervalCheckToken getInstance() {
        IntervalCheckToken result = instance;
        if (result == null) {
            synchronized (IntervalCheckToken.class) {
                result = instance;
                if (result == null) {
                    instance = result = new IntervalCheckToken();
                }
            }
        }
        return result;
    }

    //获取token需要的参数 要先保存起来
    public void checkTokenInterval(int MaxCount) {
        String cacheKey = PreferenceUtil.fetch(PreferenceKey.PREFERENCE_KEY_CACHEKEY,"");
        String deviceId = PreferenceUtil.fetch(PreferenceKey.PREFERENCE_KEY_DEVICE_ID,"");
        if(TextUtils.isEmpty(cacheKey) || TextUtils.isEmpty(deviceId)){
            DLog.i(TAG, "日志输出------cacheKey或者deviceId未空");
            return;
        }
        dispose();
        Observable.interval(DelayConstant.DELAY_500, DelayConstant.DELAY_3000, TimeUnit.MILLISECONDS).doOnNext(count -> {
            DLog.i(TAG, "日志输出------轮询第" + count + "次轮询");
            try {
                if(count < MaxCount){
                    getToken();
                }else {
                    dispose();
                    DLog.i(TAG, "日志输出-----"+MaxCount+"次轮询结束");
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }).subscribeOn(Schedulers.io()).subscribe(new Observer<Long>() {
            @Override
            public void onSubscribe(Disposable mDisposable) {
                IntervalCheckToken.this.disposable = mDisposable;
                DLog.i(TAG, "日志输出------轮询 onSubscribe Disposable");
            }

            @Override
            public void onNext(Long aLong) {
            }

            @Override
            public void onError(Throwable e) {
                DLog.i(TAG, "日志输出------轮询出错" + e.toString());
            }

            @Override
            public void onComplete() {
            }
        });
    }

    private void getToken() {
        Map<String, Object> map = new HashMap<>();
        map.put("deviceId", PreferenceUtil.fetch(PreferenceKey.PREFERENCE_KEY_DEVICE_ID, ""));
        map.put("cacheKey", AESUtil.decrypt(PreferenceUtil.fetch(PreferenceKey.PREFERENCE_KEY_CACHEKEY, "")));
       Launcher.rx(RetrofitUtil.retrofitMicroservices.create(Api.class).pollingToken(map), new Launcher.Receiver<Model<LoginToken>>() {
            @Override
            public void onSuccess(Model<LoginToken> model) {
                if (model.isSuccess()) {
                    LoginToken loginToken = model.getData();
                    boolean isLogin = PreferenceUtil.fetch(PreferenceKey.PREFERENCE_KEY_IS_LOGIN, false);
                    if(!isLogin){ //未登录
                        if (loginToken.getLoginStatus()==1) { //1登录  0未登录
                            dispose(); //关闭轮询
                            PreferenceUtil.put(PreferenceKey.PREFERENCE_KEY_TOKEN, AESUtil.encrypt(loginToken.getToken()));
                            PreferenceUtil.put(PreferenceKey.PREFERENCE_KEY_USER_ID, AESUtil.encrypt(loginToken.getUser().getId()));
                            PreferenceUtil.put(PreferenceKey.PREFERENCE_KEY_IS_LOGIN, true);

                            EventBus.getDefault().post(new LoginEvent()); //发送登录完成 消息
                        }
                    }else {
                        if (loginToken.getLoginStatus()==0) {//退出登录 清空缓存信息
                            dispose();
                            LoginUtil.loginOut();
                        }
                    }

                }else if(model.getCode()==402){ //二维码已过期
//                    dispose();
                }
            }

            @Override
            public void onFail() {

            }
        });
    }

    private void dispose() {
        if (disposable != null && !disposable.isDisposed()) {
            disposable.dispose();
            disposable = null;
        }
    }

}

相关推荐
日月云棠4 小时前
9 Double 与 Float —— IEEE 754 浮点数在 Java 中的实现
java·后端
z落落4 小时前
C#参数区别
java·算法·c#
日月云棠4 小时前
5 StringBuffer —— 线程安全的可变字符串
java·后端
happymaker06264 小时前
SpringBoot学习日记——DAY06(整合MyBatisPlus的其他功能)
java·spring boot·学习
Refrain_zc5 小时前
Android 播放器进度条改造实践:句级音频列表映射秒级时间轴
java
我命由我123455 小时前
Bugly - Bugly 基本使用( App 质量追踪平台)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
宋哥转AI5 小时前
Spring AI Graph:从0到Supervisor(一)RAG子图+Supervisor路由踩坑全记录
java·agent
Mahir085 小时前
MyBatis 深度解密:从执行流程到底层原理全解
java·后端·面试·mybatis
菜菜的顾清寒5 小时前
力扣hot100(37)栈-有效的括号
java·开发语言