Here is one simplest possible example of google maps.
First start new "Empty Views Activity" like I already explained here
In "\GoogleMapsTestSandBox\app\build.gradle.kts" I have added "com.google.android.gms:play-services-maps:19.2.0", so my dependencies looks like
Since I don't want to make google maps key public, I will add "secrets" file. To do it, first I wil add id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1" plugin in gradle "\GoogleMapsTestSandBox\app\build.gradle.kts":
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1"
}
Before I sync my gradle, I need to add two files local.defaults.properties and secrets.properties.
local.defaults.properties:
Here I gave one example of foreground service, but where reciever is in MainActivity.kt.
In this example I want to have receiveer in separate class. So in manifest I will add:
package com.milosev.separateProcessServiceExample
object IntentGlobalActions {
const val TEST_MESSAGE_ACTION = "com.milosev.TEST_MESSAGE_ACTION"
const val START_FOREGROUND_SERVICE = "startService"
}
---
Since LocalBroadcastManager is deprecated, here are examples for creating a separate process Service and sending messages from the process to the UI.
First I created new project with "Empty Views Activity":
Then I have added new service and name it "SeparateProcessServiceExample", please note that later I will derive it from MainScope:
I have disabled exported, since I don't want to share messages between other applications:
In "\SeparateProcessServiceExample\app\src\main\AndroidManifest.xml" I have added permissions for FOREGROUND_SERVICE, and since later I want to display icon in notification when my service starts, I will also need POST_NOTIFICATIONS, also I need FOREGROUND_SERVICE_DATA_SYNC permission because I will want to start with "startForeground" method:
but don't forget to enable viewBinding in your "\SeparateProcessServiceExample\app\build.gradle.kts":
buildFeatures {
viewBinding = true
}
In "\SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\SeparateProcessServiceExample.kt" first override onStartCommand and filter pro intent messages defined in IntentGlobalActions, and return START_STICKY:
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
IntentGlobalActions.START_FOREGROUND_SERVICE -> {
}
}
return START_STICKY
}
create notification:
val channelId = createNotificationChannel("my_service", "My Background Service")
val notificationBuilder = NotificationCompat.Builder(this, channelId)
val notification = notificationBuilder.setOngoing(true)
.setContentTitle("test")
.setContentText("test")
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(1)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
Where the method createNotificationChannel looks like:
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String {
val chan = NotificationChannel(
channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT
)
chan.lightColor = Color.RED
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
return channelId
}
Start service:
startForeground(101, notification)
Next, I want to have an endles task, from which I want to send messages like:
In order to use launch my SeparateProcessServiceExample to derive from CoroutineScopebyMainScope(), so now it looks like:
class SeparateProcessServiceExample : Service(), CoroutineScope by MainScope() {
Message sending from service looks like:
val intent = Intent(IntentGlobalActions.TEST_MESSAGE_ACTION)
intent.putExtra("message", "Message number: $messageNumber")
intent.setPackage(packageName)
sendBroadcast(intent)
Now, back to "\SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\MainActivity.kt", create reciever like:
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val msg = intent?.getStringExtra("message")
appendLog("Receiver: Got: $msg")
}
}
Register it:
val filter = IntentFilter(IntentGlobalActions.TEST_MESSAGE_ACTION)
registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED)
Where the method appendLog looks like:
fun appendLog(message: String) {
runOnUiThread {
binding.tvLog.append("\n$message")
val lines = binding.tvLog.text.split("\n")
if (lines.size > maxLines) {
val trimmed = lines.takeLast(maxLines).joinToString("\n")
binding.tvLog.text = trimmed
}
binding.scrollLog.post {
binding.scrollLog.fullScroll(View.FOCUS_DOWN)
}
}
}
To Stop a service from \SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\MainActivity.kt I have sent STOP_FOREGROUND_SERVICE:
val intent = Intent(this, SeparateProcessServiceExample::class.java)
intent.action = IntentGlobalActions.STOP_FOREGROUND_SERVICE
startForegroundService(intent)
and in \SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\SeparateProcessServiceExample.kt:
Now I had a problem to send a picture with some additional data to Web Api .NET Core.
Server side is same as I already explained here
Api service:
interface IWebApiService {
@Headers("Content-Type: text/json")
@POST("UploadImage")
fun uploadImage(@Body image: JsonObject): Call<UploadResponse>
}
data class UploadResponse(
@SerializedName("message")
val message: String
)
Upload image:
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import androidx.annotation.RequiresApi
import com.google.gson.JsonObject
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.ByteArrayOutputStream
import java.util.Base64
class UploadImageRetrofit(private val uploadImageRetrofitCallBacks: IUploadImageRetrofitCallBacks, private val webApiService: IWebApiService) {
@RequiresApi(Build.VERSION_CODES.O)
fun uploadImage(imgUri: Uri, context: Context): String? {
val base64Image = convertImageToBase64(context, imgUri)
val jsonValue = JsonObject().apply {
addProperty("image", base64Image)
addProperty("fileName", "magnolia.jpg")
addProperty("folderName", "spring")
}
val webApiRequest = webApiService.uploadImage(jsonValue)
webApiRequest.enqueue(object : Callback<UploadResponse> {
override fun onResponse(call: Call<UploadResponse>, response: Response<UploadResponse>) {
uploadImageRetrofitCallBacks.onResponse(call, response)
}
override fun onFailure(call: Call<UploadResponse>, t: Throwable) {
uploadImageRetrofitCallBacks.onFailure(call, t)
}
})
return null
}
@RequiresApi(Build.VERSION_CODES.O)
fun convertImageToBase64(context: Context, imgUri: Uri): String {
val inputStream = context.contentResolver.openInputStream(imgUri)
val bitmap: Bitmap = BitmapFactory.decodeStream(inputStream)
val baos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
val imageBytes: ByteArray = baos.toByteArray()
return Base64.getEncoder().encodeToString(imageBytes)
}
}
Rest is same as in previous example.
Download from here.
---
UPDATE 2024-04-06: The convertImageToBase64 method in the above example will delete EXIF data, in order not to loose EXIF data use something like this:
@RequiresApi(Build.VERSION_CODES.O)
fun convertImageToBase64(context: Context, imgUri: Uri): String {
val inputStream = context.contentResolver.openInputStream(imgUri)
val imageBytes = inputStream.use { input ->
input?.readBytes()
} ?: return "" // Handle null input stream or read failure
return Base64.getEncoder().encodeToString(imageBytes)
}