yayan před 1 rokem
rodič
revize
5ef3e90954

+ 3 - 0
app/src/main/AndroidManifest.xml

@@ -3,6 +3,9 @@
     xmlns:tools="http://schemas.android.com/tools">
 
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.CAMERA" />
+
+    <uses-feature android:name="android.hardware.camera.any" />
 
     <application
         android:name=".AlphaApplication"

+ 12 - 17
app/src/main/java/io/nexilis/alpha/ui/main/Main.kt

@@ -26,9 +26,9 @@ import io.nexilis.alpha.ui.viewmodel.MainViewModel
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun Main(navController: NavHostController) {
-    val navHostController = rememberNavController()
+    val navMainController = rememberNavController()
     val hostState = remember { SnackbarHostState() }
-    val navBackStackEntry by navHostController.currentBackStackEntryAsState()
+    val navBackStackEntry by navMainController.currentBackStackEntryAsState()
     val currentDestination = navBackStackEntry?.destination
     var isHome = true
     currentDestination?.route.let {
@@ -45,27 +45,27 @@ fun Main(navController: NavHostController) {
             TopAppBar(
                 title = { Text(text = title) },
                 navigationIcon = {
-                    NavigationIcon(navController = navHostController, isHome = isHome)
+                    NavigationIcon(navController = navMainController, isHome = isHome)
                 },
                 actions = {
-                    Actions(navController = navController)
+                    ActionsMenu(navController = navController, navMainController = navMainController)
                 }
             )
         },
         bottomBar = {
-            BottomBar(navController = navHostController, isHome = isHome)
+            BottomBar(navController = navMainController, isHome = isHome)
         },
         snackbarHost = { SnackbarHost(hostState = hostState) },
         floatingActionButton = {
             FloatingActionButton(
-                navController = navHostController,
+                navController = navMainController,
                 isHome = isHome
             )
         }
     ) { contentPadding ->
 
         NavHost(
-            navController = navHostController,
+            navController = navMainController,
             startDestination = Screen.Home.route,
             modifier = Modifier.padding(contentPadding),
             route = Graph.home
@@ -75,7 +75,7 @@ fun Main(navController: NavHostController) {
             }
             composable(route = Screen.Chats.route) {
                 Chats(
-                    navController = navHostController,
+                    navController = navMainController,
                     mainViewModel = mainViewModel
                 )
             }
@@ -84,7 +84,7 @@ fun Main(navController: NavHostController) {
             }
             navigation(route = Graph.child, startDestination = Screen.Contact.route) {
                 composable(route = Screen.Contact.route) {
-                    Contact(navController = navHostController, mainViewModel = mainViewModel)
+                    Contact(navController = navMainController, mainViewModel = mainViewModel)
                 }
                 composable(
                     route = Screen.Chat.route + "/{pin}",
@@ -93,16 +93,16 @@ fun Main(navController: NavHostController) {
                     it.arguments?.getString("pin")
                         ?.let { pin ->
                             Chat(
-                                navController = navHostController,
+                                navController = navMainController,
                                 pin = pin,
                                 mainViewModel = mainViewModel
                             )
                         }
                 }
                 composable(route = Screen.SignUp.route) {
-                    SignUp(navController = navHostController) {
+                    SignUp(navController = navMainController) {
                         if (it) {
-                            navHostController.navigate(Graph.home)
+                            navMainController.navigate(Graph.home)
                         }
                     }
                 }
@@ -123,11 +123,6 @@ fun NavigationIcon(navController: NavHostController, isHome: Boolean) {
     }
 }
 
-@Composable
-fun Actions(navController: NavHostController) {
-    HomeMenu(navController = navController)
-}
-
 @Composable
 fun BottomBar(navController: NavHostController, isHome: Boolean) {
     val navBackStackEntry by navController.currentBackStackEntryAsState()

+ 11 - 6
app/src/main/java/io/nexilis/alpha/ui/main/Menu.kt

@@ -11,24 +11,29 @@ import androidx.navigation.compose.currentBackStackEntryAsState
 import io.nexilis.alpha.ui.screen.Screen
 
 @Composable
-fun HomeMenu(navController: NavHostController) {
+fun ActionsMenu(navController: NavHostController, navMainController: NavHostController) {
     val navBackStackEntry by navController.currentBackStackEntryAsState()
-    val currentDestination = navBackStackEntry?.destination
+    val navMainBackStackEntry by navMainController.currentBackStackEntryAsState()
     var isChat = false
-    currentDestination?.route.let {
+    navBackStackEntry?.destination?.route.let {
+        it?.let {
+            isChat = it.startsWith("${Screen.Chat.route}/")
+        }
+    }
+    navMainBackStackEntry?.destination?.route.let {
         it?.let {
             isChat = it.startsWith("${Screen.Chat.route}/")
         }
     }
     if (isChat) {
-        ChatMenu(navController = navController)
+        ChatMenu(navController = navMainController)
     } else {
-        MainMenu(navController = navController)
+        GlobalMenu(navController = navController)
     }
 }
 
 @Composable
-fun MainMenu(navController: NavHostController) {
+fun GlobalMenu(navController: NavHostController) {
     var isOpenMenu by remember {
         mutableStateOf(false)
     }

+ 54 - 16
app/src/main/java/io/nexilis/alpha/ui/main/Profile.kt

@@ -1,9 +1,11 @@
 package io.nexilis.alpha.ui.main
 
-import android.graphics.Bitmap
+import android.Manifest
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.widget.Toast
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
-import androidx.activity.result.launch
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.shape.CircleShape
@@ -15,29 +17,55 @@ import androidx.compose.runtime.livedata.observeAsState
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.graphics.painter.BitmapPainter
 import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
+import androidx.core.content.FileProvider
 import androidx.hilt.navigation.compose.hiltViewModel
 import coil.compose.rememberAsyncImagePainter
 import coil.request.ImageRequest
+import io.nexilis.alpha.BuildConfig
 import io.nexilis.alpha.ui.viewmodel.MainViewModel
+import io.nexilis.service.core.createImageFile
 import io.nexilis.service.data.viewmodels.BuddyViewModel
+import java.util.Objects
+
 
 @Composable
 fun Profile(contentPadding: PaddingValues = PaddingValues(0.dp), mainViewModel: MainViewModel) {
     SideEffect {
         mainViewModel.setTitle("Profile")
     }
-    var imageUri by remember { mutableStateOf<Bitmap?>(null) }
-    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) {
-        imageUri = it
-    }
     val viewModel: BuddyViewModel = hiltViewModel()
     val me by viewModel.me.observeAsState()
+    val context = LocalContext.current
+    val file = context.createImageFile()
+    val uri = FileProvider.getUriForFile(
+        Objects.requireNonNull(context),
+        BuildConfig.APPLICATION_ID + ".provider", file
+    )
+    var imageUri by remember { mutableStateOf<Uri>(Uri.EMPTY) }
+    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
+        if (it && uri.path?.isNotEmpty() == true) {
+            me?.let { me ->
+                viewModel.changeProfile(me.f_pin, file) {
+                    imageUri = uri
+                }
+            }
+        }
+    }
+    val permissionLauncher = rememberLauncherForActivityResult(
+        ActivityResultContracts.RequestPermission()
+    ) {
+        if (it) {
+            Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT).show()
+            launcher.launch(uri)
+        } else {
+            Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT).show()
+        }
+    }
     Column(
         modifier = Modifier
             .padding(contentPadding)
@@ -48,17 +76,16 @@ fun Profile(contentPadding: PaddingValues = PaddingValues(0.dp), mainViewModel:
             Modifier.align(Alignment.CenterHorizontally),
             contentAlignment = Alignment.BottomEnd,
         ) {
-            val painter: Painter = imageUri?.let {
-                BitmapPainter(it.asImageBitmap())
-            } ?: run {
-                rememberAsyncImagePainter(
+            val painter: Painter =
+                if (imageUri.path?.isNotEmpty() == true)
+                    rememberAsyncImagePainter(imageUri)
+                else rememberAsyncImagePainter(
                     model = ImageRequest.Builder(LocalContext.current)
                         .data("https://digixplatform.com/filepalio/image/${me?.image_id}")
-                        .addHeader("Cookie" , "PHPSESSID=123;MOBILE=123")
+                        .addHeader("Cookie", "PHPSESSID=123;MOBILE=123")
                         .crossfade(true)
                         .build()
                 )
-            }
             Image(
                 painter = painter, contentDescription = "", modifier = Modifier
                     .padding(top = 8.dp, end = 16.dp, bottom = 8.dp)
@@ -67,7 +94,13 @@ fun Profile(contentPadding: PaddingValues = PaddingValues(0.dp), mainViewModel:
                 contentScale = ContentScale.Crop
             )
             IconButton(onClick = {
-                launcher.launch()
+                val permissionCheckResult =
+                    ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
+                if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
+                    launcher.launch(uri)
+                } else {
+                    permissionLauncher.launch(Manifest.permission.CAMERA)
+                }
             }) {
                 Icon(
                     imageVector = Icons.Default.AddCircle,
@@ -100,7 +133,12 @@ fun Profile(contentPadding: PaddingValues = PaddingValues(0.dp), mainViewModel:
         )
         ListItem(
             headlineContent = { Text("Phone", style = MaterialTheme.typography.bodySmall) },
-            supportingContent = { Text("08112345678", style = MaterialTheme.typography.titleMedium) },
+            supportingContent = {
+                Text(
+                    "08112345678",
+                    style = MaterialTheme.typography.titleMedium
+                )
+            },
             leadingContent = {
                 Icon(imageVector = Icons.Default.Call, contentDescription = "")
             }

+ 2 - 0
cpaas-lite/build.gradle

@@ -55,6 +55,8 @@ dependencies {
 
     implementation "com.google.dagger:hilt-android:2.48"
     kapt "com.google.dagger:hilt-android-compiler:2.48"
+
+    implementation "com.squareup.okhttp3:okhttp:4.12.0"
 }
 
 kapt {

+ 10 - 0
cpaas-lite/src/main/AndroidManifest.xml

@@ -13,5 +13,15 @@
             android:enabled="true"
             android:exported="false"
             android:foregroundServiceType="microphone|camera|mediaProjection" />
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="${applicationId}.provider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/path_provider" />
+        </provider>
     </application>
 </manifest>

+ 14 - 0
cpaas-lite/src/main/java/io/nexilis/service/core/Extension.kt

@@ -4,11 +4,15 @@ import android.content.Context
 import android.content.SharedPreferences
 import androidx.compose.ui.graphics.Color
 import io.nexilis.service.namePreference
+import java.io.File
 import java.net.URLDecoder
 import java.net.URLEncoder
 import java.nio.charset.StandardCharsets
 import java.security.MessageDigest
 import java.security.SecureRandom
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
 
 fun String.decrypt(): String {
     return Secret().decrypt(this)
@@ -29,6 +33,16 @@ fun SharedPreferences.put(key: String, value: String) {
     }
 }
 
+fun Context.createImageFile(): File {
+    val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
+    val imageFileName = "IMG_" + timeStamp + "_"
+    return File.createTempFile(
+        imageFileName,
+        ".webp",
+        externalCacheDir
+    )
+}
+
 fun String.toMD5(): String {
     val md = MessageDigest.getInstance("MD5")
     val hashInBytes = md.digest(this.toByteArray(StandardCharsets.UTF_8))

+ 1 - 0
cpaas-lite/src/main/java/io/nexilis/service/core/Incoming.kt

@@ -204,6 +204,7 @@ class Incoming private constructor() {
             val me = context.getSharedPreferences().getString("pin", "")
             val d = data.bodies["A112"] ?: ""
             val jsonArray = JSONArray(d)
+            Log.d(tag, "pushBuddies:$jsonArray")
             for (i in 0 until jsonArray.length()) {
                 val jsonObject = jsonArray.getJSONObject(i)
                 ApiRoomDatabase.getDatabase(context).buddyDao().insert(

+ 77 - 0
cpaas-lite/src/main/java/io/nexilis/service/core/Network.kt

@@ -0,0 +1,77 @@
+package io.nexilis.service.core
+
+import android.net.Uri
+import android.os.Build
+import android.util.Log
+import android.webkit.MimeTypeMap
+import androidx.core.net.toUri
+import io.nexilis.service.tag
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import okhttp3.Response
+import java.io.File
+import java.io.IOException
+import java.nio.file.Files
+import java.util.Locale
+
+
+class Network {
+
+    fun upload(url: String, file: File, completion: (Boolean) -> Unit) {
+        val client = OkHttpClient()
+        val body: RequestBody = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            MultipartBody.Builder()
+                .setType(MultipartBody.FORM)
+                .addFormDataPart(
+                    "file", file.name,
+                    file.asRequestBody(Files.probeContentType(file.toPath()).toMediaTypeOrNull())
+                )
+                .addFormDataPart("other_field", "other_field_value")
+                .build()
+        } else {
+            file.toUri()
+            MultipartBody.Builder()
+                .setType(MultipartBody.FORM)
+                .addFormDataPart(
+                    "file", file.name,
+                    file.asRequestBody(getMimeType(file.toUri()).toMediaTypeOrNull())
+                )
+                .addFormDataPart("other_field", "other_field_value")
+                .build()
+        }
+        val request: Request = Request.Builder()
+            .url(url)
+            .addHeader("User-Agent", "Mozilla/5.0")
+            .addHeader("Cookie", "PHPSESSID=123;MOBILE=123")
+            .post(body)
+            .build()
+        Log.d(tag, "Upload:enqueue:$url:${file.name}")
+        client.newCall(request).enqueue(object : Callback {
+            override fun onFailure(call: Call, e: IOException) {
+                Log.d(tag, "Upload:onFailure")
+                completion(false)
+            }
+
+            override fun onResponse(call: Call, response: Response) {
+                Log.d(tag, "Upload:onResponse:${response.code}")
+                completion(response.code == 200)
+            }
+        })
+    }
+
+    private fun getMimeType(uri: Uri): String {
+        val fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
+        MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+            fileExtension.lowercase(Locale.getDefault())
+        )?.let {
+            return it
+        }
+        return "text/plain"
+    }
+}

+ 9 - 2
cpaas-lite/src/main/java/io/nexilis/service/data/daos/BuddyDao.kt

@@ -5,6 +5,7 @@ import androidx.room.Dao
 import androidx.room.Insert
 import androidx.room.OnConflictStrategy
 import androidx.room.Query
+import androidx.room.Update
 import io.nexilis.service.data.entities.*
 
 @Dao
@@ -22,8 +23,14 @@ interface BuddyDao {
     @Insert(onConflict = OnConflictStrategy.REPLACE)
     suspend fun insert(entity: Buddy)
 
-    @Insert()
-    suspend fun insertMe(entity: Buddy)
+    @Update
+    suspend fun update(entity: Buddy)
+
+    @Query("update Buddy set image_id = :image where f_pin = :pin")
+    suspend fun updateImage(image: String, pin: String)
+
+    @Query("update Buddy set image_id = :image where type = '1'")
+    suspend fun updateImageMe(image: String)
 
     @Query("delete from Buddy")
     suspend fun deleteAll()

+ 25 - 0
cpaas-lite/src/main/java/io/nexilis/service/data/repositories/BuddyRepository.kt

@@ -4,6 +4,7 @@ import android.util.Log
 import androidx.lifecycle.LiveData
 import io.nexilis.service.Service
 import io.nexilis.service.core.Data
+import io.nexilis.service.core.Network
 import io.nexilis.service.core.toMD5
 import io.nexilis.service.data.daos.BuddyDao
 import io.nexilis.service.data.entities.Buddy
@@ -11,6 +12,7 @@ import io.nexilis.service.data.entities.MainEntity
 import io.nexilis.service.tag
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
+import java.io.File
 import javax.inject.Inject
 
 class BuddyRepository @Inject constructor(private val dao: BuddyDao) : Repository {
@@ -135,4 +137,27 @@ class BuddyRepository @Inject constructor(private val dao: BuddyDao) : Repositor
         }
     }
 
+    fun changeProfile(me: String, file: File, completion: (Boolean) -> Unit) {
+        Network().upload("https://digixplatform.com/uploader", file) {
+            if (it) {
+                Service.sendAsync(
+                    Data(
+                        code = "A003",
+                        status = System.nanoTime().toString(),
+                        f_pin = me,
+                        bodies = mutableMapOf(
+                            "A74" to file.name
+                        )
+                    )
+                ) { data ->
+                    if (data.isOk()) {
+                        dao.updateImageMe(image = file.name)
+                    }
+                    withContext(Dispatchers.Main) {
+                        completion(data.isOk())
+                    }
+                }
+            }
+        }
+    }
 }

+ 6 - 0
cpaas-lite/src/main/java/io/nexilis/service/data/viewmodels/BuddyViewModel.kt

@@ -8,6 +8,7 @@ import io.nexilis.service.data.entities.Buddy
 import io.nexilis.service.data.repositories.BuddyRepository
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
+import java.io.File
 import javax.inject.Inject
 
 @HiltViewModel
@@ -53,4 +54,9 @@ class BuddyViewModel @Inject constructor(private val repository: BuddyRepository
         viewModelScope.launch(Dispatchers.IO) {
             repository.suggestFriend(lastSequence, completion)
         }
+
+    fun changeProfile(me: String, file: File, completion: (Boolean) -> Unit) =
+        viewModelScope.launch(Dispatchers.IO) {
+        repository.changeProfile(me, file, completion)
+    }
 }

+ 6 - 0
cpaas-lite/src/main/res/xml/path_provider.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths>
+    <external-cache-path
+        name="my_images"
+        path="/" />
+</paths>