Kotlin File.deleteRecursively() doesn't work on cacheDir - android

I'm trying to delete the app's cache subfolder (or even the cache folder itself), but nothing happens.
cacheFolder = File(this.cacheDir, "/download")
deleteFolder(cacheFolder)
private fun deleteFolder(fileOrDirectory : File){
fileOrDirectory.deleteRecursively()
}
The one-liner doesn't work too
this.cacheDir.deleteRecursively()
Any ideas?

It will be better if you use a service to delete the cacheFiles
This is how I did
class DeleteCache(context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
override fun doWork(): Result {
return try {
applicationContext.cacheDir?.let {
if (it.exists()) {
val entries = it.listFiles()
if (entries != null) {
for (entry in entries) {
entry.delete()
}
}
}
}
Result.success()
} catch (e: Exception) {
Result.failure()
}
}
}

The File.deleteRecursively() function unfortunately doesn't tell you the reason for failure to delete. It just returns true or false to show whether the deletion was successful. If it returns false, the directory tree may have been partly deleted.
Instead of using the old java.io.File class, which this function is a Kotlin extension of, it is preferable to use the "newer" java.nio utility classes because these will throw an IOException that describes the reason for failure to delete. Unfortunately, Kotlin does not currently provide a deleteRecursively() function that uses java.nio. It only provides a non-recursive deleteExisting() function on the Path class. So, you could instead write your own Path.deleteRecursively() extension function to live alongside Path.deleteExisting():
import java.io.IOException
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import kotlin.io.path.deleteExisting
fun Path.deleteRecursively() {
Files.walkFileTree(
this,
object : SimpleFileVisitor<Path>() {
override fun visitFile(file: Path, attrs: BasicFileAttributes?): FileVisitResult {
file.deleteExisting()
return FileVisitResult.CONTINUE
}
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
if (exc != null)
throw exc
dir.deleteExisting()
return FileVisitResult.CONTINUE
}
}
)
}
You can then use it like this:
import kotlin.io.path.Path
...
Path(cacheDir).deleteRecursively()
and if it fails to delete the whole tree, you'll see a stack trace with an exception and a descriptive message.
Update:
The forthcoming Kotlin 1.8.0 will include an experimental function by the same name in its standard library.

Thanks for support. Now I got it working. The problem was the DownloadManager
val url = "$targetUrlFTP/$appName/$downloadingObject/$onlineVer.zip"
val request = DownloadManager.Request(Uri.parse(url))
request.setAllowedNetworkTypes((DownloadManager.Request.NETWORK_MOBILE or
DownloadManager.Request.NETWORK_WIFI))
request.setDestinationInExternalFilesDir(this, cacheFolder.toString(),
zipFileName)
val manager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
downloadID = manager.enqueue(request)
the line request.setDestinationInExternalFilesDir(this, cacheFolder.toString(), zipFileName)completely messed up the path.
Now I have
cacheFolder = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!
zipFile = File(cacheFolder, zipFileName)
...
request.setDestinationUri(Uri.fromFile(zipFile))
and everything works. Even
private fun deleteFolder(fileOrDirectory : File){
fileOrDirectory.deleteRecursively()
}
does.
Next step is to implement java.nio. Thank you Klitos for updating your post. I had no idea how to use it.

Related

Retrofit image download works but the file is always corrupted

