Browse Source

WIP: upload profile picture from camera

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>
Álvaro Brey 2 years ago
parent
commit
cb2ee730fa

+ 3 - 1
app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt

@@ -57,6 +57,7 @@ import com.nextcloud.talk.dagger.modules.ContextModule
 import com.nextcloud.talk.dagger.modules.DatabaseModule
 import com.nextcloud.talk.dagger.modules.DatabaseModule
 import com.nextcloud.talk.dagger.modules.RepositoryModule
 import com.nextcloud.talk.dagger.modules.RepositoryModule
 import com.nextcloud.talk.dagger.modules.RestModule
 import com.nextcloud.talk.dagger.modules.RestModule
+import com.nextcloud.talk.dagger.modules.UtilsModule
 import com.nextcloud.talk.dagger.modules.ViewModelModule
 import com.nextcloud.talk.dagger.modules.ViewModelModule
 import com.nextcloud.talk.jobs.AccountRemovalWorker
 import com.nextcloud.talk.jobs.AccountRemovalWorker
 import com.nextcloud.talk.jobs.CapabilitiesWorker
 import com.nextcloud.talk.jobs.CapabilitiesWorker
@@ -99,7 +100,8 @@ import javax.inject.Singleton
         UserModule::class,
         UserModule::class,
         ArbitraryStorageModule::class,
         ArbitraryStorageModule::class,
         ViewModelModule::class,
         ViewModelModule::class,
-        RepositoryModule::class
+        RepositoryModule::class,
+        UtilsModule::class
     ]
     ]
 )
 )
 @Singleton
 @Singleton

+ 5 - 12
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -169,6 +169,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
 import com.nextcloud.talk.utils.database.user.UserUtils
 import com.nextcloud.talk.utils.database.user.UserUtils
+import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
 import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
 import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
 import com.nextcloud.talk.utils.text.Spans
 import com.nextcloud.talk.utils.text.Spans
 import com.nextcloud.talk.webrtc.MagicWebSocketInstance
 import com.nextcloud.talk.webrtc.MagicWebSocketInstance
@@ -231,6 +232,9 @@ class ChatController(args: Bundle) :
     @JvmField
     @JvmField
     var eventBus: EventBus? = null
     var eventBus: EventBus? = null
 
 
+    @Inject
+    lateinit var permissionUtil: PlatformPermissionUtil
+
     val disposableList = ArrayList<Disposable>()
     val disposableList = ArrayList<Disposable>()
 
 
     var roomToken: String? = null
     var roomToken: String? = null
@@ -1111,17 +1115,6 @@ class ChatController(args: Bundle) :
         }
         }
     }
     }
 
 
-    private fun isCameraPermissionGranted(): Boolean {
-        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            return PermissionChecker.checkSelfPermission(
-                context!!,
-                Manifest.permission.CAMERA
-            ) == PermissionChecker.PERMISSION_GRANTED
-        } else {
-            true
-        }
-    }
-
     private fun startAudioRecording(file: String) {
     private fun startAudioRecording(file: String) {
         binding.messageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime()
         binding.messageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime()
         binding.messageInputView.audioRecordDuration.start()
         binding.messageInputView.audioRecordDuration.start()
@@ -3128,7 +3121,7 @@ class ChatController(args: Bundle) :
     }
     }
 
 
     fun sendPictureFromCamIntent() {
     fun sendPictureFromCamIntent() {
-        if (!isCameraPermissionGranted()) {
+        if (!permissionUtil.isCameraPermissionGranted()) {
             requestCameraPermissions()
             requestCameraPermissions()
         } else {
         } else {
             startActivityForResult(TakePhotoActivity.createIntent(context!!), REQUEST_CODE_PICK_CAMERA)
             startActivityForResult(TakePhotoActivity.createIntent(context!!), REQUEST_CODE_PICK_CAMERA)

+ 79 - 20
app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt

@@ -22,7 +22,9 @@
 package com.nextcloud.talk.controllers
 package com.nextcloud.talk.controllers
 
 
 import android.app.Activity
 import android.app.Activity
+import android.content.ActivityNotFoundException
 import android.content.Intent
 import android.content.Intent
+import android.content.pm.PackageManager
 import android.content.res.ColorStateList
 import android.content.res.ColorStateList
 import android.graphics.Bitmap
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.graphics.BitmapFactory
@@ -52,6 +54,7 @@ import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError
 import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getFile
 import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getFile
 import com.github.dhaval2404.imagepicker.ImagePicker.Companion.with
 import com.github.dhaval2404.imagepicker.ImagePicker.Companion.with
 import com.nextcloud.talk.R
 import com.nextcloud.talk.R
+import com.nextcloud.talk.activities.TakePhotoActivity
 import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
@@ -76,6 +79,7 @@ import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX
 import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC
 import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MIME_TYPE_FILTER
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MIME_TYPE_FILTER
 import com.nextcloud.talk.utils.database.user.UserUtils
 import com.nextcloud.talk.utils.database.user.UserUtils
+import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
 import io.reactivex.Observer
 import io.reactivex.Observer
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
 import io.reactivex.disposables.Disposable
@@ -95,6 +99,7 @@ import java.util.Locale
 import javax.inject.Inject
 import javax.inject.Inject
 
 
 @AutoInjector(NextcloudTalkApplication::class)
 @AutoInjector(NextcloudTalkApplication::class)
+@Suppress("Detekt.TooManyFunctions")
 class ProfileController : NewBaseController(R.layout.controller_profile) {
 class ProfileController : NewBaseController(R.layout.controller_profile) {
     private val binding: ControllerProfileBinding by viewBinding(ControllerProfileBinding::bind)
     private val binding: ControllerProfileBinding by viewBinding(ControllerProfileBinding::bind)
 
 
@@ -104,6 +109,9 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
     @Inject
     @Inject
     lateinit var userUtils: UserUtils
     lateinit var userUtils: UserUtils
 
 
+    @Inject
+    lateinit var permissionUtil: PlatformPermissionUtil
+
     private var currentUser: UserEntity? = null
     private var currentUser: UserEntity? = null
     private var edit = false
     private var edit = false
     private var adapter: UserInfoAdapter? = null
     private var adapter: UserInfoAdapter? = null
@@ -197,6 +205,7 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
         val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
         val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
         binding.avatarUpload.setOnClickListener { sendSelectLocalFileIntent() }
         binding.avatarUpload.setOnClickListener { sendSelectLocalFileIntent() }
         binding.avatarChoose.setOnClickListener { showBrowserScreen() }
         binding.avatarChoose.setOnClickListener { showBrowserScreen() }
+        binding.avatarCamera.setOnClickListener { checkPermissionAndTakePicture() }
         binding.avatarDelete.setOnClickListener {
         binding.avatarDelete.setOnClickListener {
             ncApi.deleteAvatar(
             ncApi.deleteAvatar(
                 credentials,
                 credentials,
@@ -493,7 +502,23 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
         startActivityForResult(avatarIntent, REQUEST_CODE_SELECT_REMOTE_FILES)
         startActivityForResult(avatarIntent, REQUEST_CODE_SELECT_REMOTE_FILES)
     }
     }
 
 
-    fun handleAvatar(remotePath: String?) {
+    private fun checkPermissionAndTakePicture() {
+        if (permissionUtil.isCameraPermissionGranted()) {
+            takePictureForAvatar()
+        } else {
+            requestPermissions(arrayOf(android.Manifest.permission.CAMERA), REQUEST_PERMISSION_CAMERA)
+        }
+    }
+
+    private fun takePictureForAvatar() {
+        try {
+            startActivityForResult(TakePhotoActivity.createIntent(context!!), REQUEST_CODE_TAKE_PICTURE)
+        } catch (e: ActivityNotFoundException) {
+            // TODO
+        }
+    }
+
+    private fun handleAvatar(remotePath: String?) {
         val uri = currentUser!!.baseUrl + "/index.php/apps/files/api/v1/thumbnail/512/512/" +
         val uri = currentUser!!.baseUrl + "/index.php/apps/files/api/v1/thumbnail/512/512/" +
             Uri.encode(remotePath, "/")
             Uri.encode(remotePath, "/")
         val downloadCall = ncApi.downloadResizedImage(
         val downloadCall = ncApi.downloadResizedImage(
@@ -511,30 +536,54 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
         })
         })
     }
     }
 
 
+    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+        if (requestCode == REQUEST_PERMISSION_CAMERA) {
+            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                takePictureForAvatar()
+            } else {
+                Toast
+                    .makeText(context, context?.getString(R.string.take_photo_permission), Toast.LENGTH_LONG)
+                    .show()
+            }
+        }
+    }
+
     // only possible with API26
     // only possible with API26
     private fun saveBitmapAndPassToImagePicker(bitmap: Bitmap) {
     private fun saveBitmapAndPassToImagePicker(bitmap: Bitmap) {
-        var file: File? = null
+        val file: File = saveBitmapToTempFile(bitmap) ?: return
+        openImageWithPicker(file)
+    }
+
+    private fun saveBitmapToTempFile(bitmap: Bitmap): File? {
         try {
         try {
-            FileUtils.removeTempCacheFile(
-                this.context!!,
-                AVATAR_PATH
-            )
-            file = FileUtils.getTempCacheFile(
-                this.context!!,
-                AVATAR_PATH
-            )
+            val file = createTempFileForAvatar()
             try {
             try {
-                FileOutputStream(file).use { out -> bitmap.compress(Bitmap.CompressFormat.PNG, FULL_QUALITY, out) }
+                FileOutputStream(file).use { out ->
+                    bitmap.compress(Bitmap.CompressFormat.PNG, FULL_QUALITY, out)
+                }
+                return file
             } catch (e: IOException) {
             } catch (e: IOException) {
                 Log.e(TAG, "Error compressing bitmap", e)
                 Log.e(TAG, "Error compressing bitmap", e)
             }
             }
         } catch (e: IOException) {
         } catch (e: IOException) {
             Log.e(TAG, "Error creating temporary avatar image", e)
             Log.e(TAG, "Error creating temporary avatar image", e)
         }
         }
-        if (file == null) {
-            // TODO exception
-            return
-        }
+        return null
+    }
+
+    private fun createTempFileForAvatar(): File? {
+        FileUtils.removeTempCacheFile(
+            this.context!!,
+            AVATAR_PATH
+        )
+        return FileUtils.getTempCacheFile(
+            context!!,
+            AVATAR_PATH
+        )
+    }
+
+    private fun openImageWithPicker(file: File) {
         val intent = with(activity!!)
         val intent = with(activity!!)
             .fileOnly()
             .fileOnly()
             .crop()
             .crop()
@@ -546,20 +595,24 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
         startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER)
         startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER)
     }
     }
 
 
-    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
         if (resultCode == Activity.RESULT_OK) {
         if (resultCode == Activity.RESULT_OK) {
             if (requestCode == REQUEST_CODE_IMAGE_PICKER) {
             if (requestCode == REQUEST_CODE_IMAGE_PICKER) {
-                uploadAvatar(getFile(intent))
+                uploadAvatar(getFile(data))
             } else if (requestCode == REQUEST_CODE_SELECT_REMOTE_FILES) {
             } else if (requestCode == REQUEST_CODE_SELECT_REMOTE_FILES) {
-                val pathList = intent?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
+                val pathList = data?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
                 if (pathList?.size!! >= 1) {
                 if (pathList?.size!! >= 1) {
                     handleAvatar(pathList[0])
                     handleAvatar(pathList[0])
                 }
                 }
+            } else if (requestCode == REQUEST_CODE_TAKE_PICTURE) {
+                data?.data?.path?.let {
+                    openImageWithPicker(File(it))
+                }
             } else {
             } else {
                 Log.w(TAG, "Unknown intent request code")
                 Log.w(TAG, "Unknown intent request code")
             }
             }
         } else if (resultCode == ImagePicker.RESULT_ERROR) {
         } else if (resultCode == ImagePicker.RESULT_ERROR) {
-            Toast.makeText(activity, getError(intent), Toast.LENGTH_SHORT).show()
+            Toast.makeText(activity, getError(data), Toast.LENGTH_SHORT).show()
         } else {
         } else {
             Log.i(TAG, "Task Cancelled")
             Log.i(TAG, "Task Cancelled")
         }
         }
@@ -595,7 +648,11 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
                 }
                 }
 
 
                 override fun onError(e: Throwable) {
                 override fun onError(e: Throwable) {
-                    Toast.makeText(applicationContext, "Error", Toast.LENGTH_LONG).show()
+                    Toast.makeText(
+                        applicationContext, context!!.getString(R.string.default_error_msg),
+                        Toast
+                            .LENGTH_LONG
+                    ).show()
                     Log.e(TAG, "Error uploading avatar", e)
                     Log.e(TAG, "Error uploading avatar", e)
                 }
                 }
 
 
