Android Native - Take a Picture with ActivityResult API

dimitrilc 3 Tallied Votes 6K Views Share

Introduction

In this tutorial, we will learn how to capture a picture with the new ActivityResult APIs(androidx.activity.result), which replace the startActivityForResult() and onActivityResult() APIs(from Activity class).

Additionally, we will also learn about the ContentProvider FileProvider, which we will have to set up to save our images.

Goal

At the end of the tutorial, you would have learned:

  1. How to use ActivityResult APIs to launch another Activity(default Camera) from your App.
  2. How to set up and use FileProvider.

Tools Required

  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.

Prerequisite Knowledge

  1. Intermediate Android.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Remove the default Hello World! TextView.

  3. Add a new Button inside ConstraintView using the code below.

     <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button_takePicture"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
  4. Add these string resources in strings.xml. You should modify the authorities string value if your domain name is different.

     <string name="button_takePicture">Take Picture</string>
     <string name="temp_images_dir">temp_images</string>
     <string name="temp_image">temp_image.jpg</string>
     <string name="authorities">com.example.fileprovider</string>
  5. Add a new ImageView under the Button using the code below.

     <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button"
        tools:srcCompat="@tools:sample/avatars" />
  6. Your activity_main.xml should look like the screenshot below.

1.png

Project Overview

Firstly, let us quickly go over the starter project.

  1. The project mostly has only two elements, a Button and an ImageView.
  2. The Button is centered inside the parent ConstraintLayout.
  3. The ImageView does not contain any Drawable resource at runtime. The avatar image that you see right now only shows up during development.
  4. The ImageView has 8dp margins at the top and the bottom and it is constrained to sit below the Button.

When the project is completed, our App will be able to perform the following actions:

  1. When the Button is clicked, our App will launch the default Camera app.
  2. After a picture is taken with the Camera app, Android will automatically return our App to the foreground.
  3. The newly taken picture will be displayed inside the ImageView element.
  4. Anytime we take a new picture, the ImageView will refresh its current Drawable.

FileProvider Setup

Before we can implement the ActivityResult API, we need to agree on a way to save the taken pictures first. For this tutorial, we will use the FileProvider API, which encapsulates File URIs as Content URIs(android.net.Uri). launch() functions from ActivityResultLauncher specifically require Uri objects.

To set up FileProvider for our project, there are a couple of steps that we need to follow:

  1. Define a FileProvider in the manifest (inside the <application> element, but outside of the <activity> element). We need android:grantUriPermission to be true here because the Camera App will need temporary access to Uri objects internal to our App via the FileProvider. If you are not sure what these attributes do to <provider>, you can check out the docs here.

     <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="@string/authorities"
        android:exported="false"
        android:grantUriPermissions="true">
     </provider>
  2. FileProvider can only generate Uri objects from files that you specified beforehand, so we need to pass the <meta-data> element below into <provider> to let the FileProvider know which files it can generate Uri objects for.

     <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
  3. Notice the @xml/file_paths resource. We will have to create it because it does not exist yet. Right-click on the res directory > New > Android Resource File. Create the new resource file with the parameters matching the screenshot below.
    2.png

  4. Add the xml content below into file_paths.xml.

     <paths>
        <files-path name="@strings/temp_images" path="."/>
     </paths>

Generate the Uri

Next, we will need to generate the Uri object.

  1. Inside the MainAcitivity class, copy and paste the function below.

     private fun initTempUri(): Uri {
    
     }
  2. Add the code below into initTempUri(). This code snippet creates an abstract file path to the temporary directory that will house the temporary file later.

     //gets the temp_images dir
     val tempImagesDir = File(
        applicationContext.filesDir, //this function gets the external cache dir
        getString(R.string.temp_images_dir)) //gets the directory for the temporary images dir
  3. Because this directory does not yet exist, we must create it in the file system (our App’s private storage area) with the line of the code below.

     tempImagesDir.mkdir() //Create the temp_images dir
  4. Now we need to create an abstract file path for the temporary image file.

     //Creates the temp_image.jpg file
     val tempImage = File(
        tempImagesDir, //prefix the new abstract path with the temporary images dir path
        getString(R.string.temp_image)) //gets the abstract temp_image file name
  5. Finally, we create and return the Uri object from the File object with FileProvider#getUriForFile().

     //Returns the Uri object to be used with ActivityResultLauncher
     return FileProvider.getUriForFile(
        applicationContext,
        getString(R.string.authorities),
        tempImage)

ActivityResult API

To be able to work with ActivityResult APIs, it is crucial to understand the classes below:

  1. ActivityResultCaller: interface with abstract functions registerForActivityResult(). AppCompatActivity (as well many other classes) implements this interface, so we are able to call registerForActivityResult() inside our MainActivity.
  2. ActivityResultContract: the first argument for registerForActivityResult(). There are pre-made implementations of this interface, so we do not have to implement it in this tutorial.
  3. ActivityResultContracts: a collection of pre-made ActivityResultContract implementations.
  4. ActivityResultContracts.TakePicture: the premade ActivityResultContract that we use in this tutorial to take a picture.
  5. ActivityResultCallback: A functional interface and the second argument for registerForActivityResult(). The return type of its SAM is dependent on the ActivityResultContract passed into registerForActivityResult() in the first argument. For the TakePicture object used in this tutorial, ActivityResultCallback SAM returns a Boolean.
  6. ActivityResultLauncher: the return value of registerForActivityResult(), which we will call launch() on and pass in our Uri object.

