Sibainu Relax Room

愛犬の柴犬とともに過ごす部屋

Android Handler/Looper を使ってProgressBar を実装

朝の散歩でよく合う柴犬の女子です。我が家の柴犬は活発な犬は苦手で「もう勘弁してくれよ」という顔して後ろ向きの気持ちになっています。年のせいもあるかもしれません。

概要

NAS にアップロードするときに、進行状況を告知するプログレスバーを入れてみることを考えてみました。

スレッド間の通信は調べてみると Handler と Looper を使うらしいということが分かりました。

なんとかそれらしくできましたので記録します。

WEBのみでは断片的で覚えにくいので最初に購入した Kotlin の本です。

Handler と Looper

Looper

端的に言えば、特定のスレッドの中でメッセージを回すメッセージループを表すクラスです。

Handler

Looper と対になってLooperのメッセージループにメッセージを投げ、メッセージを受け取る働きをするクラスです。

私の感覚では、次の図のイメージがあります。

class FtpAccess を修正

NAS へのアップロードのコードは次のようにしています。

これでは、アップロードの進行状況を把握することはできませんので、OutPutStream を使っての書き込み量を把握できるようにします。

//NASにファイルをアップロード
try {
    // staoreFile関数の呼び出しでアップロードする。
    ftpClient.storeFile(_filename, _ips);
    // ログ出力
    Log.d(TAG, "Upload - " + _filename);
    infolist.add("Upload - " + _filename);
} catch(Exception e){
    e.printStackTrace();
    infolist.add(e.toString());
}

上のコード部分を次のように修正します。

InputSream(_ips)は既にできているので、ByteArrayOutputStream を使ってバイト配列のバッファを作ります。

作り始めたときは、格納する配列の要素数が分からないので byte[1] としていましたが、必要数を測りながら正確に確保できる方法がありましたので byte[1024] とします。

try {
     //// staoreFile関数の呼び出しでアップロードする。
     //ftpClient.storeFile(_filename, _ips);
     //infolist.add("Upload - " + _filename);

     byte[] byteVal = new byte[1024];
     int num;
     //バイト配列のバッファを作成します
     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
     while ((num = _ips.read(byteVal, 0, byteVal.length)) > 0) {
         byteArrayOutputStream.write(byteVal, 0, num);
     }
     byteArrayOutputStream.close();

     //バッファからバイト配列を作成します
     byte[] b = byteArrayOutputStream.toByteArray();

     //OutputStream を作成します
     OutputStream os = ftpClient.storeFileStream(_filename);

     int bufferLength = 1024;
     int bytesLength = b.length;
     for (int i = 0; i < bytesLength; i += bufferLength) {
         //進行率を計算します
         int progress = (int)((i / (float) bytesLength) * 100);

         //ハンドラーにメインスレッドで実行してほしいことを POST します
         //することは PostExecutor のrunnable に書きます
         //_handler.post(new PostExecutor(progress));
         //↑↑ 上の式では一瞬で終わってしますので、post毎に1ミリ秒積算して遅らせることにしました
         delaycount += 1;
         _handler.postDelayed(new PostExecutor(progress), delaycount * 1);

         //NAS に書き込みます
         if (bytesLength - i >= bufferLength) {
             os.write(b, i, bufferLength);
         }else{
             os.write(b, i, bytesLength - i);
         }
     }

     //小数点の関係上ここまでの progress の値は 99 なので 100 にします
     //_handler.post(new PostExecutor(100));
     //↑↑ 上の式では一瞬で終わってしますので、post毎に1ミリ秒積算して遅らせることにしました
     delaycount += 1;
     _handler.postDelayed(new PostExecutor(100), delaycount * 1);

     os.flush();
     os.close();

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

     infolist.add(e.toString());
 }

クラスのメンバーに Handler が必要ですので、コントラクタの引数に Handler handle を加えています。

