UnrealEngine Androidで端末内ストレージにスクリーンショットを保存する(Android10)

# 最初に
UE4のバージョンは4.27.1
Androidのバージョンは10になります。

Android端末でのスクリーンショットの方法に関しては
https://twitter.com/pafuhana1213/status/960199124367716353
おかずさんが公開しているサンプルや
https://orenda.co.jp/blog/1791/
こちらの記事でもスクリーンショットの撮影方法に関して書いてくれていますので参考になります。

基本的には上記の記事の方法でスクリーンショットを保存する事は可能なのですが端末内ストレージに保存しようとした場合Android10では
データを保存するストレージ領域が目的・用途によって区分されてセキュリティー的に厳しくなった影響で保存されなくなってしまいました。
requestLegacyExternalStorageフラグをAndroidManifestに追加すれば
"アプリでこのフラグを指定すると、異なるディレクトリや種類の異なるメディア ファイルへのアクセス権の付与など、対象範囲別ストレージに関連する変更を一時的に無効にできます。"
ようですが
"Android 11 をターゲットとするようにアプリを更新すると、requestLegacyExternalStorage フラグは無視されます。"
されるため対応しなければいけません。

スクリーンショット自体はアプリ固有のストレージであれば保存されるのでアプリ仕様によってはそれでもいいかもしれませんがアプリを削除してしまうとスクリーンショットも一緒に消えてしまうのでアプリを消しても残しておきたい場合は端末内ストレージに保存してあげる必要があります。
今回はアプリ内に一度スクリーンショットを保存した後に端末内ストレージに保存する方法を記載します。

# 端末内ストレージに保存
まずコード全文になります。
```
  <gameActivityImportAdditions>
    <insert>
import android.os.Environment;
import android.net.Uri;
import android.media.MediaScannerConnection;
import android.content.ContentValues;
import android.content.ContentResolver;
import android.provider.MediaStore;
import android.provider.MediaStore.Images.Media;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.ByteArrayOutputStream; 
    </insert>
  </gameActivityImportAdditions>

  <gameActivityClassAdditions>
    <insert>
      public boolean AndroidThunkJava_CopyScreenShot(String ScreenShotFolder,String FileName)
      {
        Log.debug("--------------------------- AndroidThunkJava_CopyScreenShot Start ---------------------");
        ContentResolver resolver = getApplicationContext().getContentResolver();
        String path = GetScreenShotPath(ScreenShotFolder,FileName);
        if(path.contains("//"))
        {
          path.replace("//","/");
        }
        Log.debug("ScreenshotPath -> " + path);
        Bitmap bmp = GetBitmapData(path);
        if(bmp == null)
        {
          Log.debug("bmpで画像の取得が出来なかったので保存は出来ません");
          return false;
        }
        
        try
        {
          ContentValues values = new ContentValues();
          values.put(MediaStore.Images.Media.DISPLAY_NAME, FileName);
          values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
          values.put(MediaStore.Images.Media.IS_PENDING, 1);
          values.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/"+ScreenShotFolder);
          Uri collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
          Uri item = resolver.insert(collection, values);
          OutputStream outPutStream = getContentResolver().openOutputStream(item);
          ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
          //ByteArrayOutputStreamに変換する
          Log.debug("--------------------------- bmp.compress ---------------------");
          bmp.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
          //ストリームからバイトにして
          byte[] bytes = byteArrayOutputStream.toByteArray();
          //OutPutStreamに流し込む
          outPutStream.write(bytes);
          outPutStream.close();
          values.clear();
          //ロックの開放
          values.put(MediaStore.Images.Media.IS_PENDING, 0);
          resolver.update(item, values, null, null);
        }
        catch (IOException e) 
        {
          //do something
          Log.debug("AndroidThunkJava_CopyScreenShot 例外発生");
          return false;
        }
           
        Log.debug("---------------------------  AndroidThunkJava_CopyScreenShot Complete ---------------------");
           
        return true;
      }

      public String GetScreenShotPath(String ScreenShotFolder,String FileName)
      {
        try 
        {
          PackageManager packageManager = getApplicationContext().getPackageManager();
          ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
			    Bundle bundle = ai.metaData;
          String ProjectName = "";
          if (bundle.containsKey("com.epicgames.ue4.GameActivity.ProjectName"))
			    {
				    ProjectName = bundle.getString("com.epicgames.ue4.GameActivity.ProjectName");
				    Log.debug( " bundle.getString Found ProjectName = " + ProjectName);
			    }else
          {
            ProjectName = getPackageName();
            ProjectName = ProjectName.substring(ProjectName.lastIndexOf('.') + 1);
            Log.debug( "getPackageName() Found ProjectName = " + ProjectName);
          }
          String path = "";
          if(getFilesDir() != null)
          {
           path = getFilesDir().getAbsolutePath() + "/" + "UE4Game/" + ProjectName +"/" + ProjectName +"/Saved/Screenshots/Android/" +ScreenShotFolder + "/" + FileName;
           File file = new File(path);
            if(file.exists())
            {
              return path;
            }
          }

          if( getExternalFilesDir(null) != null)
          {
            path =  getExternalFilesDir(null).getAbsolutePath() + "/" + "UE4Game/" + ProjectName +"/" + ProjectName +"/Saved/Screenshots/Android/" +ScreenShotFolder + "/" + FileName;
            File file = new File(path);
            if(file.exists())
            {
              return path;
            }
          }
        }
        catch (NameNotFoundException e)
		    {
			    Log.debug( "Failed to load meta-data: NameNotFound: " + e.getMessage());
		    }
		    catch (NullPointerException e)
		    {
			    Log.debug( "Failed to load meta-data: NullPointer: " + e.getMessage());
		    }

        return "";
      }

      public Bitmap GetBitmapData(String FilePath)
      {
          try
          {
            Uri uri = Uri.parse("file://"+FilePath);
            ContentResolver resolver = getApplicationContext().getContentResolver();
            Bitmap bmp = MediaStore.Images.Media.getBitmap(resolver, uri);
            return bmp;
          }
          catch (IOException e)
          {
            Log.debug("GetBitmapDataで例外発生");
            return null;
          }
      }
    </insert>
  </gameActivityClassAdditions>
```
上記のコードはUPLで定義します。
基本的にサンプルを元にスクリーンショットをpngでアプリ内に保存した後にAndroidThunkJava_CopyScreenShotを呼び出して端末内ストレージに保存する流れになります。

