Skip to content

Commit

Permalink
ask permission now for taking a photo
Browse files Browse the repository at this point in the history
  • Loading branch information
westnordost committed Feb 28, 2022
1 parent 05a073f commit 103b662
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,42 +1,37 @@
package de.westnordost.streetcomplete.quests.note_discussion

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.pm.PackageManager.FEATURE_CAMERA_ANY
import android.graphics.Bitmap
import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import de.westnordost.streetcomplete.ApplicationConstants.ATTACH_PHOTO_MAXHEIGHT
import de.westnordost.streetcomplete.ApplicationConstants.ATTACH_PHOTO_MAXWIDTH
import de.westnordost.streetcomplete.ApplicationConstants.ATTACH_PHOTO_QUALITY
import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.data.osmnotes.deleteImages
import de.westnordost.streetcomplete.databinding.FragmentAttachPhotoBinding
import de.westnordost.streetcomplete.ktx.toast
import de.westnordost.streetcomplete.ktx.viewBinding
import de.westnordost.streetcomplete.util.ActivityForResultLauncher
import de.westnordost.streetcomplete.util.AdapterDataChangedWatcher
import de.westnordost.streetcomplete.util.decodeScaledBitmapAndNormalize
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlinx.coroutines.suspendCancellableCoroutine
import java.lang.Exception
import kotlin.coroutines.resume

