アンドロイドで任意のファイルのパスを取得する方法

今日のトレーニングは先週に引き続き軽め。ちょっと喘息の発作が出ているっぽいので無理をしないことにした。近所の坂を8.5kmほどウォーク。階段がたっぷりあるコースだったがさすがに距離が短いのでいい汗をかいた程度で終わった。特に書くこともないので、前回に引き続き、アンドロイドアプリ開発の話です。

今回は、アンドロイドアプリで任意のファイルのパスを取得する方法です。任意といっても、外部フォルダに保存されたファイルに限りますが。
さて、前回の記事で、マップ形式のファイルを読み出すのにgetExternalStorageDirectory().getPath()を使いましたが、これでは実機でSDカードに保存したデータを読むことができません。以前はできていたようですが、アンドロイドの仕様が変わって、SDカードは積極的には使いたくないデバイスになったようです。
【android】 SDカードのpathを取得する方法 | 一番かんたんなJava入門
Android:SDカード(外部ストレージ)のパスを取得する方法(機種依存対策) - Qiita

といっても、限られた容量のなかで地図データなどの大きなファイルを本体メモリーに置くのはちょっと抵抗があるものです。SDカードのファイルパスを取得する方法についてもネット上には色々記事がありますが、結局のところ仕様の壁があって、試したところあまりうまくいきませんでした。そこで、ipバイクでも実装されているファイルマネージャーアプリを使って、ファイルパスを取得するという方法に落ち着きました。
今回は、簡単なアプリを使って、外部メモリーに保存されているファイルを読み出してみたいと思います。
例によって、開発環境は下記の通りです。

  • Windows10

まずは、テスト用のファイルを用意します。メモ帳かなんかで、testfile.txtというファイルを作って、ファイルの内容を”test”とでもしておきます。このファイルをDevice file explorer を使って、エミュレータの外部メモリ領域にコピーします。やり方は前回の記事と同じです。
alasixosaka.hatenablog.com


次にアプリの作成です。
アプリとしては、ボタンを1個配置し、ボタンをクリックするとファイルマネージャーが立ち上がり、ファイルマネージャーで先程作った、テスト用のファイルを指定して、その内容を読み出して、トーストで画面に表示するというようなものになります。

まずは、エミュレータにファイルマネージャーをインストールする必要があります。エミュレータにはプレイストアがありませんので自分でファイルマネージャーのapkを拾ってきてインストールする必要があります。
今回は、OIファイルマネージャーというアプリを使いました。あまり深い意味はありません。ipバイクで使っていたので同じものにしただけです。APKファイルをを下記のリンクからダウンロードします。
https://www.apkmirror.com/

APKをインストールするには、adbというコマンドを使います。自分の場合は、c:\user\xxxx\AppData\Local\Android\sdk\platform-toolsというフォルダに入っていました。(xxxxはログイン時のユーザー名 )
Android Studioコマンドプロンプトを開いて、ディレクトリをadbが入っているフォルダに持っていき、下記のコマンドを打ち込みます。例えば、DドライブのdownloadというフォルダにAPKファイルがある場合、
adb install d:\download\org.openintents.filemanager_2.2.2-39_minAPI9(nodpi)_apkmirror.com.apk
ファイル名が長ったらしいですが、これで、OIファイルマネージャーがインストールできました。エミュレータにもアイコンが作成されています。

f:id:alasixOsaka:20190622155825j:plain
エミュレータの画面、OIファイルマネージャがインストールされている。
さて、ここからが本番です。あるアプリから別のアプリを呼び出すには、Intentを指定して呼び出す必要があります。Intentの指定方法には2通りの方法があって、明示的に示す方法と、暗黙的に示す方法があります。例えば、ある地点を検索するとか、音楽を再生するといった、アンドロイドでよく使われる方法は暗黙的に示すことができます。今回は、暗黙的に示す方法をとっています。

具体的なやり方です。

まずは、メイン画面にクリック用のボタンを一つ用意します。xlmファイルに下記のように記述し、ボタンを配置します。今回はLiniear layoutを使いました。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/btchoseFile"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Clic Me"/>


</LinearLayout>

外部ファイルの読出しにはパーミッションが必要ですので、AndroidManifest.xmlに下記の一文を挿入します。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

WRITE_EXTERNAL_STORAGEとなっていますが、今回は読出しだけですのでREAD_EXTERNAL_STORAGEでも同じことになります。

メインのjavaファイルですが、こんな感じです。

