» » Android studio. Kotlin. Saving the file in the public Download folder

Android studio. Kotlin. Saving the file in the public Download folder

In working on my project, I spent most of the time figuring out how to correctly save the file to a public folder, for example, Download. I have not been able to find a clear and concise explanation on the internet. Collected information in parts and reached the result by trial and error.
This complexity is due to many factors. Language barrier: Romanian - English, Kotlin - Java. Differences in copying methods in different versions of Android. It wasn't easy to figure it out. As a result, I am writing this guide to make life easier for those who read devlister.com.

A task

I needed to implement in my application the ability to make a database backup so that the user could access the copy from another application. For example, send a backup file to yourself by e-mail or download over the wire to a desktop computer.

I realized that all application data, including the database file, is in an internal folder that cannot be accessed from outside. Therefore, you need to somehow make a copy of the database file from the internal storage to a public folder. I selected the Download folder.

Solution

1. Editing the Manifest file

I specify in the Manifest permission to access external storage:  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<manifest     ...    package="project.name">    ...      <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <application        ...    </application>
</manifest>

I understand that this is enough. If write access is enabled, read access may not be granted.

2. I handle an exception (exception)

I packed all the code responsible for copying in try. If something goes wrong, then the application will not crash, giving the user an error message, but the code in the catch will be executed.

try { // copy data ...} catch (e: Exception) { e.printStackTrace() // if something went wrong ...}


3. Check the Android version

I look at the version information in the file  build.gradle (Module)

android {    ...
    defaultConfig {            applicationId "name.of.your.project"        minSdkVersion 23        targetSdkVersion 30                ...    }
    ...
}

My project specifies that the target version is 30 and the minimum version is 23. This means that devices with this minimum version should be able to run my application successfully. For more clarity about Android versions, you can look at the following screenshot:


In the table above, I look at the  API Level column. Android 6.0 Marshmellow  is the minimum version  of Android , 84.9% of devices will be supported.

Android 10 is marked "Q" in the code ( Build.VERSION_CODES.Q)  It turns out that copying files, namely organizing access to public folders, looks completely different from this version of Android and higher!


A piece of code with checking the Android version and some lyrics from Google developers

What for? To give you a taste of your future, a preview of what's to come. Con permiso, Captain. The hall is rented, the orchestra is involved. It's time to see if you can dance.

Apparently I learned to dance, otherwise I would not have written this article. I figured out that the code to copy a file to a public folder for Android 10+ should be different.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// version >= 29 (Android 10, 11, ...)

...                
} else {
// version < 29 (Android ..., 7,8,9)                
...

}
Therefore, without fail, I check the version and prepare to write different code. Sadness...

4. Copy if Android version >= 10 (SdkVersion >= 29)

In this case, no permission is required. In my opinion, even in the Manifesto it is possible to specify nothing. Here I really had to, as we say, "dance with a tambourine", until I figured out what was what.
val contentValues = ContentValues().apply {  put(MediaStore.MediaColumns.DISPLAY_NAME, dbFileName)
}
val dstUri = applicationContext.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (dstUri != null) {  val src = FileInputStream(applicationContext.getDatabasePath(DbName.DATABASE_NAME))  val dst = applicationContext.contentResolver.openOutputStream(dstUri)  src.copyTo(dst!!)  src.close()  dst.close()  backupOk(dbFileName)
} else backupFail()

Here I am using the  MediaStore component . Its use is similar to working with a database. At first "record" with all its fields registers in contentValues. I write here only the file name in the  DISPLAY_NAME column , although I saw in the examples that they also indicate the  SIZE  size and some other fields. But it works for me as well.

Then, using contentResolver, I insert my entry into the  MediaStore.Downloads.EXTERNAL_CONTENT_URI table . As a result, I get the path to the file in  Uri format  and only after that I copy the data using it.

You can read about different methods of copying here:  https://stackoverflow.com/questions/9292954/how-to-make-a-copy-of-a-file-in-android  I tried several of them. All working, but shorter and more concise, of course, looks like src. copyTo (dst)

