読者です 読者をやめる 読者になる 読者になる

vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Unity5】【Android】【Mac】ギャラリーを開いて選択した画像のパスを取得する

前回の最後に触れたIntentを使ってギャラリーを呼び出してみたら結構苦労した、というお話です。

やったこと

  • Unity側でボタンが押されたら、プラグインを使ってギャラリーを開く
  • ギャラリーで画像を選択したら、該当画像のパスを取得する(今回はログ出力のみ)

呼び出す側

今回の記事では取得した画像パスをUnityで受け取る処理には触れないため、前回のままとします(メソッド名は変えた方が良いですね汗)。

ギャラリーを開く

public static void OpenGridView() {
  UnityPlayer.currentActivity.runOnUiThread(new Runnable() {
    public void run() {
      Intent ntnImage = new Intent();
      ntnImage.setType("image/*");
      ntnImage.setAction(Intent.ACTION_GET_CONTENT);
      UnityPlayer.currentActivity.startActivityForResult(ntnImage, 0);
    }
  });
}
  • Intentでタイプをimageとして設定することで、画像を開くことのできるアプリで端末内にある画像を表示します
    (「"image/jpeg"」のように指定すれば、ファイルの拡張子も指定できます)。
  • 「startActivityForResult」を使うとギャラリーを開いて閉じたタイミングで、「onActivityResult」が呼ばれるようになるハズです
    (前回のコードを変更するだけだと呼ばれないのですが、これについては後述)

ギャラリーで選択された画像のパスを取得する

PluginConnector.java

public static void OpenGridView() {
  UnityPlayer.currentActivity.runOnUiThread(new Runnable() {
    public void run() {
      Intent ntnImage = new Intent();
      ntnImage.setType("image/*");
      // 画像のパス取得時のエラー回避のため、Kitkat未満・以降で処理を分ける.
      if (Build.VERSION.SDK_INT < SDKVER_KITKAT) {
        ntnImage.setAction(Intent.ACTION_GET_CONTENT);
        UnityPlayer.currentActivity.startActivityForResult(ntnImage, REQUEST_GALLERY_JELLYBEAN_BELOW);
      } else {
        ntnImage.setAction(Intent.ACTION_OPEN_DOCUMENT);
        ntnImage.addCategory(Intent.CATEGORY_OPENABLE);
        UnityPlayer.currentActivity.startActivityForResult(ntnImage, REQUEST_GALLERY_KITKAT_ABOVE);
      }
    }
  });
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  if(resultCode != RESULT_OK){
    return;
  }
  switch (requestCode)
  {
  case REQUEST_GALLERY_JELLYBEAN_BELOW:
    // 選択した画像のパスを取得する.
    String[] strColumns = {MediaStore.Images.Media.DATA };
    Cursor crsCursor = getContentResolver().query(data.getData(), strColumns, null, null, null);
    if(crsCursor.moveToFirst())
    {
        Log.d("PluginConnector", crsCursor.getString(0));
    }
    crsCursor.close();
    break;
  case REQUEST_GALLERY_KITKAT_ABOVE:
    this.GetSelectedItemPath(data);
    break;
  }
}
@TargetApi(SDKVER_KITKAT)
private void GetSelectedItemPath(Intent data)
{
  // 選択した画像のパスを取得する.
  String strDocId = DocumentsContract.getDocumentId(data.getData());
  String[] strSplittedDocId = strDocId.split(":");
  String strId = strSplittedDocId[strSplittedDocId.length - 1];

  Cursor crsCursor = getContentResolver().query(
      MediaStore.Images.Media.EXTERNAL_CONTENT_URI
      , new String[]{MediaStore.MediaColumns.DATA}
      , "_id=?"
      , new String[]{strId}
      , null);
  if (crsCursor.moveToFirst()) {
    Log.d("PluginConnector", crsCursor.getString(0));
  }
  crsCursor.close();
}
  • ギャラリーが閉じたあと「onActivityResult」が呼ばれるので、引数として渡された「Intent data」を使ってパスを取得します。
  • Kitkat以降でIntentのActionとして「Intent.ACTION_GET_CONTENT」を指定すると、選択した画像がクラウド環境にある場合などにパスが正しく取得できないため、 「Build.VERSION.SDK_INT」を使って処理を分けています。
  • 「startActivityForResult」の第二引数にセットした値が、「onActivityResult」の「int requestCode」として渡されるため、今回のように処理を分けたい場合に利用できます。
  • Kitkat以降での処理に使用している、「DocumentsContract.getDocumentId」がAPI Level 19以降でしか使えないため、メソッドとして切り分けて、「@TargetApi」でAPI Levelを指定しています。