public class MainActivity extends AppCompatActivity {
    // 識別用のコード
    private final static int CHOSE_FILE_CODE = 1002;
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
        !=PackageManager.PERMISSION_GRANTED){
            String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
            ActivityCompat.requestPermissions(MainActivity.this, permissions, 1000);
            return;
        }

        final Button clickButton =  findViewById(R.id.btchoseFile);
        clickButton.setOnClickListener(new View.OnClickListener() {

            public void onClick(View view) {
                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
                intent.setType("file/*");
                startActivityForResult(intent, CHOSE_FILE_CODE);
            }
        });

    }
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        try {
            if (requestCode == CHOSE_FILE_CODE && resultCode == RESULT_OK) {
                String filePath = data.getDataString();
                filePath=filePath.substring(filePath.indexOf("storage"));
                String decodedfilePath = URLDecoder.decode(filePath, "utf-8");
                
                String str = readFile(decodedfilePath);
                Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
            }
        } catch (UnsupportedEncodingException e) {
            // 例外処理
            e.printStackTrace();
        }
    }
    public String readFile(String decodedfilePath){
        String str = "";
        try{
            FileInputStream fis = new FileInputStream(decodedfilePath);
            InputStreamReader inputStreamReader = new InputStreamReader(fis, "UTF8");
            BufferedReader reader = new BufferedReader(inputStreamReader);

            String lineBuffer;
            while ((lineBuffer = reader.readLine()) != null){
                str =  lineBuffer;
            }

        }catch (IOException e){
            e.printStackTrace();
        }
        return str;
    }
}

例外処理については、何も処理を書いてないので、パーミッションが許可されないとそのままになってしまいます。また、ファイルが見つからない場合も同様です。
最初、パーミッションが必要だということをすっかり忘れていて、パーミッション処理を書かずにプログラムを走らせてうまくいかないので悩んでいました。デバッグをかけてようやくパーミッションが許可されていないことに気が付いて処理をつけ足してうまく動くようになりました。相変わらず初心者です。
ファイルパスを取得する暗黙的インテントの呼び出しの部分は、

public void onClick(View view) {
                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
                intent.setType("file/*");
                startActivityForResult(intent, CHOSE_FILE_CODE);
            }

です。Intent.ACTION_GET_CONTENT でファイルマネージャーを呼び出します。ここで、ファイルマネージャーが複数ある場合には選択画面が表示されることになっていますが、今回はエミュレータではちょっと不思議な画面が表示されました。
f:id:alasixOsaka:20190622154826j:plain
この画面で、メニュー(三本線)をクリックすると、ファイルマネージャーの選択画面が出ました。

f:id:alasixOsaka:20190622154926j:plain
OIファイルマネージャーの選択画面が表示された
ここで、OIファイルマネージャーを選択するとアプリが立ち上がります。
f:id:alasixOsaka:20190622155019j:plain
OIファイルマネージャーのファイル選択画面。前回使ったberlin.mapも見える。
アプリ上で、目的のファイルを指定してやります。
また、startActivityForResult(intent, REQUEST_CODE)はインテントを呼び出して、値を持ち帰る動作を指定しています。ここでは、ファイルのパスを持ち帰ります。
ファイルマネージャーからのファイルパスを受け取るところは次のようになります。

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        try {
            if (requestCode == CHOSE_FILE_CODE && resultCode == RESULT_OK) {
                String filePath = data.getDataString();
                filePath=filePath.substring(filePath.indexOf("storage"));
                String decodedfilePath = URLDecoder.decode(filePath, "utf-8");
                
                String str = readFile(decodedfilePath);
                Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
            }
        } catch (UnsupportedEncodingException e) {
            // 例外処理
            e.printStackTrace();
        }
    }

ファイルのパスはdata.getDataString()で受け取ります。ただし、OIファイルマネージャーを使うと
content://org.openintents.filemanager/storage/emulated/0/testfile.txt
のように頭に余分なものがくっついているので、
filePath.substring(filePath.indexOf("storage"))としてやって、storage以下の部分だけにしてやります。これをやらないとファイルが見つからず、エラーになります。
次の一文

String decodedfilePath = URLDecoder.decode(filePath, "utf-8");

デバッグモードで見る限り、あってもなくてもいいように思えたのですが、参考にしたサイトに書いてあったので残しています。取った時の動作は確認してません。
何をしているかというと、filePathで得られた目的のファイルのパスをURLDecorderにかけて、いわば翻訳のようなことをしているようです。
最終的には、目的のファイルのパスはdecodedfilePathに入ります。これを次の

String str = readFile(decodedfilePath);
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();

で、readFile(decodedfilePath)を呼び出して、ファイルの中身をstrに読み込んで、トーストで表示するという処理をしています。
readFile()の処理の部分は、FileInputStreamを使って、ファイルの中身を読んでstrに格納してリターンするだけの処理です。

public String readFile(String decodedfilePath){
        String str = "";
        try{
            FileInputStream fis = new FileInputStream(decodedfilePath);
            InputStreamReader inputStreamReader = new InputStreamReader(fis, "UTF8");
            BufferedReader reader = new BufferedReader(inputStreamReader);

            String lineBuffer;
            while ((lineBuffer = reader.readLine()) != null){
                str =  lineBuffer;
            }
            //reader.close();

        }catch (IOException e){
            e.printStackTrace();
        }
        return str;
    }

f:id:alasixOsaka:20190622155121j:plain
ファイルの中身が表示された
次は、この処理を前回の地図表示アプリに組み込んでみます。

参考サイト
intentからファイラーアプリ起動でファイル選択: がらくた研究室
【Android】Intentを発行してファイルパスを取得する - プログラマのはしくれダイアリー
startActivityForResultのrequestCodeを理解する - Qiita
AndroidエミュレータへのAndroidアプリのインストール・アンインストール方法 | mucchinのAndroid戦記