UnityでAndroidのデータ保存ダイアログを出す方法を実装してみた。

クロ【普通】
前回、自作アプリでのデータが保存できない状態になっていたのでその暫定対応をしました

霊夢【喜び】
今回は暫定対応から正式対応に出来るようにUnityでAndroid10でも保存できる方法を考えてみたよ

魔理沙【楽しい】
本当は今動いているアプリの動作そのままで実装できれば良かったんだけど、ちょっとプログラムの実装情報が少なくてそのままではならなかったけどな

妖夢【普通】
とはいえ、「名前を付けて保存」ダイアログを出すことでユーザーさんの任意な場所や名前で保存できるようになったので、その方法をご紹介したいと思います。

文章データをDocumentディレクトリに保存するよ

保存するファイルの種類に合わせて保存先、保存処理を変更する必要があります

クロ【普通】
詳しいお話は前回のブログを見てください
下に必要な部分だけまた書いています

UnityでAndroidアプリを作る際にAndroid10(Q)だとデータの保存に手こずった話

アプリが保存されている場所以外のディレクトリにアクセスする場合は
アクセスするファイルっごとに設定が必要

ここかイマイチ冬月も理解しきれてないところでは有るのですが、

写真や動画などのメディアデータ

DCIM/(写真データが格納されているディレクトリ)
Movies/(動画データが格納されているディレクトリ)
Pictures/(画像データが格納されているディレクトリ)

この3つのディレクトリに保存するようになりました。
(それ以外のフォルダに保存する方法もあるとは思いますが、まだ調査中です)

ソースコードも

MediaStore.Images

を使って読み書きするようです

音楽などのサウンドデータ

Music/(音楽データが格納されているディレクトリ)
Alarms/(プレイリストやアルバム情報が格納されているディレクトリ)

このあたりに保存する必要があるようです。

ソースコードも

MediaStore.Audio

を使って読み書きするようです。

霊夢【普通】
このあたりね、
保存するファイルのタイプに合わせて保存するディレクトリ先やプログラムを変更する必要があります

今回はDocumentディレクトリかDownloadディレクトリに
保存することにしました

前には書いてなかったんですが、

テキストデータやPDFやWord、Excelなどの文章データ

Document/(文章データが格納されているディレクトリ)
Download/(ブラウザ等からダウンロードした物を格納するディレクトリ)

この2つのディレクトリに保存します。

ソースコードも

Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);

を使って保存していきます。
こちらはタイプを切り替えれば画像なども保存できるみたいです。

アプリのデータ関連や、会計データは文章データなので
こちらのディレクトリに保存しようと思います。

魔理沙【喜び】
お会計データはCSVファイルで、アプリデータはJsonデータになるのでdocumentディレクトリで大丈夫だと思うぞ

保存プログラムを書くよ

妖夢【普通】
今回はAndroidのネイティブ処理を呼び出す必要があるのでJavaも書いていきます
ネイティブコードで保存ダイアログを呼び出す

クロ【悲しみ】
めんどくさいのでソースコードまるごと載せます
色んなサイトから参考にして書いてるので色々ゴッチャゴチャです
Javaコード!

package StudioCross.ReadWriteFiles;

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;

