Android Capture Photo

May 14, 2018
Store Photo In External Public Storage

Capture photo in Android involve coding a few things

NOTE: If you plan to store photo in private internal storage (context.getExternalFilesDir( Environment.DIRECTORY_PICTURES)), then App Permission and android.permission.WRITE_EXTERNAL_STORAGE probably is not required (I didn't test it though)

Capture photo

Explanation:

  • Create a MediaStore.ACTION_IMAGE_CAPTURE intent to capture photo. Handle onActivityResult for the intent result.
  • Check external storage permission as I plan to store the photo in external public directory. Handle onRequestPermissionsResult for the permission request.
  • createImageFile: create a temporary file in external public directory to store photo from camera.
  • addImageToGallery: add image file to media database/android gallery.
companion object {    private const val PERMISSION_IMAGE_CAPTURE = 1    private const val REQUEST_TAKE_PHOTO = 1}private fun capturePhoto() {    val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)    if (intent.isCallable(context)) {        if (PermissionHelper.requestExternalStoragePermission(this, null, PERMISSION_IMAGE_CAPTURE, getString(R.string.permission_message_store_photos))) {            var photoFile: File? = null            try {                photoFile = createImageFile()            }            catch (e: IOException) {                viewModel.toastMessage.value = IdResourceString(R.string.error_file)            }            if (photoFile != null) {                // Log.d(TAG, "photoFile=${photoFile.absolutePath}")                // /storage/emulated/0/Pictures/PixPin/IMG_20180501-1252301073580164.jpg                val photoURI = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.fileprovider", photoFile)                viewModel.photoFile = photoFile                intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)                startActivityForResult(intent, REQUEST_TAKE_PHOTO)            }        }    }}private fun createImageFile(): File {    val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-kkmmss"))    val imageFileName = "IMG_$timestamp"        var dir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "PixPin")    if (!dir.exists()) {        dir.mkdirs()    }    // don't worry about miliseconds conflict, createTempFile will guarantee unique file name    return File.createTempFile(imageFileName, ".jpg", dir)}private fun addImageToGallery(photoFile: File) {    val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)    val contentUri = Uri.fromFile(photoFile)    intent.data = contentUri    this.sendBroadcast(intent)}override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {    super.onActivityResult(requestCode, resultCode, data)    if (requestCode == REQUEST_TAKE_PHOTO) {        val photoFile = viewModel.photoFile        if (resultCode == RESULT_OK) {            if (photoFile == null || !photoFile.exists()) {                viewModel.toastMessage.value = IdResourceString(R.string.error_file)                return            }            // process photoFile            addImageToGallery(photoFile)            viewModel.toastMessage.value = IdResourceString(R.string.message_successful)        }        else {            if (photoFile != null && photoFile.exists()) {                photoFile.delete()            }        }        viewModel.photoFile = null    }}override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {    super.onRequestPermissionsResult(requestCode, permissions, grantResults)    if (requestCode == PERMISSION_IMAGE_CAPTURE) {        if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {            capturePhoto()        }        else {            viewModel.toastMessage.value = IdResourceString(R.string.message_permission_denied)        }    }}

NOTE: you shall bump into exception java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.res.XmlResourceParser android.content.pm.ProviderInfo.loadXmlMetaData(android.content.pm.PackageManager, java.lang.String)' on a null object reference if FileProvider.getUriForFile authority is incorrect.

ViewModel.

NOTE: photoFile is stored in ViewModel to survive configuration change (rotation).

internal val toastMessage = SingleLiveEvent<ResourceString>()internal var photoFile: File? = null

Toast

For code related to viewModel.toastMessage.value = ..., refer to Use LiveData To Show Toast Message From ViewModel.

Kotlin Extensions.

fun Intent.isCallable(context: Context): Boolean {    return resolveActivity(context.packageManager) != null}

Code of PermissionHelper.

object PermissionHelper {    fun requestLocationPermission(activity: Activity, fragment: Fragment, requestCode: Int, message: String): Boolean {        return requestPermission(activity, fragment, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION), requestCode, message)    }    fun requestExternalStoragePermission(activity: Activity, fragment: Fragment, requestCode: Int, message: String): Boolean {        return requestPermission(activity, fragment, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), requestCode, message)    }    fun requestWifiPermission(activity: Activity, fragment: Fragment, requestCode: Int, message: String): Boolean {        return requestPermission(activity, fragment, arrayOf(Manifest.permission.CHANGE_WIFI_STATE, Manifest.permission.ACCESS_WIFI_STATE), requestCode, message)    }    fun checkLocationPermission(activity: Context): Boolean {        return checkPermission(activity, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION))    }    fun checkExternalStoragePermission(activity: Context): Boolean {        return checkPermission(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE))    }    fun checkPermission(activity: Context?, permissions: Array<String>): Boolean {        for (permission in permissions) {            if (ActivityCompat.checkSelfPermission(activity!!, permission) != PackageManager.PERMISSION_GRANTED)                return false        }        return true    }    fun requestPermission(activity: Activity, fragment: Fragment?, permissions: Array<String>, requestCode: Int, message: String): Boolean {        // suspect this causes memoryleak        // boolean isAllow = checkPermission(activity, permissions);        var context: Context? = activity        if (fragment != null) {            context = fragment.context        }        val isAllow = checkPermission(context, permissions)        if (!isAllow) {            if (ActivityCompat.shouldShowRequestPermissionRationale(activity,                            permissions[0])) {                val dialog = AlertDialog.Builder(context)                        .setMessage(message)                        .setPositiveButton(android.R.string.ok) { dialog, which ->                            if (fragment == null) {                                ActivityCompat.requestPermissions(activity,                                        permissions,                                        requestCode)                            } else {                                fragment.requestPermissions(permissions, requestCode)                            }                        }                        .create()                dialog.show()            } else {                if (fragment == null) {                    ActivityCompat.requestPermissions(activity,                            permissions,                            requestCode)                } else {                    fragment.requestPermissions(permissions, requestCode)                }            }            return false        } else {            return true        }    }}

Others

Edit AndroidManifest.xml

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
    <application ...>
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>
</manifest>

Create res/xml/file_paths.xml.

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="Pictures/PixPin" />
</paths>

NOTE: Taking multiple photo before return is not possible with this intent. Some suggested using MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA (launch camera app without result), but writing the code to observe new photo taken is fairly unpredictable and might not work across platform.

NOTE: If you require more control of the camera app, you might want to explore CameraKit or Fotoapparat.

References:

❤️ Is this article helpful?

Buy me a coffee ☕ or support my work via PayPal to keep this space 🖖 and ad-free.

Do send some 💖 to @d_luaz or share this article.

✨ By Desmond Lua

A dream boy who enjoys making apps, travelling and making youtube videos. Follow me on @d_luaz

👶 Apps I built

Travelopy - discover travel places in Malaysia, Singapore, Taiwan, Japan.