리치 콘텐츠 받기

그림 1. 통합 API는 특정 UI 메커니즘(터치하여 붙여넣기 메뉴에서 붙여넣기 또는 드래그 앤 드롭 사용)과 관계없이 수신 콘텐츠를 처리할 수 있는 단일 위치를 제공합니다.

사용자는 이미지, 동영상, 기타 생생한 콘텐츠를 좋아하지만 앱에서 이러한 콘텐츠를 삽입하고 이동하는 것은 쉬운 일이 아닙니다. 앱에서 리치 콘텐츠를 더 쉽게 수신할 수 있도록 Android 12 (API 수준 31)에서는 앱이 클립보드나 키보드, 드래그 등 모든 소스의 콘텐츠를 허용할 수 있는 통합 API를 도입했습니다.

OnReceiveContentListener와 같은 인터페이스를 UI 구성요소에 연결하고 콘텐츠가 메커니즘을 통해 삽입될 때 콜백을 받을 수 있습니다. 콜백은 일반 텍스트와 스타일이 지정된 텍스트부터 마크업, 이미지, 동영상, 오디오 파일 등에 이르기까지 코드에서 모든 콘텐츠 수신을 처리하는 단일 장소가 됩니다.

이전 Android 버전과의 호환성을 위해 이 기능을 구현할 때 사용하는 것이 좋은 Core 1.7Appcompat 1.4부터 AndroidX에서도 이 API를 사용할 수 있습니다.

개요

다른 기존 API를 사용하면 메뉴 길게 터치나 드래그와 같은 각 UI 메커니즘에 상응하는 자체 API가 있습니다. 즉, 각 API와 별도로 통합하여 콘텐츠를 삽입하는 각 메커니즘에 유사한 코드를 추가해야 합니다.

다양한 작업 및 구현할 관련 API를 보여주는 이미지
그림 2. 이전에는 앱에서 콘텐츠 삽입을 위한 UI 메커니즘마다 다른 API를 구현했습니다.

OnReceiveContentListener API는 구현할 단일 API를 만들어 이러한 다양한 코드 경로를 통합하므로 개발자는 앱별 로직에 집중하고 나머지는 플랫폼에서 처리하도록 할 수 있습니다.

간소화된 통합 API를 보여주는 이미지
그림 3. 통합 API를 사용하면 모든 UI 메커니즘을 지원하는 단일 API를 구현할 수 있습니다.

또한 이 접근 방식을 사용하면 콘텐츠를 삽입하는 새로운 방법이 플랫폼에 추가될 때 앱에서 지원을 사용 설정하기 위해 코드를 추가로 변경할 필요가 없습니다. 또한 앱이 특정 사용 사례에 관한 전체 맞춤설정을 구현해야 하는 경우 동일한 방식으로 계속 작동하는 기존 API를 계속 사용할 수 있습니다.

구현

API는 단일 메서드 OnReceiveContentListener가 있는 리스너 인터페이스입니다. 이전 버전의 Android 플랫폼을 지원하려면 AndroidX Core 라이브러리에서 일치하는 OnReceiveContentListener 인터페이스를 사용하는 것이 좋습니다.

API를 사용하려면 앱에서 처리할 수 있는 콘텐츠 유형을 지정하여 리스너를 구현합니다.

Kotlin

object MyReceiver : OnReceiveContentListener {
    val MIME_TYPES = arrayOf("image/*", "video/*")
    
    // ...
    
    override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat? {
        TODO("Not yet implemented")
    }
}

Java

public class MyReceiver implements OnReceiveContentListener {
     public static final String[] MIME_TYPES = new String[] {"image/*", "video/*"};
     // ...
}

앱에서 지원하는 콘텐츠 MIME 유형을 모두 지정한 후 나머지 리스너를 구현합니다.

Kotlin

class MyReceiver : OnReceiveContentListener {
    override fun onReceiveContent(view: View, contentInfo: ContentInfoCompat): ContentInfoCompat {
        val split = contentInfo.partition { item: ClipData.Item -> item.uri != null }
        val uriContent = split.first
        val remaining = split.second
        if (uriContent != null) {
            // App-specific logic to handle the URI(s) in uriContent.
        }
        // Return anything that your app didn't handle. This preserves the
        // default platform behavior for text and anything else that you aren't
        // implementing custom handling for.
        return remaining
    }

