ダイアログリストからファイルを選択する(アンドロイド地図アプリの開発 その10)

前回、実機を使って端末の向きを検出して地図を回転することを行いました。
alasixosaka.hatenablog.com

ただ、地図は相変わらずのベルリンマップで、実際に使うには日本の地図にする必要があります。
しかし、デフォルトで日本の地図にするだけでは面白くないので、地図ファイルを選択できるようにしてみました。
地図ファイルの選択は以前にもやりましたが、その時はファイルマネージャーを起動してそこからファイルを選ぶという方法でした。
alasixosaka.hatenablog.com
それはそれで目的は達成できるので良いのですが、いろいろ調べていると、もっとスマートな方法があるようなので、今回試してみました。
それがダイアログを表示してその中のリストから選択するという方法です。

ダイアログとは

ダイアログは、警告や許可を求めるときなどに表示されるアレです。開発中のアプリでも、ファイルのアクセスやGPS位置情報のアクセスには許可が必要なので、初回のみですがダイアログが出てきて許可をするかしないかの選択をします。
ダイアログ表示にするメリットとしては、ファイルを拡張子でフィルタリングして必要な拡張子のみ表示させることが可能な点があります。マップ形式のファイルであれば拡張子はmapですし、GPX形式のファイルであれば拡張子はgpxです。なので、マップファイルを選択するときは拡張子がmapのファイルだけを選んで表示させることが可能になります。

まずは小さなアプリから

仕組みを理解するには初めから複雑なことをしていては理解が進まないので、メインアクティビティ一つの小さなアプリから始めます。

public class MainActivity extends AppCompatActivity {
    final String[] items = {"Data1", "Data2", "Data3", "Data4","Data5","Data6","Data7","Data8","Data9","Data10","Data11","Data12","Data13","Data14","Data15"};
    int selected;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.button);
        clickListener listener = new clickListener();
        button.setOnClickListener(listener);


    }

    private class clickListener implements View.OnClickListener{
        @Override
        public void onClick(View view){
            new AlertDialog.Builder(MainActivity.this).setTitle("データを選択してください").setItems(items, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int item) {
                    callToast(item);
                }
            })
            .show();
        }
    }
    // ダイアログで入力した値をtextViewに入れる - ダイアログから呼び出される
    public void callToast(int i){
        Toast.makeText(this,items[i]+"が選択されました",Toast.LENGTH_SHORT).show();
    }
}

ビューはボタンが一つだけのシンプルなものです。

