Upload Files to Amazon S3 from Android

File storage is a common feature in most modern apps whether the file be an image, video, audio file, or some other custom file.

This article will cover how to upload a photo from the photo gallery to Amazon S3 using AWS Amplify Storage, then download that image and display it in an Android app using Jetpack Compose and Coil.

Configuring Amplify

Start by running the following Amplify CLI terminal command at the root directory of your project:

$ amplify init

Answer the command prompts to finish initializing your Amplify project locally. They should look like this:

? Enter a name for the project uploadtos3android
The following configuration will be applied:

Project information
| Name: uploadtos3android
| Environment: dev
| Default editor: Visual Studio Code
| App type: android
| Res directory: app/src/main/res

? Initialize the project with the above configuration? Yes
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS profile
? Please choose the profile you want to use default

Add the Amplify Storage category:

$ amplify add storage

You can select the default answer to most questions by hitting Enter. Here are the answers I selected for the following prompts:

? Select from one of the below mentioned services: Content (Images, audio, video
, etc.)
✔ You need to add auth (Amazon Cognito) to your project in order to add storage for user files. Do you want to add auth now? (Y/n) · yes
Using service: Cognito, provided by: awscloudformation
Do you want to use the default authentication and security configuration? Default configuration
How do you want users to be able to sign in? Username
Do you want to configure advanced settings? No, I am done.
✔ Provide a friendly name for your resource that will be used to label this category in the project: · s32147b60f
✔ Provide bucket name: · uploadtos3android106dd97fb37542bea66e39dfeff2a7
✔ Who should have access: · Auth and guest users
✔ What kind of access do you want for Authenticated users? · create/update, read, delete
✔ What kind of access do you want for Guest users? · create/update, read, delete
✔ Do you want to add a Lambda Trigger for your S3 Bucket? (y/N) · no

Push the Amplify project configuration to the backend.

$ amplify push -y

Once the Storage resources have been successfully created, add the Amplify framework as a dependency for your Android project in the app build.gradle file:

// Amplify
def amplify_version = "1.31.3"
implementation "com.amplifyframework:aws-storage-s3:$amplify_version"
implementation "com.amplifyframework:aws-auth-cognito:$amplify_version"

Create a function to configure both the Auth and Storage categories in MainActivity:

private fun configureAmplify() {
    try {
        Amplify.addPlugin(AWSCognitoAuthPlugin())
        Amplify.addPlugin(AWSS3StoragePlugin())
        Amplify.configure(applicationContext)

        Log.i("kilo", "Initialized Amplify")
    } catch (error: AmplifyException) {
        Log.e("kilo", "Could not initialize Amplify", error)
    }
}

Amplify must be configured before attempting to use any of the categories. Call configureAmplify in the onCreate method of MainActivity.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    configureAmplify()

    setContent {
        Text(text = "Like and subscribe")
    }
}

If you build and run now, you should see that Amplify has been successfully configured in the console.

Selecting a Photo

Create a sealed class that will be responsible for handling the different states that will determine what is rendered to the user's screen:

sealed class ImageState {
    object Initial: ImageState()
    class ImageSelected(val imageUri: Uri): ImageState()
    object ImageUploaded: ImageState()
    class ImageDownloaded(val downloadedImageFile: File): ImageState()
}

In MainActivity add the following properties:

private var imageState = mutableStateOf<ImageState>(ImageState.Initial)

private val getImageLauncher = registerForActivityResult(GetContent()) { uri ->
    uri?.let { imageState.value = ImageState.ImageSelected(it) }
}

imageState will keep track of the current ImageState and getImageLauncher will be responsible for handling the Uri once a photo is selected.

In setContent of MainActivity.onCreate, add the following composable views:

Column(
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxSize()
) {
    when (val state = imageState.value) {
        // Show Open Gallery Button
        is ImageState.Initial -> {
            Button(onClick = { getImageLauncher.launch("image/*") }) {
                Text(text = "Open Gallery")
            }
        }
    }
}

imageState will have a default value of ImageState.Initial, so the Open Gallery button will be shown when the app is launched. Clicking the button will trigger the image launcher and show the user's photo gallery.

If you build and run the app, you will now be able to select a photo from your gallery.

Uploading a Photo

Amazon S3 uses keys to index files. Create a constant value that can be used to name and retrieve the uploaded photo.

companion object {
    const val PHOTO_KEY = "my-photo.jpg"
}

Use Amplify.Storage to upload your photo using an InputStream:

private fun uploadPhoto(imageUri: Uri) {
    val stream = contentResolver.openInputStream(imageUri)!!

    Amplify.Storage.uploadInputStream(
        PHOTO_KEY,
        stream,
        { imageState.value = ImageState.ImageUploaded },
        { error -> Log.e("kilo", "Failed upload", error) }
    )
}

This method is using callbacks to update imageState or log any errors with the upload. To see more ways to upload files, see Upload files.

Add the following snippet to the when statement in setContent:

// Show Upload Button
is ImageState.ImageSelected -> {
    Button(onClick = { uploadPhoto(state.imageUri) }) {
        Text(text = "Upload Photo")
    }
}

If you build and run now, you will be able to upload a selected photo from your photo gallery to S3.

Downloading a Photo

Create a function that will be responsible for downloading the file/photo from S3:

private fun downloadPhoto() {
    val localFile = File("${applicationContext.filesDir}/downloaded-image.jpg")

    Amplify.Storage.downloadFile(
        PHOTO_KEY,
        localFile,
        { imageState.value = ImageState.ImageDownloaded(localFile) },
        { Log.e("kilo", "Failed download", it) }
    )
}

Once you provide a key and file destination, you can use Storage.downloadFile to write the file locally. Upon success, imageState is updated to ImageState.ImageDownloaded and provides the destination of the downloaded file.

Again in the setContent block, add the following snippet to the when statement:

// Show Download Button
is ImageState.ImageUploaded -> {
    Button(onClick = ::downloadPhoto) {
        Text(text = "Download Photo")
    }
}

You will now be able to download the photo to a local file.

Showing the Photo

Add Coil as a dependency of your project in the app build.gradle file. It will be used to show the downloaded image on the screen.

// Coil
def coil_version = "1.4.0"
implementation "io.coil-kt:coil-compose:$coil_version"

Add the final when statement to setContent:

// Show downloaded image
is ImageState.ImageDownloaded -> {
    Image(
        painter = rememberImagePainter(state.downloadedImageFile),
        contentDescription = null,
        modifier = Modifier.fillMaxSize()
    )
}

When the image has been successfully downloaded and a file destination is provided, the image will now be rendered to the screen 🎉