Merge pull request #2172 from nextcloud/feature/788/upload-avatar-from-camera

Upload profile picture from camera
This commit is contained in:
Andy Scherzinger 2022-07-07 13:56:56 +02:00 committed by GitHub
commit 1577a99567
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 203 additions and 38 deletions

View File

@ -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

View File

@ -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)

View File

@ -23,6 +23,7 @@ package com.nextcloud.talk.controllers
import android.app.Activity import android.app.Activity
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 +53,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,13 +78,14 @@ 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
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
@ -91,10 +94,10 @@ import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.util.LinkedList import java.util.LinkedList
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 +107,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 +203,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 +500,19 @@ 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() {
startActivityForResult(TakePhotoActivity.createIntent(context), REQUEST_CODE_TAKE_PICTURE)
}
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 +530,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( val file = createTempFileForAvatar()
this.context!!,
AVATAR_PATH
)
file = FileUtils.getTempCacheFile(
this.context!!,
AVATAR_PATH
)
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) { return null
// TODO exception }
return
} 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 +589,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")
} }
@ -570,11 +617,11 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
builder.setType(MultipartBody.FORM) builder.setType(MultipartBody.FORM)
builder.addFormDataPart( builder.addFormDataPart(
"files[]", file!!.name, "files[]", file!!.name,
RequestBody.create(IMAGE_PREFIX_GENERIC.toMediaTypeOrNull(), file) file.asRequestBody(IMAGE_PREFIX_GENERIC.toMediaTypeOrNull())
) )
val filePart: MultipartBody.Part = MultipartBody.Part.createFormData( val filePart: MultipartBody.Part = MultipartBody.Part.createFormData(
"files[]", file.name, "files[]", file.name,
RequestBody.create(IMAGE_JPG.toMediaTypeOrNull(), file) file.asRequestBody(IMAGE_JPG.toMediaTypeOrNull())
) )
// upload file // upload file
@ -595,7 +642,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)
} }
@ -706,7 +757,7 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
) )
} }
if (controller.edit && if (controller.edit &&
controller.editableFields.contains(item.field.toString().toLowerCase(Locale.ROOT)) controller.editableFields.contains(item.field.toString().lowercase())
) { ) {
holder.binding.userInfoEditText.isEnabled = true holder.binding.userInfoEditText.isEnabled = true
holder.binding.userInfoEditText.isFocusableInTouchMode = true holder.binding.userInfoEditText.isFocusableInTouchMode = true
@ -823,6 +874,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

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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"

View File

@ -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>