Android源码解析--ClipBoardService(粘贴板)服务详解

ClipBoardService是Android的粘贴板服务,我们的复制粘贴都需要通过这个服务来完成。

1、与ClipBoardService相关的类

如下图所示, ClipBoardService服务核心的几个类:

  • android.content.ClipBoardManager 继承自android.text.ClipBoardManager, 这是一个兼容性的设计, 早期android只支持text复制。APP应用就是拿到这个对象来调用粘贴板相关的服务。
  • ClipBoardService: 粘贴板服务的服务端,各个应用的调用的复制粘贴都要到这个服务来处理。
  • ClipData 顾名思义,就是管理保存粘贴数据的, 具体的数据存储在成员变量mItems中, 这个变量是一个Item类型的数组, 每一个Item表示一项数据。
  • ClipDataDescription: 用来描述ClipData中数据的类型,Android剪切板支持三种类型:Text、Intent、以及URI。

从一个应用复制数据,然后被封装成ClipData对象传输给ClipboardService,粘贴的应用从ClipboardService获取到复制数据的应用上传的ClipData对象,然后把数据解析出来,基本的复制粘贴就可以完成了。但是Android的ClipboardService所提供的功能远远不止这些,既然ClipboardService可以传输Uri和Intent,那么要实现复制粘贴什么数据,便可以由APP本身发挥巨大的想象空间。如复制一张图片,可以先把图片的Uri通过复制粘贴到目标应用程序,目标应用程序接收到这个Uri,通过content provider就可以凭Uri取得图片。

2、在SystemServer中添加ClipBoardService服务

ClipBoardService一样是在SystemServer.java中添加的:

  ServiceManager.addService(Context.CLIPBOARD_SERVICE,
                  new ClipboardService(context));

看一下构造方法做了些什么:

 public ClipboardService(Context context) {
    mContext = context;
    mAm = ActivityManagerNative.getDefault();//获取ActivityManager对象
    mPm = context.getPackageManager();//获取PackageManager
    mUm = (IUserManager) ServiceManager.getService(Context.USER_SERVICE);//获取UserManager用户管理类
    mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);//获取权限管理类
    IBinder permOwner = null;
    try {
        permOwner = mAm.newUriPermissionOwner("clipboard");
    } catch (RemoteException e) {
        Slog.w("clipboard", "AM dead", e);
    }
    mPermissionOwner = permOwner;

	//注册了一个广播, Android支持多用户,当某个用户被删除后, 需要在ClipBoardService中移除此用户
    IntentFilter userFilter = new IntentFilter();
    userFilter.addAction(Intent.ACTION_USER_REMOVED);
    mContext.registerReceiver(new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (Intent.ACTION_USER_REMOVED.equals(action)) {
                removeClipboard(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
            }
        }
    }, userFilter);

}

构造方法方法中只是初始化了一些参数, 并注册了一个广播,用来监听Android多用户的移除操作。

下面将从一个APP使用粘贴板来进一步了解ClipBoardService

3、APP复制数据到粘贴板

3.1 复制文本

APP在使用粘贴板时, 首先拿到ClipboardManager对象, 通过这个对象就可以和ClipBoardService进行IPC通信。

//获取ClipboardManager对象
ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
//把文本封装到ClipData中
ClipData clip = ClipData.newPlainText("simple text","Hello, World!");
// Set the clipboard's primary clip.
clipboard.setPrimaryClip(clip);

3.2 复制URI

//联系人的URI 
private static final String CONTACTS = "content://com.example.contacts";
private static final String COPY_PATH = "/copy";

Uri copyUri = Uri.parse(CONTACTS + COPY_PATH + "/" + lastName);
...

ClipData clip = ClipData.newUri(getContentResolver(),"URI",copyUri);
clipboard.setPrimaryClip(clip);

3.3 复制Intent

Intent appIntent = new Intent(this, com.example.demo.myapplication.class);
...
ClipData clip = ClipData.newIntent("Intent",appIntent);

