N.Mです.
こちらの記事の後編です.ここでは,Xamarin.Forms
でメディア周りの処理で困ったことと解決策をまとめました.
Twitterへのメディア投稿
参考:https://github.com/kwmt/WebViewInputSample
デフォルトの状態でも、Twitterのメディアの投稿自体はブラウザと同様にできるのですが、JPEG画像はできても、PNGやGIF、動画については投稿できませんでした。デフォルトだと、HTMLの<input>
タグのaccept
オプションで複数種類のメディアをサポートしていても、1番先頭にあるものしか認識されないみたいです。
投稿するメディアの選択ダイアログを開く時には、前回にも出てきたFormsWebChromeClient
クラスのOnShowFileChooser
メソッドが呼ばれるようです。ここを書き換えて、複数種類のメディアを選択できるダイアログのIntent
を起動します。
//FormsWebChromeClientクラス内
//mainActivityはXamarin.AndroidでのMainActivity。FormsWebChromeClientにstatic変数として持たせて、
//MainActivityのOnCreateでそのstatic変数にMainActivity自身を渡す。
//REQUEST_IMAGE_CODEにはFormsWebChromeClient内で適当な値に設定しています。
public override bool OnShowFileChooser(Android.Webkit.WebView webView, IValueCallback filePathCallback, FileChooserParams fileChooserParams)
{
Intent intent = new Intent(Intent.ActionOpenDocument);
intent.AddCategory(Intent.CategoryOpenable);
intent.SetType("*/*");
intent.PutExtra(Intent.ExtraMimeTypes, fileChooserParams.GetAcceptTypes());
mainActivity.intentCallback = filePathCallback;
mainActivity.StartActivityForResult(intent, REQUEST_IMAGE_CODE);
return true;
}
複数種類メディアを選択できるようにするのにintent.PutExtra(Intent.ExtraMimeTypes, fileChooserParams.GetAcceptTypes());
が必要です。また、複数回メディア投稿ボタンを押しても機能するように、MainActivityでIntentのからの結果を処理する際にfilePathCallback.onReceiveValue
メソッドを呼ぶ必要があります。
MainActivity
に変数intentCallback
をもたせて、mainActivity.intentCallback = filePathCallback;
で登録することで、MainActivity
内で呼べるようにしてあります。
MainActivityでは上記で起動したIntent
からの結果を処理するように、下記のようにOnActivityResult
メソッドをオーバーライドします。
//MainActivity内
//intentCallback
//REQUEST_IMAGE_CODEにはFormsWebChromeClientでの値と同じにする。
public IValueCallback intentCallback;
protected override void OnActivityResult(int requestCode, Result resultCode, Intent resultData)
{
if (requestCode == REQUEST_IMAGE_CODE)
{
if (resultCode == Result.Ok)
{
intentCallback.OnReceiveValue(new Android.Net.Uri[] { resultData.Data });
intentCallback = null;
}
else if (resultCode == Result.Canceled)
{
intentCallback.OnReceiveValue(null);
intentCallback = null;
}
}
}
これで、起動したIntent
に対し、ファイルが選ばれた場合も、キャンセルされた場合も処理されるようになっております。FormsWebChromeClient
とMainActivity
を修正することで、JPEG以外のPNGなどの画像や、動画も選択できるようになり、投稿できるようになります。
ストレージ参照の許可
参考:https://docs.microsoft.com/ja-jp/xamarin/android/app-fundamentals/permissions?tabs=windows
画像の保存をできるようにするためには、アプリケーションに権限を付与する必要があり、これもXamarin.Forms
だけではできず、Android側での処理が必要になります。
まず、Androidマニフェストでこのアプリがどの権限を使うかを指定する必要があります。Visual Studioの場合はAndroidプロジェクトのプロパティから指定できます。保存の場合はここでWRITE_EXTERNAL_STORAGE
を指定します。
これだけだと、アプリのユーザが設定で権限を有効にしない限りは、アプリに権限が付与されないので、起動時に権限がなければユーザに権限を付与する許可を得るためのダイアログを開くようにする必要があります。Androidプロジェクト側のMainActivity
のOnCreate
メソッドで以下を呼び出します。
//yourCodeは26である必要はなく、適当な数字で大丈夫ですが、後述のOnRequestPermissionResultでのものと一致させる必要はあります。
const int yourCode = 26;
if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.WriteExternalStorage) != (int)Permission.Granted)
{
ActivityCompat.RequestPermissions(this, new String[] { Manifest.Permission.WriteExternalStorage }, yourCode);
}
許可を得られたかどうかの結果を確認するために、MainActivity
のOnRequestPermissionResult
メソッドをオーバーライドします。
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
const int yourCode = 26;
if (requestCode == yourCode)
{
for (int i = 0; i < grantResults.Length; i++)
{
if (grantResults[i] != Permission.Granted)
{
Android.OS.Process.KillProcess(Android.OS.Process.MyPid());
}
}
}
else
{
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
上記の実装では許可が得られなかったら、if (grantResults[i] != Permission.Granted)
の中で正常に機能しないとしてアプリケーションを落としていますが、保存処理だけできないようにフラグを立てるなどというように修正すれば、一部機能制限に変更することもできます。
UrlからメディアのByte情報を取得
Xamarin.FormsのWebViewだと、画像長押しによる画像の保存ができなかったので、動画ダウンロードもできるようにするために保存処理を自前で作ることにしました。URL自体は画像の場合はHTMLから、動画の場合はTwitter REST APIから取得できましたが、このURLからAndroidに保存するデータ(Byte情報)の取得で少しつまづき、調べました。
どうやら、System.Net.Http
のHttpClient
を利用すれば、実現できるようです。これを使うことで、UrlのところにHttpでデータを要求し、返ってきたレスポンスから画像や動画などのByte情報を取得できます。
//Timeoutの時間も設定できます。
HttpClient httpClient = new HttpClient{Timeout = TimeSpan.FromSeconds(15)};
//取得した画像や動画のByte列を格納する変数
byte[] imageData;
//downloadUrlは画像や動画のUrl
using (HttpResponseMessage httpResponse = await httpClient.GetAsync(downloadUrl))
{
if (httpResponse.StatusCode == System.Net.HttpStatusCode.OK)
{
//正常に取得できたというレスポンス(System.Net.HttpStatusCode.OK)ならデータを取得
imageData = await httpResponse.Content.ReadAsByteArrayAsync();
}
}
DCIMフォルダへのメディアの保存
参考:https://www.c-sharpcorner.com/article/local-file-storage-using-xamarin-form/(PCLStorageについて)
https://forums.xamarin.com/discussion/175085/i-need-to-save-ad-in-image-in-dcim(DCIMのパスについて)
今回はDCIMフォルダにダウンロードしてきた画像や動画を保存することにしました。
ファイルシステムはプラットフォームごとに全然違います。前にXamarin.Android
のみで画像のロードや、ロードした先に追加保存などをやろうとしたときは、画像ロードのIntentから取得できるものがUrlなので、このUrlをMesiaStoreに投げて、パスに変換するなど結構大変でした。少し身構えていましたが、DCIMフォルダに新規に画像を保存するだけなら、そんなに大変ではないみたいです。
またPCLStorage
を使えば、Xamarin.Forms
のプロジェクトで、各プラットフォーム共通の処理として、データ保存処理を書けるようです。(パスの取得は各プラットフォームごとに処理を書く必要がありますが)
PCLStorage
ではXamarin.Forms
プロジェクト内で以下のようにフォルダ作成や保存処理を書くことができます。
//DCIMフォルダの取得(DCIMPathの取得は後述)
IFolder DCIMFolder = await FileSystem.Current.GetFolderFromPathAsync(DCIMPath);
//DCIM内に別につくる保存用フォルダ
IFolder saveFolder;
//保存用フォルダがすでにあれば取得、なければ新規作成
ExistenceCheckResult exist = await DCIMFolder.CheckExistsAsync(saveFolderName);
if (exist == ExistenceCheckResult.FolderExists)
{
saveFolder = await DCIMFolder.GetFolderAsync(saveFolderName);
}
else
{
saveFolder = await DCIMFolder.CreateFolderAsync(saveFolderName, CreationCollisionOption.ReplaceExisting);
}
//保存するファイルを新規作成
IFile file = await saveFolder.CreateFileAsync(saveFileName, CreationCollisionOption.ReplaceExisting);
//ファイルに画像や動画のByte情報書き込み
using (System.IO.Stream stream = await file.OpenAsync(PCLStorage.FileAccess.ReadAndWrite))
{
stream.Write(imageData, 0, imageData.Length);
}
DCIMパスの取得はXamarin.Android
側で行う必要があります。前回も触れたWebViewRenderer
といったカスタムレンダラでパスを取得し、連携したXamarin.Forms
側のクラス(WebView
)に渡せば、大丈夫です。
//Xamarin.AndroidでのDCIMパスの取得
string path = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, "DCIM");
保存後のアルバムへの反映
(なんかのサイトを調べて、知ったはずなのですが、どのサイトか忘れてしまいました...)
ただ保存しただけだと、メディアに保存した画像が表示されません。保存した画像をメディアに通知しないと、Androidを再起動するまではメディアでは表示されません。
この通知はXamarin.Android
のカスタムレンダラ(WebViewRenderer
など)のコンストラクタで渡されるContext
から行うことができます。
Xamarin.Forms
で呼び出すならば、以下を呼び出すAction
をWebViewRenderer
で作っておき、WebViewRenderer
と連携しているWebView
に作ったAction
を渡すようにしましょう。
//_contextというメンバ変数をWebViewRendererに用意しておき、コンストラクタで引数のContextを代入しておく。
//imagePathは、「DCIMフォルダへのメディアの保存」のプログラムにあるIFile型のfileからfile.Pathで取得できる。
_context.SendBroadcast(new Intent(Intent.ActionMediaScannerScanFile, Android.Net.Uri.Parse("file://" + imagePath)));
まとめ
PCLStorage
を使えば、ファイル保存処理は共通処理としてXamarin.Forms
に書けますが、メディア周りに関しても、少し複雑なことをしようとすると、すぐXamarin.Android
などで各プラットフォームごとに処理を書かないといけないみたいです。