Here is my Retrofit API:
#GET
suspend fun downloadMedia(#Url url: String): Response<ResponseBody>
Here is the code that actually downloads the image from the URL and saves it to the device storage:
override fun downloadMedia(url: String): Flow<RedditResult<DownloadState>> = flow {
preferences.downloadDirFlow.collect {
if (it.isEmpty()) {
emit(RedditResult.Success(DownloadState.NoDefinedLocation))
} else {
// Actually download
val response = authRedditApi.downloadMedia(url)
if (response.isSuccessful) {
val treeUri = context.contentResolver.persistedUriPermissions.firstOrNull()?.uri
treeUri?.let { uri ->
val directory = DocumentFile.fromTreeUri(context, uri)
val file = directory?.createFile(
response.headers()["Content-Type"] ?: "image/jpeg",
UUID.randomUUID().toString().replace("-", ""))
file?.let {
context.contentResolver.openOutputStream(file.uri)?.use { output ->
response.body()?.byteStream()?.copyTo(output)
output.close()
emit(RedditResult.Success(DownloadState.Success))
}
} ?: run {
emit(RedditResult.Error(Exception("Unknown!")))
}
}
} else {
emit(RedditResult.Error(IOException(response.message())))
}
}
}
}
The file downloads and is the correct size in MB, but it somehow becomes corrupted with dimensions of 0x0 and just a blank image (when on my PC it can't even be opened).
I don't really know what I'm doing wrong as the file is being created and written to fine (which was difficult with SAF in and of itself).
Edit: I've also tried with and without #Streaming on the API function with the same results.
Turns out I was being an idiot. I was using a Retrofit instance which was using a MoshiConverter, this caused the content length to be changed and therefore the file was corrupted. Solved by using a Retrofit instance without a MoshiConverter.

Is it possible to compress a prepackaged database for room persistence library?

I have a huge database of about 150mb. Can I put the compressed version of the database e.g. zip in the asset folder for room to use or is that not possible?
PS: android studio apk compression is not sufficient enough
First you need a function which can unzip archive to a some directory:
// unzip(new File("/sdcard/whatToUnzip.zip"), new File("/toThisFolder"));
fun unzip(zipFile: File, targetDirectory: File) {
unzip(BufferedInputStream(FileInputStream(zipFile)), targetDirectory)
}
fun unzip(zipInputStream: InputStream, targetDirectory: File) {
try {//BufferedInputStream(zipFileStream)
ZipInputStream(zipInputStream).use { zipInput ->
var zipEntry: ZipEntry
var count: Int
val buffer = ByteArray(65536)
while (zipInput.nextEntry.also { zipEntry = it } != null) {
val file = File(targetDirectory, zipEntry.name)
val dir: File? = if (zipEntry.isDirectory) file else file.parentFile
if (dir != null && !dir.isDirectory && !dir.mkdirs()) throw FileNotFoundException(
"Failed to ensure directory: " + dir.absolutePath
)
if (zipEntry.isDirectory) continue
FileOutputStream(file).use { fileOutput ->
while (zipInput.read(buffer).also { count = it } != -1) fileOutput.write(
buffer,
0,
count
)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
I got it out of that stackoverflow's thread. Please read a thread to get more details. Then I added two method to work with a file from app's asset folder:
fun unzipAsset(assetsFilePath: String, context: Context, targetDirectory: File) {
unzip(context.assets.open(assetsFilePath), targetDirectory)
}
fun Context.unzipAsset(assetsFilePath: String, targetDirectory: File) = unzipAsset(
assetsFilePath,
this,
targetDirectory
)
Now we can unzip file to folder. To avoid copying an unzipped db file by room when I use createFromAsset or createFromFile methods of Room.databaseBuilder I want to unzip file to apps databases folder which used by room to store db file. That why I need additional methods to get db folder path and to check when db file already exist:
fun Context.databaseFolderPath(): File? = this.getDatabasePath("any.db").parentFile
// name – The name of the database file.
fun Context.isRoomDbFileExist(name: String): Boolean {
return this.getDatabasePath(name)?.exists() ?: false
}
And now, how to use all thinks together:
abstract class AppDatabase : RoomDatabase() {
companion object {
private const val DB_NAME = "sunflower-db"
// Create and pre-populate the database. See this article for more details:
// https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785
private fun buildDatabase(context: Context): AppDatabase {
if(!context.isRoomDbFileExist(DB_NAME)) {
// unzip db file to app's databases directory to avoid copy of unzipped file by room
context.unzipAsset("sunflower-db.zip", context.databaseFolderPath()!!)
// or unzip(File("your file"), context.databaseFolderPath()!!)
}
return Room.databaseBuilder(context, AppDatabase::class.java, DB_NAME)
//.createFromAsset(DB_NAME) // not zipped db file
.build()
}
}
}
I test this code on nice open source project - sunflower. Next I want to show screen with project structure , where sunflower-db.zip located:
The approach above works but You shouldn't take this sample as right or best solution. You should to think about avoid unzipping process from main thread. May be will be better if you implement your own SupportSQLiteOpenHelper.Factory(look like complicated).

Can we download multiple files in a Single Coroutine in Android Kotlin

Hi all Developers pls sort out my query as much as possible.
I'm working on Image Album application. Here while user click on particular album we should download all related images to that particular album.
For example album name is Tree so that album have multiple image url's array. So i should download all images from that album by array of url's
Ex : imageArray = arrayOf("url1","url2","url3","url4",....etc url(n))
i should put them in for loop or else recursion then i should download them upto (n) urls on completion of one by one.
i have written snippet for one file download here my doubt is how i can proceed to download multiple files.
should i use same coroutine for all files download or else one one coroutine for one one file
CoroutineScope(Dispatchers.IO).launch {
**//here itself i can run for loop or else any other robust/proper way to do this requirement.**
ktor.downloadFile(outputStream, url).collect {
withContext(Dispatchers.Main) {
when (it) {
is DownloadResult.Success -> {
**//on success of one file download should i call recursively to download one more file by this method - private fun downloadFile(context: Context, url: String, file: Uri)**
viewFile(file)
}
below is the code to download a single file
private fun downloadFile(context: Context, url: String, file: Uri) {
val ktor = HttpClient(Android)
contentResolver.openOutputStream(file)?.let { outputStream ->
CoroutineScope(Dispatchers.IO).launch {
ktor.downloadFile(outputStream, url).collect {
withContext(Dispatchers.Main) {
when (it) {
is DownloadResult.Success -> {
viewFile(file)
}
is DownloadResult.Error -> {
}
is DownloadResult.Progress -> {
txtProgress.text = "${it.progress}"
}
}
}
}
}
}
}
suspend fun HttpClient.downloadFile(file: OutputStream, url: String): Flow<DownloadResult> {
return flow {
try {
val response = call {
url(url)
method = HttpMethod.Get
}.response
val data = ByteArray(response.contentLength()!!.toInt())
var offset = 0
do {
val currentRead = response.content.readAvailable(data, offset, data.size)
offset += currentRead
val progress = (offset * 100f / data.size).roundToInt()
emit(DownloadResult.Progress(progress))
} while (currentRead > 0)
response.close()
if (response.status.isSuccess()) {
withContext(Dispatchers.IO) {
file.write(data)
}
emit(DownloadResult.Success)
} else {
emit(DownloadResult.Error("File not downloaded"))
}
} catch (e: TimeoutCancellationException) {
emit(DownloadResult.Error("Connection timed out", e))
} catch (t: Throwable) {
emit(DownloadResult.Error("Failed to connect"))
}
}
}
sealed class DownloadResult {
object Success : DownloadResult()
data class Error(val message: String, val cause: Exception? = null) : DownloadResult()
data class Progress(val progress: Int): DownloadResult()
}
Gradle Files i have used
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation "io.ktor:ktor-client-android:1.2.5"
Should be possible.
Create another function that will iterate through the list of files to be downloaded.
With the new function, use an async or launch coroutine function - this allows for more control over the flow of your logic, sequential behavior or asynchronous behavior respectively.
e.g. fun downloadBatch(list: List<Uri>) { GlobalScope.async(Dispatchers.IO) { //logic goes here }}
Note: GlobalScope is just for an easy example - not advisable to use in live production.
Inside the iterator/for-loop, call a function to download an individual file. This particular function should be appended with suspend at the beginning
e.g. suspend fun downloadFile(uri: Uri)
Note: the suspended function won't use any threading logic itself and depends on being nested within a functional Coroutine.
Continue to use rxJava, or try LiveData, to broadcast your files.

SourceCodeScanner not calling visitMethodCall

I'm playing with lint rules.
All my ResourceXmlDetector run without problems and pass all the tests. But Detector(), SourceCodeScanner are failing because they return 0 warnings/errors, and the reason is visitMethodCall not being called thus context.report wont either.
My code is similar to android lint-checks, for instance CipherGetInstanceDetector, but I can't find my mistake.
#Suppress("UnstableApiUsage")
class MySourceDetector : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames() = listOf("...")
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
if (context.evaluator.isMemberInClass(method, "...")) {
...
reportUsage(context, node)
}
}
private fun reportUsage(context: JavaContext, node: UCallExpression) {
context.report(
issue = ISSUE,
scope = node,
location = context.getCallLocation(
call = node,
includeReceiver = true,
includeArguments = true
),
message = ISSUE.getExplanation(TextFormat.RAW)
)
}
companion object {
#JvmField
val ISSUE = Issue.create(...Scope.JAVA_FILE_SCOPE)
}
}
The only methods stopping in break points are Issue.create and getApplicableMethodNames(). What's missing?
According UElementVisitor#DelegatingPsiVisitor.visitMethodCallExpression in source code, I found that some java or kotlin method can not be recognized as "Method":val function = node.resolve() is null.
I didn't think this was important but my test rule was:
TestLintTask.lint()
.files(TestFiles.kotlin("Test.kt", input).indented())
.issues(ISSUE)
.run()
.expectWarningCount(1)
.expect(output)
And replacing kotlin("Test.kt", input) with kotlin(input) made it work...

Anko doAsyncResult coroutines

I am new to anko and coroutines so excuse me if I am asking something trivial :)
So what I am trying to do is have the user click a button and then I want to download a JSON from the internet, store it locally and parse it. Since both operations can take considerable time I thought to use anko coroutines.
So first question is:
1. Can I use nested doAsync calls, calling the 2nd doAsync in the UIThread of the first one?
I tried it and it seems to work but it feels wrong so I was trying to find a more elegant way
Example:
doAsync {
downloadFileFromUrl(fileUrl)
uiThread {
doAsync {
IOUtils.parseFile(context!!)
val database = AppDatabase.getInstance(context!!)
val results = database.resultsDao().all
uiThread {
//show Results
}
}
}
}
2. While searching a solution for my problem I found doAsyncResult. If 1 it's not correct, is this is the correct approach? I tried already to use it but with Boolean I get errors. See below:
private fun downloadFileFromUrl(fileUrl: String): Boolean {
try{
//Download file. No doAsync calls here.
//The procedure just returns true if successful or false in case of any errors
return true
} catch (e: Exception) {
Log.e("Error: ", e.message)
return false
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
parseButton.setOnClickListener {
try {
val downloadFileResult: (AnkoAsyncContext<Boolean>.() -> Boolean) = {
::downloadFileFromUrl.invoke(fileUrl)
}
val downloadFileResultFutureValue: Future<Boolean> = doAsyncResult(null, downloadFileResult)
//Continue processing if downloadFileResultFutureValue is true
} catch (e: IOException) {
e.printStackTrace()
}
}
}
This line
val downloadFileResultFutureValue: Future<Boolean> = doAsyncResult(null, downloadFileResult)
does not compile with the following error which I don't understand how to fix:
Type inference failed: Cannot infer type parameter T in
fun <T, R> T.doAsyncResult
(
exceptionHandler: ((Throwable) → Unit)? = ...,
task: AnkoAsyncContext<T>.() → R
)
: Future<R>
None of the following substitutions
receiver: Boolean
arguments:
(
((Throwable) → Unit)?,
AnkoAsyncContext<Boolean>.() → Boolean
)
receiver: BlankFragment
arguments:
(
((Throwable) → Unit)?,
AnkoAsyncContext<BlankFragment>.() → Boolean
)
can be applied to
receiver: BlankFragment
arguments:
(
Nothing?,
AnkoAsyncContext<Boolean>.() → Boolean
)
Thanks in advance
Doing this:
doAsync {
// 1. Something
uiThread {
// 2. Nothing
doAsync {
Indeed doesn't make much sense, unless (2) is not nothing, and you just omitted some code.
If you didn't, you can just stay with this version:
doAsync {
downloadFileFromUrl(fileUrl)
IOUtils.parseFile(context!!)
val database = AppDatabase.getInstance(context!!)
val results = database.resultsDao().all
uiThread {
//show Results
}
}
Since parseFile() depends on downloadFileFromUrl() anyway, and everything runs in a coroutine, you don't become more concurrent by adding this back-and-forth.

Categories

Resources