clipboard.setPrimaryClip(clip);

4、APP从粘贴板获得数据进行粘贴

4.1 粘贴文本

// 获取Clipboard Manager
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);

String pasteData = "";
if (!(clipboard.hasPrimaryClip())) {
        //假设此应用程序一次只能处理一个项目。
     ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);

    // 假设只有文本,只处理文本
    pasteData = item.getText();
}

4.2 粘贴URI

ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
// Gets the clipboard data from the clipboard
ClipData clip = clipboard.getPrimaryClip();

if (clip != null) {
	//这里只处理uri
    ClipData.Item item = clip.getItemAt(0);
    Uri pasteUri = item.getUri();
}

4.3 粘贴Intent

ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);

//这里值处理intent
Intent pasteIntent = clipboard.getPrimaryClip().getItemAt(0).getIntent();

###4.4 第4小节 总结
上面的示例代码中都比较简单,实际使用中,可能需要同时判断从粘贴板蝴获得的数据属于text、URI、Intent中的哪一种,并且都要进行判空等处理。

5 复制和粘贴过程中ClipBoardService的源码分析

5.1 复制过程的setPrimaryClip方法

前一个小节已说到,数据赋值时, 会调用ClipBoardManager的setPrimaryClip, 看一下这里面的代码逻辑:

 public void setPrimaryClip(ClipData clip) {
    try {
        if (clip != null) {
            clip.prepareToLeaveProcess();
        }
        getService().setPrimaryClip(clip, mContext.getOpPackageName());
    } catch (RemoteException e) {
    }
}

这里面实际上就是通过Binder通信,把数据传递给ClipBoardService而已, 所以我们接着区ClipBoardService中查看对应逻辑:

public void setPrimaryClip(ClipData clip, String callingPackage) {
    synchronized (this) {
        if (clip != null && clip.getItemCount() <= 0) {
            throw new IllegalArgumentException("No items");
        }
		//1、检查相关权限 start
	//获取进程UID, 此时若是A进程通过Binder调用了setPrimaryClip方法,则获得的就是A进程的UID
        final int callingUid = Binder.getCallingUid();
        if (mAppOps.noteOp(AppOpsManager.OP_WRITE_CLIPBOARD, callingUid,
                callingPackage) != AppOpsManager.MODE_ALLOWED) {
            return;
        }
        checkDataOwnerLocked(clip, callingUid);
		//1、检查相关权限 end
        final int userId = UserHandle.getUserId(callingUid);
        PerUserClipboard clipboard = getClipboard(userId);
        revokeUris(clipboard);
        setPrimaryClipInternal(clipboard, clip);
        List<UserInfo> related = getRelatedProfiles(userId);//用来判断当前用户是否有复制权限
        if (related != null) {
            int size = related.size();
            if (size > 1) { 
                boolean canCopy = false;
                try {
                    canCopy = !mUm.getUserRestrictions(userId).getBoolean(
                            UserManager.DISALLOW_CROSS_PROFILE_COPY_PASTE);
                } catch (RemoteException e) {
                }
                // 如果允许,将可以将剪辑数据复制到相关用户。 
                // 如果不允许,则删除相关用户中的主剪辑,以防止粘贴陈旧内容。
                if (!canCopy) {
                    clip = null;
                } else {
                    clip.fixUrisLight(userId);
                }
                for (int i = 0; i < size; i++) {
                    int id = related.get(i).id;
                    if (id != userId) {
				//需要通知其他所有注册了的客户端,粘贴板的内容变化了
                        setPrimaryClipInternal(getClipboard(id), clip);
                    }
                }
            }
        }
    }
}

setPrimaryClip 方法主要做了两件事:

  • 1、就是做了一些权限判断;
  • 2、通知所有客户端,粘贴板内容更新

权限判断的具体细节,其实主要是判断是否有赋值粘贴对应uri的权限,不然随意一个APP,都能根据uri去获取系统或者其他APP的隐私数据

