Getting Started / Hands-on / ...
Getting Started with AWS
Build an Android Application
Create a simple Android application using AWS Amplify
Module 5: Add the Ability to Store Images
In this module you will add storage and the ability to associate an image with the notes in your app.
Introduction
Now that we have the notes app working, let's add the ability to associate an image with each note. In this module, you will use the Amplify CLI and libraries to create a storage service leveraging Amazon S3. Finally, you will update the Android app to enable image uploading, fetching, and rendering.
What You Will Learn
- Create a storage service
- Update your Android app - the logic to upload and download images
- Update your Android app - the user interface
Key Concepts
Storage service - Storing and querying for files like images and videos is a common requirement for most applications. One option to do this is to Base64 encode the file and send as a string to save in the database. This comes with disadvantages like the encoded file being larger than the original binary, the operation being computationally expensive, and the added complexity around encoding and decoding properly. Another option is to have a storage service specifically built and optimized for file storage. Storage services like Amazon S3 exist to make this as easy, performant, and inexpensive as possible.
Time to Complete
10 minutes
Services Used
Implementation
-
Create the Storage Service
To add image storage functionality, we'll use the Amplify storage category:
amplify add storage ? Please select from one of the below mentioned services: accept the default Content (Images, audio, video, etc.) and press enter ? Please provide a friendly name for your resource that will be used to label this category in the project: type image and press enter ? Please provide bucket name: accept the default and press enter ? Who should have access: accept the default Auth users only and press enter ? What kind of access do you want for Authenticated users? select all three options create/update, read, delete using the space and arrows keys, then press enter ? Do you want to add a Lambda Trigger for your S3 Bucket? accept the default No and press enter
After a while, you should see:
Successfully added resource image locally
-
Deploy the Storage Service
To deploy the storage service we have just created, go to your terminal and execute the command:
amplify push
Press Y to confirm and, after a while, you should see:
✔ Successfully pulled backend environment amplify from the cloud.
-
Add Amplify Storage Libraries to the Android Studio Project
Before going to the code, go back to Android Studio and add the following dependency to your module's `build.gradle` along with other `amplifyframework` implementations you added before and click Sync Now when prompted:
dependencies { ... // Amplify core dependency implementation 'com.amplifyframework:core:1.4.0' implementation 'com.amplifyframework:aws-auth-cognito:1.4.0' implementation 'com.amplifyframework:aws-api:1.4.0' implementation 'com.amplifyframework:aws-storage-s3:1.4.0' }
-
Initialize Amplify Storage Plugin at Runtime
Back to Android Studio, under java/example.androidgettingstarted, open Backend.kit and add a line in the Amplify initialization sequence in initialize() method. Complete code block should look like this:
try { Amplify.addPlugin(AWSCognitoAuthPlugin()) Amplify.addPlugin(AWSApiPlugin()) Amplify.addPlugin(AWSS3StoragePlugin()) Amplify.configure(applicationContext) Log.i(TAG, "Initialized Amplify") } catch (e: AmplifyException) { Log.e(TAG, "Could not initialize Amplify", e) }
-
Add Image CRUD methods to the Backend Class
Still in Backend.kt. Anywhere in the Backend class, add the the following three methods to upload, download and delete image from the Storage:
fun storeImage(filePath: String, key: String) { val file = File(filePath) val options = StorageUploadFileOptions.builder() .accessLevel(StorageAccessLevel.PRIVATE) .build() Amplify.Storage.uploadFile( key, file, options, { progress -> Log.i(TAG, "Fraction completed: ${progress.fractionCompleted}") }, { result -> Log.i(TAG, "Successfully uploaded: " + result.key) }, { error -> Log.e(TAG, "Upload failed", error) } ) } fun deleteImage(key : String) { val options = StorageRemoveOptions.builder() .accessLevel(StorageAccessLevel.PRIVATE) .build() Amplify.Storage.remove( key, options, { result -> Log.i(TAG, "Successfully removed: " + result.key) }, { error -> Log.e(TAG, "Remove failure", error) } ) } fun retrieveImage(key: String, completed : (image: Bitmap) -> Unit) { val options = StorageDownloadFileOptions.builder() .accessLevel(StorageAccessLevel.PRIVATE) .build() val file = File.createTempFile("image", ".image") Amplify.Storage.downloadFile( key, file, options, { progress -> Log.i(TAG, "Fraction completed: ${progress.fractionCompleted}") }, { result -> Log.i(TAG, "Successfully downloaded: ${result.file.name}") val imageStream = FileInputStream(file) val image = BitmapFactory.decodeStream(imageStream) completed(image) }, { error -> Log.e(TAG, "Download Failure", error) } ) }
These three methods simply call their Amplify counterpart. Amplify storage has three file protection levels:
- Public Accessible by all users
- Protected Readable by all users, but only writable by the creating user
- Private Readable and writable only by the creating user
For this app, we want the images to only be available to the Note owner, we are using the StorageAccessLevel.PRIVATE property.
-
Add UI Code to Capture an Image
Next step is to modify the UI to allow the user to select an image from the phone library when clicking the "Add image" button on the AddNoteACtivity.
Two changes are necessary: change the "Add Note" activity layout to add an "Add image" button and a image view, and add handler code in the activity class.
In Android Studio, under res/layout, open activity_add_note.xml file and add this Button element just above the addNote button:
<!-- after the description EditText --> <com.google.android.material.imageview.ShapeableImageView android:id="@+id/image" android:layout_width="match_parent" android:layout_height="280dp" android:layout_margin="16dp" android:scaleType="centerCrop" /> <!-- after the Space --> <Button android:id="@+id/captureImage" style="?android:attr/buttonStyleSmall" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:backgroundTint="#009688" android:text="Add image" />
In Android Studio, under java/example.androidgettingstarted, open AddNoteACtivity.kt file and add this code in the onCreate() method:
// inside onCreate() // Set up the listener for add Image button captureImage.setOnClickListener { val i = Intent( Intent.ACTION_GET_CONTENT, MediaStore.Images.Media.EXTERNAL_CONTENT_URI ) startActivityForResult(i, SELECT_PHOTO) } // create rounded corners for the image image.shapeAppearanceModel = image.shapeAppearanceModel .toBuilder() .setAllCorners(CornerFamily.ROUNDED, 150.0f) .build()
Add the required import on Intent, MediaStore and CornerFamily.
Also add this constant value in the companion object:
// add this to the companion object private const val SELECT_PHOTO = 100
Finally, add the code that receive and store the selected image to a temporary file.
Add the below code anywhere in the AddNoteACtivityclass:
//anywhere in the AddNoteActivity class private var noteImagePath : String? = null private var noteImage : Bitmap? = null override fun onActivityResult(requestCode: Int, resultCode: Int, imageReturnedIntent: Intent?) { super.onActivityResult(requestCode, resultCode, imageReturnedIntent) Log.d(TAG, "Select photo activity result : $imageReturnedIntent") when (requestCode) { SELECT_PHOTO -> if (resultCode == RESULT_OK) { val selectedImageUri : Uri? = imageReturnedIntent!!.data // read the stream to fill in the preview var imageStream: InputStream? = contentResolver.openInputStream(selectedImageUri!!) val selectedImage = BitmapFactory.decodeStream(imageStream) val ivPreview: ImageView = findViewById<View>(R.id.image) as ImageView ivPreview.setImageBitmap(selectedImage) // store the image to not recreate the Bitmap every time this.noteImage = selectedImage // read the stream to store to a file imageStream = contentResolver.openInputStream(selectedImageUri) val tempFile = File.createTempFile("image", ".image") copyStreamToFile(imageStream!!, tempFile) // store the path to create a note this.noteImagePath = tempFile.absolutePath Log.d(TAG, "Selected image : ${tempFile.absolutePath}") } } } private fun copyStreamToFile(inputStream: InputStream, outputFile: File) { inputStream.use { input -> val outputStream = FileOutputStream(outputFile) outputStream.use { output -> val buffer = ByteArray(4 * 1024) // buffer size while (true) { val byteCount = input.read(buffer) if (byteCount < 0) break output.write(buffer, 0, byteCount) } output.flush() output.close() } } }
The above code consumes the selected image as an InputStream, twice. The first InputStream creates a Bitmap image to display in the UI, the second InputStream saves a temporary file to send to the backend.
This module goes through a temporary file because the Amplify API consumes Fileobjects. While not the most efficient design, the code is simple.
To verify everything works as expected, build the project. Click Build menu and select Make Project or, on Macs, type ⌘F9. There should be no error.
-
Store Image when Notes are Created
Let's invoke the storage methods from Backend when a Note is created. Open AddNoteActivity.kt and modify the addNote.setOnClickListener() method, to add the below code after the Note object is created.
// add this in AddNoteACtivity.kt, inside the addNote.setOnClickListener() method and after the Note() object is created. if (this.noteImagePath != null) { note.imageName = UUID.randomUUID().toString() //note.setImage(this.noteImage) note.image = this.noteImage // asynchronously store the image (and assume it will work) Backend.storeImage(this.noteImagePath!!, note.imageName!!) }
-
Load image when Notes are loaded
To load images, we modify the static from method on the Note data class. That way, every time a NoteData object returned by the API is converted to a Note object, the image is loaded in parallel. When the image is loaded, we notify the LiveData's UserData to let observers know about the change. This triggers a UI refresh.
Open UserData.kt and modify the Note data class' companion object like this:
// static function to create a Note from a NoteData API object companion object { fun from(noteData : NoteData) : Note { val result = Note(noteData.id, noteData.name, noteData.description, noteData.image) if (noteData.image != null) { Backend.retrieveImage(noteData.image!!) { result.image = it // force a UI update with(UserData) { notifyObserver() } } } return result } }
-
Delete Images when Notes are Deleted
The last step is to clean up after ourselves, i.e. to delete images from the cloud storage when a user deletes a Note. If you don't do it to save storage space, do it for your AWS bills as Amazon S3 charges per Gb/month of data stored (the first 5Gb are for free, you will not be charged to run this tutorial).
Open SwipeCallback.kt and add the code below at the end of onSwipe() method:
if (note?.imageName != null) { //asynchronously delete the image (and assume it will work) Backend.deleteImage(note.imageName!!) }
-
Build and Test
To verify everything works as expected, build and run the project. Click Run icon ▶️ in the toolbar or type ^ R. There should be no error.
Assuming you are still signed in, the app starts on the list of Note you did not delete from the previous section. Use the Add Note button again to create a Note. This time, add a picture selected from the local image store.
Quit the app and restart it to verify that the image is correctly loaded.
Here is the complete flow.
Conclusion
You have built an Android application using AWS Amplify! You have added authentication to your app allowing users to sign up, sign in, and manage their account. The app also has a scalable GraphQL API configured with an Amazon DynamoDB database allowing users to create and delete notes. You have also added file storage using Amazon S3 allowing users to upload images and view them in their app.
In the last section, you will find instructions to reuse or to delete the backend we just created.
-
Share your Backend Between Multiple Projects
Amplify makes it easy to share a single backend between multiple front end applications.
In a terminal, navigate to your other project directory and execute the following command:
mkdir other-project cd other-project amplify pull ? Do you want to use an AWS profile? accept the default Yes and press enter ? Please choose the profile you want to use select the profile you want to use and press enter ? Which app are you working on? select the backend you want to share and press enter ? Choose your default editor: select you prefered text editor and press enter ? Choose the type of app that you're building select the operating system for your new project and press enter ? Do you plan on modifying this backend? most of the time, select No and press enter. All backend modifications can be done from the original iOS project.
After a few seconds, you will see the following message:
Added backend environment config object to your project. Run 'amplify pull' to sync upstream changes.
You can see the two configurations files that have been pulled out. When you answer 'Yes' to the question 'Do you plan on modifying this backend?', you also see a amplify directory
➜ other-project git:(master) ✗ ls -al total 24 drwxr-xr-x 5 stormacq admin 160 Jul 10 10:28 . drwxr-xr-x 19 stormacq admin 608 Jul 10 10:27 .. -rw-r--r-- 1 stormacq admin 315 Jul 10 10:28 .gitignore -rw-r--r-- 1 stormacq admin 3421 Jul 10 10:28 amplifyconfiguration.json -rw-r--r-- 1 stormacq admin 1897 Jul 10 10:28 awsconfiguration.json
-
Delete Your Backend
When creating a backend for a test or a prototype, or just for learning purposes, just like when you follow this tutorial, you want to delete the cloud resources that have been created.
Although the usage of this resources in the context of this tutorial fall under the free tier, it is a best practice to clean up unused resources in the cloud.
To clean your amplify project, in a terminal, execute the following command:
amplify delete
After a while, you will see the below message confirming all cloud resources have been deleted.
✔ Project deleted in the cloud Project deleted locally.
Congratulations!
You successfully built an Android application on AWS! As a great next step, dive deeper into specific AWS technologies and take your application to the next level.