Download Multiple images from Firebase Realtime Database to device storage

Viewed 191

I have a recyclerview that displays multiple images from Firebase Realtime Database. The recyclerview also has a button within it. I want this button to allow users to be able to download these images ONE AT A TIME once they click it.

Screenshot of app layout

Once users click "download" I want the images to be saved to their device storage. I've tried multiple solutions for this, but they weren't helpful as they were for either Firestore Database or only allowed for one image to be downloaded.

Code

class AbstractAdapter(private val mContext: Context, private val abstractList: List<Abstract>) :
RecyclerView.Adapter<AbstractAdapter.ViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val view = LayoutInflater.from(parent.context).inflate(R.layout.abstract_image_view, parent, false)
    return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {


    holder.download_btn.setOnClickListener {
        downloadFile()


    }

    Glide.with(mContext)
        .load(abstractList[position].abstract)
        .into(holder.imageView)
}

override fun getItemCount(): Int {
    return abstractList.size
}

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val imageView: ImageView = itemView.findViewById(R.id.abstractImageView)
    val download_btn: Button = itemView.findViewById(R.id.abstractDownloadBtn)

}

private fun downloadFile() {
    val storage = FirebaseStorage.getInstance()
    val storageRef = storage.getReferenceFromUrl("https://notes-72413.firebaseio.com/")
    val islandRef = storageRef.child("Abstract")
    val rootPath = File(Environment.getExternalStorageDirectory(), "abstract")
    if (!rootPath.exists()) {
        rootPath.mkdirs()
    }
    val localFile = File(rootPath, "imageName.jpeg")
    islandRef.getFile(localFile)
        .addOnSuccessListener(OnSuccessListener<FileDownloadTask.TaskSnapshot?> {
            Log.e("firebase ", ";local tem file created  created $localFile")
            //  updateDb(timestamp,localFile.toString(),position);
        }).addOnFailureListener(OnFailureListener { exception ->
            Log.e(
                "firebase ",
                ";local tem file not created  created $exception"
            )
        })
}


companion object {
    private const val Tag = "RecyclerView"
}

I've tried this code, but once I click the "download" button it immediately crashes and Logcat says Firebase Storage URLs must point to an object in your Storage Bucket. Please obtain a URL using the Firebase Console or getDownloadUrl()

My Firebase Realtime Database

Realtime database

There's 64 files in total

Summary

I have a recyclerview that displays images from Firebase Realtime Database. Once users click the "download" button, it only downloads one image at a time to their device storage.

Update

private fun downloadFile() {
    val storage = FirebaseStorage.getInstance()
    val storageRef = storage.getReferenceFromUrl("abstract")

    val rootPath = File(Environment.getExternalStorageDirectory(), "abstract")
    if (!rootPath.exists()) {
        rootPath.mkdirs()
    }

    val localFile = File(rootPath, "imageName.jpeg")
    storageRef.child("Abstract").downloadUrl.addOnSuccessListener { Log.e("firebase ", ";local tem file created  created $localFile")

    }.addOnFailureListener(OnFailureListener { exception ->
            Log.e("firebase ", ";local tem file not created  created $exception")
        })
}

These are the changes I made to my downloadFile function, but I still get an error:

The storage Uri could not be parsed

Second update

2022-06-11 21:36:00.536 29751-29751/com.khumomashapa.notes E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.khumomashapa.notes, PID: 29751
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:523)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
 Caused by: java.lang.reflect.InvocationTargetException
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055) 
 Caused by: java.net.MalformedURLException: no protocol: 
    at java.net.URL.<init>(URL.java:601)
    at java.net.URL.<init>(URL.java:498)
    at java.net.URL.<init>(URL.java:447)
    at com.khumomashapa.notes.adapter.AbstractAdapter.downloadFile(AbstractAdapter.kt:57)
    at com.khumomashapa.notes.adapter.AbstractAdapter.onBindViewHolder$lambda-0(AbstractAdapter.kt:35)
    at com.khumomashapa.notes.adapter.AbstractAdapter.$r8$lambda$Rrmx0DFlwJu1z6QtjG8WCQp6NQQ(Unknown Source:0)
    at com.khumomashapa.notes.adapter.AbstractAdapter$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
    at android.view.View.performClick(View.java:7216)
    at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1217)
    at android.view.View.performClickInternal(View.java:7190)
    at android.view.View.access$3500(View.java:827)
    at android.view.View$PerformClick.run(View.java:27663)
    at android.os.Handler.handleCallback(Handler.java:900)
    at android.os.Handler.dispatchMessage(Handler.java:103)
    at android.os.Looper.loop(Looper.java:219)
    at android.app.ActivityThread.main(ActivityThread.java:8349)
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055) 

Code

class AbstractAdapter(private val mContext: Context, private val abstractList: List<Abstract>) :
RecyclerView.Adapter<AbstractAdapter.ViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val view = LayoutInflater.from(parent.context).inflate(R.layout.abstract_image_view, parent, false)
    return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {


    holder.download_btn.setOnClickListener {
        downloadFile(url = String(), file = String.toString())

    }

    Glide.with(mContext)
        .load(abstractList[position].abstract)
        .into(holder.imageView)
}

override fun getItemCount(): Int {
    return abstractList.size
}

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val imageView: ImageView = itemView.findViewById(R.id.abstractImageView)
    val download_btn: Button = itemView.findViewById(R.id.abstractDownloadBtn)

}

@Throws(IOException::class)

private fun downloadFile(url: String, file: String) {
    val urlObj = URL(url)
    val fileObj = File(file)
    val conn = urlObj.openConnection()
    val buffer = ByteArray(1024)
    object : BufferedOutputStream(FileOutputStream(fileObj)) {
        var `in` = BufferedInputStream(conn.getInputStream())
        init {
            var read: Int
            while (`in`.read(buffer, 0, buffer.size) >= 0.also { read = it });
            run {
                out.write(buffer, 0, read)
            }
            out.flush()
        }
    }.use { out -> }
}
4 Answers

I found the perfect solution to my problem. All I had to do was create an OnItemClick interface to get a different result for each item click and use Download manager to download the images.

override fun onItemClick(item: String, pos:Int) {
    abstractData = item
    positionItem = pos

    if (checkSelfPermission(requireActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED ){
       requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQ_CODE)

    }else{

    startDownloading()
    }

    Toast.makeText(requireActivity(), "Saved to Internal storage/Pictures/AbstractWallpaper", Toast.LENGTH_LONG).show()

}

private fun startDownloading() {

    val request = DownloadManager.Request(Uri.parse(abstractData))
    request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
    request.setTitle("Abstract Wallpaper")
    request.setDescription("Your image is downloading")
    request.allowScanningByMediaScanner()
    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
    request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, "AbstractWallpapers")
    val manager = activity?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
    manager.enqueue(request)

    Toast.makeText(requireActivity(), "Download is starting...", Toast.LENGTH_LONG).show()
}
As you are downloading based on your storage URLs so need to use getDownloadUrl() method.
storageRef.child("Abstract").getDownloadUrl().addOnSuccessListener(new OnSuccessListener<Uri>() {
    @Override
    public void onSuccess(Uri uri) {
      ...Uri
    }
}).addOnFailureListener(new OnFailureListener() {
    @Override
    public void onFailure(@NonNull Exception exception) {
        // To handle error
    }
});

As those URLs in your database are publicly visible HTTP URLs. You can just download the files with a good old URLConnection and binary streams.

private void downloadFile(String url, String file) throws IOException {
  URL urlObj = new URL(url);
  File fileObj = new File(file);
  URLConnection conn = urlObj.openConnection();
  byte[] buffer = new byte[1024];

  try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(fileObj)) {
    try (BufferedInputStream in = new BufferedInputStream(conn.getInputStream())) {
      int read;
      while ((read = in.read(buffer, 0, buffer.length) >= 0) {
        out.write(buffer, 0, read);
      }
      out.flush();
    }
  }
}

Not tested, but for idea, I'm using okhttp and okio

fun saveImageToPicture(context: Countext, url: String, imageName: String, imageMimeType: String) {
    val appImagePath = File(Environment.DIRECTORY_PICTURES, "FolderNameInPictures")
    val date = System.currentTimeMillis()
    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, "$imageName.$imageMimeType")
        put(MediaStore.Images.Media.MIME_TYPE, "image/$imageMimeType")
        put(MediaStore.Images.Media.DATE_ADDED, date)
        put(MediaStore.Images.Media.DATE_MODIFIED, date)
    }
    var collection = if (Build.VERSION.SDK_INT >= 29 {
        values.put(MediaStore.Images.Media.RELATIVE_PATH, "$appImagePath${File.separator}")
        values.put(MediaStore.Images.Media.IS_PENDING, 1)
        MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    } else {
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    }
    val resolver = context.contentResolver
    val insertedUri = resolver.insert(collection, values)

    val outputStream = resolver.openOutputStream(insertedUri!!, "w")
    downloadFromUrlToOutputStream(url, outputStream)

    values.clear()
    if (Build.VERSION.SDK_INT >= 29 {
        values.put(MediaStore.Images.Media.IS_PENDING, 0)
    }
    resolver.update(insertedUri, values, null, null)
}

fun downloadFromUrlToOutputStream(url: String, output: OutputStream) {
    val request = Request.Builder().url(url).build()
    val response = OkHttpClient().newCall(request).execute();
    val body = response.body()
    val bufferSource = body.source()
    val sink = Okio.buffer(Okio.sink(output))
    val sinkBuffer = sink.buffer()
    val bufferSize = 1024 * 8

    while (bufferSource.read(sinkBuffer, bufferSize) != -1) {
        sink.emit()
    }

    sink.flush()
    sink.close()
    bufferSource.close()
}
Related