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)
}
In this example I will upload pictures directly from builtIn gallery using this example.
Prepare virtual device like I already explained here.
Controller looks same as here.
In AndroidManifest.xml I need only retrofit permissions:
import com.google.gson.annotations.SerializedName
import okhttp3.MultipartBody
import retrofit2.Call
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
interface IWebApiService {
@Multipart
@POST("api/UploadPictures/UploadImage")
fun uploadImage(
@Part image: MultipartBody.Part?
): Call<UploadResponse>
}
data class UploadResponse(
@SerializedName("message")
val message: String
)
This time I will create Retrofit using dependency injection:
import android.util.Log
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
internal class CreateRetrofitBuilder : ICreateRetrofitBuilder {
override fun createRetrofitBuilder(baseUrl: String): Retrofit {
return Retrofit.Builder()
.baseUrl(baseUrl)
.client(trustAllCertificates())
.addConverterFactory(GsonConverterFactory.create())
.build()
}
private fun trustAllCertificates(): OkHttpClient {
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
Log.i(MainActivity::class.simpleName, "checkClientTrusted")
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
Log.i(MainActivity::class.simpleName, "checkServerTrusted")
}
override fun getAcceptedIssuers() = arrayOf<X509Certificate>()
})
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
// Create an ssl socket factory with our all-trusting manager
val sslSocketFactory = sslContext.socketFactory
// connect to server
return OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }.build()
}
}
Here notice that I am using "GsonConverterFactory" which means that Retrofit expects JSON anwer from the server, which is why I need UploadResponse class, and from server I will respond with: