Android 10도 클립보드를 쓰고 싶어요.

입문


이 글은 Android Advent Calendar 2019의 9일째입니다!
Android에서 클립보드를 처리하려면 일반적으로 다음과 같이 ClipboardManager를 사용합니다.
화면을 시작할 때 클립보드를 검색하려면 onResume 등에서 ClipboardManager를 호출해서 검색합니다.
MainActivity.kt
class MainActivity : AppCompatActivity() {
    // 中略

    override fun onResume() {
        super.onResume()

        Snackbar.make(root, getClipboard(), Snackbar.LENGTH_SHORT).show()
    }

    private fun Context.getClipboard(): String {
        val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
        return clipboard?.primaryClip?.getItemAt(0)?.text?.toString().orEmpty()
    }
}
하지만 안드로이드 10에서 위와 같은 방법으로 클립보드의 내용을 얻으려면null은 돌아올 수 없습니다.
Android10
다음 터미널


이 현상은 Android 10이 사용자의 프라이버시를 보호하기 위해 클립보드를 가져오는 것을 제한했기 때문이다.
Limited access to clipboard data
Unless your app is the default input method editor (IME) or is the app that currently has focus, your app cannot access clipboard data on Android 10 or higher.
https://developer.android.com/about/versions/10/privacy/changes
이 글에서 쓴 바와 같이 Android 10에서 클립보드의 데이터에 접근하기 위해서는 입력 시스템 (IME) 이나 현재 초점이 있는 프로그램이 필요합니다.
따라서 Android 10에서는 몇 개의 클립보드 응용 프로그램을 사용할 수 없습니다듣다.
그래서 이 글에서 나는 어떻게 설치해야 정확하게 얻을 수 있는지 쓰고 싶다.