class AttachPhotoFragment : Fragment(R.layout.fragment_attach_photo) {

private val binding by viewBinding(FragmentAttachPhotoBinding::bind)
private val takePhoto = ActivityForResultLauncher(this, ActivityResultContracts.TakePicture())
private val launchTakePhoto = TakePhoto(this, ::askUserToAcknowledgeCameraPermissionRationale)

private lateinit var noteImageAdapter: NoteImageAdapter

val imagePaths: List<String> get() = noteImageAdapter.list


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down Expand Up @@ -67,54 +62,44 @@ class AttachPhotoFragment : Fragment(R.layout.fragment_attach_photo) {
}

private suspend fun takePhoto() {
var file: File? = null
try {
file = createImageFile()
val photoUri = FileProvider.getUriForFile(requireContext(), getString(R.string.fileprovider_authority), file)
val saved = takePhoto(photoUri)
if (!saved) {
deleteImageFile(file)
return
}
rescaleImageFile(file)

noteImageAdapter.list.add(file.path)
val filePath = launchTakePhoto(requireActivity()) ?: return
noteImageAdapter.list.add(filePath)
noteImageAdapter.notifyItemInserted(imagePaths.size - 1)
} catch (e: Exception) {
Log.e(TAG, "Unable to create photo", e)
file?.let { deleteImageFile(it) }
when (e) {
is ActivityNotFoundException -> context?.toast(R.string.no_camera_app)
else -> context?.toast(R.string.quest_leave_new_note_create_image_error)
else -> {
Log.e(TAG, "Unable to create photo", e)
context?.toast(R.string.quest_leave_new_note_create_image_error)
}
}
}
}

private fun createImageFile(): File {
val directory = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val imageFileName = "photo_" + System.currentTimeMillis() + ".jpg"
val file = File(directory, imageFileName)
if (!file.createNewFile()) throw IOException("Photo file with exactly the same name already exists")
return file
}

private fun rescaleImageFile(file: File) {
val bitmap = decodeScaledBitmapAndNormalize(file.path, ATTACH_PHOTO_MAXWIDTH, ATTACH_PHOTO_MAXHEIGHT) ?: throw IOException()
val out = FileOutputStream(file.path)
bitmap.compress(Bitmap.CompressFormat.JPEG, ATTACH_PHOTO_QUALITY, out)
}

private fun deleteImageFile(file: File) {
if (file.exists()) file.delete()
}

fun deleteImages() {
deleteImages(imagePaths)
}

/* ----------------------------------- Permission request ----------------------------------- */

/** Show dialog that explains why the camera permission is necessary. Returns whether the user
* acknowledged the rationale. */
private suspend fun askUserToAcknowledgeCameraPermissionRationale(): Boolean =
suspendCancellableCoroutine { cont ->
val dlg = AlertDialog.Builder(requireContext())
.setTitle(R.string.no_camera_permission_warning_title)
.setMessage(R.string.no_camera_permission_warning)
.setPositiveButton(android.R.string.ok) { _, _ -> cont.resume(true) }
.setNegativeButton(android.R.string.cancel) { _, _ -> cont.resume(false) }
.setOnCancelListener { cont.resume(false) }
.create()
cont.invokeOnCancellation { dlg.cancel() }
dlg.show()
}

companion object {
private const val TAG = "AttachPhotoFragment"

private const val PHOTO_PATHS = "photo_paths"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package de.westnordost.streetcomplete.quests.note_discussion

import android.Manifest
import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.FileProvider
import de.westnordost.streetcomplete.ApplicationConstants
import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.ktx.hasCameraPermission
import de.westnordost.streetcomplete.util.ActivityForResultLauncher
import de.westnordost.streetcomplete.util.decodeScaledBitmapAndNormalize
import java.io.File
import java.io.FileOutputStream
import java.io.IOException

/** Requests permission to, takes a photo and rescales it */
class TakePhoto(
activityResultCaller: ActivityResultCaller,
private val askUserToAcknowledgeCameraPermissionRationale: suspend () -> Boolean,
) {
private val requestPermission = ActivityForResultLauncher(activityResultCaller, ActivityResultContracts.RequestPermission())
private val takePhoto = ActivityForResultLauncher(activityResultCaller, ActivityResultContracts.TakePicture())

/** Returns the file path where to find the taken photo or null if no photo has been taken.
*
* May return an ActivityNotFoundException if there is no camera app or an IOException if the
* photo file could not be created. */
suspend operator fun invoke(activity: Activity): String? {
if (!activity.hasCameraPermission) {
if (!requestCameraPermission(activity)) {
return null
}
}

var file: File? = null
try {
file = createImageFile(activity)
val photoUri = FileProvider.getUriForFile(activity, activity.getString(R.string.fileprovider_authority), file)
val saved = takePhoto(photoUri)
if (!saved) {
deleteImageFile(file)
return null
}
rescaleImageFile(file)

return file.path
} catch (e: Exception) {
file?.let { deleteImageFile(it) }
throw e
}
}

private suspend fun requestCameraPermission(activity: Activity): Boolean {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA)) {
if (!askUserToAcknowledgeCameraPermissionRationale()) {
return false
}
}
return requestPermission(Manifest.permission.CAMERA)
}

private fun createImageFile(context: Context): File {
val directory = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val imageFileName = "photo_" + System.currentTimeMillis() + ".jpg"
val file = File(directory, imageFileName)
if (!file.createNewFile()) throw IOException("Photo file with exactly the same name already exists")
return file
}

private fun rescaleImageFile(file: File) {
val bitmap = decodeScaledBitmapAndNormalize(file.path, ApplicationConstants.ATTACH_PHOTO_MAXWIDTH, ApplicationConstants.ATTACH_PHOTO_MAXHEIGHT) ?: throw IOException()
val out = FileOutputStream(file.path)
bitmap.compress(Bitmap.CompressFormat.JPEG, ApplicationConstants.ATTACH_PHOTO_QUALITY, out)
}

private fun deleteImageFile(file: File) {
if (file.exists()) file.delete()
}
}
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1214,7 +1214,7 @@ Alternatively, you can leave a note (with a photo)."</string>
<string name="quest_side_select_interface_explanation">Tap one side and select the matching answer.\nDo the same for the other side if present.</string>

<string name="no_camera_permission_warning_title">Camera permission</string>
<string name="no_camera_permission_warning">The permission is necessary to be able to measure distances with the camera.</string>
<string name="no_camera_permission_warning">The permission is necessary to be able to take photos and to measure distances with the camera.</string>
<string name="ar_measure">Measure</string>
<string name="ar_core_error_sdk_too_old">This app is too old, try updating.</string>
<string name="ar_core_tracking_error_bad_state">Something went wrong. Try restarting the measuring.</string>
Expand Down

0 comments on commit 103b662

Please sign in to comment.