Android Srudioのその後の顛末とアプリを使ったファイル出力のやり方

自作の地図アプリを改良しようと思って、Android Studioを動かそうと思ったらパソコンの調子が悪くなって更新したという話を書きました。
alasixosaka.hatenablog.com
パソコンの方は、Windowsを10から11にアップグレードして、今のところ順調に動いています。
メールソフトのThunderbirdだけは、昔の環境が再現できず、しかも、起動するたびにアカウントの設定画面になるという変な状態で、結局Thunderbirdを諦めて、emclientというソフトに乗り換えました。Thunderbirdの引っ越し手順はHPにも書いてあったのですが、どうも環境設定ファイルが通常と異なった状態で保存されていたらしく、メールの保存フォルダとCドライブに分散しているような感じでした(細かいことはわからないのですが)。そのため、環境を元に戻すことを諦めて、新規構築しようとしたのですが、先に書いたように、毎度毎度アカウント設定画面が出て、うんざりしたので、乗り換えました。emclientは無料で使えるPC用のメールソフトとして最近は結構人気があるみたいです。無料で使えるのは個人でアカウントが2つ迄という制限がありますが、自分の場合はアカウントが1つなので特に問題はありませんでした。

Android Studioのインストールではまる

さて、肝心のAndroid Studioですが、これもまたひと悶着ありまして、まず、Windows10で安定に動いた実績のある、バージョン4.2.2をインストールしてみました。
インストール中に、以前の環境を引き継ぐか、新規に構築するか選択する画面が表示されたので、クリーンインストールしたので、新規構築でよいかと思って、新規構築にチェックを入れて先に進みました。たぶん、これが悪かったのだろうと思いますが、以前のプロジェクトのあるフォルダをプロジェクトの保存先に設定して新規プロジェクトを作成しようとすると、Gradleのバージョンがどうのこうのと言ってエラーが出て、新規プロジェクトが作成できない状態になりました。ネットで調べて、gradleの保存場所を調べてそのフォルダをしてしてみてもダメで、別のサイトにはJDKのバージョンが違うとか、フォルダ指定の問題などと書かれていたので、そっちも試してみたが変わらず。しゃあないので、一旦アンインストールして、Windows10ではめちゃ重かった最新版のフラミンゴをインストール。でも、症状は同じ。しかも、今度は環境を引き継ぐか、新規構築するか選択する画面が出なかった。たぶん、バージョンによって出たりでなかったりということはないと思うので、レジストリに情報が残っていてそれを利用したために選択画面が出なかったのではないかと思う。
ところが、以前のフォルダにプロジェクトを保存しないようにして、デフォルトのフォルダを使うようにしたら、何の問題もなく動いた。どうも、プロジェクト保存用のフォルダに何か環境設定に関するファイルがあるようだ(詳しく追及してないのでようだとしか言えないですが)。
とりあえず、これで問題は解決。しかも、エラーが出て動かなかった、ファイル書き込み用のサンプルプログラムが一発で問題なく動く。どうなってんの? という感じだが、結局、新規構築を選んで新しいフォルダを保存先にしたら問題なく使えるということのようだ(これも、ようだとしか言いようがないですが)。
しかも、Windows10ではあんなに重かったのに、軽快に動く。マシンのパワーがアップしたせいなのか、クリーンな環境にしたせいなのか判断しかねるが、とりあえずこれで最新版のフラミンゴが快適に使えるようになった。

ファイル保存用のアプリを作ってみる

元々が、地図アプリにログファイルを残そうという試みだったのですが、Android11から外部へのファイル書き込みの制限が厳しくなって、簡単にできなくなったところから始まったので、とりあえず、小さなアプリを作ってテストをしてみた。長い間アンドロイドアプリを作ってなかったのですっかり頭から抜け落ちていて、最初に買った参考書を引っ張り出してきて読みながら作ることになってしまいました(笑)。
やり方は、2通りあって、SAF(Strage Access Framework)を使う方法と、アプリ固有の内部ストレージないし外部ストレージに保存する方法。内部ストレージの方は容量が限られているということなので、やるとすれば外部ストレージに保存する方法になる。

SAFを使う方法

どちらかというと、SAFを使った方法の方が手軽に行える。その代わり、ファイルを保存するたびにピッカーが起動するので、バックグラウンドでログを逐次保存するという使い方はできない。ログを配列にとりだめしておいて、アプリ終了時に一気にファイルに書き込むというイメージになります。そこで、ボタンをクリックするたびに現在時刻を取得して、それを配列に代入しておいて、別のボタンをクリックすると配列の中身をSAFを使って書き出すというアプリを作ってみました。

作成したアプリの画面

まず、SAFを使うためにはパーミッションが必要なので、Android.manifest.xlmに以下のようにパーミッションを追記します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application
    ~以下略~

main.activityの最初の部分です。

private static final int PERMISSION_WRITE_EX_STR = 1;
ArrayList<String> Timedata = new ArrayList<>();
String str;

最初のPERMISSION_WRITE_EX_STRはパーミッションコードです。ここでは1としていますが、何でも構いません。パーミッションが複数ある場合はコードを区別して別々の数字を使います。
次のArrayListが時刻を格納するための配列です。
最後のstrは時刻を一時的に取得して渡すための変数です。
次がOnCreateです。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        HelloListener listener = new HelloListener();
        button.setOnClickListener(listener);
        Button button2 = findViewById(R.id.button2);
        Save listenner = new Save();
        button2.setOnClickListener(listenner);

        if (Build.VERSION.SDK_INT >= 23) {
            if(ActivityCompat.checkSelfPermission(this,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED)
            {
                ActivityCompat.requestPermissions(this,
                        new String[]{
                                Manifest.permission.WRITE_EXTERNAL_STORAGE
                        },
                        PERMISSION_WRITE_EX_STR);
            }
        }
    }

