-
Notifications
You must be signed in to change notification settings - Fork 651
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Connect SDK] Enable downloads in the Connect SDK (#9583)
* Initial implementation * minor refactors * Fix strict mode violations * implement basic download finished toast * Remove file opening code (not working), make internal * Add test * Fix test * Fix lint (unused import) * Close resource * Refactor into interfaces * Make interfaces internal * Move scope into toast manager * Better abstractions * Fix test
- Loading branch information
1 parent
701bcb0
commit 06beaa8
Showing
8 changed files
with
264 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,7 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<resources> | ||
<string name="stripe_not_yet_built">Not yet built…</string> | ||
<string name="stripe_downloading_file">Downloading file</string> | ||
<string name="stripe_download_complete">Download complete</string> | ||
<string name="stripe_unable_to_download_file">Unable to download file</string> | ||
</resources> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
connect/src/main/java/com/stripe/android/connect/webview/StripeDownloadListener.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package com.stripe.android.connect.webview | ||
|
||
import android.app.DownloadManager.STATUS_PAUSED | ||
import android.app.DownloadManager.STATUS_PENDING | ||
import android.app.DownloadManager.STATUS_RUNNING | ||
import android.content.Context | ||
import android.webkit.DownloadListener | ||
import com.stripe.android.connect.R | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.delay | ||
import kotlinx.coroutines.launch | ||
|
||
internal class StripeDownloadListener( | ||
private val context: Context, | ||
private val stripeDownloadManager: StripeDownloadManager = StripeDownloadManagerImpl(context), | ||
private val stripeToastManager: StripeToastManager = StripeToastManagerImpl(context), | ||
private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO), | ||
) : DownloadListener { | ||
|
||
override fun onDownloadStart( | ||
url: String?, | ||
userAgent: String?, | ||
contentDisposition: String?, | ||
mimetype: String?, | ||
contentLength: Long | ||
) { | ||
if (url == null) { | ||
showErrorToast() | ||
return | ||
} | ||
|
||
ioScope.launch { | ||
val downloadId = stripeDownloadManager.enqueueDownload(url, contentDisposition, mimetype) | ||
if (downloadId == null) { | ||
showErrorToast() | ||
return@launch | ||
} | ||
|
||
// Monitor the download progress and show a toast when done | ||
val query = stripeDownloadManager.getQueryById(downloadId) | ||
var isDownloading = true | ||
while (isDownloading) { | ||
val status = stripeDownloadManager.getDownloadStatus(query) | ||
if (status == null) { | ||
showErrorToast() | ||
return@launch | ||
} else if (status !in listOf(STATUS_PENDING, STATUS_RUNNING, STATUS_PAUSED)) { | ||
showOpenFileToast() | ||
isDownloading = false // download complete - exit the loop | ||
} | ||
delay(DOWNLOAD_DELAY_MS) | ||
} | ||
} | ||
} | ||
|
||
private fun showErrorToast() { | ||
stripeToastManager.showToast(context.getString(R.string.stripe_unable_to_download_file)) | ||
} | ||
|
||
private fun showOpenFileToast() { | ||
stripeToastManager.showToast(context.getString(R.string.stripe_download_complete)) | ||
} | ||
|
||
internal companion object { | ||
private const val DOWNLOAD_DELAY_MS = 1_000L | ||
} | ||
} |
69 changes: 69 additions & 0 deletions
69
connect/src/main/java/com/stripe/android/connect/webview/StripeDownloadManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package com.stripe.android.connect.webview | ||
|
||
import android.app.DownloadManager | ||
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED | ||
import android.content.Context | ||
import android.net.Uri | ||
import android.os.Environment.DIRECTORY_DOWNLOADS | ||
import android.webkit.URLUtil | ||
import com.stripe.android.connect.R | ||
|
||
/** | ||
* Provides an interface for various download and file operations. Useful for mocking in tests. | ||
*/ | ||
internal interface StripeDownloadManager { | ||
|
||
/** | ||
* Enqueues a download for the given URL, content disposition, and MIME type. | ||
* Returns null if a download could not be started. | ||
*/ | ||
fun enqueueDownload(url: String, contentDisposition: String? = null, mimeType: String? = null): Long? | ||
|
||
/** | ||
* Returns a [DownloadManager.Query] for the given download ID, used for monitoring the status of a download. | ||
*/ | ||
fun getQueryById(id: Long): DownloadManager.Query | ||
|
||
/** | ||
* Returns the status of the download represented by [query]. Maps to [DownloadManager.COLUMN_STATUS], ie. | ||
* [DownloadManager.STATUS_PENDING], [DownloadManager.STATUS_RUNNING], [DownloadManager.STATUS_PAUSED], etc. | ||
* | ||
* Returns null if the status could not be determined. This operation should not be retried if null is returned. | ||
*/ | ||
fun getDownloadStatus(query: DownloadManager.Query): Int? | ||
} | ||
|
||
internal class StripeDownloadManagerImpl(private val context: Context) : StripeDownloadManager { | ||
private val downloadManager: DownloadManager? = | ||
context.getSystemService(Context.DOWNLOAD_SERVICE) as? DownloadManager | ||
|
||
override fun enqueueDownload(url: String, contentDisposition: String?, mimeType: String?): Long? { | ||
downloadManager ?: return null | ||
|
||
val request = DownloadManager.Request(Uri.parse(url)) | ||
val fileName = URLUtil.guessFileName(url, contentDisposition, mimeType) | ||
request.setDestinationInExternalPublicDir(DIRECTORY_DOWNLOADS, fileName) | ||
request.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED) | ||
request.setTitle(fileName) | ||
request.setDescription(context.getString(R.string.stripe_downloading_file)) | ||
request.setMimeType(mimeType) | ||
|
||
return downloadManager.enqueue(request) | ||
} | ||
|
||
override fun getQueryById(id: Long): DownloadManager.Query { | ||
return DownloadManager.Query().setFilterById(id) | ||
} | ||
|
||
override fun getDownloadStatus(query: DownloadManager.Query): Int? { | ||
downloadManager ?: return null | ||
|
||
val cursor = downloadManager.query(query) ?: return null | ||
return cursor.use { resource -> | ||
resource.moveToFirst() | ||
val index = resource.getColumnIndex(DownloadManager.COLUMN_STATUS) | ||
if (index < 0) return null // status does not exist - abort | ||
resource.getInt(index) | ||
} | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
connect/src/main/java/com/stripe/android/connect/webview/StripeToastManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.stripe.android.connect.webview | ||
|
||
import android.content.Context | ||
import android.widget.Toast | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.MainScope | ||
import kotlinx.coroutines.launch | ||
|
||
/** | ||
* Provides an interface for various download and file operations. Useful for mocking in tests. | ||
*/ | ||
internal interface StripeToastManager { | ||
fun showToast(toastString: String) | ||
} | ||
|
||
internal class StripeToastManagerImpl( | ||
private val context: Context, | ||
private val scope: CoroutineScope = MainScope() | ||
) : StripeToastManager { | ||
override fun showToast(toastString: String) { | ||
scope.launch { | ||
Toast.makeText(context, toastString, Toast.LENGTH_LONG).show() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
connect/src/test/java/com/stripe/android/connect/webview/StripeDownloadListenerTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package com.stripe.android.connect.webview | ||
|
||
import android.app.DownloadManager | ||
import android.content.Context | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.test.TestScope | ||
import kotlinx.coroutines.test.UnconfinedTestDispatcher | ||
import kotlinx.coroutines.test.runTest | ||
import kotlinx.coroutines.test.setMain | ||
import org.junit.Before | ||
import org.junit.Test | ||
import org.mockito.kotlin.any | ||
import org.mockito.kotlin.anyOrNull | ||
import org.mockito.kotlin.doReturn | ||
import org.mockito.kotlin.mock | ||
import org.mockito.kotlin.verify | ||
import org.mockito.kotlin.verifyNoInteractions | ||
import org.mockito.kotlin.whenever | ||
|
||
class StripeDownloadListenerTest { | ||
|
||
private val context: Context = mock { | ||
on { getString(any()) } doReturn "Placeholder string" | ||
} | ||
private val stripeDownloadManager: StripeDownloadManager = mock { | ||
on { enqueueDownload(any(), anyOrNull(), anyOrNull()) } doReturn 123L | ||
on { getQueryById(any()) } doReturn mock() | ||
on { getDownloadStatus(any()) } doReturn DownloadManager.STATUS_SUCCESSFUL | ||
} | ||
private val stripeToastManager: StripeToastManager = mock() | ||
private val testScope = TestScope() | ||
|
||
private lateinit var stripeDownloadListener: StripeDownloadListener | ||
|
||
@Before | ||
fun setup() { | ||
Dispatchers.setMain(UnconfinedTestDispatcher()) | ||
initDownloadListener() | ||
} | ||
|
||
private fun initDownloadListener( | ||
context: Context = this.context, | ||
stripeDownloadManager: StripeDownloadManager = this.stripeDownloadManager, | ||
stripeToastManager: StripeToastManager = this.stripeToastManager, | ||
ioScope: CoroutineScope = testScope, | ||
) { | ||
stripeDownloadListener = StripeDownloadListener( | ||
context = context, | ||
stripeDownloadManager = stripeDownloadManager, | ||
stripeToastManager = stripeToastManager, | ||
ioScope = ioScope, | ||
) | ||
} | ||
|
||
@Test | ||
fun `onDownloadStart creates download request`() = runTest { | ||
val url = "https://example.com/file.pdf" | ||
val userAgent = "Mozilla/5.0" | ||
val contentDisposition = "attachment; filename=file.pdf" | ||
val mimeType = "application/pdf" | ||
val contentLength = 1024L | ||
|
||
stripeDownloadListener.onDownloadStart(url, userAgent, contentDisposition, mimeType, contentLength) | ||
testScope.testScheduler.advanceUntilIdle() | ||
|
||
verify(stripeDownloadManager).enqueueDownload(url, contentDisposition, mimeType) | ||
verify(stripeToastManager).showToast(any()) | ||
} | ||
|
||
@Test | ||
fun `onDownloadStart does nothing when URL is null`() = runTest { | ||
stripeDownloadListener.onDownloadStart(null, "", "", "", 0) | ||
testScope.testScheduler.advanceUntilIdle() | ||
|
||
verifyNoInteractions(stripeDownloadManager) | ||
verify(stripeToastManager).showToast(any()) | ||
} | ||
|
||
@Test | ||
fun `onDownloadStart shows error toast when enqueue returns null`() = runTest { | ||
whenever(stripeDownloadManager.enqueueDownload(any(), anyOrNull(), anyOrNull())).thenReturn(null) | ||
|
||
val url = "https://example.com/file.pdf" | ||
val userAgent = "Mozilla/5.0" | ||
val contentDisposition = "attachment; filename=file.pdf" | ||
val mimeType = "application/pdf" | ||
val contentLength = 1024L | ||
|
||
stripeDownloadListener.onDownloadStart(url, userAgent, contentDisposition, mimeType, contentLength) | ||
testScope.testScheduler.advanceUntilIdle() | ||
|
||
verify(stripeToastManager).showToast(any()) | ||
} | ||
} |