onActivityResultが呼ばれない

上でも少し触れましたが、この状態でアプリを動作させるとギャラリーを開いて画像を選択するところまでは実行できると思いますが、「onActivityResult」が呼ばれずパスが取得できません。

最初全く別のアプリをして作成しており、そこでは正しく動作していたので?となりました。
また、「onActivityResult」だけではなく、「onCreate」などを追加したのですがこれらも呼ばれないようでした。

半日近くかけて原因をあれこれ探したところ、Unityのマニュアルで以下の記述を見つけました。

アプリケーションは Android OS と Unity Android 間で連携する処理の一部もしくは全部を書き換えることができます。これは新しい UnityPlayerActivity の Activity を作成する形で行います。

(引用元:Android 用のプラグインをビルド - Unity - マニュアル)

…もしかして?

と思い、Assets > Plugins > Android のAndroidManifest.xmlを以下のようにUnityPlayerActivityについて書かれたActivityタグを削除したところ、 エラーでクラッシュはしたものの、「onCreate」は自作したPluginConnector.javaのものが呼ばれていました。

AndroidManifest.xml

< ?xml version="1.0" encoding="utf-8"? >
< manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.unity3d.player"
    android:installLocation="preferExternal"
    android:versionCode="1"
    android:versionName="1.0" >
    < supports-screens
        android:smallScreens="true"
        android:normalScreens="true"
        android:largeScreens="true"
        android:xlargeScreens="true"
        android:anyDensity="true"/ >
    < application
        android:theme="@android:style/Theme.NoTitleBar"
        android:icon="@drawable/app_icon"
        android:label="@string/app_name"
        android:debuggable="true" >
        < activity android:name=".PluginConnector"
          android:label="@string/app_name" >
            < intent-filter >
              < action android:name="android.intent.action.MAIN" / >
              < category android:name="android.intent.category.LAUNCHER" / >
            < /intent-filter >
            < meta-data android:name="unityplayer.UnityActivity" android:value="true" / >
        < /activity >
    < /application >
    < uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" / >
    < uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" / >
< /manifest >

結論

どうやら、「onCreate」や「onActivityResult」のように、プログラム側で自動で呼ばれるメソッドをOverrideする場合は、UnityPlayerActivity.javaが呼ばれないようにする必要があるようです。
前回使用したonClickイベントなど、自分で作成したメソッドが呼ばれる場合は特に問題なく実行されるため、より混乱してしまいました。

AndroidManifest.xmlからUnityPlayerActivityの記述を削除しただけだとエラーが発生するため、以下にあるUnityPlayerActivity.javaのコードを、PluginConnector.javaとマージします。

/Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/com/unity3d/player/UnityPlayerActivity.java

PluginConnector.javaの(今回の)最終的なコードは以下の通りです。

PluginConnector.java

package jp.plugincontroller;

import java.io.File;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Window;
import android.view.WindowManager;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;

import com.unity3d.player.UnityPlayer;

public class PluginConnector extends Activity{
    private static final int SDKVER_KITKAT = 19;
    private static final int REQUEST_GALLERY_KITKAT_ABOVE = 0;
    private static final int REQUEST_GALLERY_JELLYBEAN_BELOW = 1;

    protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code

