@@ -33,6 +33,8 @@ import androidx.annotation.RequiresApi
3333import androidx.core.content.ContextCompat
3434import androidx.core.os.bundleOf
3535import androidx.lifecycle.AndroidViewModel
36+ import androidx.lifecycle.LiveData
37+ import androidx.lifecycle.MutableLiveData
3638import androidx.lifecycle.SavedStateHandle
3739import androidx.lifecycle.viewModelScope
3840import com.samples.storage.data.SampleFiles
@@ -41,20 +43,19 @@ import kotlinx.coroutines.launch
4143import kotlinx.coroutines.withContext
4244import okhttp3.OkHttpClient
4345import okhttp3.Request
46+ import okhttp3.ResponseBody
4447import java.io.File
45- import java.io.OutputStream
4648import java.net.URLConnection
4749import java.nio.file.Files
4850import java.nio.file.attribute.FileTime
4951
50- // TODO(yrezgui): Use the viewModel in AddDocumentFragment (all the logic is already written)
52+ private const val TAG = " AddDocumentViewModel"
53+
5154// TODO(yrezgui): Create a file details property and keep it in the savedStateHandle
5255class AddDocumentViewModel (
5356 application : Application ,
54- private val savedStateHandle : SavedStateHandle
57+ savedStateHandle : SavedStateHandle
5558) : AndroidViewModel(application) {
56- private val TAG = " AddDocumentViewModel"
57-
5859 private val context: Context
5960 get() = getApplication()
6061
@@ -70,6 +71,37 @@ class AddDocumentViewModel(
7071 */
7172 private val httpClient by lazy { OkHttpClient () }
7273
74+ /* *
75+ * We keep the current [FileEntry] in the savedStateHandle to re-render it if there is a
76+ * configuration change and we expose it as a [LiveData] to the UI
77+ */
78+ private var _isDownloading : MutableLiveData <Boolean > = MutableLiveData (false )
79+ val isDownloading: LiveData <Boolean > = _isDownloading
80+
81+ /* *
82+ * We keep the current [FileEntry] in the savedStateHandle to re-render it if there is a
83+ * configuration change and we expose it as a [LiveData] to the UI
84+ */
85+ private var _currentFileEntry : MutableLiveData <FileEntry > = MutableLiveData (null )
86+ val currentFileEntry: LiveData <FileEntry > = _currentFileEntry
87+
88+ init {
89+ val fileEntryBundle = savedStateHandle.get<Bundle >(" current_file" )
90+ if (fileEntryBundle != null ) {
91+ _currentFileEntry .value = FileEntry .fromBundle(fileEntryBundle)
92+ }
93+ savedStateHandle.setSavedStateProvider(" current_file" ) { // saveState()
94+ if (_currentFileEntry .value != null ) {
95+ _currentFileEntry .value!! .toBundle()
96+ } else {
97+ Bundle ()
98+ }
99+ }
100+ }
101+
102+ /* *
103+ * Generate random filename when saving a new file
104+ */
73105 private fun generateFilename (extension : String ) = " ${System .currentTimeMillis()} .$extension "
74106
75107 /* *
@@ -91,77 +123,109 @@ class AddDocumentViewModel(
91123
92124 @Suppress(" BlockingMethodInNonBlockingContext" )
93125 suspend fun addRandomFile () {
126+ _isDownloading .postValue(true )
127+
94128 val randomRemoteUrl = SampleFiles .nonMedia.random()
95- val extension = randomRemoteUrl.substring(randomRemoteUrl.lastIndexOf(" ." ))
129+ val extension = randomRemoteUrl.substring(randomRemoteUrl.lastIndexOf(" ." ) + 1 )
96130 val filename = generateFilename(extension)
97- lateinit var outputStream: OutputStream
98131
99- try {
100- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
101- val uri = addFileToDownloadsApi29(filename)
102- outputStream = context.contentResolver.openOutputStream(uri, " w" )
103- ? : throw Exception (" ContentResolver couldn't open $uri outputStream" )
132+ withContext(Dispatchers .IO ) {
133+ try {
134+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
135+ val uri = addFileToDownloadsApi29(filename)
136+ val outputStream = context.contentResolver.openOutputStream(uri, " w" )
137+ ? : throw Exception (" ContentResolver couldn't open $uri outputStream" )
138+
139+ val responseBody = downloadFileFromInternet(randomRemoteUrl)
140+
141+ if (responseBody == null ) {
142+ _isDownloading .postValue(false )
143+ return @withContext
144+ }
145+
146+ // .use is an extension function that closes the output stream where we're
147+ // saving the file content once its lambda is finished being executed
148+ responseBody.use {
149+ outputStream.use {
150+ responseBody.byteStream().copyTo(it)
151+ }
152+ }
153+
154+ Log .d(TAG , " File downloaded ($uri )" )
104155
105- downloadFileFromInternet(randomRemoteUrl, outputStream )
106- Log .d( TAG , " File downloaded ( $uri ) " )
156+ val path = getMediaStoreEntryPathApi29(uri )
157+ ? : throw Exception ( " ContentResolver couldn't find $uri " )
107158
108- val path = getMediaStoreEntryPathApi29(uri)
109- ? : throw Exception ( " ContentResolver couldn't find $uri " )
110- scanFileApi (path) {
111- Log .d(TAG , " MediaStore updated ($path )" )
159+ // We scan the newly added file to make sure MediaStore.Downloads is always up
160+ // to date
161+ scanFilePath (path, responseBody.contentType().toString() ) {
162+ Log .d(TAG , " MediaStore updated ($path )" )
112163
113- viewModelScope.launch {
114- val fileDetails = getFileDetailsApi29(uri)
115- Log .d(TAG , " New file: $fileDetails " )
164+ viewModelScope.launch {
165+ val fileDetails = getFileDetailsApi29(uri)
166+ Log .d(TAG , " New file: $fileDetails " )
167+
168+ _currentFileEntry .postValue(fileDetails)
169+ _isDownloading .postValue(false )
170+ }
116171 }
117- }
118- } else {
119- val file = addFileToDownloadsApi21(filename)
120- outputStream = file.outputStream()
172+ } else {
173+ val file = addFileToDownloadsApi21(filename)
174+ val outputStream = file.outputStream()
121175
122- downloadFileFromInternet(randomRemoteUrl, outputStream)
123- Log .d(TAG , " File downloaded (${file.absolutePath} )" )
176+ val responseBody = downloadFileFromInternet(randomRemoteUrl)
124177
125- scanFileApi(file.absolutePath) {
126- Log .d(TAG , " MediaStore updated (${file.absolutePath} )" )
178+ if (responseBody == null ) {
179+ _isDownloading .postValue(false )
180+ return @withContext
181+ }
127182
128- viewModelScope.launch {
129- val fileDetails = if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . O ) {
130- getFileDetailsApi26(file.absolutePath)
131- } else {
132- getFileDetailsApi21(file.absolutePath )
183+ // .use is an extension function that closes the output stream where we're
184+ // saving the file content once its lambda is finished being executed
185+ responseBody.use {
186+ outputStream.use {
187+ responseBody.byteStream().copyTo(it )
133188 }
189+ }
190+
191+ Log .d(TAG , " File downloaded (${file.absolutePath} )" )
192+
193+ // We scan the newly added file to make sure MediaStore.Files is always up to
194+ // date
195+ scanFilePath(file.path, responseBody.contentType().toString()) {
196+ Log .d(TAG , " MediaStore updated ($file .path)" )
134197
135- Log .d(TAG , " New file: $fileDetails " )
198+ viewModelScope.launch {
199+ val fileDetails = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
200+ getFileDetailsApi26(file.absolutePath)
201+ } else {
202+ getFileDetailsApi21(file.absolutePath)
203+ }
204+ Log .d(TAG , " New file: $fileDetails " )
205+
206+ _currentFileEntry .postValue(fileDetails)
207+ _isDownloading .postValue(false )
208+ }
136209 }
137210 }
211+ } catch (e: Exception ) {
212+ Log .e(TAG , e.toString())
213+ _isDownloading .postValue(false )
138214 }
139- } catch (e: Exception ) {
140- Log .e(TAG , e.toString())
141215 }
142216 }
143217
144218 /* *
145219 * Downloads a random file from internet and saves its content to the specified outputStream
146220 */
147221 @Suppress(" BlockingMethodInNonBlockingContext" )
148- private suspend fun downloadFileFromInternet (
149- url : String ,
150- outputStream : OutputStream
151- ) {
222+ private suspend fun downloadFileFromInternet (url : String ): ResponseBody ? {
152223 // We use OkHttp to create HTTP request
153224 val request = Request .Builder ().url(url).build()
154225
155- withContext(Dispatchers .IO ) {
226+ return withContext(Dispatchers .IO ) {
156227 val response = httpClient.newCall(request).execute()
157-
158- // .use is an extension function that closes the output stream where we're
159- // saving the file content once its lambda is finished being executed
160- response.body?.use { responseBody ->
161- outputStream.use {
162- responseBody.byteStream().copyTo(it)
163- }
164- }
228+ return @withContext response.body
165229 }
166230 }
167231
@@ -211,9 +275,9 @@ class AddDocumentViewModel(
211275 * size, even though the file is definitely not empty. MediaStore will eventually scan the file
212276 * but it's better to do it ourselves to have a fresher state whenever we can
213277 */
214- private suspend fun scanFileApi (path : String , callback : () -> Unit ) {
278+ private suspend fun scanFilePath (path : String , mimeType : String , callback : () -> Unit ) {
215279 withContext(Dispatchers .IO ) {
216- MediaScannerConnection .scanFile(context, arrayOf(path), emptyArray( )) { path, uri ->
280+ MediaScannerConnection .scanFile(context, arrayOf(path), arrayOf(mimeType )) { _, _ ->
217281 callback()
218282 }
219283 }
0 commit comments