I am working on a learning project, that is supposed to import and export Flashcards to a txt.file which is later supposed to be used in questioning.
Currently, I am stuck on the import part and the research I did is not really working, because I am not really getting it.
I have this overall map where I save the term: definition in a map
private var flashCardMap = mutableMapOf<String, String>()
Then I have this export function
private fun export() {
println("File name:")
scan.nextLine()
val fileName = scan.nextLine()
val myFile = File(fileName)
try {
myFile.bufferedWriter().use { out->
flashCardMap.forEach {
out.write("${it.key}:${it.value}\n")
} }
println("${flashCardMap.size} cards have been saved.")
} catch (e: FileNotFoundException) {
println("File not Found.")
}
}
which exports all the cards I defined earlier in a txt. file like this with one or more Flashcard (Card = Definition)
key:value
AND here is where I am stuck. I try to import a .txt file and the including map but it does not work. It Is supposed to import the map and tell me how many Cards where imported and the add them to my current flashcardmap with which I am working in this session. Here is what I tried:
private fun import() {
println("File name:")
scan.nextLine()
val fileName = scan.nextLine()
val myFile = File("$fileName")
try {
val importMap =
myFile.readLines().chunked(2) {
it[0] to it[1]
}.toMap()
println("${importMap.size} cards have been loaded.")
flashCardMap.putAll(importMap)
} catch (e: FileNotFoundException) {
println("File not Found.")
}
}
There's actually so many ways to serialize structured data to a file, but since your example uses the key:value format, separated by new-lines, we'll stick with that.
This class should suit your needs. But it's pretty simplistic, and lacks any sort of error handling.
class Serializer(private val filePath: Path, private val delimiter: String = ":") {
fun export(data: Map<String, String>) {
filePath.toFile().writer().use { writer ->
for ((key, value) in data) {
writer.write("$key$delimiter$value\n")
}
}
}
fun import(): Map<String, String> {
val data = mutableMapOf<String, String>()
filePath.toFile().reader().use { reader ->
reader.forEachLine { line ->
val (key, value) = line.split(delimiter)
data[key] = value
}
}
return data
}
}
If you want to leverage a mature format, then the built-in java.util.Properties class can make things even easier. The only gotcha is that it uses a = delimiter by default, but it's supposed to be able to read a : delimiter too.
class PropertiesSerializer(private val filePath: Path) {
fun export(data: Map<String, String>) {
val props = Properties()
for ((key, value) in data) {
props[key] = value
}
filePath.toFile().outputStream().use { stream ->
props.store(stream, null)
}
}
fun import(): Map<String, String> {
val props = Properties()
filePath.toFile().inputStream().use { stream ->
props.load(stream)
}
return props
.map { (key, value) -> key.toString() to value.toString() }
.toMap()
}
}
Related
I wrote an App, in Kotlin with Android Studio that write some strings to a file.
All work, I can write and read inside the App, but I can't see the file looking in Documents folder.
How can I use the folder Documents as a storage space?
Thank you
These are the function I use:
fun saveTextFile(view: View, nomeFile: String, testo: String, contesto: Context){
var fileStream: FileOutputStream
try {
fileStream = contesto.openFileOutput(nomeFile, MODE_APPEND) // OK esegue append
fileStream.write(testo.toByteArray())
fileStream.close()
}catch (e: Exception){
e.printStackTrace()
}
}
fun readTextFile(view: View, nomeFile: String, contesto: Context): String{
var fileInputStream: FileInputStream? = null
fileInputStream = contesto.openFileInput(nomeFile)
var inputStreamReader: InputStreamReader = InputStreamReader(fileInputStream)
val bufferedReader: BufferedReader = BufferedReader(inputStreamReader)
val stringBuilder: StringBuilder = StringBuilder()
var text: String? = null
while ({ text = bufferedReader.readLine(); text }() != null) {
stringBuilder.append(text)
}
inputStreamReader.close();
return(stringBuilder.toString())
}
Thank you, Livio
For writing in Documents folder of your device , you just need to make use of MediaStore for the same. You can take input for this function anything that you want like String , bitmap , PdfDocument and other's too .
For Your UseCase you can do the following ,
Global Variable :
private var imageUri: Uri? = null
override suspend fun saveDocument(context : Context, text : String) {
withContext(Dispatchers.IO) {
try {
val collection =
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val dirDest = File(
Environment.DIRECTORY_DOCUMENTS,
context.getString(R.string.app_name)
)
val date = System.currentTimeMillis()
val fileName = "$date.txt"
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.RELATIVE_PATH,
"$dirDest${File.separator}")
put(MediaStore.Files.FileColumns.IS_PENDING, 1)
}
}
val imageUri = context.contentResolver.insert(collection, contentValues)
withContext(Dispatchers.IO) {
imageUri?.let { uri ->
context.contentResolver.openOutputStream(uri, "w").use { out -> out?.write(text.toByteArray())
}
contentValues.clear()
contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 0)
context.contentResolver.update(uri, contentValues, null, null)
}
}
} catch (e: FileNotFoundException) {
null
}
}
}
For Updating the already existing file , do the following . After creating file for the first time I have saved the imageUri in a global variable (If you want to store it permanently / or for a while you can use Jetpack Datastore / Shared Preference to save the same ):
suspend fun updateData(context: Context,text : String){
withContext(Dispatchers.IO) {
try {
val collection =
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val dirDest = File(
Environment.DIRECTORY_DOCUMENTS,
context.getString(R.string.app_name)
)
val fileName = "test.txt"
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(
MediaStore.MediaColumns.RELATIVE_PATH,
"$dirDest${File.separator}"
)
put(MediaStore.Files.FileColumns.IS_PENDING, 1)
}
withContext(Dispatchers.IO) {
imageUri?.let { uri ->
context.contentResolver.openOutputStream(uri, "wa").use { out ->
out?.write(text.toByteArray())
}
contentValues.clear()
contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 0)
context.contentResolver.update(uri, contentValues, null, null)
}
}
} catch (e: FileNotFoundException) {
null
}
}
}
For Reading the File , Do the following :
suspend fun read(context: Context, source: Uri): String = withContext(Dispatchers.IO) {
val resolver: ContentResolver = context.contentResolver
resolver.openInputStream(source)?.use { stream -> stream.readText() }
?: throw IllegalStateException("could not open $source")
}
private fun InputStream.readText(charset: Charset = Charsets.UTF_8): String =
readBytes().toString(charset)
This is how the final code looks like :
class MainActivity : AppCompatActivity() {
private lateinit var btn: Button
private var imageUri: Uri? = null
private lateinit var btn2: Button
private lateinit var btn3 : Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn = findViewById(R.id.btnAdd)
btn2 = findViewById(R.id.getText)
btn3 = findViewById(R.id.updateText)
btn.setOnClickListener {
lifecycleScope.launch {
saveDocument(applicationContext, "Original ")
}
}
btn3.setOnClickListener {
lifecycleScope.launch {
updateData(applicationContext,"Appended")
}
}
btn2.setOnClickListener {
lifecycleScope.launch {
imageUri?.let { it1 ->
val data = read(applicationContext, it1)
Toast.makeText(applicationContext, "The data is $data ", Toast.LENGTH_LONG)
.show()
}
}
}
}
suspend fun saveDocument(context: Context, text: String) {
withContext(Dispatchers.IO) {
try {
val collection =
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val dirDest = File(
Environment.DIRECTORY_DOCUMENTS,
context.getString(R.string.app_name)
)
val date = System.currentTimeMillis()
val fileName = "test.txt"
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(
MediaStore.MediaColumns.RELATIVE_PATH,
"$dirDest${File.separator}"
)
put(MediaStore.Files.FileColumns.IS_PENDING, 1)
}
imageUri = context.contentResolver.insert(collection, contentValues)
withContext(Dispatchers.IO) {
imageUri?.let { uri ->
context.contentResolver.openOutputStream(uri, "w").use { out ->
out?.write(text.toByteArray())
}
contentValues.clear()
contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 0)
context.contentResolver.update(uri, contentValues, null, null)
}
}
} catch (e: FileNotFoundException) {
null
}
}
}
suspend fun updateData(context: Context, text: String) {
withContext(Dispatchers.IO) {
try {
val collection =
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val dirDest = File(
Environment.DIRECTORY_DOCUMENTS,
context.getString(R.string.app_name)
)
val fileName = "test.txt"
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(
MediaStore.MediaColumns.RELATIVE_PATH,
"$dirDest${File.separator}"
)
put(MediaStore.Files.FileColumns.IS_PENDING, 1)
}
withContext(Dispatchers.IO) {
imageUri?.let { uri ->
context.contentResolver.openOutputStream(uri, "wa").use { out ->
out?.write(text.toByteArray())
}
contentValues.clear()
contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 0)
context.contentResolver.update(uri, contentValues, null, null)
}
}
} catch (e: FileNotFoundException) {
null
}
}
}
suspend fun read(context: Context, source: Uri): String = withContext(Dispatchers.IO) {
val resolver: ContentResolver = context.contentResolver
resolver.openInputStream(source)?.use { stream -> stream.readText() }
?: throw IllegalStateException("could not open $source")
}
private fun InputStream.readText(charset: Charset = Charsets.UTF_8): String =
readBytes().toString(charset)
I have three buttons . With the first I create a file , then the uri gets stored in the global variable . Then onClick of second button I add to the already existing file and then read the file using the third button using the same imageUri stored in the global variable
This is the demo for the same . Check when the buttons are being pressed and the output in the form of Toast at the bottom .
I have been trying to upload multiple images to Firebase Storage. But, I am not able to do it successfully. I could successfully upload the image (single) to the storage and add the URL of the image to the Firestore, now that I revised my code to upload up to five images, it could be any number of images from 1 to 5.
R.id.btn_submit -> {
if (validateDetails()) {
uploadImage()
}
}
The above code, calls the following function after validating the fields, which then calls the function uploadImageToCloudStorage. mSelectedImageFileUriList is private var mSelectedImageFileUriList: MutableList<Uri?>? = null. It all seems to work correctly.
private fun uploadImage() {
showProgressDialog(resources.getString(R.string.please_wait))
FirestoreClass().uploadImageToCloudStorage(
this#AddProductActivity,
mSelectedImageFileUriList,
Constants.PRODUCT_IMAGE,
Constants.PRODUCT_IMAGE_DIRECTORY_NAME,
et_product_title.text.toString().trim { it <= ' ' }
)
}
Following code is where I guess is a mistake.
fun uploadImageToCloudStorage(
activity: AddProductActivity,
imageFileURI: MutableList<Uri?>?,
imageType: String,
directoryName: String,
title: String
) {
var i = 0
val imageURLList = ArrayList<String>()
val itr = imageFileURI?.iterator()
if (itr != null) {
while (itr.hasNext()) {
val sRef: StorageReference = FirebaseStorage.getInstance().getReference(
"/$directoryName/" + imageType + "." + Constants.getFileExtension(
activity,
imageFileURI[i]
)
)
sRef.putFile(imageFileURI[i]!!)
.addOnSuccessListener { taskSnapshot ->
taskSnapshot.metadata!!.reference!!.downloadUrl
.addOnSuccessListener { uri ->
if (i < imageFileURI.size) {
i += 1
imageURLList.add(uri.toString())
} else {
activity.imageUploadSuccess(imageURLList)
}
}
}
.addOnFailureListener { exception ->
activity.hideProgressDialog()
Log.e(
activity.javaClass.simpleName,
exception.message,
exception
)
}
}
} else {
Toast.makeText(
activity,
"There is no images in the ArrayList of URI",
Toast.LENGTH_SHORT
).show()
}
}
EDIT: After receiving the first answer.
I have created a QueueSyn.kt file and added the code in the Answer. The activity where the images and the button are changed to
class AddProductActivity : BaseActivity(), View.OnClickListener, QueueSyncCallback {
The following function is called when the button is hit.
private fun uploadProductImage() {
showProgressDialog(resources.getString(R.string.please_wait))
QueueSync(
mSelectedImageFileUriList,
Constants.PRODUCT_IMAGE,
Constants.PRODUCT_IMAGE_DIRECTORY_NAME,
et_product_title.text.toString().trim { it <= ' ' },
this
).startUploading()
}
I have also implemented these two methods in the class AddProductActivity, but I don't know what should go inside this.
override fun completed(successList: MutableList<Uri>, failureList: MutableList<Uri>) {
TODO("Not yet implemented")
}
override fun getFileExtension(uri: Uri): String {
TODO("Not yet implemented")
}
Error:
This should work
import android.net.Uri
import com.google.firebase.storage.FirebaseStorage
import com.google.firebase.storage.StorageReference
import java.util.*
import kotlin.collections.ArrayList
interface QueueSyncCallback {
fun completed(successList: MutableList<Uri>, failureList: MutableList<Uri>)
fun getFileExtension(uri: Uri): String
}
class QueueSync(
imageFileURI: MutableList<Uri?>?,
private val imageType: String,
private val directoryName: String,
private val title: String,
private val callback: QueueSyncCallback,
private val maxActive: Int = 5
) {
private val queue: LinkedList<Uri> = LinkedList()
private val runningQueue: MutableList<Uri> = Collections.synchronizedList(
object : ArrayList<Uri>() {
override fun remove(element: Uri): Boolean {
val removed = super.remove(element)
if (isEmpty() && queue.isEmpty()) {
callback.completed(successList, failureList)
} else if (queue.isNotEmpty()) {
addToRunningQueue()
}
return removed
}
}
)
private val successList: MutableList<Uri> = Collections.synchronizedList(ArrayList())
private val failureList: MutableList<Uri> = Collections.synchronizedList(ArrayList())
init {
if (imageFileURI != null)
for (uri in imageFileURI) {
if (uri != null)
queue.add(uri)
}
}
private fun getLocation(uri: Uri) = "/$directoryName/$imageType.${callback.getFileExtension(uri)}"
fun startUploading() {
var i = 0
if (queue.isEmpty()) {
callback.completed(successList, failureList)
return
}
while (i < maxActive && queue.isNotEmpty()) {
addToRunningQueue()
i++
}
}
private fun addToRunningQueue() {
val uri = queue.poll()!!
runningQueue.add(uri)
uploadImageToCloudStorage(uri)
}
private fun uploadImageToCloudStorage(locationUri: Uri) {
val sRef: StorageReference = FirebaseStorage.getInstance().getReference(getLocation(locationUri))
sRef.putFile(locationUri)
.addOnSuccessListener { taskSnapshot ->
taskSnapshot.metadata!!.reference!!.downloadUrl
.addOnSuccessListener { uri ->
successList.add(uri)
runningQueue.remove(locationUri)
}
}
.addOnFailureListener {
failureList.add(locationUri)
runningQueue.remove(locationUri)
}
}
}
Since your need requires usage of threads so to prevent race conditions I had to use Collections.synchronizedList. To use this you need to implement QueueSyncCallback in your activity and pass it as a reference to QueueSync. Make sure that any piece of code written inside completed is wrapped inside runOnMainThread if it is going to access views in any way since completed will not run on main thread as far as I know. This should work however I am not able to test it since it is based on your current code.
Edit:- Answering after edit
override fun completed(successList: MutableList<Uri>, failureList: MutableList<Uri>) {
imageUploadSuccess(successList)
hideProgressDialog()
}
override fun getFileExtension(uri: Uri): String {
Constants.getFileExtension(this, imageFileURI[i])
}
I want to chain 3 coroutines by using Kotlin's extension functions. I know how to do it with regular ones, but can't manage it with extension functions. In fact, in the 2nd coroutine I can receive only one data sent from the 1st coroutine, but that's all. The program works but all I get on the console is Doc: 1st Document. What I'm doing wrong?
fun main(args: Array<String>) = runBlocking {
produceDocs().docLength().report().consumeEach {
println(it)
}
}
private fun CoroutineScope.produceDocs() = produce {
fun getDocs(): List<String> {
return listOf("1st Document", "2nd Newer Document")
}
while (this.isActive) {
val docs = getDocs()
for (doc in docs) {
send(doc)
}
delay(TimeUnit.SECONDS.toMillis(2))
}
}
private suspend fun ReceiveChannel<String>.docLength(): ReceiveChannel<Int> = coroutineScope {
val docsChannel: ReceiveChannel<String> = this#docLength
produce {
for (doc in docsChannel) {
println("Doc: $doc") // OK. This works.
send(doc.count()) // ??? Not sure where this sends data to?
}
}
}
private suspend fun ReceiveChannel<Int>.report(): ReceiveChannel<String> = coroutineScope {
val docLengthChannel: ReceiveChannel<Int> = this#report
produce {
for (len in docLengthChannel) {
println("Length: $len") // !!! Nothing arrived.
send("Report. Document contains $len characters.")
}
}
}
You have to consume each channel independently in order to make emissions go through the chain, otherwise the first emission will never be consumed:
private fun CoroutineScope.produceDocs() = produce {
fun getDocs(): List<String> {
return listOf("1st Document", "2nd Newer Document")
}
while (this.isActive) {
val docs = getDocs()
for (doc in docs) {
send(doc)
}
delay(TimeUnit.SECONDS.toMillis(2))
}
}
private suspend fun ReceiveChannel<String>.docLength() : ReceiveChannel<Int> = CoroutineScope(coroutineContext).produce {
for (doc in this#docLength) {
println("Doc: $doc") // OK. This works.
send(doc.count()) // ??? Not sure where this sends data to?
}
}
private suspend fun ReceiveChannel<Int>.report(): ReceiveChannel<String> = CoroutineScope(coroutineContext).produce {
for (len in this#report) {
println("Length: $len") // !!! Nothing arrived.
send("Report. Document contains $len characters.")
}
}
I suggest you a better approach to do the exact same thing using Flow:
private fun produceDocs(): Flow<String> = flow {
fun getDocs(): List<String> {
return listOf("1st Document", "2nd Newer Document")
}
while (true) {
val docs = getDocs()
for (doc in docs) {
emit(doc)
}
delay(TimeUnit.SECONDS.toMillis(2))
}
}
private fun Flow<String>.docLength(): Flow<Int> = flow {
collect { doc ->
println("Doc: $doc")
emit(doc.count())
}
}
private fun Flow<Int>.report(): Flow<String> = flow {
collect { len ->
println("Length: $len")
emit("Report. Document contains $len characters.")
}
}
Or better like this:
private fun produceDocs(): Flow<String> = flow {
fun getDocs(): List<String> {
return listOf("1st Document", "2nd Newer Document")
}
while (true) {
val docs = getDocs()
for (doc in docs) {
emit(doc)
}
delay(TimeUnit.SECONDS.toMillis(2))
}
}
private fun Flow<String>.docLength(): Flow<Int> = transform { doc ->
println("Doc: $doc")
emit(doc.count())
}
private fun Flow<Int>.report(): Flow<String> = transform { len ->
println("Length: $len")
emit("Report. Document contains $len characters.")
}
And collect it like this:
produceDocs().docLength().report().collect {
println(it)
}
Or even better like this:
produceDocs()
.map { doc ->
println("Doc: $doc")
doc.count()
}
.map { len ->
println("Length: $len")
"Report. Document contains $len characters."
}
.collect {
println(it)
}
I tried to create a class with annotation processor and Kotlin Poet. This is my code:
#AutoService(Processor::class)
class TailProcessor : AbstractProcessor() {
override fun process(elementTypeSet: MutableSet<out TypeElement>?, roundEnvironment: RoundEnvironment?): Boolean {
roundEnvironment?.getElementsAnnotatedWith(Tail::class.java)?.forEach {
if (it.javaClass.kotlin.isData) {
print("You are doing it right")
val className = it.simpleName.toString()
val pack = processingEnv.elementUtils.getPackageOf(it).toString()
val variables = ElementFilter.fieldsIn(elementTypeSet)
startClassGeneration(className, pack, variables)
} else {
return false
}
}
return false
}
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
override fun getSupportedAnnotationTypes(): MutableSet<String> = mutableSetOf(Tail::class.java.name)
private fun startClassGeneration(
className: String,
pack: String,
variables: MutableSet<VariableElement>
) {
val fileName = "Tail$className"
val stringToBePrinted = generateStringFromIncommingValues(variables)
val printFunction = FunSpec.builder("print").addCode("print($stringToBePrinted)").build()
val generatedClass = TypeSpec.objectBuilder(fileName).addFunction(printFunction).build()
val file = FileSpec.builder(pack, fileName).addType(generatedClass).build()
val kaptKotlinGeneratedDir = processingEnv.options[KOTLIN_DIRECTORY_NAME]
file.writeTo(File(kaptKotlinGeneratedDir, "$fileName.kt"))
}
private fun generateStringFromIncommingValues(variables: MutableSet<VariableElement>): Any {
val stringBuilder = StringBuilder()
variables.forEach {
if (it.constantValue == null) {
stringBuilder.append("null\n ")
} else {
stringBuilder.append("${it.constantValue}\n")
}
}
return stringBuilder.toString()
}
companion object {
const val KOTLIN_DIRECTORY_NAME = "sxhardha.tail"
}
}
The problem, directory and file not generating. I tried to rebuild, invalidate cache + restart, clean but none of them works. The build goes successful without any errors but I see no changes. Can you check what is wrong?
I actually found the issue. I wasn't checking right if that class is a data class or not and the condition was never met.
Instead of:
it.javaClass.kotlin.isData
Should have been:
it.kotlinMetadata as KotlinClassMetadata).data.classProto.isDataClass
But it can only be achieved by using this library here.
I'm trying to add url values (imgURL variable) taken from JSON into an ArrayList with Kotlin, but somehow when in the scope of the for loop it doesn't seem to work regardless of having my newArrayURLs array declared as a function variable. Any ideas why is this?
private fun getJsonObjectImg(Url: String): ArrayList<String>{
var paths: ArrayList<String> =ArrayList()
val params = HashMap<String, String>()
var newArrayURLs = ArrayList<String>()
val stringRequest = object : StringRequest(Request.Method.POST, Url, Response.Listener { s ->
try {
val array = JSONObject(s)
var imagesArray = array.getJSONArray("images")
for(i in 0..imagesArray.length() -1 ){
var imgURL = imagesArray.getJSONObject(i).getString("fileName")
newArrayURLs.add(imgURL)
paths.add(imgURL)
}
Log.d("pathsin TRY", paths.count().toString())
} catch(e:JSONException){
Log.d("ERROR------", e.toString())
null
}
}, Response.ErrorListener {
error: VolleyError? ->
try {
Log.d("ERROR======","V")
} catch (e: JSONException){
e.printStackTrace()
}
}) {
override fun getParams(): Map<String, String> = mapOf("uploadedBy" to id)
}
val requestQueue = Volley.newRequestQueue(this)
requestQueue.add<String>(stringRequest)
return newArrayURLs
}