Rich content insertion
最近Android 12 发布了一个新的功能,关于富文本的插入
具体关于API的使用可以看:Rich content insertion
可以看一下实现的效果:
实际可以分为两个部分:copy 和 paste,也分别是两个Application。上完这就涉及到了跨进程通信.
我们先铺垫一下,看一下如何通信的:
ClipboardService 系统
copy --> paste 到整个过程
- Copy action 是从 Coping Application 将数据copy到 ClipboardService.
- Paste action 是从 ClipboardService 将数据paste到 Pasting Application
进程通信
- ClipboardService 是系统的服务,继承SystemService,
- ClipboardService 与 Application 通过Binder通信
通信协议看:outfit /soong/.intermediates/frameworks/base/framework-minus-apex/android_common/xref31/srcjars.xref/android/content/IClipboard.java
通信的方式具象化就是:copy和paste
// == copy ==
val text = "hello woddrld"
val mClipData = ClipData.newPlainText("test", text)
manager.setPrimaryClip(mClipData)
// == paste ==
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
mClipData = manager.primaryClip
数据管理
-
ClipboardService 持有SparseArray<PerUserClipboard> 缓存数据,key 是 用户ID(多用户)
-
通过 ClipboardManager 对 Clipboard 的数据进行管理, ClipboardManager 的具体实现是 ClipboardImpl
frameworks/base/core/java/android/content/ClipboardManager.java
ClipboardManager 通过getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
获取.
数据类型
ClipboardService 持有SparseArray<PerUserClipboard> 缓存数据,key 是 用户ID(多用户)
可以看观察PerUserClipboard的 UML 图
可以发现,对于具体的内容,封装成Item.
item 可以有很多类型:text,uri,intent.
存储于ClipData 的 mItems的字段中。
权限管理
先看哪里会对权限检查
public void setPrimaryClip(ClipData clip, String callingPackage,
@UserIdInt int userId) {
synchronized (this) {
if (clip == null || clip.getItemCount() <= 0) {
throw new IllegalArgumentException("No items");
}
final int intendingUid = getIntendingUid(callingPackage, userId);
final int intendingUserId = UserHandle.getUserId(intendingUid);
if (!clipboardAccessAllowed(AppOpsManager.OP_WRITE_CLIPBOARD, callingPackage,
intendingUid, intendingUserId)) {
return;
}
// 注意这里, 继续追下去
checkDataOwnerLocked(clip, intendingUid);
setPrimaryClipInternal(clip, intendingUid);
}
}
private final void checkUriOwnerLocked(Uri uri, int sourceUid) {
if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()))
return;
final long ident = Binder.clearCallingIdentity();
try {
// 看到这里
mUgmInternal.checkGrantUriPermission(sourceUid, null,
ContentProvider.getUriWithoutUserId(uri),
Intent.FLAG_GRANT_READ_URI_PERMISSION,
ContentProvider.getUserIdFromUri(uri,UserHandle.getUserId(sourceUid)));
} finally {
Binder.restoreCallingIdentity(ident);
}
}
具体的权限检查需要看到:UriGrantsManagerService
中的 checkGrantUriPermission
方法.
这里注意这个flag:Intent.FLAG_GRANT_READ_URI_PERMISSION
Copy 和 Paste menu 的构建
这里描述了两个动作的时序图:
- 长按空白弹出paste menu,也就是FloatingToolbar.
- 选择文字弹出copy menu,也就是FloatingToolbar.
两个动作最终都是从 Editor 调用TextView 的 startActionMode() 来展现 FloatingToolbar.
区别在于,选择文字弹出copy menu的过程中,需要将文字设置背景色.
需要注意的是:FloatingToolbar 在layout的时候,会设置menu item点击的监听器(MenuItem.OnMenuItemClickListener
).
Rich text insert
综上所述:
当我们从一个应用copy到另外一个应用时,是通过ClipboardService通信,当要执行copy或者paste的时候,会弹出FlotingToobar.
点击menu item 的时候会通过MenuItem.OnMenuItemClickListener
回调。
到这里我们看一下官方API给我们暴露出的接口:
结合Google的commit记录来看:
https://android-review.googlesource.com/c/platform/frameworks/support/+/1306703
https://android-review.googlesource.com/c/platform/frameworks/support/+/1510258
三个action,复写三个方法,做了三件事:
- 检查是否需要回调
- 构建ContentInfoCompat
- 回调处理
从AppCompatEditText 中 调用到 AppCompatReceiveContentHelper 类中
public boolean onTextContextMenuItem(int id) {
// 判断是否要回调通知,如果需要则执行回调
if (maybeHandleMenuActionViaPerformReceiveContent(this, id)) {
return true;
}
return super.onTextContextMenuItem(id);
}
@Override
public boolean onDragEvent(@SuppressWarnings("MissingNullability") DragEvent event) {
if (maybeHandleDragEventViaPerformReceiveContent(this, event)) {
return true;
}
return super.onDragEvent(event);
}
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
InputConnection ic = super.onCreateInputConnection(outAttrs);
mTextHelper.populateSurroundingTextIfNeeded(this, ic, outAttrs);
ic = AppCompatHintHelper.onCreateInputConnection(ic, outAttrs, this);
String[] mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this);
if (ic != null && mimeTypes != null) {
EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes);
OnCommitContentListener onCommitContentListener = createOnCommitContentListener(this);
ic = InputConnectionCompat.createWrapper(ic, outAttrs, onCommitContentListener);
}
return ic;
}
这里复写了三个方法,对应着三个action:
- SOURCE_CLIPBOARD
- SOURCE_INPUT_METHOD
- SOURCE_DRAG_AND_DROP
总结
这里我们梳理了三块知识点:
- ClipboardService 作为banner的实现者,通信的代理.
- ClipboardService 如何管理缓存数据: SparseArray
- PerUserClipboard 和 ClipData 的数据结构
- ClipData 中存在URI 的访问权限
- ClipboardService 抽象出来的管理者:ClipboardManager
- FlotingToolbar menu 的构建menu item,展现toolbar,设置menu点击事件.
- Rich text insert 通过对 输入,FlotingToolbar的点击事件,拖拽事件的监听,构造一个新的回调接口:
OnReceiveContentListener:onReceiveContent
Resource link
api:
https://developer.android.com/about/versions/12/features/unified-content-api
https://joebirch.co/android/exploring-android-12-unified-rich-content-api/
https://developer.android.com/guide/topics/text/copy-paste?hl=zh-cn
blog:
https://www.cnblogs.com/mengdd/p/3572316.html
https://blog.csdn.net/qq475703980/article/details/89061293
android 实现剪贴板的粘贴复制
https://blog.csdn.net/uniquemei/article/details/52824000
commit change 记录:
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java;bpv=1;bpt=0