アヒルのある日

株式会社AHIRUの社員ブログです。毎週更新!社員が自由に思いついたことを書きます。

AndroidのStorage Access Framework対応

こんにちは、ちゃらいプログラマです。
アプリ内から写真フォルダにアクセスする際に今まではAndroidManifest.xmlに

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

を設定していましたがGoogleのポリシーNGになりました。
上記の定義がなくてもStorage Access Framework(SAF)に置き換えることでポリシーOKとなりましたので変更箇所を纏めます。

AndroidManifest.xml

// Android12(API32)までは権限確認が必要
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

UnityPlayerActivityを継承したActivity.java

private final int INTENT_IMAGE = 1000;

// C#側で実行される関数
public void open( String gameObjectName, String savePath )
{
    _gameObjectName = gameObjectName;
    _saveFilePath   = savePath;

    // Android13(API33)以降は権限チェック不要でそのまま開始を実行する
    if(Build.VERSION.SDK_INT > 32)
    {
        startActivity();
    }
    else
    {
        if( ContextCompat.checkSelfPermission( UnityPlayer.currentActivity, Manifest.permission.READ_EXTERNAL_STORAGE ) != PackageManager.PERMISSION_GRANTED )
        {
            ActivityCompat.requestPermissions( UnityPlayer.currentActivity, new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE }, INTENT_PERMISSION_READ_EXTERNAL_STORAGE );
        }
        else
        {
            startActivity();
        }
    }
}

// 開始
private void startActivity()
{
    UnityPlayer.currentActivity.runOnUiThread( new Runnable()
    {
        public void run()
        {
            // 写真のみに限定しています
            Intent intent = new Intent();
            intent.setType( "image/*" );
            intent.setAction( Intent.ACTION_OPEN_DOCUMENT );
            intent.addCategory( Intent.CATEGORY_OPENABLE );
            UnityPlayer.currentActivity.startActivityForResult( intent, INTENT_IMAGE );
        }
    });
}

// 開始結果
@Override
protected void onActivityResult( int requestCode, int resultCode, Intent resultData )
{
    super.onActivityResult( requestCode, resultCode, resultData );
    String savePath = "";

    switch( requestCode )
    {
        case INTENT_IMAGE:
            if( resultCode == RESULT_OK )
            {
                Uri uri = resultData.getData();
                if(uri != null)
                {
                    // 永続アクセス許可を付与(再起動後もアクセス可能にする)
                    final int takeFlags = resultData.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                    getContentResolver().takePersistableUriPermission(uri, takeFlags);
                    // ユーザが選択したファイルをコピー
                    savePath = copySafFileToLocal(uri, _saveFilePath + "/tmpImage");
                }
            }
            // 結果をC#側へ伝える
            UnityPlayer.UnitySendMessage( _gameObjectName, "CallbackOpen", savePath );
            break;
        default:
            break;
    }
}

// コピー
private String copySafFileToLocal(Uri uri, String destPrefix)
{
    String savePath = destPrefix;
    try
    {
        ContentResolver resolver = getContentResolver();

        // 拡張子取得
        String mimeType = resolver.getType(uri);
        String ext = ".png";
        if (mimeType != null)
        {
            String[] parts = mimeType.split("/");
            if (parts.length == 2) ext = "." + parts[1];
        }

        savePath += ext;

        try (InputStream inputStream   = resolver.openInputStream(uri);
             OutputStream outputStream = new FileOutputStream(new File(savePath)))
        {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1)
            {
                outputStream.write(buffer, 0, len);
            }
        }
    }
    catch (Exception e)
    {
        savePath = e.toString();
    }

    return savePath;
}

ファイルコピー処理は権限の有無に関係なく共通化できます。
普段Javaを書かないので本当に動くのか…といつもドキドキしながら書きます…

ではまた次回!