3 행 요약

  • 클립보드를 얻기 위해서는 초점을 맞춰야 한다.
  • 안드로이드에서 초점 검출이 온 것은Activity#onWindowFocusChanged이다.
  • Activity#onWindowFocusChanged에서 클립보드를 가져옵니다.
  • 원인


    우선, 원인과 해결 방법을 상세하게 조사하기 위해 Googlesource에서 Clipboard Manager의 실현을 보았습니다.
    원인 수정승낙diff는 다음과 같다.
    수정된 코드를 보면 clipboardAccessAllowed 이 방법이 이번 수정에서 큰 변화가 생겼다는 것을 발견할 수 있다.
    -    private boolean clipboardAccessAllowed(int op, String callingPackage, int callingUid) {
    +    private boolean clipboardAccessAllowed(int op, String callingPackage, int uid,
    +            @UserIdInt int userId) {
            // Check the AppOp.
    -        if (mAppOps.noteOp(op, callingUid, callingPackage) != AppOpsManager.MODE_ALLOWED) {
    +        if (mAppOps.noteOp(op, uid, callingPackage) != AppOpsManager.MODE_ALLOWED) {
                return false;
            }
            // Shell can access the clipboard for testing purposes.
    @@ -641,7 +750,6 @@
                return true;
            }
            // The default IME is always allowed to access the clipboard.
    -        int userId = UserHandle.getUserId(callingUid);
            String defaultIme = Settings.Secure.getStringForUser(getContext().getContentResolver(),
                    Settings.Secure.DEFAULT_INPUT_METHOD, userId);
            if (!TextUtils.isEmpty(defaultIme)) {
    @@ -654,16 +762,31 @@
            switch (op) {
                case AppOpsManager.OP_READ_CLIPBOARD:
                    // Clipboard can only be read by applications with focus..
    -                boolean allowed = mWm.isUidFocused(callingUid);
    +                // or the application have the INTERNAL_SYSTEM_WINDOW and INTERACT_ACROSS_USERS_FULL
    +                // at the same time. e.x. SystemUI. It needs to check the window focus of
    +                // Binder.getCallingUid(). Without checking, the user X can't copy any thing from
    +                // INTERNAL_SYSTEM_WINDOW to the other applications.
    +                boolean allowed = mWm.isUidFocused(uid)
    +                        || isInternalSysWindowAppWithWindowFocus(callingPackage);
                    if (!allowed && mContentCaptureInternal != null) {
                        // ...or the Content Capture Service
    -                    allowed = mContentCaptureInternal.isContentCaptureServiceForUser(callingUid,
    -                            userId);
    +                    // The uid parameter of mContentCaptureInternal.isContentCaptureServiceForUser
    +                    // is used to check if the uid has the permission BIND_CONTENT_CAPTURE_SERVICE.
    +                    // if the application has the permission, let it to access user's clipboard.
    +                    // To passed synthesized uid user#10_app#systemui may not tell the real uid.
    +                    // userId must pass intending userId. i.e. user#10.
    +                    allowed = mContentCaptureInternal.isContentCaptureServiceForUser(
    +                            Binder.getCallingUid(), userId);
                    }
                    if (!allowed && mAutofillInternal != null) {
                        // ...or the Augmented Autofill Service
    -                    allowed = mAutofillInternal.isAugmentedAutofillServiceForUser(callingUid,
    -                            userId);
    +                    // The uid parameter of mAutofillInternal.isAugmentedAutofillServiceForUser
    +                    // is used to check if the uid has the permission BIND_AUTOFILL_SERVICE.
    +                    // if the application has the permission, let it to access user's clipboard.
    +                    // To passed synthesized uid user#10_app#systemui may not tell the real uid.
    +                    // userId must pass intending userId. i.e. user#10.
    +                    allowed = mAutofillInternal.isAugmentedAutofillServiceForUser(
    +                            Binder.getCallingUid(), userId);
                    }
                    if (!allowed) {
                        Slog.e(TAG, "Denying clipboard access to " + callingPackage
    
    이 방법에서 isInternalSysWindowAppWithWindowFocus 현재 계정이 응용 프로그램에 초점을 맞출 수 있는지 판단한다.ISInternals Windows AppWith Windows Focus는 Windows Manager가 초점을 맞추는지 확인하기 위해 제어합니다. 초점을 맞출 때 클립보드를 사용할 수 있습니다.
    - boolean allowed = mWm.isUidFocused(callingUid);
    + boolean allowed = mWm.isUidFocused(uid) || isInternalSysWindowAppWithWindowFocus(callingPackage);
    
    
    +    /**
    +     * To check if the application has granted the INTERNAL_SYSTEM_WINDOW permission and window
    +     * focus.
    +     * <p>
    +     * All of applications granted INTERNAL_SYSTEM_WINDOW has the risk to leak clip information to
    +     * the other user because INTERNAL_SYSTEM_WINDOW is signature level. i.e. platform key. Because
    +     * some of applications have both of INTERNAL_SYSTEM_WINDOW and INTERACT_ACROSS_USERS_FULL at
    +     * the same time, that means they show the same window to all of users.
    +     * </p><p>
    +     * Unfortunately, all of applications with INTERNAL_SYSTEM_WINDOW starts very early and then
    +     * the real window show is belong to user 0 rather user X. The result of
    +     * WindowManager.isUidFocused checking user X window is false.
    +     * </p>
    +     * @return true if the app granted INTERNAL_SYSTEM_WINDOW permission.
    +     */
    +    private boolean isInternalSysWindowAppWithWindowFocus(String callingPackage) {
    +        // Shell can access the clipboard for testing purposes.
    +        if (mPm.checkPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW,
    +                    callingPackage) == PackageManager.PERMISSION_GRANTED) {
    +            if (mWm.isUidFocused(Binder.getCallingUid())) {
    +                return true;
    +            }
    +        }
    +
    +        return false;
    +    }
    
    따라서 수정된 Android 10에서는 IME 또는 초점 맞추기 응용 프로그램을 통해서만 클립보드를 얻을 수 있습니다.

    해결 방법


    해결책으로 초점을 맞출 수 있는 시기를 얻으면 클립보드도 얻을 수 있을 것이다.
    Android의 라이프 사이클은 다음 순서로 불립니다.
    onResume에서 아직 화면이 생성되지 않았기 때문에 초점을 얻으려면 onWindows FocusChanged에서 가져와야 합니다.
    라이프 사이클
    타이밍
    onCreate
    활동 시작 시
    onStart
    활동 표시 시
    onResume
    활동이 앞에 있을 때
    onWindowFocusChanged
    초점이 바뀔 때
    onWindows FocusChanged는 초점이 바뀔 때 호출되며,has Focus에서 Focus 값을 얻을 수 있습니다.hasFocus가true일 때 응용 프로그램이 초점을 맞추기 때문에 아래와 같은 글을 통해 화면이 시작될 때 클립보드의 값을 정확하게 얻을 수 있습니다.

    MainActivity.kt
    class MainActivity : AppCompatActivity() {
        // 中略
    
        override fun onWindowFocusChanged(hasFocus: Boolean) {
            super.onWindowFocusChanged(hasFocus)
    
            if (hasFocus) {
                Snackbar.make(root, getClipboard(), Snackbar.LENGTH_SHORT).show()
            }
        }
    
        private fun Context.getClipboard(): String {
            val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
            return clipboard?.primaryClip?.getItemAt(0)?.text?.toString().orEmpty()
        }
    }
    

    총결산


    지금까지 안드로이드는 아무런 문제 없이 클립보드를 사용할 수 있었다.하지만 안전한 관점에서 안드로이드 10도 어려워졌다.
    앞으로 응용 프로그램이 시작될 때 클립보드에서 데이터를 얻을 때onWindowFocusChanged에서 얻을 수 있습니다.

    좋은 웹페이지 즐겨찾기