天天看点

Android System Server大纲之ClipboardServiceAndroid System Server大纲之ClipboardService

Android System Server大纲之ClipboardService

Android复制粘贴服务

粘贴板服务

  • Android System Server大纲之ClipboardService
    • 前言
    • ClipboardService概览
    • 深入ClipboardService
      • ClipboardService主要用到的类
      • ClipboardService支持的数据类型
      • ClipboardService架构
    • 上层APP使用粘贴板服务
      • 获取与ClipboardService通信的对象
      • 复制
        • 复制文本
        • 复制Uri
        • 复制Intent
      • 粘贴
        • 粘贴文本
        • 粘贴Uri
        • 粘贴Intent
    • 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();
        // 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();

    // 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();

    // 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().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=; 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 = ;
    }
}
           

这里的代码,相信读者很大一部分已经相当熟悉,通过ClipboardManager获取到ClipData,然后通过一个循环把ClipData中的数据取出来复制给变量paste,然后通过((Editable) mText).insert()插入到输入框。

继续阅读