import com.unity3d.player.UnityPlayer;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class ReadWriteFiles extends Fragment
{

private static final String TAG = “unimgpicker”;
/// ファイル生成時のコールバック判断 ///
private static final int CREATE_FILE = 1002;
/// ファイル生成時のコールバック判断 ///
private static final int OPEN_DOCUMENT_REQUEST = 1003;

/// 保存か読み込みかを振り分けるフラグ ///
public boolean _IsSave = false;

/// 保存するデータを一時的に保持する ///
public String _DataText = “”;
/// 保存名を一時的に保持する ///
public String _FileName = “”;

/// 保存状況を数値化したもの ///
public static int _StageNum = 0;
/// 読み込んだテキストデータ ///
public static String _LoadDataText = “”;

/**
* テキストファイルを保存する
* String fileName : 保存するファイル名
* String dataText : 保存するテキストデータ
**/
public static void CallSetCreateTextFile(String fileName, String dataText)
{
// 色々呼び出す
Activity unityActivity = UnityPlayer.currentActivity;

ReadWriteFiles readWriteFiles = new ReadWriteFiles();
readWriteFiles._StageNum = 0;
readWriteFiles._LoadDataText = “”;
readWriteFiles._DataText = dataText;
readWriteFiles._FileName = fileName;
readWriteFiles._IsSave = true;

FragmentTransaction transaction = unityActivity.getFragmentManager().beginTransaction();

transaction.add(readWriteFiles, TAG);
transaction.commit();
}

/**
* テキストファイルを読み込みする
* String fileName : 読み込みするファイル名
**/
public static void CallSetReadTextFile(String fileName)
{
// 色々呼び出す
Activity unityActivity = UnityPlayer.currentActivity;

ReadWriteFiles readWriteFiles = new ReadWriteFiles();
readWriteFiles._StageNum = 0;
readWriteFiles._LoadDataText = “”;
readWriteFiles._DataText = “”;
readWriteFiles._FileName = fileName;
readWriteFiles._IsSave = false;

FragmentTransaction transaction = unityActivity.getFragmentManager().beginTransaction();

transaction.add(readWriteFiles, TAG);
transaction.commit();
}

/**
* 呼び出せたら処理されるコールバック
**/
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);

if(_IsSave == true)
{
SetCreateTextFile(_FileName, _DataText);
}
else
{
SetReadTextFile(_FileName);
}
}

/**
* テキストファイルを保存する
* String fileName : 保存するファイル名
* String dataText : 保存するテキストデータ
**/
private void SetCreateTextFile(String fileName, String dataText)
{
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(“text/plain”);
intent.putExtra(Intent.EXTRA_TITLE, fileName);

startActivityForResult(intent, CREATE_FILE);
}

/**
* テキストファイルを読み込みする
* String fileName : 読み込みするファイル名
**/
private void SetReadTextFile(String fileName)
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType(“text/plain”);
if(fileName != “”)
{
intent.putExtra(Intent.EXTRA_TITLE, fileName);
}

startActivityForResult(intent, OPEN_DOCUMENT_REQUEST);
}

/**
* テキストファイルを保存状況を返却する
* return : 1:成功 0:保存中 -1:保存失敗
**/
public static int CallReadWriteComplete()
{
return _StageNum;
}

/**
* 保存されているテキストデータ
* return : 1:成功 0:保存中 -1:保存失敗
**/
public static String CallReadTextData()
{
return _LoadDataText;
}

/// ファイル作成後にデータを保存する際に呼び出される
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData)
{
super.onActivityResult(requestCode, resultCode,resultData );

// データ作成時
if(resultCode == Activity.RESULT_OK)
{
if(resultData.getData() == null)
{
/// 書き込みが失敗した
_StageNum = -1;
return;
}

Uri uri = resultData.getData();
Context context = getActivity().getApplicationContext();

if (requestCode == CREATE_FILE)
{
try(OutputStream outputStream = context.getContentResolver().openOutputStream(uri))
{
if(outputStream != null)
{
outputStream.write(_DataText.getBytes());
// 書き込みが成功した
_StageNum = 1;

return;
}
}
catch(Exception e)
{
/// 書き込みが失敗した
_StageNum = -1;
}

return;
}
else if (requestCode == OPEN_DOCUMENT_REQUEST)
{
// 読み込み
String str = “”;
StringBuffer buf = new StringBuffer();

try (InputStream inputStream = context.getContentResolver().openInputStream(uri))
{
if (inputStream != null)
{
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
while ((str = reader.readLine()) != null)
{
buf.append(str + “\n” );
}
}
inputStream.close();
}
catch(Exception e)
{
/// 書き込みが失敗した
_StageNum = -1;
return;
}
_LoadDataText = buf.toString();

// 書き込みが成功した
_StageNum = 1;

return;
}
}

/// 書き込みが失敗した
_StageNum = -1;
}
}

なーんかこのコード表示のプラグイン、
インデントが無くなっちゃうんだよねぇ……

霊夢【喜び】
このソースコードをReadWriteFiles.javaと名前をつけて
「XXXXXProject\Assets\Plugins\Android\ReadWriteFiles」に保存するとネイティブ側の対応は完了ね

