Trying to use kotlin-dotnet, but this is not working, using kotlin object class, to manage singleton
Got error Could not find /asset/env on the classpath whereas the env file is in /assets folder as mentionned in the doc
object EnvVariables {
val envVar: Dotenv =
dotenv {
directory = "/assets"
filename = "env"
}
}
object RetrofitInstance {
val api: TodoService by lazy {
Retrofit.Builder()
.baseUrl(EnvVariables.envVar["BASE_URL"])
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(TodoService::class.java)
}
}
Most likely the doc is not accurate
In file DotEnvReader.java
return ClasspathHelper
.loadFileFromClasspath(location.replaceFirst("./", "/"))
.collect(Collectors.toList());
So, you can fix it by adding "."
var dotenv = dotenv {
directory = "./assets" //use ./ instead of /
filename = "env"
}
Related
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 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).
I'm trying to upload multiple files.
val ktorVersion = "1.5.0"
val serializationVersion = "1.0.1"
That is how I'm doing that:
override suspend fun uploadFiles(
binaryFiles: Map<String,ByteArray>
): BaseResponse<List<String>> {
return client.submitForm {
url(fileUploadUrl)
method = HttpMethod.Post
body = MultiPartFormDataContent(
formData {
headers{
append("Content-Type", "application/json")
append("Authorization", "Bearer $token}")
}
binaryFiles.entries.forEach {
append(
key = "files",
value = it.value,
headers = Headers.build {
append(HttpHeaders.ContentDisposition, "filename=${it.key}")
}
)
}
}
)
}
}
But it throws exception
kotlinx.serialization.SerializationException: Serializer for class 'MultiPartFormDataContent' is not found.
Mark the class as #Serializable or provide the serializer explicitly.
at kotlinx.serialization.internal.Platform_commonKt.serializerNotRegistered(Platform.common.kt:91)
at kotlinx.serialization.SerializersKt__SerializersKt.serializer(Serializers.kt:130)
at kotlinx.serialization.SerializersKt.serializer(Unknown Source)
at io.ktor.client.features.json.serializer.KotlinxSerializerKt.buildSerializer(KotlinxSerializer.kt:82)
at io.ktor.client.features.json.serializer.KotlinxSerializerKt.access$buildSerializer(KotlinxSerializer.kt:1)
at io.ktor.client.features.json.serializer.KotlinxSerializer.writeContent$ktor_client_serialization(KotlinxSe
at io.ktor.client.features.json.serializer.KotlinxSerializer.write(KotlinxSerializer.kt:26)
at io.ktor.client.features.json.JsonFeature$Feature$install$1.invokeSuspend(JsonFeature.kt:150)
at io.ktor.client.features.json.JsonFeature$Feature$install$1.invoke(JsonFeature.kt)
at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:243)
at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:113)
at io.ktor.util.pipeline.SuspendFunctionGun.proceedWith(SuspendFunctionGun.kt:123)
at io.ktor.client.features.HttpCallValidator$Companion$install$1.invokeSuspend(HttpCallValidator.kt:106)
at io.ktor.client.features.HttpCallValidator$Companion$install$1.invoke(HttpCallValidator.kt)
at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:243)
at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:113)
at io.ktor.client.features.HttpRequestLifecycle$Feature$install$1.invokeSuspend(HttpRequestLifecycle.kt:37)
at io.ktor.client.features.HttpRequestLifecycle$Feature$install$1.invoke(HttpRequestLifecycle.kt)
at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:243)
at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:113)
at io.ktor.util.pipeline.SuspendFunctionGun.execute(SuspendFunctionGun.kt:133)
at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:79)
at io.ktor.client.HttpClient.execute(HttpClient.kt:187)
at io.ktor.client.statement.HttpStatement.executeUnsafe(HttpStatement.kt:104)
at com.example.package.core.data.network.services.upload.FileUploadApiImpl.uploadFiles(FileUploadApi.kt:99)
I have also tried in that way but got same problem:
suspend fun uploadFilesTest(
binaryFiles: Map<String,ByteArray>
): BaseResponse<List<String>> {
return client.post(fileUploadUrl) {
headers {
append("Content-Type", ContentType.Application.Json)
append("Authorization", "Bearer $token")
}
body = MultiPartFormDataContent(
formData {
binaryFiles.entries.forEach {
this.appendInput(
key = "files",
size = it.value.size.toLong(),
headers = Headers.build {
append(HttpHeaders.ContentDisposition, "filename=${it.key}")
},
){ buildPacket { writeFully(it.value) }}
}
}
)
}
}
Looking at your last snippet where you say you have the same problem, it seems like you are specifying content-type in headers like this
append("Content-Type", ContentType.Application.Json)
but you also want it to be multipart by setting body to be MultiPartFormDataContent which also needs to be defined as a header content-type:multipart/form-data; ... so it might be that this conflict is causing the issue.
I actually just tried it myself in my code and that seemed to be the issue for me.
I did this for Android but it may also work for ktor. At first, add this line to dependencies in the build project gradle:
classpath "org.jetbrains.kotlin:kotlin-serialization:1.5.21"
Add also these to the build app gradle:
plugins {
...
id 'kotlinx-serialization'
}
dependencies {
...
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2'
}
I want to retreive key from local.properties file that looks like :
sdk.dir=C\:\\Users\\i30mb1\\AppData\\Local\\Android\\Sdk
key="xxx"
and save this value in my BuildConfig.java via gradle Kotlin DSL. And later get access to this field from my project.
Okay. I found solutions.
For Android Projects :
In my build.gradle.kts I create a value that retrieves my key:
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
val key: String = gradleLocalProperties(rootDir).getProperty("key")
And in the block buildTypes I write it:
buildTypes {
getByName("debug") {
buildConfigField("String", "key", key)
}
}
And in my Activity now I can retrieve this value:
override fun onCreate() {
super.onCreate()
val key = BuildConfig.key
}
For Kotlin Projects:
We can create an extension that help us to retrieve desired key:
fun Project.getLocalProperty(key: String, file: String = "local.properties"): Any {
val properties = java.util.Properties()
val localProperties = File(file)
if (localProperties.isFile) {
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
properties.load(reader)
}
} else error("File from not found")
return properties.getProperty(key)
}
And use this extension when we would like
task("printKey") {
doLast {
val key = getLocalProperty("key")
println(key)
}
}
If you don't have access to gradleLocalProperties (it's only accessible for android projects):
val prop = Properties().apply {
load(FileInputStream(File(rootProject.rootDir, "local.properties")))
}
println("Property:" + prop.getProperty("propertyName"))
Don't forget imports:
import java.io.File
import java.io.FileInputStream
import java.util.*
put your property in
local.properties
and in build.gradle(app).kts file reference it as such:
gradleLocalProperties(rootDir).getProperty("YOUR_PROP_NAME")
I am using the following code to instantiate all the classes included in a certain package.
DexFile df = new DexFile(getPackageCodePath());
for (Enumeration<String> iter = df.entries(); iter.hasMoreElements(); ) {
String className = iter.nextElement();
if (className.contains(packageName) && !className.contains("$")) {
myClasses.add(Class.forName(className).newInstance());
}
}
Unfortunately it is not working properly anymore. Since Android Studio 2 and Gradle 2.0.0, the DexFile entries no longer include all the classes within the app but only the classes belonging to the com.android.tools package.
Is this a known issue?
Looks like this issue is related to the new InstantRun feature in the Android Plugin for Gradle 2.0.0.
getPackageCodePath() gets a String pointing towards the base.apk file in the Android file system. If we unzip that apk we can find one or several .dex files inside its root folder. The entries obtained from the method df.entries() iterates over the .dex files found in that root folder in order to obtain all of its compiled classes.
However, if we are using the new Android Plugin for Gradle, we will only find the .dex related to the android runtime and instant run (packages com.tools.android.fd.runtime, com.tools.android.fd.common and com.tools.android.tools.ir.api). Every other class will be compiled in several .dex files, zipped into a file called instant-run.zip and placed into the root folder of the apk.
That's why the code posted in the question is not able to list all the classes within the app. Still, this will only affect Debug builds since the Release ones don't feature InstantRun.
To access all DexFiles you can do this
internal fun getDexFiles(context: Context): List<DexFile> {
// Here we do some reflection to access the dex files from the class loader. These implementation details vary by platform version,
// so we have to be a little careful, but not a huge deal since this is just for testing. It should work on 21+.
// The source for reference is at:
// https://android.googlesource.com/platform/libcore/+/oreo-release/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
val classLoader = context.classLoader as BaseDexClassLoader
val pathListField = field("dalvik.system.BaseDexClassLoader", "pathList")
val pathList = pathListField.get(classLoader) // Type is DexPathList
val dexElementsField = field("dalvik.system.DexPathList", "dexElements")
#Suppress("UNCHECKED_CAST")
val dexElements = dexElementsField.get(pathList) as Array<Any> // Type is Array<DexPathList.Element>
val dexFileField = field("dalvik.system.DexPathList\$Element", "dexFile")
return dexElements.map {
dexFileField.get(it) as DexFile
}
}
private fun field(className: String, fieldName: String): Field {
val clazz = Class.forName(className)
val field = clazz.getDeclaredField(fieldName)
field.isAccessible = true
return field
}
for get all dex files of an app use below method.
public static ArrayList<DexFile> getMultiDex()
{
BaseDexClassLoader dexLoader = (BaseDexClassLoader) getClassLoader();
Field f = getField("pathList", getClassByAddressName("dalvik.system.BaseDexClassLoader"));
Object pathList = getObjectFromField(f, dexLoader);
Field f2 = getField("dexElements", getClassByAddressName("dalvik.system.DexPathList"));
Object[] list = getObjectFromField(f2, pathList);
Field f3 = getField("dexFile", getClassByAddressName("dalvik.system.DexPathList$Element"));
ArrayList<DexFile> res = new ArrayList<>();
for(int i = 0; i < list.length; i++)
{
DexFile d = getObjectFromField(f3, list[i]);
res.add(d);
}
return res;
}
//------------ other methods
public static ClassLoader getClassLoader()
{
return Thread.currentThread().getContextClassLoader();
}
public static Class<?> getClassByAddressName(String classAddressName)
{
Class mClass = null;
try
{
mClass = Class.forName(classAddressName);
} catch(Exception e)
{
}
return mClass;
}
public static <T extends Object> T getObjectFromField(Field field, Object arg)
{
try
{
field.setAccessible(true);
return (T) field.get(arg);
} catch(Exception e)
{
e.printStackTrace();
return null;
}
}