1+ package com.reactnativecommunity.webview.extension.file
2+
3+ import android.Manifest
4+ import android.app.AlertDialog
5+ import android.content.ContentValues
6+ import android.content.Context
7+ import android.content.DialogInterface
8+ import android.content.Intent
9+ import android.content.pm.PackageManager
10+ import android.os.Build
11+ import android.os.Environment
12+ import android.provider.MediaStore
13+ import android.util.Base64
14+ import android.util.Log
15+ import android.webkit.MimeTypeMap
16+ import android.widget.Toast
17+ import androidx.annotation.RequiresApi
18+ import androidx.core.content.ContextCompat
19+ import java.io.File
20+ import java.io.FileOutputStream
21+
22+ internal object Base64FileDownloader {
23+
24+ /* *
25+ * This method checks for WRITE_EXTERNAL_STORAGE permission for Android 23 - 29 and requests if necessary
26+ */
27+ fun downloadBase64File (context : Context , base64 : String , downloadingMessage : String , requestFilePermission : (String ) -> Unit ) {
28+ val base64Parts = base64.split(" ," )
29+ if (base64Parts.size != 2 ) {
30+ Log .e(" Base64FileDownloader" , " Unable to parse base64 to download a file. Base64 value was: $base64 " )
31+ return
32+ }
33+ val (mimeType, extension) = getMimeTypeAndFileExtension(base64Parts[0 ])
34+
35+ showAlertDialog(context, extension) {
36+ Toast .makeText(context, downloadingMessage, Toast .LENGTH_LONG ).show()
37+ val fileBytes = Base64 .decode(base64Parts[1 ], Base64 .DEFAULT )
38+ saveFileToDownloadsFolder(context, base64, fileBytes, mimeType, extension, requestFilePermission)
39+ }
40+ }
41+
42+ /* *
43+ * This method is called from PermissionListener after WRITE_EXTERNAL_STORAGE permission is granted
44+ */
45+ fun downloadBase64FileWithoutPermissionCheckAndDialog (
46+ context : Context ,
47+ base64 : String ,
48+ downloadingMessage : String ,
49+ ) {
50+ val base64Parts = base64.split(" ," )
51+ if (base64Parts.size != 2 ) {
52+ Log .e(" Base64FileDownloader" , " Unable to parse base64 to download a file. Base64 value was: $base64 " )
53+ return
54+ }
55+ val (mimeType, extension) = getMimeTypeAndFileExtension(base64Parts[0 ])
56+ Toast .makeText(context, downloadingMessage, Toast .LENGTH_LONG ).show()
57+ val fileBytes = Base64 .decode(base64Parts[1 ], Base64 .DEFAULT )
58+ saveFileToDownloadsFolder(context, base64, fileBytes, mimeType, extension, {})
59+ }
60+
61+ private fun showAlertDialog (context : Context , extension : String , onPositiveButtonClick : () -> Unit ) {
62+ AlertDialog .Builder (context)
63+ .apply {
64+ setMessage(" Do you want to download \n File.${extension} ?" )
65+ setCancelable(false )
66+ setPositiveButton(" Download" , object : DialogInterface .OnClickListener {
67+ override fun onClick (p0 : DialogInterface ? , p1 : Int ) {
68+ onPositiveButtonClick()
69+ }
70+ })
71+ setNegativeButton(" Cancel" , object : DialogInterface .OnClickListener {
72+ override fun onClick (p0 : DialogInterface ? , p1 : Int ) {
73+ // Do nothing
74+ }
75+ })
76+ }
77+ .create()
78+ .show()
79+ }
80+
81+
82+ private fun getMimeTypeAndFileExtension (header : String ): Pair <String , String > {
83+ val mimeType = Regex (" data:(.*?);base64" ).find(header)?.groupValues?.get(1 ) ? : " application/octet-stream"
84+ val extension = MimeTypeMap .getSingleton().getExtensionFromMimeType(mimeType) ? : " bin"
85+ return mimeType to extension
86+ }
87+
88+ private fun saveFileToDownloadsFolder (
89+ context : Context ,
90+ base64 : String ,
91+ fileBytes : ByteArray ,
92+ mimeType : String ,
93+ extension : String ,
94+ requestFilePermission : (String ) -> Unit ,
95+ ) {
96+ try {
97+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
98+ saveFileForAndroidQAndLater(context, fileBytes, mimeType, extension)
99+ } else {
100+ saveFileForAndroidLowerQ(context, base64, fileBytes, extension, requestFilePermission)
101+ }
102+ } catch (e: Exception ) {
103+ Log .e(" Base64FileDownloader" , " Unable to download base64 file. Base64 value was: $base64 " , e)
104+ Toast .makeText(context, " Invalid filename or type" , Toast .LENGTH_LONG ).show()
105+ }
106+ }
107+
108+ @RequiresApi(Build .VERSION_CODES .Q )
109+ private fun saveFileForAndroidQAndLater (context : Context , fileBytes : ByteArray , mimeType : String , extension : String ) {
110+ val resolver = context.contentResolver
111+ val fileName = " File.$extension "
112+ val values = ContentValues ().apply {
113+ put(MediaStore .Downloads .DISPLAY_NAME , fileName) // When using this approach System automatically adds a number to the file (1), (2) if the same name already present
114+ put(MediaStore .Downloads .MIME_TYPE , mimeType)
115+ put(MediaStore .Downloads .IS_PENDING , 1 )
116+ put(MediaStore .Downloads .RELATIVE_PATH , Environment .DIRECTORY_DOWNLOADS )
117+ }
118+ val uri = resolver.insert(MediaStore .Downloads .getContentUri(MediaStore .VOLUME_EXTERNAL_PRIMARY ), values)
119+ if (uri != null ) {
120+ resolver.openOutputStream(uri).use { it?.write(fileBytes) }
121+ values.clear()
122+ values.put(MediaStore .Downloads .IS_PENDING , 0 )
123+ resolver.update(uri, values, null , null )
124+ showDownloadingFinishedToast(context)
125+ }
126+ }
127+
128+ private fun saveFileForAndroidLowerQ (
129+ context : Context ,
130+ base64 : String ,
131+ fileBytes : ByteArray ,
132+ extension : String ,
133+ requestFilePermission : (String ) -> Unit ,
134+ ) {
135+ if (Build .VERSION .SDK_INT >= 23 &&
136+ ContextCompat .checkSelfPermission(context, Manifest .permission.WRITE_EXTERNAL_STORAGE ) != PackageManager .PERMISSION_GRANTED
137+ ) {
138+ requestFilePermission(base64)
139+ return
140+ }
141+ val downloadsDir = Environment .getExternalStoragePublicDirectory(Environment .DIRECTORY_DOWNLOADS )
142+ if (! downloadsDir.exists()) downloadsDir.mkdirs()
143+ val file = getFileToSaveWithAvailableName(downloadsDir, extension)
144+ FileOutputStream (file).use {
145+ it.write(fileBytes)
146+ // Notify MediaScanner to show up file in the folder immediately
147+ context.sendBroadcast(Intent (Intent .ACTION_MEDIA_SCANNER_SCAN_FILE ).setData(android.net.Uri .fromFile(file)))
148+ showDownloadingFinishedToast(context)
149+ }
150+ }
151+
152+ /* *
153+ * For Android <Q we have to manually create a unique name for the file
154+ * File, File (1), File (2) and so on
155+ */
156+ private fun getFileToSaveWithAvailableName (downloadsDir : File , extension : String ): File {
157+ var suffix = 1
158+ var file = File (downloadsDir, " File.$extension " )
159+ while (file.exists()) {
160+ file = File (downloadsDir, " File ($suffix ).$extension " )
161+ suffix++
162+ }
163+ return file
164+ }
165+
166+ private fun showDownloadingFinishedToast (context : Context ) {
167+ Toast .makeText(context, " Downloaded successfully" , Toast .LENGTH_LONG ).show()
168+ }
169+ }
0 commit comments