魔理沙【悲しみ】
ここからは余談だけど、ここに行き着くまで丸一日かかってるぞ、あまりの情報の無さに冬月の白髪が増えたという噂が……

ということで、コードのすべてを解説しちゃうととんでもない長さになるので要点だけ説明します。

Unityの場合、UnityPlayerを継承するけど今回はしませんでした

まずは

public class ReadWriteFiles extends Fragment

クラス定義ですがFragmentを継承します。

データの読み書きの際に、ファイルアクセス時にイベントが発行されるので何かしらのActivityを継承する必要があります。

で、Unityの場合はUnityPlayerを継承してクラスを作るのが一般的ですが他のプラグインを入れたときとか、アセットを適応したときに、そのプログラムがUnityPlayerを継承している時があり、競合が起こってしまうことがあります。

競合してしまうとその解決がとんでもなくめんどくさいので、
今回はFragmentを継承してイベント管理することにしました。

ファイルの書き込みはIntent(Intent.ACTION_CREATE_DOCUMENT)
で行います。

保存本体の処理は、

Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(“text/plain”);
intent.putExtra(Intent.EXTRA_TITLE, fileName);

 

startActivityForResult(intent, CREATE_FILE);

ここになります。

最後の行がファイルが作成されたらイベントを起こすための処理です。

Intent(Intent.ACTION_CREATE_DOCUMENT);

この部分の種類を変えることで
データの保存から読み込みに変更することができます。
今回はファイルを作成して保存の種類に設定しています。

intent.setType(“text/plain”);

ここで保存するファイルのタイプを設定しています。
現在text/plainとなっているように書類データとしています。

下に書き方が有るので任意のファイルに合わせて設定してください。

妖夢【普通】
画像データを保存したい場合は
image/jpeg JPEGファイル(.jpg, .jpeg)
image/png PNGファイル
image/gif GIFファイル
image/bmp bmpファイル
にすると良いと思います

クロ【悲しみ】
拡張子は勝手に入らないのでこちらで設定する必要があるよ
設定を間違えるとデータはテキストデータなのに、種類が画像データみたいな変なデータが出来上がっちゃうから気をつけてね

Qiita Content-Typeの一覧
https://qiita.com/AkihiroTakamura/items/b93fbe511465f52bffaa

データを読み込み終えたらイベントが発行されます

保存時は関数にデータを送るだけでいいのですが
読み込み時はデータを受け取る必要があるのでデータを読み込むまで待つ必要が出てきます。

