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:([^;]+)" ).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