The Phone Camera and Data Storage

The phone really is a Swiss army knife. It is a phone, camera, mp3 player -- and a million other things when you consider all the apps that you can download -- the flashlight app can come in very useful. The camera is used for lots of things: snaps, video, virtual reality, car safety apps. MyRuns1 uses the camera in a simple manner -- just to take a photo for the user profile. In this lecture, we discuss how to use the camera.

What this lecture will teach you

Demo projects

The demo code used in this lecture include:

Resources

Many applications want to take a photo. We will discuss how an app can launch the camera, the user can take a photo and the control returns back to the app that invoked the camera. You will do this for the MyRuns Lab 1.

The app works as follows. The first free images are: 1) after the app is first launched; 2) when SnapNow is selected; 3) when the camera app is launched.

The next set of screen dumps show the remainder of the operations in the app workflow: 4) after the photo is take; 5) after the image has been inserted by replacing the existing photo.

Model-View-ViewModel (MVVM) architecture

mvvm

One nice thing about the MVVM architecture is that the UI layer does not need to handle the business logic to determine what data to be shown. This way user interfaces can be changed freely without impacting the business logic of the app. Similarly, the ViewModel layer can be updated freely without impacting the Model layer. We will see many examples of MVVM in this course. We will start with something simple by removing the Model layer. which you will see later in the course when we talk about database.

Our program has only View and ViewModel. LiveData is used in the ViewModel to communicate information from ViewModel to View that should update and redraw the screen.

A user can use our app to take a picture using the phone's builtin camera app and then replace the existing picture if it exists. The code also shows a way for other activities (e.g., camera app) to return to the calling activity. For example registerForActivityResult() is the return callback for the camera app after taking a photo.

First, note that in the manifest we have to be granted permission to use the camera app.

<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Next, we need to ask the user for the permission to use the camera and write/read the external storage. 

fun checkPermissions(activity: Activity?) {
if (Build.VERSION.SDK_INT < 23) return
if (ContextCompat.checkSelfPermission(activity!!, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA), 0)
}
}

CameraControlActivity

The onCreate() code sets of the view, asks for user permission, gets a reference to the image saved on the phone via  uniform resource identifier (URI). Uri is a string of characters used to identify a name or a web resource

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
imageView = findViewById(R.id.imageProfile)
textView = findViewById(R.id.text_view)

Util.checkPermissions(this)

val tempImgFile = File(getExternalFilesDir(null), tempImgFileName)
tempImgUri = FileProvider.getUriForFile(this, "com.xd.camerademokotlin", tempImgFile)
val bitmap = Util.getBitmap(this, tempImgUri)
imageView.setImageBitmap(bitmap)

}
The activation of the camera is via implicit intents. Next, we will discuss how to implement this in the code.

You probably are getting use to this now but if you want to start another activity you need to fire an intent -- and that is exactly what we do below. We create an intent using MediaStore.ACTION_IMAGE_CAPTURE action.

Implicit and explicit intents

We have now used two different types of intents called Implicit and explicit intents. We have used explicit intents numerous times to start activities such as shown below where we set the intent up to explicitly start a particular activity by naming the target activity

val intent = Intent(this@ActivityA, ActivityC::class.java)

The second type of intents are implicit intents, which we will use in this lecture. Implicit intents do not name a target but the action, as shown below. Implicit intents are often used to activate components in other applications. For example:

val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
intent.putExtra(MediaStore.EXTRA_OUTPUT, tempImgUri)
cameraResult.launch(intent)

The intent specifies the ACTION_IMAGE_CAPTURE action. The system resolves to start the activity capable of handling that action without explicitly starting a camera app. The Android system tries to find an app that is capable of performing the requested action in the implicit intent. It does this by considering the requested action, the data that has been passed in the intent (e.g., output picture location).

One issue with implicit intents is that you have no control over the app; for example in the case of the camera or the gallery; you fire it and hope for the best. This is a limitation of the approach.

Firing an implicit intent to take the photo

The steps needed to take a photo and store it in a file are as follows:

val cameraResult: ActivityResultLauncher<Intent> = registerForActivityResult(StartActivityForResult()){ result: ActivityResult ->
if(result.resultCode == Activity.RESULT_OK){
val bitmap = Util.getBitmap(this, tempImgUri)
myViewModel.userImage.value = bitmap
}
}

In the code above, we check if there is a result by asking if "result.resultCode == Activity.RESULT_OK". If the answer is yes, we pass the captured picture in Bitmap to the ViewModel layer *not to the UI*. The idea is letting the View layer to "observe" (or sense) if there is a new picture in the ViewModel. If the answer is yes, it updates the app screen to show the picture.

Below we show an implementation of a simple ViewModel with only one property (e.g., userImage). Our custom ViewModel MyViewModel implements the ViewModel class. The userImage property is a LiveData object, which holds a Bitmap object. So what is LiveData? In simple words, LiveData is a data holder and it is used to observe the changes of the data it holds.  LiveData is lifecycle-aware, meaning whenever a data is updated or changed then the updates will be sent to only those app components (e.g., Activities or Fragments) which are in active state. If the app component is not active (e.g., pushed to the background) and in the future, if it becomes active, then the updated data will be sent to that app component automatically. We use MutableLiveData when we want its data to be modified or we want to re assign a new value on it.
class MyViewModel: ViewModel() {
val userImage = MutableLiveData<Bitmap>()
}

Observe LiveData changes

Whenever we update the bitmap object held by the LiveData within the ViewModel (e.g., myViewModel.userImage.value = bitmap), the View layer needs to know in order for the UI to be updated. This is done using an Observer. In the following code, "it" as a parameter has a reference to the Bitmap. So whenever there is a change, the Lambda code will be executed. Here we simply update the imageView to show the picture.

myViewModel.userImage.observe(this, Observer { it ->
imageView.setImageBitmap(it)
})

Lastly, to create a ViewModel object in the MainActivity, we use a ViewModelProvider, which takes a LifecycleOwner object as a parameter. This means the observer will be alive all the time the fragment/activity is alive.

myViewModel = ViewModelProvider(this).get(MyViewModel::class.java)

Recover saved state after the MainActivity is destroyed

When the MainActivity is destroyed upon configuration change (e.g., changing the orientation of the phone), the onSaveInstanceState() callback will be triggered for you to save light state data of your system using Bundle. 
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(TEXTVIEW_KEY, line)
}

As an example, we recover the picture Uri from the Bundle that is passed by the system to the newly created activity.

override fun onCreate(savedInstanceState: Bundle?) {

...
if(savedInstanceState != null)
line = savedInstanceState.getString(TEXTVIEW_KEY);
}

Storage -- how Android stores app data

Android has a number of options to store the apps data. Take a look at the developer notes for more details. MyRuns1 will require you to save data to a file.

The options for storage in Android can be summarized as:

We will use all of these options when building out the complete MyRuns app.

Also read write to a file in Kotlin.