GetScreenShotPathにはスクリーンショットを保存しているフォルダ名とスクリーンショットのファイル名を渡してパスを生成しています。
アプリ内に保存した際にパスは取って来れるのですがそのパスではjavaの方で画像を取得する事は出来ない為ここでパスを生成するようにしています。

getFilesDirとgetExternalFilesDirはそれぞれアプリ固有の内部ストレージと外部ストレージのパスを取得する為の処理になります。
これはShippingとDevelopmentでスクリーンショットの保存先が内部ストレージと外部ストレージと変わる為です。
またGetScreenShotPathのProjectNameでBundleと為の両方を使用しているのは
AndroidManifest.xmlで
android:name="com.epicgames.ue4.GameActivity.ProjectName" android:value="hoge"
のmeta-dataの定義があるとそちらの名前でフォルダが作成される為getPackageName()だけで取得しているとパスが合わなくなる為です。

パスを生成した後は
```
public Bitmap GetBitmapData(String FilePath)
      {
          try
          {
            Uri uri = Uri.parse("file://"+FilePath);
            ContentResolver resolver = getApplicationContext().getContentResolver();
            Bitmap bmp = MediaStore.Images.Media.getBitmap(resolver, uri);
            return bmp;
          }
          catch (IOException e)
          {
            Log.debug("GetBitmapDataで例外発生");
            return null;
          }
      }
```
GetBitmapDataでUriを用意してBitmap形式で画像を用意してあげます。

```
ContentValues values = new ContentValues();
          values.put(MediaStore.Images.Media.DISPLAY_NAME, FileName);
          values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
          values.put(MediaStore.Images.Media.IS_PENDING, 1);
          values.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/"+ScreenShotFolder);
```
Bitmapを用意した後はContentValuesでファイルの名前・拡張子・保存先の指定を行い

```
 OutputStream outPutStream = getContentResolver().openOutputStream(item);
          ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
          //ByteArrayOutputStreamに変換する
          Log.debug("--------------------------- bmp.compress ---------------------");
          bmp.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
```
bmp.compressで保存し
```
 //ストリームからバイトにして
          byte[] bytes = byteArrayOutputStream.toByteArray();
          //OutPutStreamに流し込む
          outPutStream.write(bytes);
          outPutStream.close();
          values.clear();
          //ロックの開放
          values.put(MediaStore.Images.Media.IS_PENDING, 0);
          resolver.update(item, values, null, null);
```
outPutStreamで書き出して終了になります。


# 最後に
今回はBitmap形式で保存する方法ですが
ACTION_CREATE_DOCUMENTを使用する事で保存する事が出来ますがこちらがユーザーに保存先とファイル名を決めてもらい保存する流れのようなのであらかじめ決めたフォルダとファイル名で保存したかったので今回はBitmap形式での保存する方法を使いました。
ただ今後AndroidのOSのアップデートによってはACTION_CREATE_DOCUMENTを使用しないと端末内ストレージに保存出来ないようになる事もありそうです。