5.2 粘贴过程的getPrimaryClip方法

查看ClipBoardManager中的此方法:

public ClipData getPrimaryClip() {
    try {
        return getService().getPrimaryClip(mContext.getOpPackageName());
    } catch (RemoteException e) {
        return null;
    }
}

这里也是通过Binder 实际上调用的ClipBoardService的getPrimaryClip:

public ClipData getPrimaryClip(String pkg) {
    synchronized (this) {
        if (mAppOps.noteOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(),
                pkg) != AppOpsManager.MODE_ALLOWED) {
            return null;
        }
	//赋予该pkg相应权限, 下面第6节会分析
        addActiveOwnerLocked(Binder.getCallingUid(), pkg);
	//返回ClipData给客户端
        return getClipboard().primaryClip;
    }
}

5.3 重要方法 ClipData.coerceToText()

在前面讲到,粘贴板的数据类型有text、Intent、URI三种, 而客户端并不知道粘贴板的内容是哪一个类型, 所以在粘贴的时候需要去判断。如果不想去判断, 那就可以用ClipData的一个方法:coreceToText, 它可以将粘贴板内容强制转换为String。示例如下:

ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
//强制转为文本
String text = clipData。coreceToText(this.toString());

我们看看这个方法的具体实现:

public CharSequence coerceToText(Context context) {
//1、如果当前ClipData包含text,则直返回
CharSequence text = getText();
if (text != null) {
return text;
}

        //2、如果包含uri, 则返回uri相关内容
        Uri uri = getUri();
        if (uri != null) {

            //2.1、如果能将uri作为纯文本流打开,这是最好的显示方式
            FileInputStream stream = null;
            try {
                AssetFileDescriptor descr = context.getContentResolver()
                        .openTypedAssetFileDescriptor(uri, "text/*", null);
                stream = descr.createInputStream();
                InputStreamReader reader = new InputStreamReader(stream, "UTF-8");

                //2.2 得到文本流,将其返回
                StringBuilder builder = new StringBuilder(128);
                char[] buffer = new char[8192];
                int len;
                while ((len=reader.read(buffer)) > 0) {
                    builder.append(buffer, 0, len);
                }
                return builder.toString();

            } catch (FileNotFoundException e) {
              //2.3 无法作为文本流打开
            } catch (IOException e) {
                //2.4 文件损坏,返回错误信息
                Log.w("ClippedData", "Failure loading text", e);
                return e.toString();

            } finally {
                if (stream != null) {
                    try {
                        stream.close();
                    } catch (IOException e) {
                    }
                }
            }

            //2.5如果无法将uri作为流打开,则直接将uri转为string
            return uri.toString();
        }

        //3、如果是intent,则转为文本,虽然不太友好
        Intent intent = getIntent();
        if (intent != null) {
            return intent.toUri(Intent.URI_INTENT_SCHEME);
        }
        // 什么都没有则返回空
        return "";
    }

可以看到,强转的过程,对URI类型的数据做了很好的解析的,尽可能是返回数据更友好。不过,也需要提供该URI的ContentProvider实现对应的方法,才能正确转换。

此外,还有两个类似的方法:

  • public CharSequence coerceToStyledText(Context context);

  • public String coerceToHtmlText(Context context);

总结

ClipBoardService相对比较独立,也比较好分析。这里再简单总结一下:

  • 1、CBS作为系统服务,是在SystemServer中被添加注册的;
    -** 2、CBS可以复制粘贴的数据类型有三种:文本、URI、Intent;**
  • 3、和CBS相关的类有:ClipBoardService(服务端)、ClipBoardManager(客户端)、ClipData(具体的数据bean)、ClipDataDescription(用来描述ClipData);
  • 4、可以通过ClipData.coerceToText 把粘贴板的数据强转为文本;
  • 5、CBS中进行了权限校验。

阅读了代码以后,又一次感叹Google的技术功底是真的深,值得不断去学习。

参考《深入理解Android》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值