0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Trying Embedded Photo Picker

Posted at

概要

Embedded Photo PickerはAndroid 16で追加された新しいAPIです。(Android Developers Blog記事)

API Referenceを見るとU Extensions 15の記載もありますのでいずれAndroid 14でもSDK Extensionsから使えそうです使えました。

大きな特徴はパーミッションの取得なしでPhoto PickerをアプリのUIに組み込めるところでしょう。自前でPhoto Pickerを実装するより柔軟性は落ちますがEmbedded Photo Pickerでもある程度はカスタマイズ可能です。1

実装方法

まだドキュメントがないので手探りで実装してみました。

Embedded Photo Pickerを表示

まずEmbedded Photo Pickerを表示するにはSurfaceViewが必要です。layout xmlかComposeのAndroidView内で定義しましょう。

また、SurfaceView上のタッチイベントに反応するためにSurfaceView#setZOrderOnTop()はtrueを設定しておきましょう。

// It's necessary for handling touch and click events
surfaceView.setZOrderOnTop(true)

次にEmbeddedPhotoPickerProviderFactoryからEmbeddedPhotoPickerProviderのインスタンスを取得します。

val provider = EmbeddedPhotoPickerProviderFactory.create(context)

EmbeddedPhotoPickerProvider#openSession()関数からEmbedded Photo Pickerを提供しているMediaProviderシステムと接続します。
EmbeddedPhotoPickerFeatureInfoはEmbedded Photo Pickerをカスタマイズするための各種オプションを設定できます。
EmbeddedPhotoPickerClientはcallbackインターフェースでEmbedded Photo Pickerから呼び出されるのでアプリ側で処理を実装します。(後述)

val hostToken = surfaceView.hostToken
val width = surfaceView.width
val height = surfaceView.height
val featureInfo = EmbeddedPhotoPickerFeatureInfo.Builder().build()
val clientExecutor = Executors.newSingleThreadExecutor()
val callback = EmbeddedPhotoPickerClientImpl()

provider.openSession(hostToken, displayId, width, height, featureInfo, clientExecutor, callback)

class EmbeddedPhotoPickerClientImpl() : EmbeddedPhotoPickerClient {
    // 省略
}

また、この際にEmbeddedPhotoPickerProvider内部でbindServiceを使ってMedia Providerに接続するので以下の設定がAndroidManifest.xmlに必要です。

<manifest>
    <queries>
        <intent>
            <action android:name="com.android.photopicker.core.embedded.EmbeddedService.BIND" />
        </intent>
    </queries>
<manifest>

セッションが開かれるとEmbeddedPhotoPickerClient#onSessionOpened()関数がEmbeddedPhotoPickerSessionのインスタンスとともにコールバックされます。この時に渡されるsessionはEmbedded Photo Pickerを制御するための関数が定義されているのでどこかに保持しておきます。そしてsession.surfacePackageSurfaceView#setChildSurfacePackage()関数に渡すことでEmbedded Photo PickerのUIがSurfaceView上に描画されます。

class EmbeddedPhotoPickerClientImpl() : EmbeddedPhotoPickerClient {
    private var session: EmbeddedPhotoPickerSession? = null

    override fun onSessionOpened(session: EmbeddedPhotoPickerSession) {
		// To render photo picker on the surfaceView
        surfaceView.setChildSurfacePackage(session.surfacePackage)
        this.session = session
    }
}

写真の選択・決定

ユーザーがPhoto Picker上で選択した写真のUriやOKボタンを押したイベントはEmbeddedPhotoPickerClientを通してアプリに伝えられます。

onUriPermissionGranted()とonUriPermissionRevoked()ではユーザーが選択 or 選択解除した写真のUriのリストが渡されます。

そして、ユーザーがOKボタンをクリックするとonSelectionComplete()が呼ばれるのでその際には選択された写真をアプリに保存するなどの処理を行います。2

処理を終わった後に選択を解除したい場合はEmbeddedPhotoPickerSession#requestRevokeUriPermission()を呼び出すことで選択を解除できます。

class EmbeddedPhotoPickerClientImpl : EmbeddedPhotoPickerClient {
    private var session: EmbeddedPhotoPickerSession? = null
    private var selectedPhotoUris: List<Uri> = emptyList()

    override fun onSelectionComplete() {
        val uris = selectedPhotoUris
		// TODO: Do something on the background thread
        session?.requestRevokeUriPermission(uris)
        selectedPhotoUris = emptyList()
    }

    override fun onUriPermissionGranted(uris: MutableList<Uri>) {
        selectedPhotoUris = buildList {
            addAll(selectedPhotoUris)
            uris.forEach {
                if (!selectedPhotoUris.contains(it)) {
                    add(it)
                }
            }
        }
    }

    override fun onUriPermissionRevoked(uris: MutableList<Uri>) {
        selectedPhotoUris = buildList {
            addAll(selectedPhotoUris)
            uris.forEach {
                remove(it)
            }
        }
    }
}

キャプチャできるのでは?

アプリ内のUIだからキャプチャしたらパーミッション無しで写真読めるのでは?と試したのですがキャプチャできません。

キャプチャする方法はいろいろありますがPixelCopyでSurfaceViewをキャプチャしようとしてもERROR_SOURCE_NO_DATAで失敗しました。

他の方法も試しましたがSurfaceView領域は何も描画されません。

ただし、Power + Volume Down長押しなどユーザーが手動でキャプチャするのは可能です。

その他

  • SurfaceView#getHostToken()はdeprecatedになっていてAttachedSurfaceControl.getInputTransferToken()を使えと書いてあるけどInputTransferTokenは型が違うのでサンプルでは仕方なくSurfaceView#getHostToken()を使っている...
  • 初期状態だとPhoto Pickerがほとんどスクロールしないし選択ボタンも表示されなくて使い方がよくわからない... Expandedをtrueに設定すると期待通りに動作します。
  • sessionをclose()してしまうとPhoto Pickerの状態(スクロール位置, 選択中の写真等)が失われてしまうので状態を残したい場合はsessionを維持しておく必要がある
  • Embedded Photo PickerのUIの実装はこちらから確認できます。システム側でも普通にCompose使ってるんですね。

Sample app

雑なコードですが一応動きます
https://github.com/tsuyosh/embedded-photo-picker-sample

まとめ

別プロセスでUIを構築してそれをアプリ内に描画するやり方はアプリのUXを改善しつつユーザーのプライバシーも保護できる面白いやり方だと思いました。3

Reference

  1. Light or Darkモード、アクセントカラー、MIMEタイプ、選択可能な最大数などなど

  2. 渡されたUriはデータにアクセスできる期間が限られるのですぐに取り込んでおきましょう。

  3. Privacy Sandboxも同じような手法で広告を描画しているようです

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?