copy

    public class FtpAccess implements Callable<String> {
        private final String _ftpUsername;
        private final String _ftpPassword;
        private final String _ftpServer;
        private final String _ftpDirectory;
        private final String _filename;
        private final InputStream _ips;
        private final Handler _handler;
        //コンストラクタです
        public FtpAccess(String ftpUsername,
                         String ftpPassword,
                         String ftpServer,
                         String ftpDirectory,
                         String filename,
                         InputStream ips,
                         Handler handler){
            _ftpUsername = ftpUsername;
            _ftpPassword = ftpPassword;
            _ftpServer = ftpServer;
            _ftpDirectory = ftpDirectory;
            _filename = filename;
            _ips = ips;
            _handler = handler;
        }
        // 非同期内でセットした文字列を返す
        @WorkerThread
        @Override
        public String call() {
            //ファイルの一覧表を返します
            ArrayList<String> infolist = new ArrayList<>();
            FTPClient ftpClient = new FTPClient();
            try {
                //デフォルト ポートでリモート ホストに接続され、システムに割り当てられたポートで現在のホストから発信されるソケットを開きます
                ftpClient.connect(_ftpServer);
                //指定されたユーザーとパスワードを使用して FTP サーバーにログインします
                ftpClient.login(_ftpUsername, _ftpPassword);
                //データ転送を行うために接続するデータ ポートを開くようにサーバーに指示されます
                ftpClient.enterLocalPassiveMode();
                //多くの FTP サーバーはデフォルトで BINARY になっています
                ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
                //文字化け対策
                ftpClient.setControlEncoding("CP932");
                //ディレクトリ移動
                ftpClient.changeWorkingDirectory(_ftpDirectory);
                //ファイル一覧の取得
                String[] filenames;
                filenames = ftpClient.listNames();

                for (String filename : filenames) {
                    Log.d("ServerFileAccess", filename);
                    infolist.add(filename);
                }

                //NASにファイルをアップロード
                try {
                    //// staoreFile関数の呼び出しでアップロードする。
                    //ftpClient.storeFile(_filename, _ips);

                    Log.d("byte", "byte - 1");
                    byte[] byteVal = new byte[1024];
                    int num;
                    //バイト配列のバッファを作成します
                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                    while ((num = _ips.read(byteVal, 0, byteVal.length)) != -1) {
                        byteArrayOutputStream.write(byteVal, 0, num);
                    }
                    byteArrayOutputStream.close();

                    //バッファからバイト配列を作成します
                    byte[] b = byteArrayOutputStream.toByteArray();

                    //OutputStream を作成します
                    OutputStream os = ftpClient.storeFileStream(_filename);
                    int bufferLength = 1024;
                    int bytesLength = b.length;
                    int delaycount = 0;
                    for (int i = 0; i < bytesLength; i += bufferLength) {
                        //進行率を計算します
                        int progress = (int)((i / (float) bytesLength) * 100);

                        //ハンドラーにメインスレッドで実行してほしいことを POST します
                        //することは PostExecutor のrunnable に書きます
                        //_handler.post(new PostExecutor(progress));
                        //↑↑ 上の式では一瞬で終わってしますので、post毎に1ミリ秒積算して遅らせることにしました
                        delaycount += 1;
                        _handler.postDelayed(new PostExecutor(progress), delaycount * 1);

                        //NAS に書き込みます
                        if (bytesLength - i >= bufferLength) {
                            os.write(b, i, bufferLength);
                        }else{
                            os.write(b, i, bytesLength - i);
                        }
                    }

                    //小数点の関係上ここまでの progress の値は 99 なので 100 にします
                    //_handler.post(new PostExecutor(100));
                    //↑↑ 上の式では一瞬で終わってしますので、post毎に1ミリ秒積算して遅らせることにしました
                    delaycount += 1;
                    _handler.postDelayed(new PostExecutor(100), delaycount * 1);

                    os.flush();
                    os.close();

                } catch(Exception e){
                    e.printStackTrace();
                    infolist.add(e.toString());
                }
            } catch (Exception e) {
                e.printStackTrace();
                infolist.add(e.toString());
            } finally {
                infolist.add("処理を終了します");
                try {
                    ftpClient.logout();
                    ftpClient.disconnect();
                } catch (Exception e) {
                    infolist.add(e.toString());
                }
            }
            return join("\n",infolist);
        }
    }

class PostExecutor

WorkerThread から呼び出されるコールバック関数です。

run 関数の中にしたいことを書きます。

今回はプログレスバーに呼び出し毎に依頼があるprogressの値をセットしています。

copy

    private class PostExecutor implements Runnable {
    //進行率を受け取るためメンバーを作ります
        int _progress;

        //コンストラクタ
        public PostExecutor(int progress) {
            _progress = progress;
        }

        //メインスレッドであることをアノテーションします
        @UiThread
        @Override
        public void run() {
            // プログレスバーに値をセットします
            progressbar.setProgress(_progress, true);

      //この場合効果ありませんでしたので止めます
            //Animator animation = ObjectAnimator.ofInt(progressbar,"progress", _progress);
            //animation.setDuration(1000); // 1秒間でアニメーションする
            //animation.setInterpolator(new DecelerateInterpolator());
            //animation.start();
        }
    }

