I am writing something to read a Json in Android with Kotlin.
The Goal is to either start the ChooseLoginActicity if there is no User.Json available or to otherwise read the Json and start the MainActivity.
The problem is that the line to open the File into a Input Stream always gets an FileNotFoundException even if the User.Json exists with the Permission -rw-rw---- and got created with Context.MODE_PRIVATE, by the same Application.
This Function got called in the onCreate Function of the Activity.
fun readJsonFile() {
try {
val inputStream = openFileInput("User.json")
val json = inputStream.bufferedReader().use { it.readText() }
val gson = Gson()
val user = gson.fromJson(json, User::class.java)
CurrentUser.initUser(user.id,user.name)
startActivity(Intent(this, MainActivity::class.java))
} catch (e: FileNotFoundException) {
startActivity(Intent(this, ChooseLoginActivity::class.java))
}
}
Make sure you wrote the correct file name. Pay attention about case sensitivity.
The code itself should work fine.
Related
I download a file using Retrofit and save it in a subfolder in the download directory.
when I check with the phone's file manager, it is downloaded and saved correctly. For example, in the following path:
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path + "/MyApp"
But when I open the file manager with intent like ACTION_GET_CONTENT or ACTION_OPEN_DOCUMENT the downloaded file is not visible.
In addition, if I rename the file or copy and paste it through the phone's file manager, everything will be fixed in the same path
Also, saving in the downloads folder is done without any problem
But when the subfolder is created and it is supposed to be saved there, this problem occurs
There is no problem with downloading by DownloadManager, but i want use retofit
Download function:
suspend fun download(url: String, targetPath: String, progressRetrofit: RetrofitProgress) = flow {
try {
val response = apiService.download(url).awaitResponse()
val body = response.body()
if (response.isSuccessful && body != null) {
try {
val file = File(targetPath)
body.byteStream().use { inputStream ->
FileOutputStream(file).use { outputStream ->
val data = ByteArray(1024)
var read: Int
var currentDownloadSize = 0L
val fileSize = body.contentLength()
while (inputStream.read(data).also { read = it } != -1) {
outputStream.write(data, 0, read)
currentDownloadSize += read
withContext(Dispatchers.Main)
{
progressRetrofit.onProgressUpdate((currentDownloadSize * 100 / fileSize).toInt(), fileSize, currentDownloadSize)
}
}
withContext(Dispatchers.Main)
{
progressRetrofit.onProgressUpdate((currentDownloadSize * 100 / fileSize).toInt(), fileSize, currentDownloadSize)
}
outputStream.close()
outputStream.flush()
}
}
emit(NetworkResult.Success(true))
} catch (e: Exception) {
emit(NetworkResult.Failure(e.message.toString()))
errorMessage(e.message.toString(), true)
}
} else {
emit(NetworkResult.Failure(response.message()))
errorMessage(response.errorBody().toString(), true)
}
} catch (e: Exception) {
emit(NetworkResult.Failure(e.message.toString()))
errorMessage(e.message.toString(), true)
}
}
Hmmmm...
You are right...
It happens.
But only if with ACTION_OPEN_DOCUMENT the user directly uses the Downloads item.
Instead the user should browse the device, and go to Download directory and then to the subdirectory.
(Note: The first ends with an s, the real directory is without s.).
After some more tests it appeared if the file was scanned by the MediaStore.
So add some few lines of code to let it be scanned after download.
Since Android 6.0 there are bunch of changes in file sharing behaviour.
Please take a look on FileProvider and look further through API changes in the official android documentation, e.g. like this.
Without extra details in your code or even reproducible code sample I can not help more.
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.
I am trying to save/export a file on the user Documents shared folder so it can be persistent if the application is deleted (it's an export of the user work on the application). Following the official documentation to create a save a file on the shared folder, I have this basic implementation:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
registerActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { writeInFile(it, "this is a test") }
}
}
}
private fun createFile() {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply{
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "filename.txt")
putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.DIRECTORY_DOCUMENTS)
}
startActivityForResult(intent, WRITE_REQUEST_CODE)
//this.registerActivity.launch(intent)
}
private fun writeInFile(uri: Uri, text: String) {
val outputStream: OutputStream
try {
Log.i("export", uri.toString())
outputStream = contentResolver.openOutputStream(uri)!!
val bw = BufferedWriter(OutputStreamWriter(outputStream))
bw.write(text)
bw.flush()
bw.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
The createFile function is called from a click event to export the data.
The Activity for choosing the folder to save is launching, but saving is failing. I am getting the following error:
2021-06-23 18:24:43.268 2853-2871/? E/DatabaseUtils: Writing exception to parcel
java.lang.IllegalArgumentException: Parent document isn't a directory
at com.android.internal.content.FileSystemProvider.createDocument(FileSystemProvider.java:244)
at com.android.providers.downloads.DownloadStorageProvider.createDocument(DownloadStorageProvider.java:207)
at android.provider.DocumentsProvider.callUnchecked(DocumentsProvider.java:1124)
at android.provider.DocumentsProvider.call(DocumentsProvider.java:1067)
at android.content.ContentProvider.call(ContentProvider.java:2448)
at android.content.ContentProvider$Transport.call(ContentProvider.java:517)
at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:295)
at android.os.Binder.execTransactInternal(Binder.java:1154)
at android.os.Binder.execTransact(Binder.java:1123)
2021-06-23 18:24:43.269 1559-1680/? W/DocumentsContract: Failed to create document
java.lang.IllegalArgumentException: Parent document isn't a directory
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:172)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:142)
at android.content.ContentProviderProxy.call(ContentProviderNative.java:732)
at android.content.ContentProviderClient.call(ContentProviderClient.java:603)
at android.content.ContentResolver.call(ContentResolver.java:2395)
at android.provider.DocumentsContract.createDocument(DocumentsContract.java:1371)
at com.android.documentsui.DocumentsAccess$RuntimeDocumentAccess.createDocument(DocumentsAccess.java:157)
at com.android.documentsui.picker.CreatePickedDocumentTask.run(CreatePickedDocumentTask.java:79)
at com.android.documentsui.picker.CreatePickedDocumentTask.run(CreatePickedDocumentTask.java:42)
at com.android.documentsui.base.CheckedTask.doInBackground(CheckedTask.java:65)
at android.os.AsyncTask$3.call(AsyncTask.java:394)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at com.android.documentsui.ProviderExecutor.run(ProviderExecutor.java:104)
I just found that the issue wasn't in my implementation, but my Android emulator. I tried to create a folder from the file manager (outside my application) and I was getting the same error. So, I tried testing my application on a physical device, it worked perfectly!
The issue with my emulator was that the SD card was corrupted (either it wasn't set up or something went wrong at some point without me realizing it). Resetting that fixed the issue
I currently have the following working code to import an existing user-chosen SQLite db file from the device into my app, which already had a db:
// here goes code to .close() the app's db if isOpen()
// val userChosenUri: Uri? = data?.data
val outStream = getDatabasePath(DATABASE_NAME).outputStream()
val inStream = userChosenUri.toString().let {
userChosenUri?.let { it1 ->
contentResolver.openInputStream(
it1
)
}
}
inStream.use { input ->
outStream.use { output ->
if (output != null) {
input?.copyTo(output)
}
}
}
I'm trying to replace it with code I found on the Android Room guide:
Room.databaseBuilder(this, AppDatabase::class.java, DATABASE_NAME)
.createFromFile(File(userChosenUri!!.path))
.build()
But this last snippet doesn't seem to do anything. It doesn't crash, but it also doesn't replace the db.
Does anyone know what I'm missing?
AFAIK, createFromFile() only creates the database from the file if the database does not already exist. If you want to use this for an import or restore-from-backup scenario, I think that you would need to close and delete the existing database before trying to use your latter code snippet.
I'm currently working on a solution how to update and install APK file programmatically - very similar like this issue. My app doesn't use Google Services.
For updating purposes, APK-files with ascending versions are stored on an internal server and a C# Web service returns the latest version of the APK file.
I do this with Retrofit2:
#Streaming
#GET("/ws/webservice.svc/getUpdate")
fun getUpdate(
#Query("Programm") program: String,
#Query("version") version: String,
#Query("test") test: Boolean
): Single<String>
and LiveData:
override fun getUpdate() {
disposables += api.getUpdate(
program = context.getString(R.string.app_name),
version = context.getString(R.string.app_version),
test = isTest
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onSuccess = {
liveData.value = GetUpdate(it)
},
onError = {
liveData.value = Error("Error getUpdate: " + it.message)
}
)
}
The problem that I'm facing, is that the response of that API call (which means - the latest APK file) has a base64String representation like shown in the image below - this is for example only a part of the server response when the API call is made in browser.
Is it somehow possible to generate a "real" APK file from this String representation after downloading it, so after that I can probably be able to install it on the device? I know that this is weird, but the customer wants me to re-use the same web service for this purposes.
I found a similar issue here. How can be this done in Kotlin? Thanks in advance.
Yes you need to decode the base64 into a ByteArray then write the bytes to a location with the postfix .apk. What you have is a String where the bytes are encoded using the base64 encoding scheme.
Since your using kotlin you might what to look here to get the ByteArray![1] from the String. Then just ensure the file you write has .apk extension.
[1] https://developer.android.com/reference/kotlin/java/util/Base64.Decoder#decode(kotlin.String)
AndroidManifest.xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
MVVM with LiveData:
override fun getUpdate() {
disposables += api.getUpdate(
program = context.getString(R.string.app_name) + ".apk",
version = context.getString(R.string.app_version),
test = isTest
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map(::decodeToAPK)
.subscribe({
liveData.value = GetUpdate
}, {
liveData.value = Error("Error getUpdate: " + it.message)
})
}
private fun decodeToAPK(base64String: String) {
val byteArrayAPK = Base64.decode(base64String, Base64.DEFAULT)
val file =
File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "yourApp.apk")
try {
val fileOutputStream = FileOutputStream(file)
fileOutputStream.write(byteArrayAPK)
fileOutputStream.flush()
fileOutputStream.close()
} catch (e: UnsupportedEncodingException) {
Log.d(null, "error writing APK")
e.printStackTrace()
}
}
sealed class SomeAction : Action {
data class Error(val message: String) : SomeAction()
object GetUpdate : SomeAction()
}