    public static void OpenImageView() {
        UnityPlayer.currentActivity.runOnUiThread(new Runnable() {
            public void run() {
                Intent ntnImage = new Intent();
                ntnImage.setType("image/*");

                if (Build.VERSION.SDK_INT < SDKVER_KITKAT) {
                    ntnImage.setAction(Intent.ACTION_GET_CONTENT);
                    UnityPlayer.currentActivity.startActivityForResult(ntnImage, REQUEST_GALLERY_JELLYBEAN_BELOW);
                } else {
                    ntnImage.setAction(Intent.ACTION_OPEN_DOCUMENT);
                    ntnImage.addCategory(Intent.CATEGORY_OPENABLE);
                    UnityPlayer.currentActivity.startActivityForResult(ntnImage, REQUEST_GALLERY_KITKAT_ABOVE);
                }
            }
        });
    }
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(resultCode != RESULT_OK)
        {
            return;
        }
        switch (requestCode)
        {
            case REQUEST_GALLERY_JELLYBEAN_BELOW:
                // 選択した画像のパスを取得する.
                String[] strColumns = {MediaStore.Images.Media.DATA };
                Cursor crsCursor = getContentResolver().query(data.getData(), strColumns, null, null, null);
                if(crsCursor.moveToFirst())
                {
                    Log.d("PluginConnector", crsCursor.getString(0));
                }
                crsCursor.close();
                break;

            case REQUEST_GALLERY_KITKAT_ABOVE:
                this.GetSelectedItemPath(data);
                break;
        }
    }
    @TargetApi(SDKVER_KITKAT)
    private void GetSelectedItemPath(Intent data)
    {
        // 選択した画像のパスを取得する.
        String strDocId = DocumentsContract.getDocumentId(data.getData());
        String[] strSplittedDocId = strDocId.split(":");
        String strId = strSplittedDocId[strSplittedDocId.length - 1];

        Cursor crsCursor = getContentResolver().query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                , new String[]{MediaStore.MediaColumns.DATA}
                , "_id=?"
                , new String[]{strId}
                , null);

        if (crsCursor.moveToFirst()) {
            Log.d("PluginConnector", crsCursor.getString(0));
        }
        crsCursor.close();
    }
    public static String GetDcimPath()
    {
        File filDcimDir =
                Environment.getExternalStoragePublicDirectory(
                        Environment.DIRECTORY_DCIM);
        // DCIMディレクトリのパスを返す.
        return (filDcimDir.getPath());
    }
    public static void ShowToast()
    {
        // 戻るボタン押下時にトーストを表示.
        UnityPlayer.currentActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(UnityPlayer.currentActivity, "もう一度押すとアプリを終了します", Toast.LENGTH_SHORT).show();
            }
        });
    }
    // --- Copied from UnityPlayerActivity.java ---

    // Setup activity layout
    @Override protected void onCreate (Bundle savedInstanceState)
    {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);

        getWindow().setFormat(PixelFormat.RGBX_8888); // <--- This makes xperia play happy

        mUnityPlayer = new UnityPlayer(this);
        if (mUnityPlayer.getSettings ().getBoolean ("hide_status_bar", true))
        {
            setTheme(android.R.style.Theme_NoTitleBar_Fullscreen);
            getWindow ().setFlags (WindowManager.LayoutParams.FLAG_FULLSCREEN,
                    WindowManager.LayoutParams.FLAG_FULLSCREEN);
        }

        setContentView(mUnityPlayer);
        mUnityPlayer.requestFocus();
    }
    // Quit Unity
    @Override protected void onDestroy ()
    {
        mUnityPlayer.quit();
        super.onDestroy();
    }

    // Pause Unity
    @Override protected void onPause()
    {
        super.onPause();
        mUnityPlayer.pause();
    }

    // Resume Unity
    @Override protected void onResume()
    {
        super.onResume();
        mUnityPlayer.resume();
    }

    // This ensures the layout will be correct.
    @Override public void onConfigurationChanged(Configuration newConfig)
    {
        super.onConfigurationChanged(newConfig);
        mUnityPlayer.configurationChanged(newConfig);
    }

    // Notify Unity of the focus change.
    @Override public void onWindowFocusChanged(boolean hasFocus)
    {
        super.onWindowFocusChanged(hasFocus);
        mUnityPlayer.windowFocusChanged(hasFocus);
    }

    // For some reason the multiple keyevent type is not supported by the ndk.
    // Force event injection by overriding dispatchKeyEvent().
    @Override public boolean dispatchKeyEvent(KeyEvent event)
    {
        if (event.getAction() == KeyEvent.ACTION_MULTIPLE)
            return mUnityPlayer.injectEvent(event);
        return super.dispatchKeyEvent(event);
    }

    // Pass any events not handled by (unfocused) views straight to UnityPlayer
    @Override public boolean onKeyUp(int keyCode, KeyEvent event)     { return mUnityPlayer.injectEvent(event); }
    @Override public boolean onKeyDown(int keyCode, KeyEvent event)   { return mUnityPlayer.injectEvent(event); }
    @Override public boolean onTouchEvent(MotionEvent event)          { return mUnityPlayer.injectEvent(event); }
    /*API12*/ public boolean onGenericMotionEvent(MotionEvent event)  { return mUnityPlayer.injectEvent(event); }

    // ------
}

ま、公式マニュアルはちゃんと読みましょう、ということですね…。

次は取得したパスをUnity側に渡して、画像を表示するところを作ってまとめるつもりです。

参考

プラグインの作成

ギャラリーを開く+選択した画像のパスを取得する

API Level