f:id:alasixOsaka:20190907145715j:plain
起動時の画面。ボタンが一つ表示されるだけ。
ボタンをクリックするとダイアログが表示されてリストが出てきます。
f:id:alasixOsaka:20190907145755j:plain
ボタンをクリックするとダイアログが表示され、リストが出てきます。
ここで、なんでもいいから適当にクリックすると、「xxxが選択されました」とトースト表示されます。
f:id:alasixOsaka:20190907145912j:plain
選択したのがトースト表示される。
表示するデータは冒頭でString[] items = {.....}で定義している。ダイアログを表示する部分はpublic void onClick(View view){... の部分で、AlertDialog.Builderをnewして、setTitleでタイトル表示、setItemsでリスト表示を行っている。setItemsは第一引数が表示するリスト(ここでは文字列配列のitems)、第二引数がクリックリスナー。何番目をクリックしたかは、その後の@Override以下の部分でint itemに受け渡される。それをcallToastで処理して表示させている。

ダイアログ表示を別クラスにする

実際の地図アプリはいろいろなクラスが集まって一つのアプリになっている。メインアクティビティではボタンを配置していろいろなことを処理するようにしているので、そこに地図ファイル選択のボタンを配置してファイル選択用のアクティビティを起動できるようするのが最終目的になる。そこで、メインアクティビティとは別にファイル選択専用のアクティビティを作って動かしてみた。また、そうすることで、マップ形式のふぁいるだけでなく、gpx形式のファイルも同じプログラムを使って選択することが可能になる。
しかし、ちょっと複雑になってくると素人の悲しさで途端にあちこちにはままってしまった。
まずは、メインアクティビティから。

public class MainActivity extends AppCompatActivity implements FileListDialog.onFileListDialogListener {
    
    String sdPath = Environment.getExternalStorageDirectory().getPath();
    private final static int REQUEST_PERMISSION = 1002;
    String fillter = "map";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (Build.VERSION.SDK_INT >= 23) {
            // permissionの確認
            checkPermission();
        }
        else {
            setupSearch();
        }




    }

    private void setupSearch(){
        Button button = findViewById(R.id.button);
        clickListener listener = new clickListener();
        button.setOnClickListener(listener);
    }

    private void checkPermission() {
        if (ActivityCompat.checkSelfPermission(this,
                Manifest.permission.READ_EXTERNAL_STORAGE) ==
                PackageManager.PERMISSION_GRANTED){

            setupSearch();
        }
        // 拒否していた場合
        else{
            requestLocationPermission();
        }
    }

    private void requestLocationPermission() {
        if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                Manifest.permission.READ_EXTERNAL_STORAGE)) {
            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                    REQUEST_PERMISSION);

        } else {
            Toast toast = Toast.makeText(this,
                    "許可されないとアプリが実行できません", Toast.LENGTH_SHORT);
            toast.show();

            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,},
                    REQUEST_PERMISSION);

        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        if (requestCode == REQUEST_PERMISSION) {
            // 使用が許可された
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                setupSearch();

            } else {
                // それでも拒否された時の対応
                Toast toast = Toast.makeText(this,
                        "これ以上なにもできません", Toast.LENGTH_SHORT);
                toast.show();
            }
        }
    }

    private class clickListener implements View.OnClickListener{
        private FileListDialog.onFileListDialogListener listener;

        @Override
        public void onClick(View view){
            FileListDialog dlg = new FileListDialog(MainActivity.this);
            dlg.setOnFileListDialogListener(MainActivity.this);
            dlg.show(sdPath,sdPath,fillter);

        }

    }
    
    @Override
    public void onClickFileList(File file) {
        if (file == null) {
            Toast.makeText(this, "ファイルが取得できませんでした", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, file.getName(), Toast.LENGTH_SHORT).show();

        }
    }
}

メインアクティビティは大したことをしていない。はじめのシンプルなアクティビティと同様にメインアクティビティの画面はシンプルなボタン表示しかない。ボタンをクリックするとダイアログを表示する別アクティビティに飛ぶようになっている。

    private class clickListener implements View.OnClickListener{
        private FileListDialog.onFileListDialogListener listener;

        @Override
        public void onClick(View view){
            FileListDialog dlg = new FileListDialog(MainActivity.this);
            dlg.setOnFileListDialogListener(MainActivity.this);
            dlg.show(sdPath,sdPath,fillter);

        }

    }

の部分がそうだ。
あらかじめ、FileListDialog.javaとして別クラスを生成しておき、FileListDialog dlg = new FileListDialog(MainActivity.this)の部分でインスタンスを生成しています。
そして、dlg.setOnFileListDialogListener(MainActivity.this)でインターフェイスを実装しています。
また、dlg.show(sdPath, sdPath, fillter)でダイアログを表示しています。第一引数はディレクトリ、第二引数はタイトル、第三引数がフィルターです。
フィルターは冒頭の部分でfillter="map"として、拡張子がmapのファイルを抽出するようにしています。実はここがはまりどころの一つだった。詳細は後で書くことにする。

ダイアログで選択したファイル名は、

    @Override
    public void onClickFileList(File file) {
        if (file == null) {
            Toast.makeText(this, "ファイルが取得できませんでした", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, file.getName(), Toast.LENGTH_SHORT).show();

        }
    }

の部分でトースト表示している。fileには選択したファイルがファイル形式で格納されている。file.getName()でファイル名が得られるので、 Toast.makeText(this, file.getName(), Toast.LENGTH_SHORT).show();
でトースト表示してやっている。
メインアクティビティでは、FileListDialog.onFileListDialogListenerをimplemetしておき、onClickFileListをオーバーライドすることで、ダイアログで選択したファイルをfileに格納している。
この部分、参考サイトのやり方をまねて書いている。
alldaysyu-ya.blogspot.com