uploadNas

ハンドラー・ルーパーの構築は、onCreate の中でしてもよかったのでしょうが WorkerThread の直近の UiThread の関数の中で作ることにしました。

この部分を修正します。

FtpAccess backgroundReceiver = new FtpAccess(ftpUsername,
                                             ftpPassword,
                                             ftpServer,
                                             ftpDirectory,
                                             filename,
                                             ips);
//executorService を作成
ExecutorService executorService  = Executors.newSingleThreadExecutor();
//executorService.submit の引数に JsonPostHttp の実体を渡します
//バックグラウンドで非同期処理が行われ戻り値が future に格納されます
Future<String> future = executorService.submit(backgroundReceiver);

追加コードは2行のみで、これだけでハンドラー・ルーパーを構築します。

そして、FtpAccess の引数に修正したハンドラーを渡しています。

// ワーカースレッドで取込みを開始する
// ハンドラー・ルーパーを構築します
Looper mainLooper = Looper.getMainLooper();
Handler handler = HandlerCompat.createAsync(mainLooper);

//クラス FtpAccess の実体を作成
FtpAccess backgroundReceiver = new FtpAccess(ftpUsername,
                                             ftpPassword,
                                             ftpServer,
                                             ftpDirectory,
                                             filename,
                                             ips,
                                             handler);

//FtpAccess backgroundReceiver = new FtpAccess(
//                                             ftpUsername,
//                                             ftpPassword,
//                                             ftpServer,
//                                             ftpDirectory,
//                                             filename,
//                                             ips);

//executorService を作成
ExecutorService executorService  = Executors.newSingleThreadExecutor();
//executorService.submit の引数に JsonPostHttp の実体を渡します
//バックグラウンドで非同期処理が行われ戻り値が future に格納されます
Future<String> future = executorService.submit(backgroundReceiver);

copy

    @UiThread
    private void uploadNas(String ftpUsername,
                           String ftpPassword,
                           String ftpServer,
                           String ftpDirectory,
                           String filename,
                           InputStream ips) {
        
        // ハンドラー・ルーパーを構築します
        // ワーカースレッドで取込みを開始する
        Looper mainLooper = Looper.getMainLooper();
        Handler handler = HandlerCompat.createAsync(mainLooper);

        //クラス FtpAccess の実体を作成
        FtpAccess backgroundReceiver = new FtpAccess(
                ftpUsername,
                ftpPassword,
                ftpServer,
                ftpDirectory,
                filename,
                ips,
                handler);
    
        //FtpAccess backgroundReceiver = new FtpAccess(
        //        ftpUsername,
        //        ftpPassword,
        //        ftpServer,
        //        ftpDirectory,
        //        filename,
        //        ips);

        //executorService を作成
        ExecutorService executorService  = Executors.newSingleThreadExecutor();
        //executorService.submit の引数に JsonPostHttp の実体を渡します
        //バックグラウンドで非同期処理が行われ戻り値が future に格納されます
        Future<String> future = executorService.submit(backgroundReceiver);

        String result = "";
        try {
            //future から値を取り出します
            result = future.get();
        }
        catch(ExecutionException | InterruptedException ex) {
            Log.w(TAG, "非同期処理結果の取得で例外発生: ", ex);
        }
        resultInfo(result);
    }

実行結果

上は起動直後の画像です。

下は Shutter をタップした結果です。撮影が行われ NAS へ撮影した画像ファイルが保存されたところです。

プログレスバーは灰色から100%完了で黄色になりました。バーの左から右に黄色が走るのが見えましたが一瞬の出来事でした。

機能しているようですが、1~2メガバイトの写真では一瞬でした。

_handler.post(new PostExecutor(progress));

の式では一瞬で終わってしますので、post毎に1ミリ秒積算して遅らせることにしました。

delaycount += 1;
_handler.postDelayed(new PostExecutor(progress), delaycount * 1);

こうすることで3秒程で左から右へ走らせることができました。

このままでは、2MBのイメージファイルだと2000回 POST していますので、必要のない POST を繰り返してどうかなと思います。

このあたりの調整は、アップロードするバイト数は事前に分かっていますので、全体の 100分の1 あるいは 50分の1 を超えた毎、例えば2MBなら20KB あるいは 40KB 毎に postDelayed するとか、表現方法はいろいろ考えられます。

