comparison app/src/main/java/com/bartsent/simpleresizer/EditImage.kt @ 42:45e4df5226c0

Shares, but creates multiple versions of cruft file.
author David Barts <n5jrn@me.com>
date Sat, 10 Apr 2021 09:02:44 -0700
parents cfb19d4ccf78
children 9cb9bb5da247
comparison
equal deleted inserted replaced
41:9231f1a41a59 42:45e4df5226c0
27 import androidx.lifecycle.ViewModelProvider 27 import androidx.lifecycle.ViewModelProvider
28 import androidx.preference.PreferenceManager 28 import androidx.preference.PreferenceManager
29 import com.bartsent.simpleresizer.databinding.ActivityEditImageBinding 29 import com.bartsent.simpleresizer.databinding.ActivityEditImageBinding
30 import com.bartsent.simpleresizer.lib.ThreadPools 30 import com.bartsent.simpleresizer.lib.ThreadPools
31 import com.bartsent.simpleresizer.lib.getScaledInstance 31 import com.bartsent.simpleresizer.lib.getScaledInstance
32 import com.google.android.material.floatingactionbutton.FloatingActionButton
32 import java.io.File 33 import java.io.File
33 import java.io.IOException 34 import java.io.IOException
34 import java.util.concurrent.Callable 35 import java.util.concurrent.Callable
35 import java.util.concurrent.Future 36 import java.util.concurrent.Future
36 37
37 class EditImage : AppCompatActivity() { 38 class EditImage : AppCompatActivity() {
38 class State: ViewModel() { 39 class State: ViewModel() {
39 var uri: Uri? = null 40 var uri: Uri? = null
40 var bitmap: Bitmap? = null 41 var bitmap: Bitmap? = null
41 var reader: Future<Unit>? = null 42 var reader: Future<Unit>? = null
43 var permissionsCallback: (() -> Unit)? = null
44 var sharable: Boolean = false
42 } 45 }
43 private lateinit var viewModel: State 46 private lateinit var viewModel: State
44 47
45 private val STDDIMS = arrayOf<Int>(1600, 1280, 1024, 800, 640, 512, 400, 320).apply { 48 private val STDDIMS = arrayOf<Int>(1600, 1280, 1024, 800, 640, 512, 400, 320).apply {
46 sort() 49 sort()
47 } 50 }
51
52 private val IMAGE_TO_SEND = "sent_image.jpg"
48 53
49 private lateinit var binding: ActivityEditImageBinding 54 private lateinit var binding: ActivityEditImageBinding
50 55
51 override fun onCreate(savedInstanceState: Bundle?) { 56 override fun onCreate(savedInstanceState: Bundle?) {
52 super.onCreate(savedInstanceState) 57 super.onCreate(savedInstanceState)
130 } 135 }
131 136
132 // User has opened a new image. 137 // User has opened a new image.
133 if (imageUri != viewModel.uri) { 138 if (imageUri != viewModel.uri) {
134 viewModel.uri = imageUri 139 viewModel.uri = imageUri
140 makeMundane()
135 binding.progressBar.visibility = ProgressBar.VISIBLE 141 binding.progressBar.visibility = ProgressBar.VISIBLE
136 viewModel.reader = ThreadPools.WORKERS.submit(Callable<Unit> { 142 viewModel.reader = ThreadPools.WORKERS.submit(Callable<Unit> {
137 val newBitmap = contentResolver.openInputStream(imageUri).use { 143 val newBitmap = contentResolver.openInputStream(imageUri).use {
138 BitmapFactory.decodeStream(it) 144 BitmapFactory.decodeStream(it)
139 } 145 }
151 157
152 // Rotation (of the phone). 158 // Rotation (of the phone).
153 val oldBitmap = viewModel.bitmap 159 val oldBitmap = viewModel.bitmap
154 if (oldBitmap != null) 160 if (oldBitmap != null)
155 setImage(oldBitmap) 161 setImage(oldBitmap)
162 binding.fabulous.visibility = if (viewModel.sharable) FloatingActionButton.VISIBLE else FloatingActionButton.GONE
156 } 163 }
157 164
158 override fun onDestroy() { 165 override fun onDestroy() {
159 // Read tasks may get badly constipated, since the image may well be on 166 // Read tasks may get badly constipated, since the image may well be on
160 // cloud server like Google Pictures. So be sure to terminate any active 167 // cloud server like Google Pictures. So be sure to terminate any active
235 else 242 else
236 Bitmap.createScaledBitmap(oldBitmap, newWidth, newHeight, true) 243 Bitmap.createScaledBitmap(oldBitmap, newWidth, newHeight, true)
237 runOnUiThread { 244 runOnUiThread {
238 binding.progressBar.visibility = ProgressBar.INVISIBLE 245 binding.progressBar.visibility = ProgressBar.INVISIBLE
239 setImage(newBitmap) 246 setImage(newBitmap)
247 makeFabulous()
240 } 248 }
241 } 249 }
242 } 250 }
243 251
244 private fun showCustomScaleDialog(): Unit { 252 private fun showCustomScaleDialog(): Unit {
312 } 320 }
313 } 321 }
314 322
315 fun cancelClicked(view: View): Unit { 323 fun cancelClicked(view: View): Unit {
316 unsetImage() 324 unsetImage()
325 makeMundane()
317 finish() 326 finish()
327 }
328
329 private fun makeFabulous(): Unit {
330 binding.fabulous.visibility = FloatingActionButton.VISIBLE
331 viewModel.sharable = true
332 }
333
334 private fun makeMundane(): Unit {
335 binding.fabulous.visibility = FloatingActionButton.GONE
336 viewModel.sharable = false
318 } 337 }
319 338
320 private val REQUEST_WRITE_EXTERNAL = 42 339 private val REQUEST_WRITE_EXTERNAL = 42
321 340
322 override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray): Unit { 341 override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray): Unit {
323 if (requestCode != REQUEST_WRITE_EXTERNAL) { 342 if (requestCode != REQUEST_WRITE_EXTERNAL) {
324 Log.e("EditImage", "unexpected request code in onRequestPermissionsResult!") 343 Log.e("EditImage", "unexpected request code in onRequestPermissionsResult!")
325 return 344 return
326 } 345 }
327 if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { 346 if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
328 doneClicked(null) 347 val cb = viewModel.permissionsCallback
348 if (cb != null) {
349 viewModel.permissionsCallback = null
350 cb()
351 }
329 } else { 352 } else {
330 showError(getString(R.string.error_unable_no_permissions)) 353 showError(getString(R.string.error_unable_no_permissions))
331 } 354 }
332 } 355 }
333 356
334 fun requestWritePermission(): Unit { 357 private fun requestWritePermission(): Unit {
335 requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_EXTERNAL) 358 requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_EXTERNAL)
336 } 359 }
337 360
338 fun doneClicked(view: View?): Unit { 361 private fun needsWritePermission(callback: () -> Unit): Boolean {
339 // If we need WRITE_EXTERNAL_STORAGE, request it and bail. We will be called again
340 // (with the permission) if it is granted.
341 if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q) { 362 if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q) {
342 if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { 363 if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
364 viewModel.permissionsCallback = callback
343 if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) 365 if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE))
344 AlertDialog.Builder(this).also { 366 AlertDialog.Builder(this).also {
345 it.setMessage(getString(R.string.permission_needed, getString(R.string.app_name))) 367 it.setMessage(
368 getString(
369 R.string.permission_needed,
370 getString(R.string.app_name)
371 )
372 )
346 it.setNeutralButton(R.string.ok_text) { dialog, _ -> 373 it.setNeutralButton(R.string.ok_text) { dialog, _ ->
347 dialog.dismiss() 374 dialog.dismiss()
348 requestWritePermission() 375 requestWritePermission()
349 } 376 }
350 it.create() 377 it.create()
351 }.show() 378 }.show()
352 else 379 else
353 requestWritePermission() 380 requestWritePermission()
354 return 381 return true
355 } 382 }
356 } 383 }
384 return false
385 }
386
387 fun shareClicked(view: View): Unit {
388 // If we need WRITE_EXTERNAL_STORAGE, request it and bail. We will be called again
389 // (with the permission) if it is granted.
390 if (needsWritePermission({ shareClicked(view) }))
391 return
357 392
358 // If we get here, we have permission to save (if we need it). 393 // If we get here, we have permission to save (if we need it).
394 val contentValues = makeContentValues(IMAGE_TO_SEND)
395
396 // Delete any old file(s)
397 val cols = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q)
398 arrayOf<String>(MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE, MediaStore.MediaColumns.RELATIVE_PATH)
399 else
400 arrayOf<String>(MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE)
401 val query = StringBuilder()
402 for (col in cols) {
403 if (query.isNotEmpty())
404 query.append(" and ")
405 query.append(col)
406 query.append(" = ?")
407 }
408 try {
409 contentResolver.delete(
410 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
411 query.toString(),
412 cols.map { contentValues.getAsString(it) }.toTypedArray()
413 )
414 } catch (e: Exception) {
415 Log.e("EditImage", "unexpected exception when sharing!", e)
416 }
417
418 // Save new file, use it to share data.
419 saveAs(contentValues) {
420 val shareIntent: Intent = Intent().apply {
421 action = Intent.ACTION_SEND
422 putExtra(Intent.EXTRA_STREAM, it)
423 type = "image/jpeg"
424 }
425 startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.share_text)))
426 }
427 }
428
429 fun doneClicked(view: View): Unit {
430 // If we need WRITE_EXTERNAL_STORAGE, request it and bail. We will be called again
431 // (with the permission) if it is granted.
432 if (needsWritePermission({ doneClicked(view) }))
433 return
434
435 // If we get here, we have permission to save (if we need it).
436 val image = viewModel.bitmap!!
437 var fileName = getFileName(viewModel.uri!!)
438 if (fileName == null) {
439 val d = java.util.Date()
440 fileName = "IMG_%tY%tm%td_%tH%tM%tS".format(d, d, d, d, d, d)
441 }
442 val dot = fileName.lastIndexOf('.')
443 if (dot != -1)
444 fileName = fileName.substring(0, dot)
445 fileName = "${fileName}_${image.width}x${image.height}.jpg"
446 saveAs(makeContentValues(fileName)) {
447 unsetImage()
448 makeMundane()
449 finish()
450 }
451 }
452
453 private fun makeContentValues(fileName: String): ContentValues = ContentValues().apply {
454 put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
455 put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
456 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
457 put(MediaStore.MediaColumns.RELATIVE_PATH,
458 File(Environment.DIRECTORY_PICTURES, getString(R.string.app_name)).path)
459 }
460 }
461
462 private fun saveAs(contentValues: ContentValues, callback: (Uri) -> Unit) {
359 val image = viewModel.bitmap!! 463 val image = viewModel.bitmap!!
360 binding.progressBar.visibility = ProgressBar.VISIBLE 464 binding.progressBar.visibility = ProgressBar.VISIBLE
361 ThreadPools.WORKERS.execute { 465 ThreadPools.WORKERS.execute {
362 val contentValues = ContentValues().apply {
363 var fileName = getFileName(viewModel.uri!!)
364 if (fileName == null) {
365 val d = java.util.Date()
366 fileName = "IMG_%tY%tm%td_%tH%tM%tS".format(d, d, d, d, d, d)
367 }
368 val dot = fileName.lastIndexOf('.')
369 if (dot != -1)
370 fileName = fileName.substring(0, dot)
371 fileName = "${fileName}_${image.width}x${image.height}.jpg"
372 put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
373 put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
374 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
375 put(MediaStore.MediaColumns.RELATIVE_PATH,
376 File(Environment.DIRECTORY_PICTURES, getString(R.string.app_name)).path)
377 }
378 }
379 var errorMessage: String? = null 466 var errorMessage: String? = null
467 val myUri = try {
468 contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
469 } catch(e: Exception) {
470 Log.e("EditImage", "unexpected exception when saving!", e)
471 null
472 }
380 try { 473 try {
381 val myUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
382 if (myUri == null) { 474 if (myUri == null) {
383 throw IOException(getString(R.string.error_create_mediastore)) 475 throw IOException(getString(R.string.error_create_mediastore))
384 } 476 }
385 val stream = contentResolver.openOutputStream(myUri) 477 val stream = contentResolver.openOutputStream(myUri)
386 if (stream == null) { 478 if (stream == null) {
404 errorMessage = e.message ?: getString(R.string.error_unexpected, e::class.qualifiedName) 496 errorMessage = e.message ?: getString(R.string.error_unexpected, e::class.qualifiedName)
405 } 497 }
406 runOnUiThread { 498 runOnUiThread {
407 binding.progressBar.visibility = ProgressBar.INVISIBLE 499 binding.progressBar.visibility = ProgressBar.INVISIBLE
408 if (errorMessage == null) { 500 if (errorMessage == null) {
409 unsetImage() 501 callback(myUri!!)
410 finish()
411 } else { 502 } else {
412 showError(errorMessage) 503 showError(errorMessage)
413 } 504 }
414 } 505 }
415 } 506 }