Sibainu Relax Room

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

Android CameraX を始めてみて3

柴犬は、はだか祭りの準備が進んでいるのか気になるようです。

概要

なかなか進展がなかった cameraX で撮影した画像ファイルの NAS へのアップロードですができるようになりました。

URI の取得とコルーチンがカギになりました。

かなり勉強になりました。多分忘れ必要になるときがありますので記録します。

cameraX のCameraX のスタートガイドがなければURIの取得は無理でした。いろいろ教えていただきました。

次に紹介する本はちょっと過激な表紙ですが、本の内容はまじめてよく理解できる書き方をしています。先の「Androidアプリ開発」で飛ばしているところを丁寧に説明しているので、これで理解が早まりました。お勧めです。

しかもこれが100円で買え、ボリュームがすごい量です。なので著者に感謝です。

NASにアップロード

左が撮影時のスナップショットです。撮影前の NAS の保存先ホルダーのファイル一覧を取得して表示します。

右が撮影後 SMB をタップして NAS の保存先ホルダーのファイル一覧を取得して表示したものです。撮影したファイル(赤枠)が表示されて保存が確認できました。

撮影したファイル「2024-02-□□□□□□-05-712.jpg」が表示されました。

アップロードできた Synology の NASの状況です。

takePhoto() 関数

takePhoto() 関数の imageCapture.takePicture を修正しました。

        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun
                        onError(exc: ImageCaptureException) {
                    Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
                }

                override fun
                        onImageSaved(output: ImageCapture.OutputFileResults) {
                    val msg = "Photo capture succeeded: ${output.savedUri}"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d(TAG, msg)

                    val projection = arrayOf(MediaStore.MediaColumns.DATA)
                    //アプリケーションのデータベースに接続して URI を検索します
                    val cursor =
                        applicationContext.contentResolver.query(
                            Uri.parse(
                                output.savedUri.toString()
                            ), projection, null, null, null
                        )

                    if (cursor != null) {
                        var path: String? = null
                        //最初のレコードにカーソルを移します
                        if (cursor.moveToFirst()) {
                            //path はカラムのインデックスが 0 のようです
                            path = cursor.getString(0)
                            //スマホのテキストビューにパスを表示します
                            findViewById<TextView>(R.id.tvinfo).text = path
                        }
                        cursor.close()
                        if (path != null) {
                            //パスが取得できたので File を作成します
                            val file = File(path)
                            //ftpDirectory は "ftp://192.168.□□.□□/" 不要
                            val filelist = FtpAccess().Await(
                                "□□□□",
                                "□□□□□□□□□□",
                                "192.168.□□.□□",
                                "□□□□□□/□□□□□□/",
                                file
                            )
                            findViewById<TextView>(R.id.tvinfo).text = filelist
                        }
                    }
                }
            }
        )

copy

    private fun takePhoto() {
        // Get a stable reference of the modifiable image capture use case
        val imageCapture = imageCapture ?: return

        // Create time stamped name and MediaStore entry.
        val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
            .format(System.currentTimeMillis())
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, name)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
                put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
            }
        }

        // Create output options object which contains file + metadata
        val outputOptions = ImageCapture.OutputFileOptions
            .Builder(
                contentResolver,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                contentValues
            )
            .build()

        // Set up image capture listener, which is triggered after photo has
        // been taken
        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun
                        onError(exc: ImageCaptureException) {
                    Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
                }

                override fun
                        onImageSaved(output: ImageCapture.OutputFileResults) {
                    val msg = "Photo capture succeeded: ${output.savedUri}"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d(TAG, msg)

                    val projection = arrayOf(MediaStore.MediaColumns.DATA)
                    //アプリケーションのデータベースに接続して URI を検索します
                    val cursor =
                        applicationContext.contentResolver.query(
                            Uri.parse(
                                output.savedUri.toString()
                            ), projection, null, null, null
                        )

                    if (cursor != null) {
                        var path: String? = null
                        //最初のレコードにカーソルを移します
                        if (cursor.moveToFirst()) {
                            //path はカラムのインデックスが 0 のようです
                            path = cursor.getString(0)
                            //スマホのテキストビューにパスを表示します
                            findViewById<TextView>(R.id.tvinfo).text = path
                        }
                        cursor.close()
                        if (path != null) {
                            //パスが取得できたので File を作成します
                            val file = File(path)
                            //ftpDirectory は "ftp://192.168.□□.□□/" 不要
                            val filelist = FtpAccess().Await(
                                "□□□□",
                                "□□□□□□□□□□",
                                "192.168.□□.□□",
                                "□□□□□□/□□□□□□/",
                                file
                            )
                            findViewById<TextView>(R.id.tvinfo).text = filelist
                        }
                    }
                }
            }
        )
    }

runBlocking

launch() 関数から作成したコルーチンの中で、接続したフォルダーのファイル一覧を得て、MainActivity のビューテキストに表示しようとしました。

launch() 関数から作成したコルーチンでいろいろ試しましたが、現在の私の力量ではコルーチンの中で作成したデータをメインスレッドに送ることはできませんでした。

ですので、メインスレッドをブロッキングする runBlocking で作成するコルーチンを使うことにします。

SmbAccess()、FtpAccess()関数をやめて、クラスに作り直しました。

クラス SmbAccess

ファイル一覧を表示します。

copy

//fun initStart() から呼び出すようにしています
class SmbAccess {
    // 非同期内でセットした文字列を返す
    fun Await(user: String,
              password: String,
              domain: String,
              smbroot: String): String? {
        var ayncString: String?
        //ここでコルーチンを使います
        runBlocking {
            ayncString = execute(user,
                                 password,
                                 domain,
                                 smbroot)
        }
        return ayncString
    }

    suspend fun execute(user: String,
                        password: String,
                        domain: String,
                        smbroot: String): String?{
        //ファイルの一覧表を返します
        return withContext(Dispatchers.IO){
            val infolist = mutableListOf<String>()
            val prop = Properties()
            prop.setProperty("jcifs.smb.client.minVersion", "SMB202")
            prop.setProperty("jcifs.smb.client.maxVersion", "SMB311")
            val bc = BaseContext(jcifs.config.PropertyConfiguration(prop))
            try {
                val auth = NtlmPasswordAuthenticator(domain, user, password)
                val cifsCon = bc.withCredentials(auth)
                val sf = SmbFile(smbroot, cifsCon)
                try {
                    Log.d("ServerFileAccess", sf.server)
                    Log.d("ServerFileAccess", sf.share)
                    Log.d("ServerFileAccess", sf.name)
                    Log.d("ServerFileAccess", sf.path)
                    if (sf.exists()) {
                        val filenames: Array<String> = sf.list()
                        for (i in filenames.indices) {
                            Log.d("ServerFileAccess", filenames[i])
                            infolist.add(filenames[i])
                        }
                        Log.d("ServerFileAccess", "ファイル有")
                    } else {
                        Log.d("ServerFileAccess", "ファイル無")
                    }
                } catch (ex: Exception) {
                    Log.d("ServerFileAccess", ex.toString())
                } finally {
                    if (sf != null) {
                        sf.close();
                    }
                    Log.d("ServerFileAccess", "finally")
                }
            } catch  (ex: Exception) {
                Log.d("ServerFileAccess", ex.toString())
                infolist.add("接続できませんでした")
            } finally {
                infolist.add("処理を終了します")
            }
            //返される文字列です
            infolist.joinToString("\n")
        }
    }
}

クラス FtpAccess

File が取得できれば、アップロードは次のような処理でできました。

     //NASにファイルをアップロード
     val inputStream = FileInputStream(file)
     val fileName = file.name

     //アップロードします
     ftpClient.storeFile(fileName, inputStream)
     inputStream.close()

copy

//fun takePhoto() から呼び出すようにしています
class FtpAccess {
    // 非同期内でセットした文字列を返す
    fun Await(ftpUsername: String,
              ftpPassword: String,
              ftpServer: String,
              ftpDirectory: String,
              file: File): String? {
        Log.d("Coroutine", "executeAwait start")
        var ayncString: String?
        //ここでコルーチンを使います
        runBlocking {
            ayncString = execute(ftpUsername,
                                 ftpPassword,
                                 ftpServer,
                                 ftpDirectory,
                                 file)
        }
        return ayncString
    }

    suspend fun execute(ftpUsername: String,
                        ftpPassword: String,
                        ftpServer: String,
                        ftpDirectory: String,
                        file: File): String? {
        //ファイルの一覧表を返します
        return withContext(Dispatchers.IO) {
            var infolist = mutableListOf<String>()
            val ftpClient = FTPClient()
            try {
                //デフォルト ポートでリモート ホストに接続され、システムに割り当てられたポートで現在のホストから発信されるソケットを開きます
                ftpClient.connect(ftpServer)

                //指定されたユーザーとパスワードを使用して FTP サーバーにログインします
                ftpClient.login(ftpUsername, ftpPassword)

                //データ転送を行うために接続するデータ ポートを開くようにサーバーに指示されます
                ftpClient.enterLocalPassiveMode()

                //多くの FTP サーバーはデフォルトで BINARY になっています
                ftpClient.setFileType(FTP.BINARY_FILE_TYPE)

                Log.d("ServerFileAccess", ftpClient.status)

                //ディレクトリ移動
                ftpClient.changeWorkingDirectory(ftpDirectory)

                //
                val filenames: Array<String> = ftpClient.listNames()
                for (i in filenames.indices) {
                    Log.d("ServerFileAccess", filenames[i])
                    infolist.add(filenames[i])
                }

                //NASにファイルをアップロード
                val inputStream = FileInputStream(file)
                val fileName = file.name
                ftpClient.storeFile(fileName, inputStream)
                inputStream.close()

            } catch (e: Exception) {
                e.printStackTrace()
                infolist.add(e.printStackTrace().toString())
            } finally {
                Log.d("ServerFileAccess", "finally")
                infolist.add("処理を終了します")
                ftpClient.logout()
                ftpClient.disconnect()
            }
            infolist.joinToString("\n")
        }
    }
}

