AndroidでCloud Storage を使ってみる

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

写真や動画などを保管、提供することができるFirebase Cloud Storageを使ってみた。
プロジェクトは、
・ダウンロードボタンを押すとCloudStorageに保存された画像を取得してImageViewに表示
・アップロードボタンを押すとデバイス内のフォルダへ遷移し、写真を選択するとCloudStorageへアップロード
という簡単な構成で作った。

準備

これをもとにfirebaseをプロジェクトに追加する。
Android プロジェクトに Firebase を追加する  |  Android 向け Firebase

Firebaseコンソールを開き、Cloud StorageのRulesを設定する。
通常はFirebase Authenticationで認証されたユーザーのみにアクセス権をあげるのが良さそう。
Cloud Storage 用の Firebase セキュリティ ルールを理解する  |  Firebase Storage

service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth != null;
    }
  }
}

今回は自分で使ってみるだけなので、認証なしでアクセスできるようルールを変更。

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write:if true;
    }
  }
}

また画像のダウンロードを試すため、CloudStorageのFilesにimageフォルダを作成し、
その中に"sample.jpeg"という名前で画像を追加しておいた。

モジュールのbuild.gradleにCloud Storageの依存関係を追加する。

dependencies {
    //Firebase Android BoM(部品構成表)を使用すると、アプリは常に互換性のあるバージョンのライブラリを使用する
    implementation platform('com.google.firebase:firebase-bom:30.3.1')
    implementation 'com.google.firebase:firebase-storage-ktx'
}

アップロードを試すためにデバイス内のフォルダにアクセスする必要があるため、
AndroidManifest内にパーミッションを追加。

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

activity_main.xml

ImageViewとButton2つのレイアウトを作成。

<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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center">

        <ImageView
            android:id="@+id/download_image"
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:src="@drawable/ic_launcher_foreground"
            android:background="@color/cardview_dark_background"/>

        <Button
            android:id="@+id/download_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="Download Image"/>

        <Button
            android:id="@+id/upload_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="Upload Image"/>
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

CloudStorageの参照

//ストレージのインスタンスを取得
val storage = Firebase.storage
//referenceを取得
var storageRef = storage.reference

//child()を使って下位のフォルダやファイルも参照できる
var imageRef: StorageReference? = storageRef.child("image")
var sampleRef = storageRef.child("image/sample.jpeg")

ダウンロード

メモリにダウンロード

アプリが持つメモリより大きいファイルだとクラッシュしてしまうため、最大サイズを指定する。

val ONE_MEGABYTE: Long = 1024 * 1024
sampleRef.getBytes(ONE_MEGABYTE).addOnSuccessListener {
    val bitmap =  BitmapFactory.decodeByteArray(it, 0,  it.size)
    findViewById<ImageView>(R.id.download_image).setImageBitmap(bitmap) 
    Log.d("cloud storage", "download success")
}.addOnFailureListener {
    Log.d("cloud storage", "download failure $it")
}
ローカルファイルにダウンロード
val localFile = File.createTempFile("images", "jpg")
sampleRef.getFile(localFile).addOnSuccessListener {
   val bitmap = BitmapFactory.decodeFile(localFile.absolutePath)
   findViewById<ImageView>(R.id.download_image).setImageBitmap(bitmap)
   Log.d("cloud storage", "download ${localFile.absolutePath}")
}.addOnFailureListener {
   Log.d("cloud storage", "download failure $it")
}

アップロード

val stream = contentResolver.openInputStream(uri) //デバイス内の画像のURIからstreamに変換

val storageRef = storage.reference
val userImageRef = storageRef.child("image/${uri.path}") //参照を作成して、画像の保存先にする
val uploadTask = userImageRef.putStream(stream!!) //上記の参照にstreamを追加
uploadTask.addOnSuccessListener {
    Log.d("cloud storage", "upload success")
    stream.close()
}.addOnFailureListener { error ->
    Log.d("cloud storage", "upload failure $error")
}

MainActivity.kt全体コード

あらかじめアップロードしておいた画像がダウンロードされImageViewに表示、
選択した画像がCloud StorageのImageフォルダ内に保存されていることを確認した。

class MainActivity : AppCompatActivity() {

    private lateinit var storage:FirebaseStorage

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        storage = Firebase.storage

        val downloadButton = findViewById<Button>(R.id.download_button)
        downloadButton.setOnClickListener {
            downloadImage()
        }

        val uploadButton = findViewById<Button>(R.id.upload_button)
        uploadButton.setOnClickListener {
            checkOpenDocumentPermission()
        }
    }

    //Cloud Storageからダウンロード
    private fun downloadImage(){
        val storageRef = storage.reference
        val pathReference = storageRef.child("image/sample.jpeg")

        val ONE_MEGABYTE: Long = 1024 * 1024
        pathReference.getBytes(ONE_MEGABYTE).addOnSuccessListener {
            val bitmap =  BitmapFactory.decodeByteArray(it, 0,  it.size)
            findViewById<ImageView>(R.id.download_image).setImageBitmap(bitmap)
            Log.d("cloud storage", "download success")
        }.addOnFailureListener {
            Log.d("cloud storage", "download failure $it")
        }
    }
 
    //デバイス内ストレージのアクセス権
    private fun checkOpenDocumentPermission(){
        if(ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){
            selectPhoto()
        }else{
            openDocumentLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
        }
    }

    private val openDocumentLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()){ isGranted:Boolean ->
            if(isGranted){
                selectPhoto()
            }else{
                Toast.makeText(this, "デバイス内の写真やメディアへのアクセスが許可されませんでした。", Toast.LENGTH_SHORT).show()
            }
        }

    //デバイスから画像を選ぶ
    private fun selectPhoto() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = "image/*"
        }
        selectPhotoLauncher.launch(intent)
    }

    private val selectPhotoLauncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ result ->
            if(result.resultCode != RESULT_OK){
                return@registerForActivityResult
            }else{
                try {
                    var bitmap: Bitmap?
                    result.data?.data.also { uri ->
                        bitmap =
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
                                val source = ImageDecoder.createSource(contentResolver, uri!!)
                                ImageDecoder.decodeBitmap(source)
                            } else {
                                MediaStore.Images.Media.getBitmap(contentResolver, uri)
                            }
                        findViewById<ImageView>(R.id.download_image).setImageBitmap(bitmap)

                        if(uri != null) uploadImage(uri)
                    }
                }catch (e:Exception){
                    Log.d("cloud storage", e.toString())
                    Toast.makeText(this, "select photo Error", Toast.LENGTH_SHORT).show()
                }
            }
        }

    //Cloud Storageへのアップロード
    private fun uploadImage(uri: Uri) {
        val storageRef = storage.reference
        val stream = contentResolver.openInputStream(uri)
        val userImageRef = storageRef.child("image/${uri.path}")
        val uploadTask = userImageRef.putStream(stream!!)
        uploadTask.addOnSuccessListener {
            Log.d("cloud storage", "upload success")
            stream.close()
        }.addOnFailureListener { error ->
            Log.d("cloud storage", "upload failure $error")
            }
    }

}