/// ファイル作成後にデータを保存する際に呼び出される
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData)
{
super.onActivityResult(requestCode, resultCode,resultData );以下略

この関数はデータの保存や読み込みが完了時に呼び出される関数で
読み込み時にここデータの復元を行っています。

データの書き込みの完了タイミングをUnityで調べるため、
上の関数に管理するための変数を用意すると扱いやすいです

詳細の動きはソースコードを見てください。

Unityからネイティブコードを呼び出します

霊夢【普通】
あとは作ったJavaのコードをUnity側で呼ぶだけね
このあたりもそこまで難しくないわ

C#コード!

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// データ保存周りの処理管理
/// </summary>
public class DataStorageProcessingManager : MonoBehaviour
{
/// <summary> インスタンス変数 </summary>
public static DataStorageProcessingManager _Instance;

[Header(“パネル設定”)]
/// <summary> 読み込みオブジェクト </summary>
public GameObject _LoadObject;

#if PLATFORM_ANDROID
/// <summary> Andorid操作 </summary>
private AndroidJavaClass _CallSAFAndroid;
#endif

/// <summary>
/// データの読み込みが完了したときに呼び出される関数
/// </summary>
/// <param name=”dataText”>読み込んだデータ</param>
public delegate void LoadCompleteProcessing(string dataText);
/// <summary> データの読み込みが完了したときに呼び出される関数 </summary>
private LoadCompleteProcessing _LoadCompleteProcessing;

/// <summary>
/// 起動処理
/// </summary>
private void Awake()
{

}

/// <summary>
/// 初期化処理
/// </summary>
void Start()
{
_Instance = this;
#if PLATFORM_ANDROID
_CallSAFAndroid = new AndroidJavaClass(“StudioCross.ReadWriteFiles.ReadWriteFiles”);
#elif PLATFORM_IOS

#else

#endif

_LoadObject.SetActive(false);
}

/// <summary>
/// 更新処理
/// </summary>
void Update()
{

}

/// <summary>
/// 書き込み処理
/// </summary>
/// <param name=”fileName”>ファイル名(拡張子もつける)</param>
/// <param name=”dataText”>保存するデータ</param>
public static void CallSetSave(string fileName, string dataText)
{
if(_Instance != null)
{
_Instance.SetSave(fileName, dataText);
}
}

/// <summary>
/// 読み込み処理
/// </summary>
/// <param name=”fileName”>ファイル名(拡張子もつける)</param>
/// <param name=”loadCompleteProcessing”> データの読み込みが完了したときに呼び出される関数 </param>
public static void CallSetLoad(string fileName, LoadCompleteProcessing loadCompleteProcessing)
{
if (_Instance != null)
{
_Instance.SetLoad(fileName, loadCompleteProcessing);
}
else
{
loadCompleteProcessing(“”);
}
}
#if PLATFORM_ANDROID

/// <summary>
/// 書き込み処理
/// </summary>
/// <param name=”fileName”>ファイル名(拡張子もつける)</param>
/// <param name=”dataText”>保存するデータ</param>
private void SetSave(string fileName, string dataText)
{
_LoadObject.SetActive(true);
_CallSAFAndroid.CallStatic(“CallSetCreateTextFile”, fileName, dataText);

Debug.Log(“書き込み開始”);

StartCoroutine(CallReadWriteComplete(true));
}

/// <summary>
/// 読み込み
/// </summary>
/// <param name=”fileName”>ファイル名(拡張子もつける)</param>
/// <param name=”loadCompleteProcessing”> データの読み込みが完了したときに呼び出される関数 </param>
private void SetLoad(string fileName, LoadCompleteProcessing loadCompleteProcessing)
{
_LoadObject.SetActive(true);
_CallSAFAndroid.CallStatic(“CallSetReadTextFile”, fileName);
_LoadCompleteProcessing = loadCompleteProcessing;

Debug.Log(“読み込み開始”);

StartCoroutine(CallReadWriteComplete(false));
}

/// <summary>
/// 読み込み完了まで待つ
/// </summary>
/// <returns></returns>
private IEnumerator CallReadWriteComplete(bool isSave)
{
int status = 0;
while (status == 0)
{
status = _CallSAFAndroid.CallStatic<int>(“CallReadWriteComplete”);
yield return null;
}

if (status == 1)
{
// 読み込み完了
if (isSave == false)
{
if (_LoadCompleteProcessing != null)
{
_LoadCompleteProcessing(_CallSAFAndroid.CallStatic<string>(“CallReadTextData”));
}
}
}
else
{
// エラーが発生
Debug.Log(“読み書きでエラーが発生”);
}

_LoadObject.SetActive(false);

Debug.Log(“読み書き完了”);
}
#elif PLATFORM_IOS

/// <summary>
/// 書き込み処理
/// </summary>
/// <param name=”fileName”>ファイル名(拡張子もつける)</param>
/// <param name=”dataText”>保存するデータ</param>
private void SetSave(string fileName, string dataText)
{

}

#else

/// <summary>
/// 読み込み
/// </summary>
/// <param name=”fileName”>ファイル名(拡張子もつける)</param>
/// <param name=”loadCompleteProcessing”> データの読み込みが完了したときに呼び出される関数 </param>
private void SetLoad(string fileName, LoadCompleteProcessing loadCompleteProcessing)
{

}

#endif
}

実際に動かすとこんな感じです

気軽にコメントをどうぞ!

この記事に関すること冬月に聞きたいこと等、小さいことでもコメントしていただける嬉しいです。
冬月に直接連絡したい方は下のお問合せフォームをお使いください。(メール送信されます)

内容に問題なければ、下記の「コメントを送信する」ボタンを押してください。
※メールアドレスは公開されることは有りません。

Android | Unity | ゲーム製作の関連記事