initstart() 関数

copy

    private fun initstart() {
        findViewById<Button>(R.id.btLeft).setOnClickListener() {
            // 非同期処理
            //smbroot は "sm://192.168.□□.□□/□□□□□□□□□□□□□□/"
            val filelist = SmbAccess().Await("□□□□",
                "□□□□□□□□□□",
                "192.168.□□.□□",
                "sm://192.168.□□.□□/□□□□□□/□□□□□□/"
            )
            findViewById<TextView>(R.id.tvinfo).text = filelist
        }

        findViewById<Button>(R.id.btRight).setOnClickListener() {
            findViewById<TextView>(R.id.tvinfo).text = "FTP"
            // 非同期処理
            //ftpDirectory は "ftp://192.168.□□.□□/" 不要
            //FtpAccess("□□□□",
            //    "□□□□□□□□□□",
            //    "192.168.□□.□□",
            //    "□□□□□□/□□□□□□/")
        }
    }
}

フチベニベンケイの様子

黄1、黄2、緑3は先週と比較しますと葉っぱが大きくなりました。赤5も新しい芽はまだ見えていませんが元気な感じです。

緑4はかなり変化がありました。

芽が大きくなった感じがします。

もっと大きな変化は右上の側面からのアップから良くわかります。葉っぱが立ってきたことです。今後が楽しみです。

コルーチンの失敗例

今もコルーチンを十分に理解できていないのですが、はじめのころは次のようなコルーチンを書いていました。

実行するといきなり落ちました。

class FtpAccess {

    var ayncString = ""

    fun Await(ftpUsername: String,
              ftpPassword: String,
              ftpServer: String,
              ftpDirectory: String,
              file: File): String? {
        Log.d("Coroutine", "executeAwait start")

        ayncString = execute(ftpUsername,
                             ftpPassword,
                             ftpServer,
                             ftpDirectory,
                             file).toString()
        return ayncString
    }

    fun execute(ftpUsername: String,
                        ftpPassword: String,
                        ftpServer: String,
                        ftpDirectory: String,
                        file: File): String? {
        return runBlocking {
            val deferred1 = async {
                val infolist = mutableListOf<String>()

                val ftpClient = FTPClient()  << ここでエラーになっているようです

                try {

                ~~省略~~

                } catch (e: Exception) {
                    e.printStackTrace()
                    infolist.add(e.printStackTrace().toString())
                } finally {
                    Log.d("ServerFileAccess", "finally")
                    infolist.add("処理を終了します")
                    ftpClient.logout()
                    ftpClient.disconnect()
                }
                infolist.joinToString("\n")
            }
            //runBlocking の戻り値  →  execute の戻り値
            deferred1.await()
        }
    }

次のように変えて実行してみました。

 class FtpAccess {

    var ayncString = ""

    fun Await(ftpUsername: String,
              ftpPassword: String,
              ftpServer: String,
              ftpDirectory: String,
              file: File): String? {
        Log.d("Coroutine", "executeAwait start")

        ayncString = execute(ftpUsername,
                             ftpPassword,
                             ftpServer,
                             ftpDirectory,
                             file).toString()
        return ayncString
    }

    fun execute(ftpUsername: String,
                        ftpPassword: String,
                        ftpServer: String,
                        ftpDirectory: String,
                        file: File): String? {
        return runBlocking {

            val deferred1 = async {
                val infolist = mutableListOf<String>()

                try {

                    val ftpClient = FTPClient()  << これのみの記述です

                } catch (e: Exception) {
                    e.printStackTrace()
                    infolist.add(e.printStackTrace().toString())
                } finally {
                    Log.d("ServerFileAccess", "finally")
                    infolist.add("処理を終了します")
                }
                infolist.joinToString("\n")
            }
            deferred1.await()
        }
    } 

エラーメッセージは次のメッセージでした。

Kotlin.Unit

このコルーチン構成では val ftpClient = FTPClient() できないようです。

今回はこれまでとします。