Use ActivityResult APIs to launch another Activity

Now that we are familiar with the classes in androidx.activity.result.contract, we can finally use them in our code.

  1. Add the function below into the MainActivity class.

     private fun registerTakePictureLauncher(path: Uri) {
    
     }
  2. Get references to the Button and ImageView objects.

     val button = findViewById<Button>(R.id.button) //gets the Button object
     val imageView = findViewById<ImageView>(R.id.imageView) //gets the ImageView object
  3. Then, we call registerForActivityResult(), passing in the premade ActivityResultContracts.TakePicture and setting the ImageView URI. There a few different ImageView#setX() functions, but setImageURI() was chosen here for simplicity because we already have an Uri object. You should also test the image dimensions on older versions of Android; that topic is out of scope for this tutorial, so I picked the simplest method calls to use.

     //Creates the ActivityResultLauncher
     val resultLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()){
        imageView.setImageURI(null) //rough handling of image changes. Real code need to handle different API levels.
        imageView.setImageURI(path)
     }
  4. Finally, we bind the Button click actions to launch the Camera activity.

     //Launches the camera when button is pressed.
     button.setOnClickListener {
        resultLauncher.launch(path) //launches the activity here
     }
  5. Inside onCreate(), call the two functions that we have created like below.

     //initialize temp image directory on first launch
     val tempImageUri = initTempUri()
    
     registerTakePictureLauncher(tempImageUri) //Binds button to launch camera activity

Run the App

Launch the App to test if it works. When you are inside the simulated Camera app, hold Alt + WASD/Mouse to move around.

The app behavior should be similar to the animation below.

Camera.gif

Solution Code

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.example.daniwebandroidcamerapictureintent">

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.DaniwebAndroidCameraPictureIntent">
       <activity
           android:name=".MainActivity"
           android:exported="true">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
       <provider
           android:name="androidx.core.content.FileProvider"
           android:authorities="@string/authorities"
           android:exported="false"
           android:grantUriPermissions="true">
           <meta-data
               android:name="android.support.FILE_PROVIDER_PATHS"
               android:resource="@xml/file_paths" />
       </provider>
   </application>

</manifest>

MainActivity.kt

package com.example.daniwebandroidcamerapictureintent

import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import java.io.File

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       //initialize temp image directory on first launch
       val tempImageUri = initTempUri()

       registerTakePictureLauncher(tempImageUri) //Binds button to launch camera activity
   }

   private fun initTempUri(): Uri {
       //gets the temp_images dir
       val tempImagesDir = File(
           applicationContext.filesDir, //this function gets the external cache dir
           getString(R.string.temp_images_dir)) //gets the directory for the temporary images dir

       tempImagesDir.mkdir() //Create the temp_images dir

       //Creates the temp_image.jpg file
       val tempImage = File(
           tempImagesDir, //prefix the new abstract path with the temporary images dir path
           getString(R.string.temp_image)) //gets the abstract temp_image file name

       //Returns the Uri object to be used with ActivityResultLauncher
       return FileProvider.getUriForFile(
           applicationContext,
           getString(R.string.authorities),
           tempImage)
   }

   private fun registerTakePictureLauncher(path: Uri) {
       val button = findViewById<Button>(R.id.button) //gets the Button object
       val imageView = findViewById<ImageView>(R.id.imageView) //gets the ImageView object

       //Creates the ActivityResultLauncher
       val resultLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()){
           imageView.setImageURI(null) //rough handling of image changes. Real code need to handle different API levels.
           imageView.setImageURI(path)
       }

       //Launches the camera when button is pressed.
       button.setOnClickListener {
           resultLauncher.launch(path) //launches the activity here
       }
   }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/button_takePicture"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <ImageView
       android:id="@+id/imageView"
       android:layout_width="wrap_content"
       android:layout_height="0dp"
       android:layout_marginTop="8dp"
       android:layout_marginBottom="8dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/button"
       tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Camera Picture Intent</string>
   <string name="button_takePicture">Take Picture</string>
   <string name="temp_images_dir">temp_images</string>
   <string name="temp_image">temp_image.jpg</string>
   <string name="authorities">com.example.fileprovider</string>
</resources>

file_paths.xml

<paths>
   <files-path name="@strings/temp_images" path="."/>
</paths>

Summary

We have learned how to capture an image with the default Camera and return back to our Activity using ActivityResult APIs as well as how to use FileProvider.

The full project code can be found here https://github.com/dmitrilc/DaniwebAndroidCameraPictureIntent

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.