@@ -823,6 +880,8 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
         private const val DEFAULT_RETRIES: Long = 3
         private const val DEFAULT_RETRIES: Long = 3
         private const val MAX_SIZE: Int = 1024
         private const val MAX_SIZE: Int = 1024
         private const val REQUEST_CODE_IMAGE_PICKER: Int = 1
         private const val REQUEST_CODE_IMAGE_PICKER: Int = 1
+        private const val REQUEST_CODE_TAKE_PICTURE: Int = 2
+        private const val REQUEST_PERMISSION_CAMERA: Int = 1
         private const val FULL_QUALITY: Int = 100
         private const val FULL_QUALITY: Int = 100
         private const val HIGH_EMPHASIS_ALPHA: Float = 0.87f
         private const val HIGH_EMPHASIS_ALPHA: Float = 0.87f
         private const val MEDIUM_EMPHASIS_ALPHA: Float = 0.6f
         private const val MEDIUM_EMPHASIS_ALPHA: Float = 0.6f

+ 38 - 0
app/src/main/java/com/nextcloud/talk/dagger/modules/UtilsModule.kt

@@ -0,0 +1,38 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.dagger.modules
+
+import android.content.Context
+import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
+import com.nextcloud.talk.utils.permissions.PlatformPermissionUtilImpl
+import dagger.Module
+import dagger.Provides
+import dagger.Reusable
+
+@Module(includes = [ContextModule::class])
+class UtilsModule {
+    @Provides
+    @Reusable
+    fun providePermissionUtil(context: Context): PlatformPermissionUtil {
+        return PlatformPermissionUtilImpl(context)
+    }
+}

