Android System Server大纲之ClipboardService
Android复制粘贴服务
粘贴板服务
前言
ClipboardService即安卓粘贴板服务,是Android系统中一个纯软件实现的服务,提供APP可以在进程内或跨进程方便使用复制粘贴。在Android设备中,粘贴复制已经是必配的基本功能,对用户使用Android设备也增强很大的便利性,增强用户体验。
对于打多数Android设备的用户而言,ClipboardService主要就是用来实现文字的复制和粘贴,但是Android的粘贴板服务是不是只是提供文字的复制和粘贴呢?答案是否定的。Android的ClipboardService所能提供的功能远远不止普通文本的复制和粘贴。但是不管用户复制粘贴什么类型的数据,都好像复制粘贴文字一样的方便和实用。
ClipboardService概览
因为ClipboardSercie是System Server,所以ClipboardService可以给上层任何一个APP进行复制粘贴的数据传输。看看ClipboardService的架构,上代码:
public class ClipboardService extends IClipboard.Stub {
public void setPrimaryClip();
public android.content.ClipData getPrimaryClip();
public android.content.ClipDescription getPrimaryClipDescription();
public boolean hasPrimaryClip();
public void addPrimaryClipChangedListener();
public void removePrimaryClipChangedListener();
/**
* Returns true if the clipboard contains text; false otherwise.
*/
public boolean hasClipboardText();
}
从上述代码看到,ClipboardService继承IClipboard.Stub,也就是说ClipboardService本身是一个Binder通信的实现,作为Android IPC通信中,在C/S架构的模式中,ClipboardService是S端。setPrimaryClip()等这些接口将在下文中一一论述其作用。既然ClipboardService继承IClipboard.Stub,参考另外一篇文章《Android系统之System Server大纲》中的服务启动过程,ClipBoardService是通过ServiceManager.addService()的方式启动,其启动的代码在frameworks/base/services/java/com/android/server/SystemServer.java中:
if (!disableNonCoreServices) {
traceBeginAndSlog("StartClipboardService");
try {
ServiceManager.addService(Context.CLIPBOARD_SERVICE,
new ClipboardService(context));
} catch (Throwable e) {
reportWtf("starting Clipboard Service", e);
}
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
看ServiceManager.addService()时,传入了Context.CLIPBOARD_SERVICE作为name,后文将会论述name的作用。这里还有一点值得注意,启动ClipboardService是有条件的,当disableNonCoreServices()返回ture的时候,将不会启动ClipboardService,看看disableNonCoreServices()的是实现过程:
boolean disableNonCoreServices = SystemProperties.getBoolean("config.disable_noncore", false);
因此,如果系统的属性系统中把config.disable_noncore配置成true,那么当前的设备将不支持粘贴板服务。
深入ClipboardService
ClipboardService主要用到的类
frameworks/base/core/java/android/content/ClipData.java
frameworks/base/core/java/android/content/ClipData$Item.java
frameworks/base/core/java/android/content/ClipDescription.java
ClipData对象封装了数据的描述和数据的本身,clipboard统一时间只会持有一个ClipData对象。ClipData所持有的数据描述被封装到ClipDescription对象中,ClipDescription封装了数据的metadata,ClipData所持有的数据被真正封装到ClipData Item中,一个ClipData可以持有多个ClipData Item,ClipData$Item可以封装文本,Uri和Intent。
ClipboardService支持的数据类型
如ClipboardService主要用到的类中的赘述,ClipData$Item可以封装文本,Uri和Intent,也即是说ClipboardService支持的数据类型包括文本,Uri和Intent共三种。
ClipboardService架构
![Screenshot from 2017-02-16 17:05:42.png](X:/Screenshot from 2017-02-16 17:05:42.png “”)
如上图,从一个应用复制数据,然后被封装成ClipData对象传输给ClipboardService,粘贴的应用从ClipboardService获取到复制数据的应用上传的ClipData对象,然后把数据解析出来,基本的复制粘贴就可以完成了。但是Android的ClipboardService所提供的功能远远不止这些,既然ClipboardService可以传输Uri和Intent,那么要实现复制粘贴什么数据,便可以由APP本身发挥巨大的想象空间。如复制一张图片,可以先把图片的Uri通过复制粘贴到目标应用程序,目标应用程序接收到这个Uri,通过content provider就可以凭Uri取得图片。
上层APP使用粘贴板服务
获取与ClipboardService通信的对象
在Android系统中,与ClipboardService通信的对象便是ClipboardManager,在应用中获取这个对象,还是通过老方法,Context.getSystemService()。在ClipboardService概览章节中,启动ClipboardService的代码如下:
if (!disableNonCoreServices) {
......
ServiceManager.addService(Context.CLIPBOARD_SERVICE,
new ClipboardService(context));
......
}
所以,Context.getSystemService()传入的参数应是Context.CLIPBOARD_SERVICE,如:
ClipboardManager clipManager = (ClipboardManager)Context.getSystemService(Context.CLIPBOARD_SERVICE);
Context.getSystemService(Context.CLIPBOARD_SERVICE)获取到的对象一定是ClipboardManager吗?回顾文章《Android System Server大纲之VibratorService》的APP使用VibratorService章节,探索Context.getSystemService(Context.CLIPBOARD_SERVICE)的过程,在frameworks/base/core/java/android/app/SystemServiceRegistry.java中找到如下代码:
registerService(Context.CLIPBOARD_SERVICE, ClipboardManager.class,
new CachedServiceFetcher<ClipboardManager>() {
@Override
public ClipboardManager createService(ContextImpl ctx) {
return new ClipboardManager(ctx.getOuterContext(),
ctx.mMainThread.getHandler());
}});
上述代码中,Context.CLIPBOARD_SERVICE对应的正是ClipboardManager。看ClipboardManager封装的ClipboardService的远程句柄是否是和ClipboardService的Binder是否对应:
public class ClipboardManager extends android.text.ClipboardManager {
private static IClipboard sService;
static private IClipboard getService() {
synchronized (sStaticLock) {
.....
IBinder b = ServiceManager.getService("clipboard");
sService = IClipboard.Stub.asInterface(b);
return sService;
}
}
回顾第一章节ClipboardService概览中ClipboardService的启动,如下:
if (!disableNonCoreServices) {
try {
ServiceManager.addService(Context.CLIPBOARD_SERVICE,
new ClipboardService(context));
.....
}
Context中的Context.CLIPBOARD_SERVICE值如下:
public static final String CLIPBOARD_SERVICE = "clipboard";
因此,ServiceManager.getService(“clipboard”)正好是ClipboardService的句柄。所以,ClipboardManager中的sService对象正好是远程服务ClipboardService的远程控制端。得到了ClipboardManager,那么就可以开始复制粘贴。
复制
复制文本
Android把复制的步骤封装的相当简单,开发者调用极少的api便可实现复制的功能,复制文本的过程如下:
//获取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);
调用ClipboardManager.setPrimaryClip()便把ClipData上传到了ClipboardService。
复制Uri
// Creates a Uri based on a base Uri and a record ID based on the contact's last name
// Declares the base URI string
private static final String CONTACTS = "content://com.example.contacts";
// Declares a path string for URIs that you use to copy data
private static final String COPY_PATH = "/copy";
// Declares the Uri to paste to the clipboard
Uri copyUri = Uri.parse(CONTACTS + COPY_PATH + "/" + lastName);
...
// Creates a new URI clip object.
ClipData clip = ClipData.newUri(getContentResolver(),"URI",copyUri);
clipboard.setPrimaryClip(clip);
复制Intent
// Creates the Intent
Intent appIntent = new Intent(this, com.example.demo.myapplication.class);
...
// Creates a clip object with the Intent in it. Its label is "Intent" and its data is
// the Intent object created previously
ClipData clip = ClipData.newIntent("Intent",appIntent);
clipboard.setPrimaryClip(clip);
粘贴
粘贴文本
// Gets a handle to the Clipboard Manager
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
String pasteData = "";
if (!(clipboard.hasPrimaryClip())) {
ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
// Examines the item on the clipboard. If getText() does not return null, the clip item contains the
// text. Assumes that this application can only handle one item at a time.
ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
// Gets the clipboard as text.
pasteData = item.getText();
}
粘贴Uri
// Gets a handle to the Clipboard Manager
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
// Gets the clipboard data from the clipboard
ClipData clip = clipboard.getPrimaryClip();
if (clip != null) {
// Gets the first item from the clipboard data
ClipData.Item item = clip.getItemAt(0);
// Tries to get the item's contents as a URI
Uri pasteUri = item.getUri();
}
粘贴Intent
// Gets a handle to the Clipboard Manager
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
// Checks to see if the clip item contains an Intent, by testing to see if getIntent() returns null
Intent pasteIntent = clipboard.getPrimaryClip().getItemAt(0).getIntent();
对于如何结合Android的其它功能灵活使用Uri或Intent,本文就不再赘述了,读者自行发挥想象的空间,开发出有趣的功能。
Android系统中提供的编辑框复制粘贴功能
在Android系统中,当长按一个输入框,会弹出一个粘贴的按钮,或者在输入框中选定一些文字,也会弹出复制的按钮,如下图:
这个实现过程在哪里呢?这个功能是否也是使用上文中的ClipboardService呢?Android中的输入框控件是EditText,EditText是TextView的子类,实现这个功能便在TextView中,复制的实现过程如下:
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
case ID_COPY:
setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
stopTextActionMode();
return true;
}
}
上述代码中getTransformedText(min, max)就是获取选中输入框中的文本的起始下标,和末尾下标,也就是选定的文本的长度,和上文中生成ClipData的步骤一致。继续看setPrimaryClip()方法:
private void setPrimaryClip(ClipData clip) {
ClipboardManager clipboard = (ClipboardManager) getContext().
getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(clip);
}
setPrimaryClip()和上文中提到的文本复制一致,所以Android的这个复制功能就是用的ClipboardService。既然复制是用ClipboardService,那么粘贴的功能肯定也是。但是,还是得看看这个过程:
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
case ID_PASTE:
paste(min, max, true /* withFormatting */);
return true;
}
}
点击粘贴按钮后,调用paste()方法,继续看这个方法:
private void paste(int min, int max, boolean withFormatting) {
ClipboardManager clipboard =
(ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = clipboard.getPrimaryClip();
if (clip != null) {
boolean didFirst = false;
for (int i=0; i<clip.getItemCount(); i++) {
final CharSequence paste;
if (withFormatting) {
paste = clip.getItemAt(i).coerceToStyledText(getContext());
} else {
// Get an item as text and remove all spans by toString().
final CharSequence text = clip.getItemAt(i).coerceToText(getContext());
paste = (text instanceof Spanned) ? text.toString() : text;
}
if (paste != null) {
if (!didFirst) {
Selection.setSelection((Spannable) mText, max);
((Editable) mText).replace(min, max, paste);
didFirst = true;
} else {
((Editable) mText).insert(getSelectionEnd(), "\n");
((Editable) mText).insert(getSelectionEnd(), paste);
}
}
}
stopTextActionMode();
sLastCutCopyOrTextChangedTime = 0;
}
}
这里的代码,相信读者很大一部分已经相当熟悉,通过ClipboardManager获取到ClipData,然后通过一个循环把ClipData中的数据取出来复制给变量paste,然后通过((Editable) mText).insert()插入到输入框。