From f44c528b4f0c02d3e79517c411b92d8554d8473d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Kr=C3=BCger?= Date: Thu, 15 Dec 2022 16:30:28 +0100 Subject: [PATCH] Move image picker logic to new class 'PickImage' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a preparation to solve issue #2555 and centralize the image picker functionality. See: #2555 Signed-off-by: Tim Krüger --- .../nextcloud/talk/profile/ProfileActivity.kt | 88 ++++--- .../com/nextcloud/talk/utils/PickImage.kt | 217 ++++++++++++++++++ 2 files changed, 258 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/utils/PickImage.kt diff --git a/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt b/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt index aec79c469..783700885 100644 --- a/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt @@ -3,6 +3,8 @@ * * @author Tobias Kaminsky * @author Andy Scherzinger + * @author Tim Krüger + * Copyright (C) 2022 Tim Krüger * Copyright (C) 2022 Andy Scherzinger * Copyright (C) 2021 Tobias Kaminsky * @@ -28,7 +30,6 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.drawable.ColorDrawable import android.net.Uri -import android.os.Bundle import android.text.Editable import android.text.TextUtils import android.text.TextWatcher @@ -46,8 +47,6 @@ import androidx.recyclerview.widget.RecyclerView import autodagger.AutoInjector import com.github.dhaval2404.imagepicker.ImagePicker 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.activities.BaseActivity 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.UserProfileFieldsOverall 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.theme.ViewThemeUtils import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils 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_PREFIX 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.permissions.PlatformPermissionUtil import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable @@ -81,13 +77,7 @@ import io.reactivex.schedulers.Schedulers import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody -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 java.util.LinkedList import javax.inject.Inject @@ -102,15 +92,14 @@ class ProfileActivity : BaseActivity() { @Inject lateinit var userManager: UserManager - @Inject - lateinit var permissionUtil: PlatformPermissionUtil - private var currentUser: User? = null private var edit = false private var adapter: UserInfoAdapter? = null private var userInfo: UserProfileData? = null private var editableFields = ArrayList() + private lateinit var pickImage: PickImage + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) @@ -128,9 +117,11 @@ class ProfileActivity : BaseActivity() { binding.userinfoList.setItemViewCacheSize(DEFAULT_CACHE_SIZE) currentUser = userManager.currentUser.blockingGet() val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) - binding.avatarUpload.setOnClickListener { sendSelectLocalFileIntent() } - binding.avatarChoose.setOnClickListener { showBrowserScreen() } - binding.avatarCamera.setOnClickListener { checkPermissionAndTakePicture() } + + pickImage = PickImage(this, currentUser) + binding.avatarUpload.setOnClickListener { pickImage.selectLocal() } + binding.avatarChoose.setOnClickListener { pickImage.selectRemote() } + binding.avatarCamera.setOnClickListener { pickImage.takePicture() } binding.avatarDelete.setOnClickListener { ncApi.deleteAvatar( credentials, @@ -541,7 +532,7 @@ class ProfileActivity : BaseActivity() { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_PERMISSION_CAMERA) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - takePictureForAvatar() + pickImage.takePicture() } else { Toast .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?) { super.onActivityResult(requestCode, resultCode, data) - if (resultCode == Activity.RESULT_OK) { - 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") + when (resultCode) { + Activity.RESULT_OK -> { + pickImage.handleActivityResult( + requestCode, + resultCode, + data + ) { uploadAvatar(it.toFile()) } + } + ImagePicker.RESULT_ERROR -> { + Toast.makeText(this, getError(data), Toast.LENGTH_SHORT).show() + } + else -> { + Log.i(TAG, "Task Cancelled") } - } 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?) { @@ -865,15 +866,8 @@ class ProfileActivity : BaseActivity() { companion object { 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_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 MEDIUM_EMPHASIS_ALPHA: Float = 0.6f } diff --git a/app/src/main/java/com/nextcloud/talk/utils/PickImage.kt b/app/src/main/java/com/nextcloud/talk/utils/PickImage.kt new file mode 100644 index 000000000..2da9426eb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/PickImage.kt @@ -0,0 +1,217 @@ +/* + * Nextcloud Talk application + * + * @author Tim Krüger + * Copyright (C) 2022 Tim Krüger + * + * 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 . + */ +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 { + override fun onResponse(call: Call, response: Response) { + saveBitmapAndPassToImagePicker(BitmapFactory.decodeStream(response.body()!!.byteStream())) + } + + override fun onFailure(call: Call, 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 + } +}