しかし、はじめは、implemetの記述をしておらず、ダイアログが出て選択できるが、そこからファイル名を得ることができずに困っていた。同じように書いてもエラーになるし、理由がわからなかった。
通常、オーバーライドは、継承した親クラスの処理を子クラスが上書きすると説明されている。しかし、メインアクティビティでは、継承しているのは、extends AppCompatActivity と書かれているだけで、onClickFileListとは関係ない。なので@Overrideとするとエラーになる。
ここで、FileListDialog.onFileListDialogListenerをimplementしてやることでオーバーライドができるようになり、FileListDialogクラスの方から選んだファイル名を持ってこれるようになった。こういうやり方もあるのだなと勉強になった。

一方、FileListDialog.javaの方は、ほぼ参考サイトの記述をそのまま使っている。ダイアログ表示の部分はpublic void show (String path, String title,String fillter) {... 以下の部分で、pathでディレクトリのパスをtitleでダイアログに表示するタイトルを、fillterで拡張子のフィルターを受け取っている。
参考サイトと少し変えているところが、拡張子にフィルターをかけるところで、拡張子が一致しているかの判断は、file.getName().endsWith(fill))で行っている。
また、ファイルリスト自体は、File(path).listFiles()で得られるが、フィルタリングしているのでフィルタリング後のファイルリストを作成する必要がある。はじめは、これをやらなかったので、クリックしても別のファイル名が選ばれていて、ありゃりゃとなった。フィルタリング後のファイルリストは、countをカウント変数として、ファイル名がディレクトリか拡張子が一致した場合のみ、finalFileList[count] = fileとして、配列に格納し、カウンタをインクリメントしている。ファイル名の方はアレイリストに加えていって、String [] array = arrayList.toArray(new String[arrayList.size()])で文字列配列に変換し、最後にAlertDialog.Builder()で表示している。