5. Check if permissions are obtained

For Android 9 and below, it is not enough to specify access permissions in the Manifest. They still need to be received. Access to shared folders can be given to the application by the user if he is requested to do so or he himself specified permissions in the settings.


Settings -> Applications -> Permissions

In order to check if my application has permission to access the storage, I write the following code:

if (ContextCompat.checkSelfPermission(applicationContext,    Manifest.permission.WRITE_EXTERNAL_STORAGE)     == PackageManager.PERMISSION_GRANTED) {  // разрешение есть    ...                    
} else {  // no permission    ...                    
}
Attempts to copy without permission will not succeed.

6. Asking for permission if there is none

You can ask the user to give permission to the application to copy files to shared folders. It might look something like this:


Give an app access to photos, media, and files on the device?

The request code consists of two parts. The first is asking. The second one receives a response (callback).

ActivityCompat.requestPermissions(this,  arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),  Const.REQUEST_CODE)

This simple code will show the user a window with a question from the screenshot above and will offer to allow ( allow ) or deny ( deny ) access. Request_code  - any number that you later need to check to make sure that the answer came specifically for your application.

7. Processing the user's response

I will receive the answer in another part of the program using the  onRequestPermissionsResult function . It is triggered when the user clicks " Allow " or " Deny ".

override fun onRequestPermissionsResult(        requestCode: Int,        permissions: Array<out String>,        grantResults: IntArray    ) {        super.onRequestPermissionsResult(requestCode, permissions, grantResults)        if (requestCode == Const.REQUEST_CODE && permissions[0] == Manifest.permission.WRITE_EXTERNAL_STORAGE) {            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {                try {                    copyFile()                } catch (e: Exception) {                    e.printStackTrace()                    backupFail()                }
            } else backupFail()        }    }

I give the code above in the form in which it is written for me without gaps. I check that the WRITE_EXTERNAL_STORAGE permission  has arrived  and  the requestCode  also matches. Then just look at the result. If it is  PERMISSION_GRANTED , then I copy, remembering to pack the copy in a try.

8. Copy if Android version < 10 (SdkVersion < 29)

The copy code in this case was packed into a separate function, because it is launched from different parts of the program. If permission is obtained, then I immediately copy and after receiving permission, if it has not yet been.

private fun copyFile() {        val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)        if (downloadDir.canWrite()) {            val dbFileName = Const.BACKUP_FILENAME_MASK +                    fHelper.dateFromLong(applicationContext,                        fHelper.dateFromToday(),                        Const.YEAR_MONTH_DAY) + ".db"            val src = FileInputStream(applicationContext.getDatabasePath(DbName.DATABASE_NAME))            val dst = FileOutputStream(File(downloadDir, dbFileName))            src.copyTo(dst)            src.close()            dst.close()            backupOk(dbFileName)        } else backupFail()    }

Answer

Creating a copy of the database works on all Android devices. My application does not support versions less than  6.0 Marshmellow (SdkVersion 23) , so I did not check on the earliest versions.

I really hope that my description of the process will save you time, because I have not seen it in such a complete form on the Internet.


Related Articles

Comments(2)

    Roland wrote:

    Thx a lot. Your article helped me a lot!

     

    but from api 30 on you might have a problem:

     

    the developer guide says:

    More recent versions of Android rely more on a file's purpose than its location for determining an app's ability to access, and write to, a given file. In particular, if your app targets Android 11 (API level 30) or higher, theWRITE_EXTERNAL_STORAGE permission doesn't have any effect on your app's access to storage. This purpose-based storage model improves user privacy because apps are given access only to the areas of the device's file system that they actually use.

     

    see more under https://developer.android.com/training/data-storage

     

     

    Adam Moore wrote:

    I was having the same problems as you and I'm a native English speaker. This guide was exactly what I needed.

    Thanks for taking the time to write up this article.

Add Your Comment

reload, if the code cannot be seen

All comments will be moderated before being published.