layout/activity_main.xml

レイアウトはこんな感じで作っています。

xmlファイルも掲載しておきます。

copy

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/guideline3"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </androidx.camera.view.PreviewView>

    <Button
        android:id="@+id/bt_shutter"
        android:layout_width="100dp"
        android:layout_height="48dp"
        android:layout_marginTop="5dp"
        android:layout_marginEnd="5dp"
        android:text="@string/bt_shutter"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/bt_post"
        android:layout_width="100dp"
        android:layout_height="48dp"
        android:layout_marginEnd="5dp"
        android:text="@string/bt_post"
        app:layout_constraintBottom_toTopOf="@+id/scrollView2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toBottomOf="@+id/bt_shutter" />

    <ScrollView
        android:id="@+id/scrollView2"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="@+id/guideline3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="@+id/guideline2"
        app:layout_constraintVertical_bias="1.0">

        <TextView
            android:id="@+id/tvinfo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="TextView" />
    </ScrollView>

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.85" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.20" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.88" />

    <SeekBar
        android:id="@+id/seekBar"
        style="@style/Widget.AppCompat.SeekBar.Discrete"
        android:layout_width="0dp"
        android:layout_height="29dp"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        android:max="10"
        android:progress="3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/progressBar"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/viewFinder"
        app:layout_constraintVertical_bias="0.551" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="400dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        android:progress="0"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.654"
        app:layout_constraintStart_toEndOf="@+id/seekBar"
        app:layout_constraintTop_toBottomOf="@+id/viewFinder"
        app:layout_constraintVertical_bias="0.56" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.java 全コード

ここは、私の記録帳ですので MainActivity.java 全コードも記録します。

base64Encode にも byte[1] を使っていましたので修正しました。

copy

package org.sibainu.relax.room.cameracjavanas_call1;

