- Details
- Written by: Stanko Milosev
- Category: Android
- Hits: 55
- AndroidManifest.xml
- activity_main.xml
- MainActivity.kt
- SeparateProcessServiceExample.kt
- IntentGlobalActions.kt
- Below, I tried to explain everything line by line.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SeparateProcessServiceExample">
<service
android:name=".SeparateProcessServiceExample"
android:enabled="true"
android:exported="false"
android:permission="android.permission.FOREGROUND_SERVICE"
android:foregroundServiceType="dataSync"
android:process=":SeparateProcessServiceExample" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
\SeparateProcessServiceExample\app\src\main\res\layout\activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<ScrollView
android:id="@+id/scrollLog"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
android:scrollbars="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/btnStart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/tvLog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Log:"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:typeface="monospace"
android:gravity="top|start"
android:background="#EEEEEE"
android:padding="8dp" />
</ScrollView>
<Button
android:id="@+id/btnStart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start"
android:layout_marginBottom="64dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
\SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\MainActivity.kt:
package com.milosev.separateProcessServiceExample
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.milosev.separateProcessServiceExample.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val maxLines = 500
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val msg = intent?.getStringExtra("message")
appendLog("Receiver: Got: $msg")
}
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
100
)
}
val filter = IntentFilter(IntentGlobalActions.TEST_MESSAGE_ACTION)
registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED)
binding.btnStart.setOnClickListener {
val intent = Intent(this, SeparateProcessServiceExample::class.java)
intent.action = IntentGlobalActions.START_FOREGROUND_SERVICE
startForegroundService(intent)
}
}
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)
}
}
}
}
\SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\SeparateProcessServiceExample.kt:
package com.milosev.separateProcessServiceExample
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SeparateProcessServiceExample : Service(), CoroutineScope by MainScope() {
private var job: Job? = null
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
IntentGlobalActions.START_FOREGROUND_SERVICE -> {
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()
startForeground(101, notification)
var messageNumber = 0
job = launch {
while(true) {
messageNumber++
val intent = Intent(IntentGlobalActions.TEST_MESSAGE_ACTION)
intent.putExtra("message", "Message number: $messageNumber")
intent.setPackage(packageName)
sendBroadcast(intent)
delay(1_000)
}
}
}
}
return START_STICKY
}
@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
}
}
\SeparateProcessServiceExample\app\src\main\java\com\milosev\separateProcessServiceExample\IntentGlobalActions.kt:
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:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />Also in AndroidManifest.xml I have added service under node application. "foregroundServiceType" I need because of the error:
To call Service.startForeground(), the <service> element of manifest file must have the foregroundServiceType attribute specified
<service android:name=".SeparateProcessServiceExample" android:enabled="true" android:exported="false" android:permission="android.permission.FOREGROUND_SERVICE" android:foregroundServiceType="dataSync" android:process=":SeparateProcessServiceExample" />So my "\SeparateProcessServiceExample\app\src\main\AndroidManifest.xml" looks like:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SeparateProcessServiceExample">
<service
android:name=".SeparateProcessServiceExample"
android:enabled="true"
android:exported="false"
android:permission="android.permission.FOREGROUND_SERVICE"
android:foregroundServiceType="dataSync"
android:process=":SeparateProcessServiceExample" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Now I have addded new object "IntentGlobalActions", for better readibility, which I will use to define actions for intent:
object IntentGlobalActions {
const val TEST_MESSAGE_ACTION = "com.milosev.TEST_MESSAGE_ACTION"
const val START_FOREGROUND_SERVICE = "startService"
}
My "\SeparateProcessServiceExample\app\src\main\res\layout\activity_main.xml" looks like:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<ScrollView
android:id="@+id/scrollLog"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
android:scrollbars="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/btnStart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/tvLog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Log:"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:typeface="monospace"
android:gravity="top|start"
android:background="#EEEEEE"
android:padding="8dp" />
</ScrollView>
<Button
android:id="@+id/btnStart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start"
android:layout_marginBottom="64dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Ask user to allow notification permission:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
100
)
}
}
Setup View Binding:
binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root)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:
private var job: Job? = null
...
job = launch {
while(true) {
delay(10_000)
}
}
In order to use launch my SeparateProcessServiceExample to derive from CoroutineScope by MainScope(), 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:
IntentGlobalActions.STOP_FOREGROUND_SERVICE -> {
job?.cancel()
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelfResult(startId)
}
Example download from here.
- Details
- Written by: Stanko Milosev
- Category: Android
- Hits: 592
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)
}
- Details
- Written by: Stanko Milosev
- Category: Android
- Hits: 594
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />Api service:
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:
return Ok(new { message = "Image uploaded successfully." });
Otherwise I would receive error like:
com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $Where ICreateRetrofitBuilder looks like:
import retrofit2.Retrofit
interface ICreateRetrofitBuilder {
fun createRetrofitBuilder(baseUrl: String): Retrofit
}
onResponse and onFailure I will also inject and use the like call backs:
import android.app.AlertDialog
import retrofit2.Call
import retrofit2.Response
class UploadImageRetrofitCallBacks(private val alertDialogBuilder: AlertDialog.Builder) :
IUploadImageRetrofitCallBacks {
override fun onResponse(call: Call<UploadResponse>, response: Response<UploadResponse>) {
if (!response.isSuccessful) {
alertDialogBuilder.setMessage(response.errorBody()!!.charStream().readText())
.setCancelable(false)
.setNeutralButton("OK") { dialog, _ ->
dialog.dismiss()
}
val alert = alertDialogBuilder.create()
alert.setTitle("Error")
alert.show()
} else {
alertDialogBuilder.setMessage("Response: ${response.body()?.message.toString()}")
.setCancelable(false)
.setNeutralButton("OK") { dialog, _ ->
dialog.dismiss()
}
val alert = alertDialogBuilder.create()
alert.setTitle("Success")
alert.show()
}
}
override fun onFailure(call: Call<UploadResponse>, t: Throwable) {
alertDialogBuilder.setMessage(t.message)
.setCancelable(false)
.setNeutralButton("OK") { dialog, _ ->
dialog.dismiss()
}
val alert = alertDialogBuilder.create()
alert.setTitle("Error")
alert.show()
}
}
Where interface looks like:
import retrofit2.Call
import retrofit2.Response
interface IUploadImageRetrofitCallBacks {
fun onResponse(call: Call<UploadResponse>, response: Response<UploadResponse>)
fun onFailure(call: Call<UploadResponse>, t: Throwable)
}
Upload image:
import android.content.Context
import android.net.Uri
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class UploadImageRetrofit(private val uploadImageRetrofitCallBacks: IUploadImageRetrofitCallBacks, private val webApiService: IWebApiService) {
fun uploadImage(imgUri: Uri, context: Context): String? {
val inputStream = context.contentResolver.openInputStream(imgUri)
val mediaType = context.contentResolver.getType(imgUri)?.toMediaTypeOrNull()
val requestFile = inputStream?.use {
it.readBytes().toRequestBody(mediaType)
}
val imagePart: MultipartBody.Part? = requestFile?.let {
MultipartBody.Part.createFormData("image", "image.jpg", it)
}
val webApiRequest = webApiService.uploadImage(imagePart)
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
}
}
At the end, MainActivity looks like this:
import android.app.AlertDialog
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var uploadImage: UploadImageRetrofit
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
uploadImage = UploadImageRetrofit(
UploadImageRetrofitCallBacks(AlertDialog.Builder(this@MainActivity)),
CreateRetrofitBuilder().createRetrofitBuilder("https://10.0.2.2:7181/")
.create(IWebApiService::class.java)
)
}
private val galleryLauncher =
this.registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { images ->
images.forEach { imgUri ->
uploadImage.uploadImage(imgUri, this)
}
}
fun onOpenGalleryAndUploadButtonClick(view: View) {
galleryLauncher.launch("image/*")
}
}
Download from here.
- Details
- Written by: Stanko Milosev
- Category: Android
- Hits: 745
In order to work in all versions in AndroidManifest.xml I have added:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />Please notice if you use MANAGE_EXTERNAL_STORAGE most probably you app will be rejected on play store To check storage permission for all versions of Android I have used following code:
@RequiresApi(Build.VERSION_CODES.R)
fun checkLocalStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
val uri = Uri.parse("package:${BuildConfig.APPLICATION_ID}")
startActivity(
Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
uri
)
)
}
}
}
In app\build.gradle.kts I have added buildConfig = true,
buildFeatures {
buildConfig = true
}
so that piece of code
val uri = Uri.parse("package:${BuildConfig.APPLICATION_ID}")
works
Also, for retrofit I will need permission:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />In app\build.gradle.kts I have added retrofit:
implementation( "com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
Then I have added WebApiService interface:
import okhttp3.MultipartBody
import retrofit2.Call
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
interface WebApiService {
@Multipart
@POST("api/UploadPictures/UploadImage")
fun uploadImage(
@Part image: MultipartBody.Part?
): Call<UploadResponse>
}
data class UploadResponse(
@SerializedName("message")
val message: String
)
Retrofit is almost the same as I already explained here, except the post method:
val imageFile = File(imagePath)
val requestBody = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
val imagePart = MultipartBody.Part.createFormData("image", imageFile.name, requestBody)
val apiService = retrofit.create(WebApiService::class.java)
val webApiRequest = apiService.uploadImage(imagePart)
Here how the whole file looks like:
import android.app.AlertDialog
import android.util.Log
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.asRequestBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class UploadImageRetrofit {
fun uploadImage(imagePath: String, alertDialogBuilder: AlertDialog.Builder): String? {
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
val client = OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://10.0.2.2:7181/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
val imageFile = File(imagePath)
val requestBody = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
val imagePart = MultipartBody.Part.createFormData("image", imageFile.name, requestBody)
val apiService = retrofit.create(WebApiService::class.java)
val webApiRequest = apiService.uploadImage(imagePart)
webApiRequest.enqueue(object : Callback<UploadResponse> {
override fun onResponse(call: Call<UploadResponse>, response: Response<UploadResponse>) {
if (!response.isSuccessful) {
alertDialogBuilder.setMessage(response.errorBody()!!.charStream().readText())
.setCancelable(false)
.setNeutralButton("OK") { dialog, _ ->
dialog.dismiss()
}
val alert = alertDialogBuilder.create()
alert.setTitle("Error")
alert.show()
} else {
alertDialogBuilder.setMessage("Response: ${response.body()?.message.toString()}")
.setCancelable(false)
.setNeutralButton("OK") { dialog, _ ->
dialog.dismiss()
}
val alert = alertDialogBuilder.create()
alert.setTitle("Success")
alert.show()
}
}
override fun onFailure(call: Call<UploadResponse>, t: Throwable) {
alertDialogBuilder.setMessage(t.message)
.setCancelable(false)
.setNeutralButton("OK") { dialog, _ ->
dialog.dismiss()
}
val alert = alertDialogBuilder.create()
alert.setTitle("Error")
alert.show()
}
})
return null
}
}
MainActivity.kt:
import android.app.AlertDialog
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import android.view.View
import androidx.annotation.RequiresApi
class MainActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
checkLocalStoragePermission()
}
fun onUploadImageButtonClick(view: View) {
val uploadImageRetrofit = UploadImageRetrofit()
val alertDialogBuilder = AlertDialog.Builder(this@MainActivity)
uploadImageRetrofit.uploadImage("/sdcard/Download/IMG_20240120_133805.jpg", alertDialogBuilder)
}
@RequiresApi(Build.VERSION_CODES.R)
fun checkLocalStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
val uri = Uri.parse("package:${BuildConfig.APPLICATION_ID}")
startActivity(
Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
uri
)
)
}
}
}
}
Download from here
---
Web API .Net Core controller:
using Microsoft.AspNetCore.Mvc;
namespace UploadPictures.Controllers;
[ApiController]
[Route("api/[controller]")]
public class UploadPicturesController : Controller
{
private readonly string _uploadPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "UploadPictures", "uploads");
[HttpPost]
[Route("UploadImage")]
public async Task<IActionResult> UploadImage()
{
try
{
if (!Request.HasFormContentType)
{
return BadRequest("Invalid content type. Must be multipart/form-data.");
}
var form = await Request.ReadFormAsync();
var file = form.Files.FirstOrDefault();
if (file == null)
{
return BadRequest("No image file found in the request.");
}
// Generate a unique filename
var filename = Path.GetRandomFileName() + Path.GetExtension(file.FileName);
var filePath = Path.Combine(_uploadPath, filename);
// Create the upload directory if it doesn't exist
Directory.CreateDirectory(_uploadPath);
// Save the uploaded file
await using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return Ok(new { message = "Image uploaded successfully." });
}
catch (Exception ex)
{
// Log the error for debugging
Console.WriteLine(ex.ToString());
return StatusCode(500, "Internal server error.");
}
}
}
Download Visual Studio .NET Core example from here