    companion object {
        val MIME_TYPES = arrayOf("image/*", "video/*")
    }
}

Java

 public class MyReceiver implements OnReceiveContentListener {
     public static final String[] MIME_TYPES = new String[] {"image/*", "video/*"};

     @Override
     public ContentInfoCompat onReceiveContent(View view, ContentInfoCompat contentInfo) {
         Pair split = contentInfo.partition(
                 item -> item.getUri() != null);
         ContentInfo uriContent = split.first;
         ContentInfo remaining = split.second;
         if (uriContent != null) {
             // App-specific logic to handle the URI(s) in uriContent.
         }
         // Return anything that your app didn't handle. This preserves the
         // default platform behavior for text and anything else that you aren't
         // implementing custom handling for.
         return remaining;
     }
 }

앱에서 이미 인텐트와의 공유를 지원하면 콘텐츠 URI 처리를 위해 앱별 로직을 재사용할 수 있습니다. 남아 있는 데이터를 반환하여 이 데이터의 처리를 플랫폼에 위임합니다.

리스너를 구현한 후에는 앱의 적절한 UI 요소에서 리스너를 설정합니다.

Kotlin

class MyActivity : Activity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        val myInput = findViewById(R.id.my_input)
        ViewCompat.setOnReceiveContentListener(myInput, MyReceiver.MIME_TYPES, MyReceiver())
    }
}

Java

public class MyActivity extends Activity {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         // ...

         AppCompatEditText myInput = findViewById(R.id.my_input);
         ViewCompat.setOnReceiveContentListener(myInput, MyReceiver.MIME_TYPES, new MyReceiver());
     }
}

URI 권한

읽기 권한은 OnReceiveContentListener에 전달된 페이로드의 모든 콘텐츠 URI에 대해 플랫폼에서 자동으로 부여 및 해제합니다.

일반적으로 앱은 서비스나 활동에서 콘텐츠 URI를 처리합니다. 장기 실행 처리의 경우 WorkManager를 사용하세요. 이를 구현할 때는 Intent.setClipData를 사용하여 콘텐츠를 전달하고 FLAG_GRANT_READ_URI_PERMISSION 플래그를 설정하여 권한을 타겟 서비스나 활동으로 확장합니다.

또는 현재 컨텍스트 내에서 백그라운드 스레드를 사용하여 콘텐츠를 처리할 수 있습니다. 이 경우 플랫폼에서 권한이 조기에 취소되지 않도록 리스너가 수신한 payload 객체에 대한 참조를 유지해야 합니다.

맞춤 뷰

앱에서 맞춤 View 서브클래스를 사용하는 경우 OnReceiveContentListener를 우회하지 않도록 해야 합니다.

View 클래스가 onCreateInputConnection 메서드를 재정의하는 경우 Jetpack API InputConnectionCompat.createWrapper를 사용하여 InputConnection를 구성합니다.

View 클래스가 onTextContextMenuItem 메서드를 재정의한다면 메뉴 항목이 R.id.paste 또는 R.id.pasteAsPlainText일 때 super에 위임합니다.

키보드 이미지 API와 비교

OnReceiveContentListener API는 기존 키보드 이미지 API의 다음 버전으로 생각할 수 있습니다. 이 통합 API는 키보드 이미지 API의 기능과 일부 추가 기능을 지원합니다. 기기 및 기능 호환성은 Jetpack 라이브러리를 사용하는지 Android SDK의 네이티브 API를 사용하는지에 따라 다릅니다.

표 1. Jetpack에서 지원되는 기능 및 API 수준
작업 또는 기능 키보드 이미지 API에서 지원 통합 API에서 지원
키보드에서 삽입 예(API 수준 13 이상) 예(API 수준 13 이상)
길게 터치 메뉴에서 붙여넣기를 사용하여 삽입 아니요
드래그 앤 드롭을 사용하여 삽입 아니요 예(API 수준 24 이상)
표 2. 네이티브 API에 지원되는 기능과 API 수준
작업 또는 기능 키보드 이미지 API에서 지원 통합 API에서 지원
키보드에서 삽입 예(API 수준 25 이상) 예(Android 12 이상)
길게 터치 메뉴에서 붙여넣기를 사용하여 삽입 아니요
드래그 앤 드롭을 사용하여 삽입 아니요