この記事は「もくもく会ブログリレー」 5 日目 の記事です。

ExpoSDKを使えば簡単に実装が可能

Expoにはexpo-screen-captureという便利なライブラリがあり、これを使うと簡単に実装することができます。

expo-screen-capture
https://docs.expo.dev/versions/latest/sdk/screen-capture/

下記が公式ドキュメントを参考に最低限のコードで実装したプログラムになります。

import { useEffect } from 'react';
import { Alert, Platform, StyleSheet, Text, View } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import * as ScreenCapture from 'expo-screen-capture';
import * as MediaLibrary from 'expo-media-library';

export default function App() {

const hasPermission = async () => {
if (Platform.OS === "android") {
const mediaLibrary = await MediaLibrary.requestPermissionsAsync()
return mediaLibrary.granted
}

const screenCapture = await ScreenCapture.requestPermissionsAsync();
return screenCapture.granted
}

useEffect(() => {
let subscription: ScreenCapture.Subscription

const init = async () => {
const isGranted = await hasPermission()
if (isGranted) {
subscription = ScreenCapture.addScreenshotListener(() => {
Alert.alert("スクショを検知しました")
});
}
}

init()

return () => {
subscription.remove()
}
}, [])

return (

Open up screen capture app!


);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

これでスクリーンショットを行う度にScreenCapture.addScreenshotListener関数が実行され「スクショを検知しました」ってアラートが表示されるはずです。

Permissionの確認いる???

ここでネイティブ実装に慣れてる人達は下記のような疑問を抱くでしょう。

「Androidでスクリーンショットの検知が出来るのってAndroid 14 API level 34からで、Permission権限の許可をユーザーからもらう必要なかったよね?」
「しかも何でMediaLibraryのPermission権限を許可してもらう必要あるの?いらないよね?」

はい、仰る通りです。

ですがExpoSDK expo-screen-captureを使ってると話が変わってきます。

expo-screen-captureについて

スクリーンショットを検知するaddScreenshotListener関数は2020年8月15日に実装されています。

packages/expo-screen-capture/src/ScreenCapture.ts L94
https://github.com/expo/expo/commit/80a34b8180eaeca50803da0e36e13ab5f4a0224d#diff-1802fd2828796f09faebdc8129728adea02105994f65240b11f4f3a2cfcc5affR94-R96

2020年8月15日時点でAndroidのOSバージョンは Android 10 API level 29 となっており、本来ならばスクリーンショットの検知は出来ないはずです。

2019年9月3日Android 10リリース

2020年9月8日Android 11リリース

ではどのようにして実現しているのか、コードを見てみましょう。

addScreenshotListener

ここからは2024年7月現在の最新のコードで説明をします。

expo-screen-captureのKotlinコードにBuild.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKEと条件分岐してるコードが存在します。
https://github.com/expo/expo/blob/4ad19638a7e70b97c383c6af8430a3667557470b/packages/expo-screen-capture/android/src/main/java/expo/modules/screencapture/ScreenCaptureModule.kt#L33C1-L43C6

この条件分岐は至る所にあるのですが、公式ドキュメントを見る限りだと現在実行されてるSDKバージョンと各Android OS毎に割り当てられるVERSION_CODEを比較して、条件を分岐してるようです。

Build.VERSION.SDK_INT
Build.VERSION_CODES.UPSIDE_DOWN_CAKE

UPSIDE_DOWN_CAKEはAndroid 14に該当するので、Android 14だった場合は下記のコードが実行されることになり、Android 14から追加されたスクリーンショットを検知するActivity.ScreenCaptureCallbackがオーバーライドされています。

screenCaptureCallback = Activity.ScreenCaptureCallback {
sendEvent(eventName)
}

条件分岐から外れた場合は下記コードが実行されてます。

https://github.com/expo/expo/blob/4ad19638a7e70b97c383c6af8430a3667557470b/packages/expo-screen-capture/android/src/main/java/expo/modules/screencapture/ScreenCaptureModule.kt#L39C1-L41C10

screenshotEventEmitter = ScreenshotEventEmitter(context) {
sendEvent(eventName)
}

このScreenshotEventEmitterクラスで見るべきコードはcontentObserver L21です。

ここではonChangeがオーバーライドされており、UIメインスレッドに何か変更がある毎にonChangeが実行されるようになっております。

この時にhasPermissionsでPermissionの確認を行なっております。

override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
if (isListening) {
if (!hasPermissions(context)) {
Log.e(“expo-screen-capture”, “Could not listen for screenshots, do not have READ_EXTERNAL_STORAGE permission.”)
return
}
val path = getFilePathFromContentResolver(context, uri)
if (path != null && isPathOfNewScreenshot(path)) {
previousPath = path
onCapture()
}
}
}

後続処理を見ると察しがつきますが、コンテンツの変更を検知し受け取ったuriで画像が存在するか確認を行なった上で、存在してたらスクリーンショットが行われたと判定し、onCapture関数を実行しているわけです。

onCaptureは僕たちがaddScreenshotListenerに渡してる関数にあたります。

なので、画像を参照するためにPermission権限をユーザーに許可してもらう必要があるのです。

でもユーザーにどんな説明をして権限を許可してもらうの?

はい、僕もそう思います。

画像を参照するアプリで元々権限の許可を貰っている場合を除いて、Android13以下では使う場面は限定されると思います。

なので最初に書いたコードは下記条件分岐を追加するのが望ましいと思います。

import * as Device from 'expo-device';
...etc

const hasPermission = async () => {
const isAndroid = Platform.OS === "android"
const { platformApiLevel } = Device

// iOSの場合
if (!isAndroid) {
return true
}
// nullの可能性を考慮
if (!platformApiLevel) {
return false
}
// API levelが34以上の場合はpermisionを聞く必要はない
if (platformApiLevel >= 34) {
return true
}
// API levelが33以下の場合 or 不要ならこのコードは消す
const mediaLibrary = await MediaLibrary.requestPermissionsAsync()
return mediaLibrary.granted
}

まとめ

  • iOSはiOS 7以上であれば使用可能であり、iOS7は既にサポート終了バージョンなので考慮する必要はないと思われる。
  • AndroidはネイティブAPIを使うのであればAndroid 14 API level 34以上から使用可能である。
  • expo-screen-captureを使えばAndroid 13 API level 33以下でも使用可能であるが、Permission問題が出てくる。

明日の記事は、檜山さんの「 CodeCatalyst(Amazon Q)でTerraform」です。