ボタンは2つあるので、buttonとbutton2を使います。それぞれにリスナーを宣言しています。HelloListenerを使っているのはサンプルのコードをそのまま使ったためで深い意味はありません。
if以下がパーミッションチェックの部分です。作法に従ってパーミッションがなければ、許可のダイアログをだして許可を求めます。

パーミッション許可の画面

次が、そのパーミッション許可ダイアログの部分です。

    @Override
    public void onRequestPermissionsResult(
            int requestCode, String[] permission, int[] grantResults
    ){
        if (grantResults.length <= 0) { return; }
        switch(requestCode){
            case PERMISSION_WRITE_EX_STR: {
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    /// 許可が取れた場合・・・
                    /// 必要な処理を書いておく
                    
                } else {
                    /// 許可が取れなかった場合・・・
                    Toast.makeText(this,

                            "アプリを起動できません....", Toast.LENGTH_LONG).show();
                    finish();
                }
            }
            return;
        }
    }

ここでは許可が取れた場合の処理を何も書いていませんが、一応それでも動きます。あまり良いと思いませんが、ボタンのクリックを待って処理するだけなのでこうなっています。
次にボタンを押したときの処理です。BUTTONと書かれたボタンを押したときの処理は次のようになっています。ボタンがBUTTONとなっているのもサンプルのコードをそのまま使っただけで変えるのが面倒だったのでそうしているだけです。

    private class HelloListener implements View.OnClickListener{
        @RequiresApi(api = Build.VERSION_CODES.N)
        @Override
        public void onClick(View view){
            showTime();
        }
    }

とくに何もなく、showTimeを呼び出しています。
そのshowTimeはこうなっていて、getNowDateで現在時刻を取得してstrに代入し、TextViewに表示しています。それから、Timedataという配列に時刻を追記しています。

    @RequiresApi(api = Build.VERSION_CODES.N)
    private void showTime() {
        TextView text = (TextView)findViewById(R.id.textView);
        str = getNowDate();
        text.setText(str);
        Timedata.add(str);
    }

getNowDateは次のようになっています。Date関数を呼び出して、dfで定義したフォーマットで時刻を返します。

    @RequiresApi(api = Build.VERSION_CODES.N)
    public static String getNowDate() {
        final DateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault());
        Date date = new Date();
        return df.format(date);
    }

次にSAVEと書かれたボタンを押したときの処理です。これが肝心のSAFの処理になります。
saveTimeを呼び出すだけです。

    private class Save implements View.OnClickListener{
        @Override
        public void onClick(View view) {saveTime();}
    }

saveTimeが、SAFの処理そのもので、ACTION_CREATE_DOCUMENTのインテントを発行してピッカーを起動します。
この処理の仕方は、下記のサイトを参考にしました。
pisuke-code.com

    private void saveTime(){
        String fileName = str + ".log";
        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("*/*");
        intent.putExtra(Intent.EXTRA_TITLE, fileName);
        startActivityForResult(intent, 12345);

    }

ピッカーが発行されると下記のようなファイル選択画面が表示されます。

ファイル選択画面

ファイル名はデフォルトで最後に取得した時刻に".log"をつけたものにしています。この画面で変更することもできます。
ファイルの保存先は、”strage\emulated\0\download\”フォルダになります。
最後は保存ボタンを押したときの処理です。onActivityResultにやってきます。このアプリでは、別の処理はないので識別する必要はないのですが、リクエストコードを12345にしてピッカーを呼び出していますので、リクエストコードは12345が返ってきます。また、リザルトコードとリザルトデータも持ってきます。

  public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
        super.onActivityResult(requestCode, resultCode, resultData);

        if (requestCode == 12345 && resultCode == RESULT_OK) {
            if(resultData.getData() != null) {
                // データを作成されたファイルに書き込む
                Uri uri = resultData.getData();

                try (OutputStream outputStream =
                                 getContentResolver().openOutputStream(uri)) {
                    if (outputStream != null) {
                            /// データを書き込み
                        for (int i=0;i<Timedata.size();i++) {
                            outputStream.write(Timedata.get(i).getBytes(StandardCharsets.UTF_8));
                            outputStream.write(",".getBytes(StandardCharsets.UTF_8));
                        }

                    }
                } catch (Exception e) {
                        e.printStackTrace();
                }

                /// トーストで保存された旨を通知
                Toast.makeText(this, "保存場所 : " + uri.getPath(),
                        Toast.LENGTH_LONG).show();
                Timedata.clear();
            }
        }
    }

リザルトデータから、resultData.getData()でファイル格納場所のUriを得ています。
データの書き込みは、outputStreamを使っています。

OutputStream outputStream =getContentResolver().openOutputStream(uri)

として、ファイルを開いて、forループでデータを配列から読みだして書き込んでいます。

outputStream.write(Timedata.get(i).getBytes(StandardCharsets.UTF_8));

その下の行は、データの区切りのためにカンマを出力しています。
この部分の処理は作法としてtry{ ~} catch{~}の処理を使わないといけないようです。
ファイルの中身はこんな感じです。

2023/07/09 13:28:16,2023/07/09 13:28:16,2023/07/09 13:28:18,2023/07/09 13:28:19,

画面のレイアウトはリニアレイアウトを使いました。これも特に意味はなく、入門書のサンプルプログラムをそのまま使っただけです。

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

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp"
        android:layout_marginTop="5dp"
        android:background="#ffffff"

        android:textSize="25sp"/>

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button" />

    <Button
        android:id="@+id/button2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Save" />


</LinearLayout>

長くなってしまったので、もう一つのアプリ固有の外部ストレージを使う方法は、次回に書くことにします。こちらの方は結構苦戦しました。