学而实习之 不亦乐乎

Android 10 剪贴板(Clipboard)的适配和解决方案

2023-07-04 21:31:58

一、概述

Android 10(Q) 开始对剪贴板增加了限制,当应用没有获取到焦点的时候,无法获取剪贴板内容。

之前监听剪贴板变化代码如下:

ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.addPrimaryClipChangedListener(new ClipboardManager.OnPrimaryClipChangedListener() {
  @Override
  public void onPrimaryClipChanged() {
     if (clipboard.hasPrimaryClip() && clipboard.getPrimaryClip().getItemCount() > 0) {
        CharSequence txt = clipboard.getPrimaryClip().getItemAt(0).getText();
        String result = String.valueOf(txt);
        //result即为拿到的剪贴板上的数据,后续根据需要来作处理即可
     }
  }
});

Android 10 以后,如何获取剪贴板数据呢?

二、解决方法

方案一:在onResume中,通过 post 延时到界面拥有焦点时读取剪切板

也就是在页面恢复时,延迟个1秒杀左右再去检查剪贴板内容

@Override
protected void onResume() {
    super.onResume();
    new Handler().postDelayed(new Runnable() {
        public void run() {
            ClipboardManager cm = (ClipboardManager) MainActivity.this.getSystemService(Context.CLIPBOARD_SERVICE);
            if (!cm.hasPrimaryClip()) {
                return;
            }
            //剪切板操作
            ...
    }, 1000);
}

也可以在界面焦点发生变化时

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        //获取剪切板内容逻辑写到这里。
    }
}

方案二:借助悬浮窗开启前台服务监听

有弊端,个人应用测试使用可以,正式环境还是不要这么做了,只给有这方面需求的朋友参考一下

悬浮窗的创建

需要注意的是Flags的设定,只要一个FLAG_NOT_TOUCH_MODAL就好了,一定不要有FLAG_NOT_FOCURABLE即不能让悬浮窗的焦点离开

但以上设定会有一个问题,就是返回操作等会失效,因为焦点在悬浮窗上,只能通过点击应用本身的返回按钮来解决。

layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

创建前台服务

其实就是创建一个通知,重点是在通知中开启前台服务startForeground()并在onDestroy()中关闭stopForeground(true)

private void createNotification() {
    String channelId = getPackageName() + System.currentTimeMillis();
    NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channelId).setAutoCancel(true);
    builder.setContentText("悬浮监听剪贴板")
            .setWhen(System.currentTimeMillis())
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setOngoing(false)
            .setContentIntent(null)
            .setDefaults(NotificationCompat.DEFAULT_ALL);
    NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    NotificationChannel channel = new NotificationChannel(channelId, getPackageName(), NotificationManager.IMPORTANCE_HIGH);
    manager.createNotificationChannel(channel);
    startForeground(100, builder.build());
}

@Override
public void onDestroy() {
    stopForeground(true);
    super.onDestroy();
}

完整代码如下:

public class FloatClipboardService extends Service {
    private View mView;
    private WindowManager windowManager;   

    @Override
    public void onDestroy() {
        if (mView != null) windowManager.removeView(mView);
        MyApplication.isFloatClipboardShow = false;
        stopForeground(true);
        super.onDestroy();
    }
   
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        createNotification();
        initWindow();
        return super.onStartCommand(intent, flags, startId);
    }
   
    private void createNotification() {
       String channelId = getPackageName() + System.currentTimeMillis();
       NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channelId).setAutoCancel(true);
       builder.setContentText("悬浮监听剪贴板")
               .setWhen(System.currentTimeMillis())
               .setPriority(NotificationCompat.PRIORITY_DEFAULT)
               .setOngoing(false)
               .setContentIntent(null)
               .setDefaults(NotificationCompat.DEFAULT_ALL);
       NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
       NotificationChannel channel = new NotificationChannel(channelId, getPackageName(), NotificationManager.IMPORTANCE_HIGH);
       manager.createNotificationChannel(channel);
       startForeground(100, builder.build());
    }
   
    private void initWindow() {
        if (Settings.canDrawOverlays(this)) {
            windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
            //region 设置LayoutParams
            WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
            layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
            layoutParams.format = PixelFormat.RGBA_8888; //背景透明效果
            // 悬浮窗口长宽值,单位为 px 而非 dp
            layoutParams.width = dip2px(95);
            layoutParams.height = dip2px(45);
            layoutParams.gravity = 51; //想要x,y生效,一定要指定Gravity为top和left //Gravity.TOP | Gravity.LEFT
            // 启动位置
            layoutParams.x = 128;
            layoutParams.y = 128;
            //endregion

            //加载悬浮窗布局
            FloatClipboardBinding floatView = FloatClipboardBinding.inflate(LayoutInflater.from(FloatClipboardService.this));
            mView = floatView.getRoot();
            mView.setAlpha((float) 0.8);

            // 悬浮窗控件事件
            floatView.btnFloatClipboardClose.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    stopSelf();
                }
            });

            // 监听剪贴板
            floatView.tvFloatClipboardContent.setText("");
            ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
            clipboardManager.addPrimaryClipChangedListener(new ClipboardManager.OnPrimaryClipChangedListener() {
                Long cur;
                @Override
                public void onPrimaryClipChanged() {
                    if(cur != null){
                        if(System.currentTimeMillis() - cur < 1500) return;
                    }

                    cur = System.currentTimeMillis();
                    if (clipboardManager.hasPrimaryClip() && clipboardManager.getPrimaryClip().getItemCount() > 0) {
                        CharSequence txt = clipboardManager.getPrimaryClip().getItemAt(0).getText();
                        String str = PubUtil.getUrl(String.valueOf(txt));
                        if (!TextUtils.isEmpty(str)) {
                            floatView.tvFloatClipboardContent.setText(str);
                            String content = "\n" + str;
                            File clipboardFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + File.separator + "Clipboard.txt");

                            // 由于在载入时就检查了文件是否存在,此处不再作检查
                            try {
                                FileOutputStream fos = new FileOutputStream(clipboardFile, true);
                                fos.write(content.getBytes());
                                fos.close();
                                floatView.tvFloatClipboardStatus.setText("保存成功");
                            } catch (IOException e) {
                                floatView.tvFloatClipboardStatus.setText("保存失败");
                                floatView.tvFloatClipboardContent.setText(e.toString());
                            }
                        }

                        new Handler().postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                floatView.tvFloatClipboardStatus.setText("等待复制");
                                floatView.tvFloatClipboardContent.setText("");
                            }
                        }, 1500);
                    }
                }

            });

            //加载悬浮窗到窗口管理器
            windowManager.addView(mView, layoutParams);
            MyApplication.isFloatClipboardShow = true;
        }
    }
 
    private int dip2px(int dipValue) {
        float density = this.getResources().getDisplayMetrics().density;
        return (int) (dipValue * density + 0.5f);
    }
}