Move image picker logic to new class 'PickImage'

This is a preparation to solve issue #2555 and centralize the image picker
functionality.

See: #2555

Signed-off-by: Tim Krüger <t@timkrueger.me>
This commit is contained in:
Tim Krüger 2022-12-15 16:30:28 +01:00 committed by Marcel Hibbe
parent c9739e69af
commit f44c528b4f
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
2 changed files with 258 additions and 47 deletions

View File

@ -3,6 +3,8 @@
* *
* @author Tobias Kaminsky * @author Tobias Kaminsky
* @author Andy Scherzinger * @author Andy Scherzinger
* @author Tim Krüger
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de> * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com> * Copyright (C) 2021 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
* *
@ -28,7 +30,6 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextUtils import android.text.TextUtils
import android.text.TextWatcher import android.text.TextWatcher
@ -46,8 +47,6 @@ import androidx.recyclerview.widget.RecyclerView
import autodagger.AutoInjector import autodagger.AutoInjector
import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePicker
import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError
import com.github.dhaval2404.imagepicker.ImagePicker.Companion.with
import com.github.dhaval2404.imagepicker.constant.ImageProvider
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.activities.TakePhotoActivity import com.nextcloud.talk.activities.TakePhotoActivity
@ -61,19 +60,16 @@ import com.nextcloud.talk.models.json.userprofile.Scope
import com.nextcloud.talk.models.json.userprofile.UserProfileData import com.nextcloud.talk.models.json.userprofile.UserProfileData
import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
import com.nextcloud.talk.ui.dialog.ScopeDialog import com.nextcloud.talk.ui.dialog.ScopeDialog
import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.FileUtils
import com.nextcloud.talk.utils.Mimetype.IMAGE_JPG import com.nextcloud.talk.utils.Mimetype.IMAGE_JPG
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.PickImage
import com.nextcloud.talk.utils.PickImage.Companion.REQUEST_PERMISSION_CAMERA
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
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
@ -81,13 +77,7 @@ import io.reactivex.schedulers.Schedulers
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.LinkedList import java.util.LinkedList
import javax.inject.Inject import javax.inject.Inject
@ -102,15 +92,14 @@ class ProfileActivity : BaseActivity() {
@Inject @Inject
lateinit var userManager: UserManager lateinit var userManager: UserManager
@Inject
lateinit var permissionUtil: PlatformPermissionUtil
private var currentUser: User? = null private var currentUser: User? = null
private var edit = false private var edit = false
private var adapter: UserInfoAdapter? = null private var adapter: UserInfoAdapter? = null
private var userInfo: UserProfileData? = null private var userInfo: UserProfileData? = null
private var editableFields = ArrayList<String>() private var editableFields = ArrayList<String>()
private lateinit var pickImage: PickImage
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
@ -128,9 +117,11 @@ class ProfileActivity : BaseActivity() {
binding.userinfoList.setItemViewCacheSize(DEFAULT_CACHE_SIZE) binding.userinfoList.setItemViewCacheSize(DEFAULT_CACHE_SIZE)
currentUser = userManager.currentUser.blockingGet() currentUser = userManager.currentUser.blockingGet()
val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
binding.avatarUpload.setOnClickListener { sendSelectLocalFileIntent() }
binding.avatarChoose.setOnClickListener { showBrowserScreen() } pickImage = PickImage(this, currentUser)
binding.avatarCamera.setOnClickListener { checkPermissionAndTakePicture() } binding.avatarUpload.setOnClickListener { pickImage.selectLocal() }
binding.avatarChoose.setOnClickListener { pickImage.selectRemote() }
binding.avatarCamera.setOnClickListener { pickImage.takePicture() }
binding.avatarDelete.setOnClickListener { binding.avatarDelete.setOnClickListener {
ncApi.deleteAvatar( ncApi.deleteAvatar(
credentials, credentials,
@ -541,7 +532,7 @@ class ProfileActivity : BaseActivity() {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_PERMISSION_CAMERA) { if (requestCode == REQUEST_PERMISSION_CAMERA) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
takePictureForAvatar() pickImage.takePicture()
} else { } else {
Toast Toast
.makeText(context, context.getString(R.string.take_photo_permission), Toast.LENGTH_LONG) .makeText(context, context.getString(R.string.take_photo_permission), Toast.LENGTH_LONG)
@ -597,27 +588,37 @@ class ProfileActivity : BaseActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) { when (resultCode) {
if (requestCode == REQUEST_CODE_IMAGE_PICKER) { Activity.RESULT_OK -> {
val uri: Uri = data?.data!! pickImage.handleActivityResult(
uploadAvatar(uri.toFile()) requestCode,
} else if (requestCode == REQUEST_CODE_SELECT_REMOTE_FILES) { resultCode,
val pathList = data?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS) data
if (pathList?.size!! >= 1) { ) { uploadAvatar(it.toFile()) }
handleAvatar(pathList[0]) }
} ImagePicker.RESULT_ERROR -> {
} else if (requestCode == REQUEST_CODE_TAKE_PICTURE) { Toast.makeText(this, getError(data), Toast.LENGTH_SHORT).show()
data?.data?.path?.let { }
openImageWithPicker(File(it)) else -> {
} Log.i(TAG, "Task Cancelled")
} else {
Log.w(TAG, "Unknown intent request code")
} }
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(this, getError(data), Toast.LENGTH_SHORT).show()
} else {
Log.i(TAG, "Task Cancelled")
} }
// if (requestCode == REQUEST_CODE_IMAGE_PICKER) {
// val uri: Uri = data?.data!!
// uploadAvatar(uri.toFile())
// } else if (requestCode == REQUEST_CODE_SELECT_REMOTE_FILES) {
// val pathList = data?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
// if (pathList?.size!! >= 1) {
// handleAvatar(pathList[0])
// }
// } else if (requestCode == REQUEST_CODE_TAKE_PICTURE) {
// data?.data?.path?.let {
// openImageWithPicker(File(it))
// }
// } else {
// Log.w(TAG, "Unknown intent request code")
// }
} }
private fun uploadAvatar(file: File?) { private fun uploadAvatar(file: File?) {
@ -865,15 +866,8 @@ class ProfileActivity : BaseActivity() {
companion object { companion object {
private const val TAG: String = "ProfileController" private const val TAG: String = "ProfileController"
private const val AVATAR_PATH = "photos/avatar.png"
private const val REQUEST_CODE_SELECT_REMOTE_FILES = 22
private const val DEFAULT_CACHE_SIZE: Int = 20 private const val DEFAULT_CACHE_SIZE: Int = 20
private const val DEFAULT_RETRIES: Long = 3 private const val DEFAULT_RETRIES: Long = 3
private const val MAX_SIZE: Int = 1024
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 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,217 @@
/*
* Nextcloud Talk application
*
* @author Tim Krüger
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.utils
import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.util.Log
import autodagger.AutoInjector
import com.github.dhaval2404.imagepicker.ImagePicker
import com.github.dhaval2404.imagepicker.constant.ImageProvider
import com.nextcloud.talk.activities.TakePhotoActivity
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.controllers.base.BaseController
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class PickImage(
private val controller: BaseController,
private var currentUser: User?
) {
@Inject
lateinit var ncApi: NcApi
@Inject
lateinit var permissionUtil: PlatformPermissionUtil
init {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
}
fun selectLocal() {
val activity = controller.activity
if (activity != null) {
ImagePicker.Companion.with(activity)
.provider(ImageProvider.GALLERY)
.crop()
.cropSquare()
.compress(MAX_SIZE)
.maxResultSize(MAX_SIZE, MAX_SIZE)
.createIntent { intent -> controller.startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER) }
}
}
private fun selectLocal(file: File) {
val activity = controller.activity
if (activity != null) {
ImagePicker.Companion.with(activity)
.provider(ImageProvider.URI)
.crop()
.cropSquare()
.compress(MAX_SIZE)
.maxResultSize(MAX_SIZE, MAX_SIZE)
.setUri(Uri.fromFile(file))
.createIntent { intent -> controller.startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER) }
}
}
fun selectRemote() {
val activity = controller.activity
if (activity != null) {
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_MIME_TYPE_FILTER, Mimetype.IMAGE_PREFIX)
val avatarIntent = Intent(activity, RemoteFileBrowserActivity::class.java)
avatarIntent.putExtras(bundle)
controller.startActivityForResult(avatarIntent, REQUEST_CODE_SELECT_REMOTE_FILES)
}
}
fun takePicture() {
if (permissionUtil.isCameraPermissionGranted()) {
controller.startActivityForResult(
TakePhotoActivity.createIntent(controller.context),
REQUEST_CODE_TAKE_PICTURE
)
} else {
controller.requestPermissions(
arrayOf(android.Manifest.permission.CAMERA),
REQUEST_PERMISSION_CAMERA
)
}
}
private fun handleAvatar(remotePath: String?) {
val uri = currentUser!!.baseUrl + "/index.php/apps/files/api/v1/thumbnail/512/512/" +
Uri.encode(remotePath, "/")
val downloadCall = ncApi.downloadResizedImage(
ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
uri
)
downloadCall.enqueue(object : Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
saveBitmapAndPassToImagePicker(BitmapFactory.decodeStream(response.body()!!.byteStream()))
}
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
// unused atm
}
})
}
// only possible with API26
private fun saveBitmapAndPassToImagePicker(bitmap: Bitmap) {
val file: File = saveBitmapToTempFile(bitmap) ?: return
selectLocal(file)
}
private fun saveBitmapToTempFile(bitmap: Bitmap): File? {
try {
val file = createTempFileForAvatar()
try {
FileOutputStream(file).use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, FULL_QUALITY, out)
}
return file
} catch (e: IOException) {
Log.e(TAG, "Error compressing bitmap", e)
}
} catch (e: IOException) {
Log.e(TAG, "Error creating temporary avatar image", e)
}
return null
}
private fun createTempFileForAvatar(): File {
FileUtils.removeTempCacheFile(
controller.context,
AVATAR_PATH
)
return FileUtils.getTempCacheFile(
controller.context,
AVATAR_PATH
)
}
fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?, handleImage: (uri: Uri) -> Unit) {
if (resultCode != Activity.RESULT_OK) {
Log.w(
TAG,
"Check result code before calling 'PickImage#handleActivtyResult'. It should be ${
Activity
.RESULT_OK
}, but it is $resultCode!"
)
return
}
when (requestCode) {
REQUEST_CODE_IMAGE_PICKER -> {
val uri: Uri = data?.data!!
handleImage(uri)
}
REQUEST_CODE_SELECT_REMOTE_FILES -> {
val pathList = data?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
if (pathList?.size!! >= 1) {
handleAvatar(pathList[0])
}
}
REQUEST_CODE_TAKE_PICTURE -> {
data?.data?.path?.let {
selectLocal(File(it))
}
}
else -> {
Log.w(TAG, "Unknown intent request code")
}
}
}
companion object {
private const val TAG: String = "PickImage"
private const val MAX_SIZE: Int = 1024
private const val AVATAR_PATH = "photos/avatar.png"
private const val FULL_QUALITY: Int = 100
const val REQUEST_CODE_IMAGE_PICKER: Int = 1
const val REQUEST_CODE_TAKE_PICTURE: Int = 2
const val REQUEST_PERMISSION_CAMERA: Int = 1
const val REQUEST_CODE_SELECT_REMOTE_FILES = 22
}
}