import static java.lang.String.join;
import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Preview;
import androidx.camera.core.resolutionselector.ResolutionSelector;
import androidx.camera.core.resolutionselector.ResolutionStrategy;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.os.HandlerCompat;
import android.Manifest;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Size;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MainActivity extends AppCompatActivity {

    private final static String TAG = "camerax";
    private final static String JSON_TAG = "jason_process";
    private static String FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS";
    private ImageCapture imagecapture;
    private Uri imageuri;
    private ExecutorService cameraExecutor;
    private Button bt;
    private TextView tv;
    private SeekBar seekbar;
    private Camera camera;
    private ProgressBar progressbar;
    private final int REQUEST_CODE_PERMISSIONS = 100;
    private final String[] REQUIRED_PERMISSIONS = {Manifest.permission.CAMERA,
            Manifest.permission.WRITE_EXTERNAL_STORAGE};
    private final String FTPUSERNAME = "sora";
    private final String  FTPPASSWORD = "MaMaSiba#0220#Ryu";
    private final String FTPSERVER = "192.168.0.5";
    private final String FTPDIRECTORY = "photo/JIJISora/";
    private final String JSONSERVER = "https://www.sibainu.org/";
    private final String JSONNAME = "jsonusername";
    private final String JSONID = "jsonid";
    private final String JSONACCESS = "jsonaccess";
    private final String JSONKEY = "jsonkey";
    private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;

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

        cameraExecutor = Executors.newSingleThreadExecutor();
        cameraProviderFuture = ProcessCameraProvider.getInstance(this);

        if (allPermissionsGranted()) {
            //startCamera();
        } else {
            ActivityCompat.requestPermissions(
                    this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
            );
        }

        clickListener ls = new clickListener();

        bt = findViewById(R.id.bt_shutter);
        bt.setOnClickListener(ls);

        bt = findViewById(R.id.bt_post);
        bt.setOnClickListener(ls);

        tv = findViewById(R.id.tvinfo);

        seekbar = findViewById(R.id.seekBar);
        seekbar.setProgress(0, true);

        progressbar = findViewById(R.id.progressBar);

        changeListener cl = new changeListener();
        seekbar.setOnSeekBarChangeListener(cl);
    }

    private boolean allPermissionsGranted() {
        for (String requiredPermission : REQUIRED_PERMISSIONS) {
            if (ContextCompat.checkSelfPermission(
                    this, requiredPermission
            ) != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

    private void startCamera() {
        ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);

        cameraProviderFuture.addListener(() -> {
            try {
                // カメラのライフサイクルをアプリのライフサイクルに紐づけ
                ProcessCameraProvider cameraProvider = cameraProviderFuture.get();

                // プレビューの設定
                PreviewView previewView = findViewById(R.id.viewFinder);

                Preview preview = new Preview.Builder().build();
                preview.setSurfaceProvider(previewView.getSurfaceProvider());

                // イメージキャプチャーを受け取るオブジェクト
                imagecapture = new ImageCapture.Builder()
                        //.setTargetResolution(new Size(640,480))
                        .setResolutionSelector(
                                new ResolutionSelector.Builder()
                                        .setResolutionStrategy(
                                                new ResolutionStrategy(new Size(4000, 3000),
                                                                       ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER))
                                        .build()
                        )
                        //.setTargetRotation(Surface.ROTATION_90)
                        //.setJpegQuality(60)
                        .build();

                // 背面カメラをデフォルト
                CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;

                ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
                        .build();
                // 既存の廃棄
                cameraProvider.unbindAll();

                // カメラをビュー・イメージキャプチャーに紐づけ
                //cameraProvider.bindToLifecycle(this, cameraSelector, preview, imagecapture);
                camera = cameraProvider.bindToLifecycle(this,
                                                        cameraSelector,
                                                        preview,
                                                        imageAnalysis,
                                                        imagecapture);
                camera.getCameraControl().setZoomRatio(0.1f);

            } catch (ExecutionException | InterruptedException e) {
                Log.e(TAG, e.getLocalizedMessage(), e);
            }
        }, ContextCompat.getMainExecutor(this));
    }

    private void takePhoto(int id) {

        if (imagecapture == null) {
            return;
        }
        // タイムスタンプされた名前を作るため時間を「yyyyMMddHHmmssSSS」形式にフォーマット
        SimpleDateFormat dateFormat = new SimpleDateFormat(FILENAME_FORMAT, Locale.JAPAN);

        // 現在の日時を取得。
        Date now = new Date(System.currentTimeMillis());

        // 取得した日時データを設定したフォーマットに整形した文字列を生成。
        String nowStr = dateFormat.format(now);

        // ストレージに格納する画像のファイル名を生成。
        // ファイル名の一意を確保するためにミリ秒のタイムスタンプの値を利用。
        String name = "Photo_" + nowStr;

        // ContentValuesオブジェクトを生成。
        ContentValues values = new ContentValues();

        // 画像ファイルのタイトル名を設定。
        values.put(MediaStore.Images.Media.TITLE, name);

        // 画像ファイルの表示名を設定。
        values.put(MediaStore.Images.Media.DISPLAY_NAME, name);

        // 画像ファイルの種類を設定。
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");

        // 共有ストレージに保存するパス
        // P  Constant Value: 28
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
            values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image");
        }

        // ContentResolverオブジェクトを生成。
        ContentResolver resolver = getContentResolver();

        // ContentResolverを使ってURIオブジェクトを生成。
        imageuri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

        // file + metadata 出力オプションオブジェクトの作成
        ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(
                resolver,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                values).build();

        // これがないとキャプチャーでエラーになります
        values.clear();

        Log.d(TAG,"outputOptions");

        // 撮影後にトリガーされる画像キャプチャのリスナーを設定する
        imagecapture.takePicture(
                outputOptions,
                ContextCompat.getMainExecutor(this),
                new ImageCapture.OnImageSavedCallback() {
                    @Override
                    public void onError(ImageCaptureException error) {
                        Log.e(TAG, "Photo capture failed: " + error.toString(), error);
                    }
                    @Override
                    public void onImageSaved(ImageCapture.OutputFileResults
                                                     outputFileResults) {

                        CharSequence msg = "Photo capture succeeded: " +
                                outputFileResults.getSavedUri();
                        Toast.makeText(MainActivity.this,
                                msg,
                                Toast.LENGTH_SHORT).show();
                        Uri uri = Uri.parse(outputFileResults.getSavedUri().toString());
                        String uristr = outputFileResults.getSavedUri().toString();

                        try {
                            Log.d(TAG, "begin try");
                            InputStream ips = resolver.openInputStream(uri);

                            if (id == R.id.bt_shutter) {
                                //画像のアップロードの実行
                                String[] projection = {MediaStore.MediaColumns.DATA};
                                //アプリケーションのデータベースに接続して DATA を検索し格納されている path を取得
                                Cursor cursor =
                                        getContentResolver().query(
                                                imageuri, projection, null, null, null
                                        );

                                if (cursor != null) {
                                    String imagepath = "";
                                    //最初のレコードにカーソルを移します
                                    if (cursor.moveToFirst()) {
                                        //カラムは一つだけなのでインデックスは 0
                                        imagepath = cursor.getString(0).toString();
                                    }
                                    cursor.close();

                                    uploadNas(FTPUSERNAME,
                                        FTPPASSWORD,
                                        FTPSERVER,
                                        FTPDIRECTORY,
                                        name + ".jpg",
                                        ips);
                                }

                            } else if (id == R.id.bt_post) {
                                //POSTリクエストの実行(画像の文字エンコード)
                                Map<String, String> postBody =
                                        requestImageBody(JSONNAME,
                                                JSONID,
                                                JSONACCESS,
                                                JSONKEY,
                                                ips);
                                uploadJson(JSONSERVER,postBody);
                            }

                            ips.close();

                        } catch (Exception e) {

                        }

                        Intent intent = new Intent(MainActivity.this, ImageCheckActivity.class);
                        intent.putExtra("uri", uristr);
                        startActivity(intent);
                    }
                });
    }

    private class clickListener implements View.OnClickListener {
        @Override
        public void onClick(View view) {
            int id = view.getId();
            if (id == R.id.bt_shutter) {
                progressbar.setProgress(0, true);
                tv.setText("処理を開始します");
                takePhoto(id);
            } else if (id == R.id.bt_post) {
                progressbar.setProgress(0, true);
                tv.setText("処理を開始します");
                takePhoto(id);
            }
        }
    }

    private class changeListener implements SeekBar.OnSeekBarChangeListener {
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            //ドラッグされると呼ばれる
            camera.getCameraControl().setLinearZoom((float) progress/seekbar.getMax());
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            //タッチされた時に呼ばれる
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            //リリースされた時に呼ばれる
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        seekbar.setProgress(0, true);
        startCamera();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        cameraExecutor.shutdown();
    }

    // takePhoto() から呼び出すようにしています
    @UiThread
    private void uploadNas(String ftpUsername,
                           String ftpPassword,
                           String ftpServer,
                           String ftpDirectory,
                           String filename,
                           InputStream ips) {

        // ハンドラー・ルーパーを構築します
        // ワーカースレッドで取込みを開始する
        Looper mainLooper = Looper.getMainLooper();
        Handler handler = HandlerCompat.createAsync(mainLooper);

        //クラス JsonPostHttp の実体を作成
        FtpAccess backgroundReceiver = new FtpAccess(
                ftpUsername,
                ftpPassword,
                ftpServer,
                ftpDirectory,
                filename,
                ips,
                handler);

        //FtpAccess backgroundReceiver = new FtpAccess(
        //        ftpUsername,
        //        ftpPassword,
        //        ftpServer,
        //        ftpDirectory,
        //        filename,
        //        ips);

        //executorService を作成
        ExecutorService executorService  = Executors.newSingleThreadExecutor();
        //executorService.submit の引数に JsonPostHttp の実体を渡します
        //バックグラウンドで非同期処理が行われ戻り値が future に格納されます
        Future<String> future = executorService.submit(backgroundReceiver);

        String result = "";
        try {
            //future から値を取り出します
            result = future.get();
        }
        catch(ExecutionException | InterruptedException ex) {
            Log.w(TAG, "非同期処理結果の取得で例外発生: ", ex);
        }
        resultInfo(result);
    }

    // handler から呼び出されます
    private class PostExecutor implements Runnable {
        int _progress;
        public PostExecutor(int progress) {
            _progress = progress;
        }

        @UiThread
        @Override
        public void run() {
            // プログレスバーにセットします
            progressbar.setProgress(_progress, true);
        }
    }

    @UiThread
    private void resultInfo(String result) {
        tv.setText(result);
    }

    // uploadNas() から呼び出すようにしています
    public class FtpAccess implements Callable<String> {
        private final String _ftpUsername;
        private final String _ftpPassword;
        private final String _ftpServer;
        private final String _ftpDirectory;
        private final String _filename;
        private final InputStream _ips;
        private final Handler _handler;
        //コンストラクタです
        public FtpAccess(String ftpUsername,
                         String ftpPassword,
                         String ftpServer,
                         String ftpDirectory,
                         String filename,
                         InputStream ips,
                         Handler handler){
            _ftpUsername = ftpUsername;
            _ftpPassword = ftpPassword;
            _ftpServer = ftpServer;
            _ftpDirectory = ftpDirectory;
            _filename = filename;
            _ips = ips;
            _handler = handler;
        }
        // 非同期内でセットした文字列を返す
        @WorkerThread
        @Override
        public String call() {
            //ファイルの一覧表を返します
            ArrayList<String> infolist = new ArrayList<>();
            FTPClient ftpClient = new FTPClient();
            try {
                //デフォルト ポートでリモート ホストに接続され、システムに割り当てられたポートで現在のホストから発信されるソケットを開きます
                ftpClient.connect(_ftpServer);
                //指定されたユーザーとパスワードを使用して FTP サーバーにログインします
                ftpClient.login(_ftpUsername, _ftpPassword);
                //データ転送を行うために接続するデータ ポートを開くようにサーバーに指示されます
                ftpClient.enterLocalPassiveMode();

                //多くの FTP サーバーはデフォルトで BINARY になっています
                ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
                //文字化け対策
                ftpClient.setControlEncoding("CP932");
                //ディレクトリ移動
                ftpClient.changeWorkingDirectory(_ftpDirectory);
                //ファイル一覧の取得
                String[] filenames;
                filenames = ftpClient.listNames();

                for (String filename : filenames) {
                    infolist.add(filename);
                }

                //NASにファイルをアップロード
                try {
                    //// staoreFile関数の呼び出しでアップロードする。
                    //ftpClient.storeFile(_filename, _ips);
                    //// ログ出力
                    //Log.d(TAG, "Upload - " + _filename);
                    //infolist.add("Upload - " + _filename);

                    byte[] byteVal = new byte[1024];
                    int num;
                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                    while ((num = _ips.read(byteVal, 0, byteVal.length)) > 0) {
                        byteArrayOutputStream.write(byteVal, 0, num);
                    }
                    byteArrayOutputStream.close();
                    byte[] b = byteArrayOutputStream.toByteArray();

                    OutputStream os = ftpClient.storeFileStream(_filename);
                    int bufferLength = 1024;
                    int bytesLength = b.length;
                    int delaycount = 0;
                    for (int i = 0; i < bytesLength; i += bufferLength) {
                        int progress = (int)((i / (float) bytesLength) * 100);

                        //ハンドラーにメインスレッドで実行してほしいことを POST します
                        //することは PostExecutor のrunnable に書きます
                        //_handler.post(new PostExecutor(progress));
                        /↑↑ 上の式では一瞬で終わってしますので、post毎に1ミリ秒積算して遅らせることにしました
                        delaycount += 1;
                        _handler.postDelayed(new PostExecutor(progress), delaycount * 1);

                        if (bytesLength - i >= bufferLength) {
                            os.write(b, i, bufferLength);
                        }else{
                            os.write(b, i, bytesLength - i);
                        }
                    }

                    //小数点の関係上ここまでの progress の値は 99 なので 100 にします
                    //_handler.post(new PostExecutor(100));
                    //↑↑ 上の式では一瞬で終わってしますので、post毎に1ミリ秒積算して遅らせることにしました
                    delaycount += 1;
                    _handler.postDelayed(new PostExecutor(100), delaycount * 1);

                    os.flush();
                    os.close();

                } catch(Exception e){
                    e.printStackTrace();
                    infolist.add(e.toString());
                }
            } catch (Exception e) {
                e.printStackTrace();
                infolist.add(e.toString());
            } finally {
                infolist.add("処理を終了します");
                try {
                    ftpClient.logout();
                    ftpClient.disconnect();
                } catch (Exception e) {
                    infolist.add(e.toString());
                }
            }
            return join("\n",infolist);
        }
    }

    @UiThread
    private void uploadJson(String jsonUri, Map<String, String> postParams) {
        //クラス JsonPostHttp の実体を作成
        JsonPostHttp backgroundReceiver = new JsonPostHttp(jsonUri, postParams);
        //executorService を作成
        ExecutorService executorService  = Executors.newSingleThreadExecutor();
        //executorService.submit の引数に JsonPostHttp の実体を渡します
        //バックグラウンドで非同期処理が行われ戻り値が future に格納されます
        Future<String> future = executorService.submit(backgroundReceiver);
        String result = "";
        try {
            //future から値を取り出します
            result = future.get();
        }
        catch(ExecutionException | InterruptedException ex) {
            Log.w(TAG, "非同期処理結果の取得で例外発生: ", ex);
        }
        resultInfo(result);
    }
    public class JsonPostHttp implements Callable<String> {
        private final String _jsonUrl;
        private Map<String,String> _postParams;
        //コンストラクタです
        public JsonPostHttp(String jsonUrl,
                            Map<String, String> postParams) {
            _jsonUrl = jsonUrl;
            _postParams = postParams;
        }
        // 非同期内でセットした文字列を返す
        @WorkerThread
        @Override
        public String call() {
            ArrayList<String> infolist = new ArrayList<>();
            HttpURLConnection conn = null;
            try {
                //bodyData の作成
                Map<String, String> postParams = _postParams;
                StringBuilder postData = new StringBuilder();
                for(Map.Entry<String, String> entry: postParams.entrySet()){
                    if (!entry.getKey().equals("")) {
                        if (postData.length() != 0) {
                            postData.append("&");
                        }
                        postData.append(URLEncoder.encode(entry.getKey(), "utf-8"));
                        postData.append("=");
                        if (entry.getKey() == "data"){
                            postData.append(entry.getValue());
                        }else{
                            postData.append(URLEncoder.encode(entry.getValue(), "utf-8"));
                        }
                    }
                }
                String bodyData = postData.toString();

                URL url = new URL(_jsonUrl);

                conn = (HttpURLConnection) url.openConnection();
                // 接続に使ってもよい時間を設定。
                conn.setConnectTimeout(10000);
                // データ取得に使ってもよい時間。
                conn.setReadTimeout(20000);

                conn.setRequestMethod("POST");

                conn.setDoOutput(true);

                conn.setRequestProperty("Content-Type",
                        "application/json; charset=utf-8");
                conn.setUseCaches(false);

                OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream(),
                        StandardCharsets.UTF_8);

                out.write(bodyData);
                out.flush();
                out.close();

                conn.connect();

                String info = "";
                int status = conn.getResponseCode();
                if (status == HttpURLConnection.HTTP_OK) {
                    //コネクト成功
                    InputStream is = conn.getInputStream();
                    info = isToString(is);
                    is.close();
                    infolist.add(info);
                } else {
                    infolist.add("サーバーに接続できませんでした");
                }

            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (conn != null) conn.disconnect();
            }
            return join("\n",infolist);
        }
    }

    private String isToString(InputStream is) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
        StringBuffer sb = new StringBuffer();
        char[] b = new char[1024];
        int line;
        while(0 <= (line = reader.read(b))) {
            sb.append(b, 0, line);
        }
        return sb.toString();
    }

    private Map<String, String> requestJsonBody(String jsonusername,
                                                String jsonuserid,
                                                String jsonaccess,
                                                String jsonkey,
                                                Map<String, Object> jsondata) {
        Map<String, String> postmap = new HashMap<>();
        postmap.put("username", jsonusername);
        postmap.put("userid", jsonuserid);
        postmap.put("access", jsonaccess);
        postmap.put("key", jsonkey);
        JSONObject json = new JSONObject(jsondata);
        String jsonstr = json.toString();
        postmap.put("data", jsonstr);
        return postmap;
    }

    @UiThread
    private Map<String, String> requestImageBody(String jsonusername,
                                                 String jsonuserid,
                                                 String jsonaccess,
                                                 String jsonkey,
                                                 InputStream ips) {
        Map<String, String> postmap = new HashMap<>();
        postmap.put("username", jsonusername);
        postmap.put("userid", jsonuserid);
        postmap.put("access", jsonaccess);
        postmap.put("key", jsonkey);
        Log.d(TAG, "jsonkey");
        String imgstr = base64Encode(ips);
        Log.d(TAG, "requestImageBody : " + imgstr);
        postmap.put("data", imgstr);
        return postmap;
    }

    public String base64Encode(InputStream ips) {
        tv.setText("base64Encode");
        String encodedStr = "";

        try {
            // byte[1]としていましたが余りにも遅いので修正しました
            byte[] byteVal = new byte[1024];
            int num;
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            // while (ips.read(byteVal) > 0) {
            // byte[1024]に変更したのでここも修正しました
            while ((num = ips.read(byteVal, 0, byteVal.length)) > 0){
                byteArrayOutputStream.write(byteVal, 0, num);
            }

            byteArrayOutputStream.close();
            byte[] b = byteArrayOutputStream.toByteArray();

            // Base64へのエンコード
            encodedStr = Base64.getEncoder().encodeToString(b);
            //encodedStr = Base64.getUrlEncoder().withoutPadding().encodeToString(b);

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

}

今はここまでとします。