+ 26 - 0
app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtil.kt

@@ -0,0 +1,26 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils.permissions
+
+interface PlatformPermissionUtil {
+    fun isCameraPermissionGranted(): Boolean
+}

+ 41 - 0
app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt

@@ -0,0 +1,41 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils.permissions
+
+import android.Manifest
+import android.content.Context
+import android.os.Build
+import androidx.core.content.PermissionChecker
+
+class PlatformPermissionUtilImpl(private val context: Context) : PlatformPermissionUtil {
+
+    override fun isCameraPermissionGranted(): Boolean {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return PermissionChecker.checkSelfPermission(
+                context,
+                Manifest.permission.CAMERA
+            ) == PermissionChecker.PERMISSION_GRANTED
+        } else {
+            true
+        }
+    }
+}

+ 11 - 0
app/src/main/res/layout/controller_profile.xml

@@ -98,6 +98,17 @@
                 android:src="@drawable/ic_mimetype_folder"
                 android:src="@drawable/ic_mimetype_folder"
                 app:tint="@color/colorPrimary" />
                 app:tint="@color/colorPrimary" />
 
 
+            <ImageButton
+                android:id="@+id/avatar_camera"
+                android:layout_width="@dimen/min_size_clickable_area"
+                android:layout_height="@dimen/min_size_clickable_area"
+                android:layout_marginLeft="@dimen/standard_half_margin"
+                android:layout_marginRight="@dimen/standard_half_margin"
+                android:background="@drawable/round_corner"
+                android:contentDescription="@string/set_avatar_from_camera"
+                android:src="@drawable/ic_baseline_photo_camera_24"
+                app:tint="@color/colorPrimary" />
+
             <ImageButton
             <ImageButton
                 android:id="@+id/avatar_delete"
                 android:id="@+id/avatar_delete"
                 android:layout_width="@dimen/min_size_clickable_area"
                 android:layout_width="@dimen/min_size_clickable_area"

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -532,5 +532,6 @@
     <string name="reactions_tab_all">All</string>
     <string name="reactions_tab_all">All</string>
     <string name="send_without_notification">Send without notification</string>
     <string name="send_without_notification">Send without notification</string>
     <string name="call_without_notification">Call without notification</string>
     <string name="call_without_notification">Call without notification</string>
+    <string name="set_avatar_from_camera">Set avatar from camera</string>
 
 
 </resources>
 </resources>