Capture photo in Android involve coding a few things
- Use
MediaStore.ACTION_IMAGE_CAPTURE
intent withMediaStore.EXTRA_OUTPUT
to capture photo - Optional: Use
Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
to add image file to media database/android gallery. - To save photo in external public storage (Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)), we need
android.permission.WRITE_EXTERNAL_STORAGE
and configure FileProvider - If we are using external public storage, we need to request App Permission for Storage on
Android 6.0/Marshmallow
and up.
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. HandleonActivityResult
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: