AWS Mobile Blog

Creating Mobile Apps with Dynamic Content Stored in Amazon S3

by Glenn Dierkes | on | in S3 | Permalink | Comments |  Share

Version 2 of the AWS Mobile SDK

  • This article and sample apply to Version 1 of the AWS Mobile SDK. If you are building new apps, we recommend you use Version 2. For details, please visit the AWS Mobile SDK page.
  • This content is being maintained for historical reference.

Native mobile apps allow image files and other content assets to be bundled with the app code, which typically means that updates to those assets entails deploying a new version of the app to your end users. This blog post demonstrates how to store your content in Amazon S3 and access it in app code from a local store on the device. We’ll show how your app code can quickly check with Amazon S3 if the content needs to be updated, so that all you need to do to change the content is upload new files to Amazon S3.

Example Code Overview

This blog post includes example code for an app that displays a dynamic image. First, the app determines if it has the appropriate image. Then, it either retrieves the latest version from Amazon S3, or loads the image from the local store.

Checking and Displaying an Image

The following function displays the image in the app. This main function checks if a newer image is available from Amazon S3. If a newer image is available, the app retrieves it, stores it locally, and then displays the image.

iOS

-(void)displayImage:(AmazonS3Client*)s3
           withName:(NSString*)imageName
         fromBucket:(NSString*)bucketName
{
    if ( [self isNewImageAvailable:s3
                          withName:imageName
                        fromBucket:bucketName] ) {
        [self getRemoteImage:s3
                    withName:imageName
                  fromBucket:bucketName];
    }

    NSData *imageData = [self getLocalImage:imageName];
    UIImage *image = [UIImage imageWithData:imageData];
    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];

    [self.view addSubview:imageView];
}

Android

private void displayImage( ImageView view, 
                   AmazonS3Client s3, 
                   String imageName, 
                   String bucketName ) {
    if ( this.isNewImageAvailable( s3, imageName, bucketName ) ) {
        this.getRemoteImage( s3, imageName, bucketName ); 
    }

    InputStream stream = this.getLocalImage( imageName ); 
    view.setImageDrawable( Drawable.createFromStream( stream, "src" ) ); 
}

Checking If a Newer Image is Available

To see if the remote image has changed, we use Amazon S3’s getObjectMetadata operation. This operation allows us to check the local file date versus the last modified date of the image object in Amazon S3. If the remote image file’s date is later, then we need to update our local copy.

iOS

-(BOOL)isNewImageAvailable:(AmazonS3Client*)s3
                  withName:(NSString*)imageName
                fromBucket:(NSString*)bucketName
{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                         NSUserDomainMask,
                                                         YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSArray *pathComponents = [NSArray arrayWithObjects:documentsDirectory,
                               imageName,
                               nil ];
    NSString *filePath = [NSString pathWithComponents:pathComponents];
    NSDictionary *attributes = [[NSFileManager defaultManager]
                                    attributesOfItemAtPath:filePath
                                    error:nil];
    if ( attributes == nil ) {
        return YES;
    }
    else {
        NSDate *date = [attributes fileModificationDate];
        S3GetObjectMetadataRequest *request  = nil;
        S3GetObjectMetadataResponse *response = nil;

        request = [[S3GetObjectMetadataRequest alloc] initWithKey:imageName
                                                       withBucket:bucketName];

        response = [s3 getObjectMetadata:request];
        if ( [response.lastModified compare:date] == NSOrderedDescending ) {
            return YES;
        }
        else {
            return NO;
        }
    }
}

Android

private boolean isNewImageAvailable( AmazonS3Client s3, 
                          String imageName, 
                          String bucketName ) {
    File file = new File( this.getApplicationContext().getFilesDir(), 
              imageName );
    if ( !file.exists() ) {
        return true;
    }

    ObjectMetadata metadata = s3.getObjectMetadata( bucketName, 
                                                              imageName );
    long remoteLastModified = metadata.getLastModified().getTime();

    if ( file.lastModified() < remoteLastModified ) {
        return true;
    }
    else {
        return false;
    }
}

Getting an Image from Amazon S3

To retrieve an image from Amazon S3, we use the GetObject operation as shown in the following code. The code also stores the image data locally.

iOS

-(void)getRemoteImage:(AmazonS3Client*)s3
             withName:(NSString*)imageName
           fromBucket:(NSString*)bucketName
{
    S3GetObjectRequest *request = 
       [[S3GetObjectRequest alloc] initWithKey:imageName withBucket:bucketName];
    S3GetObjectResponse *response = [s3 getObject:request];
    
    [self storeImageLocally:response.body withName:imageName];
}

Android

private void getRemoteImage( AmazonS3Client s3, 
                    String imageName, 
                    String bucketName ) {
    S3Object object = s3.getObject( bucketName, imageName );
    this.storeImageLocally( object.getObjectContent(), imageName );
}

Storing the Image Locally

As stated previously, once we have the image, we want to store it in the app’s local file store. This local copy of the image allows us to display an image without network connectivity and helps us determine when we need to refresh the image from Amazon S3.

iOS

-(void)storeImageLocally:(NSData*)imageData withName:(NSString*)imageName
{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                         NSUserDomainMask,
                                                         YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    
    NSArray *pathComponents = [NSArray arrayWithObjects:documentsDirectory,
                               imageName,
                               nil ];
    NSString *filePath = [NSString pathWithComponents:pathComponents];
    [imageData writeToFile:filePath atomically:YES];
}

Make sure to mark the file as not backed up to iCloud by following these instructions.

Android

private void storeImageLocally( InputStream stream, 
                   String imageName ) {
    FileOutputStream outputStream;
    try {
        outputStream = openFileOutput( imageName, 
                       Context.MODE_PRIVATE);

        int length = 0;
        byte[] buffer = new byte[1024];
        while ( ( length = stream.read( buffer ) ) > 0 ) {
            outputStream.write( buffer, 0, length );
        }

        outputStream.close();
    } 
    catch ( Exception e ) {
        Log.d( "Store Image", "Can't store image : " + e );
    } 
}

Loading the Local Image

When our local version of the image is current, we load the image from the local file store. The following code shows how to load the image.

Tip: You should include the image in the app’s assets. This allows you to load an image in the app, even if the network is not available the first time the app is launched.

iOS

-(NSData*)getLocalImage:(NSString*)imageName
{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                         NSUserDomainMask,
                                                         YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSArray *pathComponents = [NSArray arrayWithObjects:documentsDirectory,
                               imageName,
                               nil ];
    NSString *filePath = [NSString pathWithComponents:pathComponents];
    
    return [NSData dataWithContentsOfFile:filePath];
}

Android

private InputStream getLocalImage( String imageName ) {
    try {
        return openFileInput( imageName );
    }
    catch ( FileNotFoundException exception ) {
        return null;
    }
}

Update Frequency

How frequently you intend to change the image dictates how often you should check for a new version in your app. Checking for an updated image as few times as possible will minimize your Amazon S3 costs. If you deploy an app with this functionality, all you need to do is update the image in your Amazon S3 bucket (using the AWS Management Console for example) to give your app a new image.

Retrieving Content Asynchronously

In the name of brevity, the example code in this blog post performs synchronous network requests for the image. In a real-world app, synchronous network calls should not be made from the main UI thread. You can reference the following posts to learn how to make the example code asynchronous:

iOS

Android

 

Summary

This blog post showed how to create a mobile app that displays a dynamic image stored in Amazon S3. This strategy applies to more than just images. Any content or media you want to be dynamic in your app could benefit from this approach. Please let us know what you think by leaving a comment below.