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》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章