クリックしたときの処理は、public void onClick(DialogInterface dialog, int which) {... 以下の部分で、ディレクトリが選ばれていたらそのディレクトリ内のファイルリストを再度表示し、ファイルが選ばれていたらそのファイルを返す処理を行っている。

public class FileListDialog extends Activity implements View.OnClickListener, DialogInterface.OnClickListener {

    private Context _parent = null;
    private File[] _dialog_file_list;
    private onFileListDialogListener _listener = null;
    private int _select_count = -1;
    private boolean _is_directory_select = false;
    private String fill = "";
    private ArrayList<String> arrayList ;
    private File [] finalFileList;

    public void setDirectorySelect(boolean is){
        _is_directory_select = is;
    }

    public boolean isDirectorySelect(){
        return _is_directory_select;
    }

    public FileListDialog(Context context){
        _parent = context;
    }

    public void show (String path, String title,String fillter) {
        fill = fillter;
        arrayList = new ArrayList<>();
        try {
            _dialog_file_list = new File(path).listFiles();
            if(_dialog_file_list==null){
                if(_listener!=null){
                    //_listener.onClickFileList(null);
                }
            }else {
                finalFileList = new File[_dialog_file_list.length];
                int count = 0;
                String name = "";

                for (File file : _dialog_file_list){
                    //ディレクトリ名ないし拡張子がfillに一致していればリスト化する
                    if(file.isDirectory()||file.getName().endsWith(fill)) {
                        name = file.getName();
                        //ディレクトリ名なら名前の後ろに"/"をつける
                        if(file.isDirectory()){
                            name = name + "/";
                        }
                        //アレイリストにファイル名(ディレクトリ名)を追加
                        arrayList.add(name);
                        //フィルタリング後の最終リストを作成。
                        finalFileList[count] = file;
                        count++;
                    }
                }
                //作成したアレイリストを文字列配列に変換
                String [] array = arrayList.toArray(new String[arrayList.size()]);
                //ダイアログに表示
                new AlertDialog.Builder(_parent).setTitle(title).setItems(array,this).show();
            }
        }catch (SecurityException se){

        }catch (Exception e){

        }
    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        _select_count = which;
        if((_dialog_file_list==null)||(_listener==null)){

        }else {
            //File file = _dialog_file_list[which];
            File file = finalFileList[which];
            //もしディレクトリだったら選択されたディレクトリのファイルリストを再度表示する
            if(file.isDirectory() && !isDirectorySelect()){
                show(file.getAbsolutePath(), file.getPath(),fill);
            //ファイルが選択されていれば、fileを返す。
            }else {
                _listener.onClickFileList(file);
            }
        }

    }

    @Override
    public void onClick(View v) {

    }

    public void setOnFileListDialogListener(onFileListDialogListener listener) {
        _listener = listener;
    }

    public interface onFileListDialogListener{
        public void onClickFileList(File file);
    }
}

メインアクティビティでは、ボタンがクリックされると引数として、検索するディレクトリ、表示するタイトル(ディレクトリ名と同じ)、検索する拡張子を入れて、dlg.show(sdPath,sdPath,fillter)としてやってダイアログ表示させる。
クリックで選ばれたファイルは public void onClickFileList(File file) {...  のところでfileとして返される。

地図アプリに実装する

ファイル選択ができるようになったので、地図アプリの方に実装してみる。ここまでできれば実装自体はさして難しくない。しかし、アプリを起動するたびに毎度毎度地図ファイルを選択するのは芸がないので、いぜんやったのと同じように、SharedPreferenceを使って、マップファイルの初期値を入れておいて、前回選んだファイルが次回の起動時に反映されるようにしておく必要がある。また、もともとのプログラムはベルリンの地図を選ぶように書かれているので、そこの部分をSharedPreferenceから値を拾ってくるように書き換える必要がある。
メインアクティビティでは、ボタンを一つ追加し、”Map File Select"とした。

f:id:alasixOsaka:20190907183054j:plain
メイン画面にファイル選択のボタンを追加

ファイル選択ボタンをbtMapfileSelectとしてリスナ登録しておき、クリック処理のところをswitch(id){... で分岐処理して、ファイル選択ダイアログを出すようにしている。case R.id.btMapfileSelect:以下がその処理の部分で、
fillter="map"として拡張子を”map"として、 Path = sdPath で外部ファイルのルートディレクトリを指定している。

            switch (id) {

                case R.id.btRotationalView:
                    Intent RMV = new Intent(getApplicationContext(),RotateMapViewer.class);
                    startActivity(RMV);
                    break;

                case R.id.btMapfileSelect:
                    fillter="map";
                    SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
                    MAP_FILE = preferences.getString(fillter, "berlin.map");
                    Path = sdPath;
                    FileListDialog dlg = new FileListDialog(MainActivity.this);
                    dlg.setOnFileListDialogListener(MainActivity.this);
                    dlg.show(Path,Path,fillter);


                    break;
            }

ファイル選択後の処理は、SharedPreferenceの処理を追記して、ファイル名を書き換えている。

     public void onClickFileList(File file) {
        if (file == null) {
            Toast.makeText(this, "ファイルが取得できませんでした", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, file.getName(), Toast.LENGTH_SHORT).show();
            SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = preferences.edit();
            editor.putString(fillter, file.getName());
            editor.putString("path", file.getPath());
            editor.apply();

        }
    }

選んだファイルを反映せせる部分は、検索したところ、SImplestMapViewer.javaとSamplesBaseActivity.javaに該当箇所があった。デバッガーを動かして確認したところ、SamplesBaseActivity.javaの方を使っているみたいだったが、念のため両方をSharedPreferenceからとってくるように書き換えた。
それぞれの該当箇所は下記の通り。retuen "berlin.map"となっているところをコメントアウトし、SharedPreferenceからファイル名を引っ張ってくるようにした。

     protected String getMapFileName() {
        SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
        return preferences.getString("map", "berlin.map");
        //return "berlin.map";
    }
     protected String getMapFileName() {
        SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);

        String mapfile = (MainActivity.launchUrl == null) ? null : MainActivity.launchUrl.getQueryParameter("mapfile");
        if (mapfile != null) {
            return mapfile;
        }
        return preferences.getString("map", "berlin.map");
        //return "berlin.map";
    }

ただ、このままでは、ファイルを別ファイルに変えても、マップの中心位置がそのままなので、GPSの最後の位置を覚えておいてそこを中心にするとか工夫が必要になる。そのあたりは、次回以降で行っていきたい。

実機ではうまくいかない

この方法、実機では内部のストレージにしかアクセスできないのでうまくいきませんでした。ファイルマネージャーを使った処理をした時に、散々調べてSDカードのディレクトリを調べる王道がないことをすっかり忘れていました。マップファイルの選択は今のところファイルマネージャーを使うしかなさそうです。したがって、この方法はGPXファイルやPOIのファイルを選択するときに使うことを検討したいと思います。