|
8 | 8 |
|
9 | 9 | package com.nextcloud.talk.utils |
10 | 10 |
|
11 | | -import android.app.Activity |
12 | 11 | import android.content.Intent |
13 | 12 | import android.graphics.Bitmap |
14 | 13 | import android.graphics.BitmapFactory |
15 | 14 | import android.net.Uri |
16 | 15 | import android.os.Bundle |
17 | 16 | import android.provider.MediaStore |
18 | 17 | import android.util.Log |
| 18 | +import android.widget.Toast |
| 19 | +import androidx.activity.ComponentActivity |
19 | 20 | import androidx.activity.result.ActivityResultLauncher |
| 21 | +import androidx.lifecycle.lifecycleScope |
20 | 22 | import androidx.core.content.FileProvider |
21 | 23 | import autodagger.AutoInjector |
22 | 24 | import com.github.dhaval2404.imagepicker.ImagePicker |
23 | 25 | import com.github.dhaval2404.imagepicker.constant.ImageProvider |
| 26 | +import com.nextcloud.talk.R |
24 | 27 | import com.nextcloud.talk.api.NcApi |
25 | 28 | import com.nextcloud.talk.application.NextcloudTalkApplication |
26 | 29 | import com.nextcloud.talk.data.user.model.User |
27 | 30 | import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity |
28 | 31 | import com.nextcloud.talk.utils.bundle.BundleKeys |
29 | 32 | import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil |
| 33 | +import kotlinx.coroutines.CoroutineScope |
| 34 | +import kotlinx.coroutines.Dispatchers |
| 35 | +import kotlinx.coroutines.Job |
| 36 | +import kotlinx.coroutines.launch |
| 37 | +import kotlinx.coroutines.withContext |
30 | 38 | import okhttp3.ResponseBody |
31 | 39 | import retrofit2.Call |
32 | 40 | import retrofit2.Callback |
33 | 41 | import retrofit2.Response |
34 | 42 | import java.io.File |
35 | 43 | import java.io.FileOutputStream |
36 | 44 | import java.io.IOException |
| 45 | +import java.io.InputStream |
37 | 46 | import javax.inject.Inject |
38 | 47 |
|
| 48 | +private const val FULL_QUALITY: Int = 100 |
| 49 | + |
| 50 | +/** |
| 51 | + * Launches [block] and closes [body] when the job completes, including when the scope is already cancelled |
| 52 | + * (e.g. the activity was destroyed) and [block] never starts. Closing twice is safe. |
| 53 | + */ |
| 54 | +internal fun CoroutineScope.launchClosing(body: ResponseBody?, block: suspend CoroutineScope.() -> Unit): Job { |
| 55 | + val job = launch(block = block) |
| 56 | + job.invokeOnCompletion { body?.close() } |
| 57 | + return job |
| 58 | +} |
| 59 | + |
| 60 | +/** |
| 61 | + * Decodes the avatar thumbnail and writes it as PNG. Blocking, call off the main thread. |
| 62 | + * Returns null and logs the cause if the body is missing, does not decode or cannot be written. |
| 63 | + */ |
| 64 | +internal fun decodeAvatarToFile( |
| 65 | + body: ResponseBody?, |
| 66 | + decode: (InputStream) -> Bitmap? = { BitmapFactory.decodeStream(it) }, |
| 67 | + createFile: () -> File |
| 68 | +): File? { |
| 69 | + val bitmap = body?.use { decode(it.byteStream()) } |
| 70 | + if (bitmap == null) { |
| 71 | + val cause = if (body == null) "request returned no body" else "could not be decoded" |
| 72 | + Log.e("PickImage", "Avatar thumbnail $cause") |
| 73 | + return null |
| 74 | + } |
| 75 | + return try { |
| 76 | + val file = createFile() |
| 77 | + val written = FileOutputStream(file).use { out -> |
| 78 | + bitmap.compress(Bitmap.CompressFormat.PNG, FULL_QUALITY, out) |
| 79 | + } |
| 80 | + if (!written) throw IOException("Bitmap.compress returned false") |
| 81 | + file |
| 82 | + } catch (e: IOException) { |
| 83 | + Log.e("PickImage", "Error saving the avatar image", e) |
| 84 | + null |
| 85 | + } |
| 86 | +} |
| 87 | + |
39 | 88 | @AutoInjector(NextcloudTalkApplication::class) |
40 | | -class PickImage(private val activity: Activity, private var currentUser: User?) { |
| 89 | +class PickImage(private val activity: ComponentActivity, private var currentUser: User?) { |
41 | 90 |
|
42 | 91 | @Inject |
43 | 92 | lateinit var ncApi: NcApi |
@@ -112,42 +161,28 @@ class PickImage(private val activity: Activity, private var currentUser: User?) |
112 | 161 | ) |
113 | 162 | downloadCall.enqueue(object : Callback<ResponseBody> { |
114 | 163 | override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) { |
115 | | - saveBitmapAndPassToImagePicker( |
116 | | - startImagePickerForResult, |
117 | | - BitmapFactory.decodeStream(response.body()!!.byteStream()) |
118 | | - ) |
| 164 | + val body = response.body() |
| 165 | + activity.lifecycleScope.launchClosing(body) { |
| 166 | + val file = withContext(Dispatchers.IO) { |
| 167 | + decodeAvatarToFile(body) { createTempFileForAvatar() } |
| 168 | + } |
| 169 | + if (file != null) { |
| 170 | + selectLocal(startImagePickerForResult, file) |
| 171 | + } else { |
| 172 | + showAvatarError() |
| 173 | + } |
| 174 | + } |
119 | 175 | } |
120 | 176 |
|
121 | 177 | override fun onFailure(call: Call<ResponseBody>, t: Throwable) { |
122 | | - // unused atm |
| 178 | + Log.e(TAG, "Failed to download the avatar thumbnail", t) |
| 179 | + showAvatarError() |
123 | 180 | } |
124 | 181 | }) |
125 | 182 | } |
126 | 183 |
|
127 | | - // only possible with API26 |
128 | | - private fun saveBitmapAndPassToImagePicker( |
129 | | - startImagePickerForResult: ActivityResultLauncher<Intent>, |
130 | | - bitmap: Bitmap |
131 | | - ) { |
132 | | - val file: File = saveBitmapToTempFile(bitmap) ?: return |
133 | | - selectLocal(startImagePickerForResult, file) |
134 | | - } |
135 | | - |
136 | | - private fun saveBitmapToTempFile(bitmap: Bitmap): File? { |
137 | | - try { |
138 | | - val file = createTempFileForAvatar() |
139 | | - try { |
140 | | - FileOutputStream(file).use { out -> |
141 | | - bitmap.compress(Bitmap.CompressFormat.PNG, FULL_QUALITY, out) |
142 | | - } |
143 | | - return file |
144 | | - } catch (e: IOException) { |
145 | | - Log.e(TAG, "Error compressing bitmap", e) |
146 | | - } |
147 | | - } catch (e: IOException) { |
148 | | - Log.e(TAG, "Error creating temporary avatar image", e) |
149 | | - } |
150 | | - return null |
| 184 | + private fun showAvatarError() { |
| 185 | + Toast.makeText(activity, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() |
151 | 186 | } |
152 | 187 |
|
153 | 188 | private fun createTempFileForAvatar(): File { |
@@ -195,7 +230,6 @@ class PickImage(private val activity: Activity, private var currentUser: User?) |
195 | 230 | private const val MAX_SIZE: Int = 1024 |
196 | 231 | private const val AVATAR_PATH = "photos/avatar.png" |
197 | 232 | private const val CAMERA_CAPTURE_PATH = "photos/camera_capture.jpg" |
198 | | - private const val FULL_QUALITY: Int = 100 |
199 | 233 | const val REQUEST_PERMISSION_CAMERA: Int = 1 |
200 | 234 | } |
201 | 235 | } |
0 commit comments