Android Handler/Looper を使ってProgressBar を実装
朝の散歩でよく合う柴犬の女子です。我が家の柴犬は活発な犬は苦手で「もう勘弁してくれよ」という顔して後ろ向きの気持ちになっています。年のせいもあるかもしれません。
概要
NAS にアップロードするときに、進行状況を告知するプログレスバーを入れてみることを考えてみました。
スレッド間の通信は調べてみると Handler と Looper を使うらしいということが分かりました。
なんとかそれらしくできましたので記録します。
2024年2月26日現在
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 を加えています。
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の値をセットしています。
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);
@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ファイルも掲載しておきます。
<?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] を使っていましたので修正しました。
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; } }
今はここまでとします。