Hi I am new in Android development and my app is completely database oriented.
In my app I am using the method of copying a database file from the assets folder.
It will increase the size of the apk.
I want to copy it from the internet the first time my app runs on the phone.
How do I download the database file to my app database folder.
This is how I did it:
I have an implementation of DownloadManager that deals with downloading the DB. In my case the DB is significantly big so DownloadManager is a good option for effectively dealing with large downloads.
One thing to note when implementing DownloadManager; It's recommended that you download files first as temporary files and then move them to the final location. This is to avoid weird security issues I faced when doing so. Also download notification visibility may affect what permissions are required, if you decide to have no notifications at all you need to add a DOWNLOAD_WITHOUT_NOTIFICATION.
Permissions required:
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE " />
DownloadManager implementation
/**
* Manages all DB retrieval and file level validation.
*
*/
class DataBaseInitializationRepository(private val app: Application, private val dbHelper: DatabaseFileHelper) {
fun initiateDB( callback: DownloadCompleteCallback){
val dbUrl = dbHelper.getDbUrl()
val tempDbFile = dbHelper.getTempDbFile()
val permanentDbFile = dbHelper.getPermanentDbFile()
if (!permanentDbFile.exists() && tempDbFile.length() <= 0) {
val downloadManager = app.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downLoadDBRequest = DownloadManager.Request(Uri.parse( dbUrl ))
.setTitle( app.getString( R.string.download_db_title ) )
.setDescription(app.getString( R.string.download_db_description ))
.setDestinationInExternalFilesDir( app,
null,
tempDbFile.path
)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val downloadId = downloadManager.enqueue( downLoadDBRequest )
registerReceiver( callback, downloadId )
}else{
callback.onComplete(0L)
}
}
private fun registerReceiver(callback: DownloadCompleteCallback, downloadId: Long){
val receiver = object: BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (id == downloadId){
//Move index reads to reusable function
val downloadManager = app.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val query = DownloadManager.Query()
query.setFilterById( downloadId )
val data = downloadManager.query( query )
if(data.moveToFirst() && data.count > 0){
val statusIndex = data.getColumnIndex(DownloadManager.COLUMN_STATUS)
val status = data.getInt( statusIndex )
if(status == DownloadManager.STATUS_SUCCESSFUL){
val localUriIndex = data.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
val localFile = File(
data.getString( localUriIndex )
.replace("file://","" )
)
if(localFile.exists()){
permanentlyStoreDb(localFile)
callback.onComplete(id)
}else{
callback.onFail("Initial Database Download Failed - File not found")
}
}else if(status == DownloadManager.STATUS_FAILED){
val reasonIndex = data.getColumnIndex(DownloadManager.COLUMN_REASON)
val reason = data.getInt( reasonIndex )
if(reason == DownloadManager.ERROR_FILE_ALREADY_EXISTS){
callback.onComplete(id)
}else{
callback.onFail("Initial Database Download Failed: $reason")
}
}
}else{
callback.onFail("Initial Database Download Failed - Unable to read download metadata")
}
}
}
}
app.registerReceiver( receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) )
}
private fun permanentlyStoreDb(tempFile: File): File {
val permanentDbFile = dbHelper.getPermanentDbFile()
try {
if(tempFile.exists()) {
tempFile.copyTo(
permanentDbFile,
true,
1024
)
tempFile.delete()
}else{
throw IOException("Temporal DB file doesn't exist")
}
}catch (ioex: IOException) {
throw IOException("Unable to copy DB to permanent storage:", ioex)
}
return permanentDbFile
}
/**
* Allows download completion to be notified back to the calling view model
*/
interface DownloadCompleteCallback{
fun onComplete(downloadId: Long)
fun onFail(message: String)
}
}
DatabaseFileHelper contains the logic to determine the temporary file, the permanent DB location and the DB URL where the download will happen. This is the logic I used for the temporary file:
fun getTempDbFile(): File {
return File.createTempFile(<FILE-LOCATION>, null, app.cacheDir)
}
So in case you want to notify a running Activity/Fragment, you only need to pass a DownloadCompleteCallback implementation to this component to get it.
In case you are using Room, just make sure your implementation of RoomDatabase uses the following on your getInstance method
.createFromFile( dbFileHelper.getPermanentDbFile() )
just copy it from web instead of copying it form assets. Hint
Related
I am trying to download a file into the external public directory but the Uri returned by downloadManager.getUriForDownloadedFile(requestId) isn't usable. I'm unable to launch an ACTION_OPEN intent with it, even though this same process works for Android 10.
I suspect this has something to do with missing updated permissions on Android 13, but there are no errors logged in logcat.
I am able to get it working as expected by using setDestinationInExternalFilesDir to store the file inside the private applications directory and using a ContentResolver to copy it into the phones external media storage, but that is a lot of code and very verbose. Using setDestinationInExternalPublicDir from DownloadManager is a lot cleaner and concise.
This is how I am creating and enqueuing my request
val request = DownloadManager
.Request(Uri.parse(downloadUrl))
.setDestinationInExternalPublicDir(
Environment.DIRECTORY_MOVIES,
"${UUID.randomUUID()}.mp4"
)
.setMimeType("video/mp4")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setTitle("Saving...")
.setRequiresCharging(false)
.setAllowedOverMetered(true)
.setAllowedOverRoaming(true)
requestId = downloadManager.enqueue(request)
And this is how I am listening for download completion and attempting to use the Uri.
private val downloadBroadCastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val requestId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) ?: -1
val query = DownloadManager.Query()
query.setFilterById(requestId)
val cursor = downloadManager.query(query)
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
when (cursor.getInt(columnIndex)) {
DownloadManager.STATUS_SUCCESSFUL -> {
LOG.info("onReceive: Video download completed!")
val uri = downloadManager.getUriForDownloadedFile(requestId)
context.startActivity(
Intent(Intent.ACTION_VIEW, uri).apply {
setDataAndType(uri, "video/mp4")
}
)
}
}
}
}
I have an implementation of DownloadManager that works correctly on most devices I've tested it. I recently started testing on a Samsung Galaxy S10 (Android 9), and I noticed a completely different behavior. the queued download takes up to 10 min to even start, I can see this because my download's request visibility is VISIBILITY_VISIBLE_NOTIFY_COMPLETED so it shows up as a notification after several min of the download request being queued.
When the download completes (either failing or succeeding) I also noticed that I don't get a call to my registered BroadcastReceiver until probably other 10 min, I know iths finished because, it's visible on the notifications section of the OS.
Has anyone faced this or know how to make DownloadManager behave as its expected?
I've considered rewriting the component so I don't depend on DownloadManager but that exactly what Im trying to avoid.
This is my implementation:
Permissions:
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE " />
DownloadManager implementation
/**
* Manages all DB retrieval and file level validation.
*
*/
class DataBaseInitializationRepository(private val app: Application, private val dbHelper: DatabaseFileHelper) {
fun initiateDB( callback: DownloadCompleteCallback){
val dbUrl = dbHelper.getDbUrl()
val tempDbFile = dbHelper.getTempDbFile()
val permanentDbFile = dbHelper.getPermanentDbFile()
if (!permanentDbFile.exists() && tempDbFile.length() <= 0) {
val downloadManager = app.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downLoadDBRequest = DownloadManager.Request(Uri.parse( dbUrl ))
.setTitle( app.getString( R.string.download_db_title ) )
.setDescription(app.getString( R.string.download_db_description ))
.setDestinationInExternalFilesDir( app,
null,
tempDbFile.path
)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val downloadId = downloadManager.enqueue( downLoadDBRequest )
registerReceiver( callback, downloadId )
}else{
callback.onComplete(0L)
}
}
private fun registerReceiver(callback: DownloadCompleteCallback, downloadId: Long){
val receiver = object: BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (id == downloadId){
//Move index reads to reusable function
val downloadManager = app.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val query = DownloadManager.Query()
query.setFilterById( downloadId )
val data = downloadManager.query( query )
if(data.moveToFirst() && data.count > 0){
val statusIndex = data.getColumnIndex(DownloadManager.COLUMN_STATUS)
val status = data.getInt( statusIndex )
if(status == DownloadManager.STATUS_SUCCESSFUL){
val localUriIndex = data.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
val localFile = File(
data.getString( localUriIndex )
.replace("file://","" )
)
if(localFile.exists()){
permanentlyStoreDb(localFile)
callback.onComplete(id)
}else{
callback.onFail("Initial Database Download Failed - File not found")
}
}else if(status == DownloadManager.STATUS_FAILED){
val reasonIndex = data.getColumnIndex(DownloadManager.COLUMN_REASON)
val reason = data.getInt( reasonIndex )
if(reason == DownloadManager.ERROR_FILE_ALREADY_EXISTS){
callback.onComplete(id)
}else{
callback.onFail("Initial Database Download Failed: $reason")
}
}
}else{
callback.onFail("Initial Database Download Failed - Unable to read download metadata")
}
}
}
}
app.registerReceiver( receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) )
}
private fun permanentlyStoreDb(tempFile: File): File {
val permanentDbFile = dbHelper.getPermanentDbFile()
try {
if(tempFile.exists()) {
tempFile.copyTo(
permanentDbFile,
true,
1024
)
tempFile.delete()
}else{
throw IOException("Temporal DB file doesn't exist")
}
}catch (ioex: IOException) {
throw IOException("Unable to copy DB to permanent storage:", ioex)
}
return permanentDbFile
}
/**
* Allows download completion to be notified back to the calling view model
*/
interface DownloadCompleteCallback{
fun onComplete(downloadId: Long)
fun onFail(message: String)
}
}
DatabaseFileHelper contains the logic to determine the temporary file, the permanent DB location and the DB URL where the download will happen. This is the logic I used for the temporary file:
fun getTempDbFile(): File {
return File.createTempFile(<FILE-LOCATION>, null, app.cacheDir)
}
I am calling below function to download a binary file.
fun downloadFile(
baseActivity: Context,
batteryId: String,
downloadFileUrl: String?,
title: String?
): Long {
val directory =
File(Environment.getExternalStorageDirectory().toString() + "/destination_folder")
if (!directory.exists()) {
directory.mkdirs()
}
//Getting file extension i.e. .bin, .mp4 , .jpg, .png etc..
val fileExtension = downloadFileUrl?.substring(downloadFileUrl.lastIndexOf("."))
val downloadReference: Long
var objDownloadManager: DownloadManager =
baseActivity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val uri = Uri.parse(downloadFileUrl)
val request = DownloadManager.Request(uri)
//Firmware file name as batteryId and extension
firmwareFileSubPath = batteryId + fileExtension
request.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
"" + batteryId + fileExtension
)
request.setTitle(title)
downloadReference = objDownloadManager.enqueue(request) ?: 0
return downloadReference
}
Once the file got downloaded I am receiving it in below onReceive() method of Broadcast receiver:
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
intent.extras?.let {
//retrieving the file
val downloadedFileId = it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID)
val downloadManager =
getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val uri: Uri = downloadManager.getUriForDownloadedFile(downloadedFileId)
viewModel.updateFirmwareFilePathToFirmwareTable(uri)
}
}
}
I am downloading the files one by one and wants to know that which file is downloaded.
Based on the particular file download, I have to update the entry in my local database.
So, here in onReceive() method how can I identify that which specific file is downloaded?
Thanks.
One way to identify your multiple downloads simultaneously is to track id returned from DownloadManager to your local db mapped to given entry when you call objDownloadManager.enqueue(request).
Document of DownloadManager.enquque indicates that:
Enqueue a new download. The download will start automatically once the download manager is ready to execute it and connectivity is available.
So, if you store that id mapped to your local database entry for given record then during onReceive() you can identify back to given record.
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
intent.extras?.let {
//retrieving the file
val downloadedFileId = it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID)
// Find same id from db that you stored previously
val downloadManager =
getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val uri: Uri = downloadManager.getUriForDownloadedFile(downloadedFileId)
viewModel.updateFirmwareFilePathToFirmwareTable(uri)
}
}
}
Here, it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID) returns you the same id for which download was started previously and enqueue returned.
Document for EXTRA_DOWNLOAD_ID indicates that:
Intent extra included with ACTION_DOWNLOAD_COMPLETE intents, indicating the ID (as a long) of the download that just completed.
You have the Uri of file, now simply get the file name to identify the file, you can use following function to get file name
fun getFileName(uri: Uri): String? {
var result: String? = null
when(uri.scheme){
"content" -> {
val cursor: Cursor? = getContentResolver().query(uri, null, null, null, null)
cursor.use {
if (it != null && it.moveToFirst()) {
result = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
}
else -> {
val lastSlashIndex = uri.path?.lastIndexOf('/')
if(lastSlashIndex != null && lastSlashIndex != -1) {
result = uri.path!!.substring(lastSlashIndex + 1)
}
}
}
return result
}
I am downloading files using Android's Download Manager and storing the files on removeable SD Card. It is working on Android OS6 Marshmallow and Android OS8 Oreo but NOT working on Android OS9 PIE.
setDestinationURI() is being used to store files on removable SD Card.
private fun setExternalStorageDir() {
val arrayOfFiles = getExternalFilesDirs(Environment.DIRECTORY_DOWNLOADS) //as per official documentation
parentDirectory = if (arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null)
arrayOfFiles[1].toString() //External Storage Dir Path being retrieved successfully
else if (arrayOfFiles.size == 1 && arrayOfFiles[0] != null)
arrayOfFiles[0].toString() //Internal Storage being treated as External Storage by Android
else
Environment.DIRECTORY_DOWNLOADS //Internal Storage Download Folder
}
//Request Builder Code. Request is being sucessfully executed in OS 6 and 8 respectively
val request = DownloadManager.Request(downloadUri).apply {
setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
setAllowedOverRoaming(false)
setTitle("Image Downloading")
setNotificationVisibility(VISIBILITY_VISIBLE)
setDestinationUri(Uri.fromFile(File("$parentDirectory/sampleImage.png")))
}
private val onDownloadComplete = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
val query = DownloadManager.Query()
query.setFilterById(reference)
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val cursor = downloadManager.query(query)
cursor.moveToFirst()
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
val status = cursor.getInt(statusIndex)
if (status == DownloadManager.STATUS_SUCCESSFUL) {
} else if (status == DownloadManager.STATUS_FAILED) {
Log.e("DOWNLOAD BROADCAST", "Download failed")
val reasonIndex = cursor.getColumnIndex(DownloadManager.COLUMN_REASON)
val reason = cursor.getInt(reasonIndex)
Log.e("DOWNLOAD BROADCAST", reason.toString())
}
}
}
DownloadManager fails executing request on Android PIE OS9 with status 403
PS: Min SDK 21, Targetted 29. Runtime Permissions are handled.
W/DownloadManager: [37] Stop requested with status FILE_ERROR: Failed to generate filename: java.io.IOException: Permission denied"
I am using Exoplayer to create my own music player. I am also adding the option to download the track but I have a problem when I am trying to download the track that I am playing. I add a notification to the download to check the progress of the download and it appears but it even doesn't start. What I think is that it might have some kind of problem with the buffering cache and the download since they are stored in the same folder.
To download the tracks I do the following:
override fun addDownloadTrack(track: Track) {
getIfTrackIsCached.run({ isCached ->
if (!isCached) {
val data = Util.toByteArray(track.title.byteInputStream())
val downloadRequest =
DownloadRequest(track.id, DownloadRequest.TYPE_PROGRESSIVE, Uri.parse(track.href), Collections.emptyList(), track.id, data)
DownloadService.sendAddDownload(context, ExoPlayerDownloadService::class.java, downloadRequest, false)
}
}, ::onError, GetIfTrackIsCached.Params(track.id))
}
This is the DownloadService:
class ExoPlayerDownloadService : DownloadService(
FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
Constants.CHANNEL_DOWNLOAD_ID,
R.string.exo_download_notification_channel_name) {
private val manager: DownloadManager by inject()
private val channelIsCached: ChannelPublisher<CachedMedia> by inject(PUBLISHER_IS_CACHED)
private val notificationHelper: DownloadNotificationHelper by inject()
private var nextNotificationId: Int = FOREGROUND_NOTIFICATION_ID
override fun onCreate() {
super.onCreate()
if (!manager.isInitialized) {
manager.maxParallelDownloads = MAX_PARALLEL_DOWNLOADS
}
}
override fun getDownloadManager(): DownloadManager = manager
override fun getForegroundNotification(downloads: MutableList<Download>?): Notification {
var text = ""
var index = 1
downloads?.forEach { text += "${if (downloads.size > 1) "${index++} - " else ""}${Util.fromUtf8Bytes(it.request.data)}\n" }
return notificationHelper.buildProgressNotification(R.drawable.ic_stat_downloading, null, text, downloads)
}
override fun getScheduler(): Scheduler? = null
override fun onDownloadChanged(download: Download?) {
val notification = when (download?.state) {
Download.STATE_COMPLETED -> {
channelIsCached.publish(CachedMedia(download.request.id, true))
notificationHelper.buildDownloadCompletedNotification(R.drawable.ic_stat_download_complete, null, Util.fromUtf8Bytes(download.request.data))
}
Download.STATE_FAILED ->
notificationHelper.buildDownloadFailedNotification(R.drawable.ic_stat_download_failed, null, Util.fromUtf8Bytes(download.request.data))
else -> null
}
notification?.let { NotificationUtil.setNotification(this#ExoPlayerDownloadService, ++nextNotificationId, it) }
}
companion object {
private const val MAX_PARALLEL_DOWNLOADS = 3
private const val FOREGROUND_NOTIFICATION_ID = 2000
}
}
And to create the cache I use this:
SimpleCache(File(androidContext().cacheDir, CACHE_MEDIA_FOLDER), NoOpCacheEvictor(), get<DatabaseProvider>())
How can I avoid conflicts between buffering cache and downloaded files?
I had this issue also, and found the solution!
The downloading documentation states
The CacheDataSource.Factory should be configured as read-only to avoid downloading that content as well during playback.
To do this you must call setCacheWriteDataSinkFactory(null) on your CacheDataSource.Factory object.
This will prevent the stream from writing to the cache, allowing the downloader to write as expected.