user2196650
user2196650

Reputation: 47

AWS Android SDK Metadata support issue

So I've got a simple Android App that can upload a file to an AWS S3 bucket, I'm looking to see how I can go about storing the filename in the object URL using metadata support from AWS.

How would I got about doing so?

Below is my MainActivity.java file:

package com.example.s3imagetest;

import java.net.URL;
import java.util.Date;


import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.ResponseHeaderOverrides;

public class MainActivity extends Activity {

    private AmazonS3Client s3Client = new AmazonS3Client(
            new BasicAWSCredentials(Constants.ACCESS_KEY_ID,
                    Constants.SECRET_KEY));

    private Button selectPhoto = null;
    private Button showInBrowser = null;

    private static final int PHOTO_SELECTED = 1;

    /** Called when the activity is first created. */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Log.v("OnCreate", "OnCreate Was Called");

        s3Client.setRegion(Region.getRegion(Regions.EU_WEST_1));


        setContentView(R.layout.activity_main);

        selectPhoto = (Button) findViewById(R.id.button1);
        selectPhoto.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                // Start the image picker.
                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
                intent.setType("image/*");
                startActivityForResult(intent, PHOTO_SELECTED);
            }
        });

        showInBrowser = (Button) findViewById(R.id.button2);
        showInBrowser.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                new S3GeneratePresignedUrlTask().execute();             
            }
        });
    }

    // This method is automatically called by the image picker when an image is 
    // selected.

    protected void onActivityResult(int requestCode, int resultCode,
            Intent imageReturnedIntent) {
        super.onActivityResult(requestCode, resultCode, imageReturnedIntent);

        switch (requestCode) {
        case PHOTO_SELECTED:
            if(resultCode == RESULT_OK) {
                Uri selectedImage = imageReturnedIntent.getData();
                new S3PutObjectTask().execute(selectedImage);
            }
        }
    }

    // Display an Alert message for error or failure
    protected void displayAlert(String title, String message) {
        AlertDialog.Builder confirm = new AlertDialog.Builder(this);
        confirm.setTitle(title);
        confirm.setMessage(message);

        confirm.setNegativeButton(MainActivity.this.getString(R.string.ok),
                new OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();                       
                    }
                });
        confirm.show().show();
    }

    protected void displayErrorAlert(String title, String message) {
        AlertDialog.Builder confirm = new AlertDialog.Builder(this);
        confirm.setTitle(title);
        confirm.setMessage(message);

        confirm.setNegativeButton(MainActivity.this.getString(R.string.ok), 
                new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        MainActivity.this.finish();                     
                    }
                });
        confirm.show().show();
    }

    private class S3PutObjectTask extends AsyncTask<Uri, Void, S3TaskResult> {
        ProgressDialog dialog;

        protected void onPreExecute() {
            Log.v("S3PutObjectTask", "Its Beginning ");
            dialog = new ProgressDialog(MainActivity.this);
            dialog.setMessage(MainActivity.this.getString(R.string.uploading));
            dialog.setCancelable(false);
            dialog.show();
            Log.v("S3PutObjectTask", "onPreExecute Done");
        }


        protected S3TaskResult doInBackground(Uri... uris) {
            Log.v("S3TaskResult", "Uri" + uris);
            if (uris == null || uris.length != 1) {
                return null;
            }

            // The file location of the image selected.
            Uri selectedImage = uris[0];

            ContentResolver resolver = getContentResolver();
            String fileSizeColumn[] = { OpenableColumns.SIZE };

            Cursor cursor = resolver.query(selectedImage, fileSizeColumn, null,
                    null, null);

            cursor.moveToFirst();

            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
            String size = null;
            if (!cursor.isNull(sizeIndex)) {
                size = cursor.getString(sizeIndex);
            }

            cursor.close();

            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(resolver.getType(selectedImage));

            if (size != null) {
                metadata.setContentLength(Long.parseLong(size));
            }

            S3TaskResult result = new S3TaskResult();

            // Put the image data into S3.
            try {
                Log.v("PutData into S3", "Data has bee sent");
                //s3Client.createBucket(Constants.getPictureBucket());

                PutObjectRequest por = new PutObjectRequest(
                        Constants.PICTURE_BUCKET, Constants.PICTURE_NAME,
                        resolver.openInputStream(selectedImage), metadata);
                Log.v("PubtObjectRequest", "Selected Image");
                s3Client.putObject(por);
                Log.v("S3Client.putOb", "por");
                Toast.makeText(getApplicationContext(), "Photo Upload Success", Toast.LENGTH_LONG).show();

            } catch (Exception exception) {

                result.setErrorMessage(exception.getMessage());
            }

            return result;
        }

        protected void onPostExecute(S3TaskResult result) {

            dialog.dismiss();

            if (result.getErrorMessage() != null) {

                displayErrorAlert(
                        MainActivity.this
                                .getString(R.string.upload_failure_title),
                        result.getErrorMessage());
            }
        }
    }

    private class S3GeneratePresignedUrlTask extends
            AsyncTask<Void, Void, S3TaskResult> {

        protected S3TaskResult doInBackground(Void... voids) {

            S3TaskResult result = new S3TaskResult();

            try {
                // Ensure that the image will be treated as such.
                Log.v("S3GeneratePresignedUrlTask", "S3TaskResult");
                ResponseHeaderOverrides override = new ResponseHeaderOverrides();
                override.setContentType("image/jpeg");

                // Generate the pre-signed URL.

                // Added an hour's worth of milliseconds to the current time.
                Date expirationDate = new Date(
                        System.currentTimeMillis() + 3600000);
                GeneratePresignedUrlRequest urlRequest = new GeneratePresignedUrlRequest(
                        Constants.PICTURE_BUCKET, Constants.PICTURE_NAME);
                urlRequest.setExpiration(expirationDate);
                urlRequest.setResponseHeaders(override);

                URL url = s3Client.generatePresignedUrl(urlRequest);

                result.setUri(Uri.parse(url.toURI().toString()));

            } catch (Exception exception) {

                result.setErrorMessage(exception.getMessage());
            }

            return result;
        }

        protected void onPostExecute(S3TaskResult result) {

            if (result.getErrorMessage() != null) {

                displayErrorAlert(
                        MainActivity.this
                                .getString(R.string.browser_failure_title),
                        result.getErrorMessage());
            } else if (result.getUri() != null) {

                // Display in Browser.
                startActivity(new Intent(Intent.ACTION_VIEW, result.getUri()));
            }
        }
    }

    private class S3TaskResult {
        String errorMessage = null;
        Uri uri = null;

        public String getErrorMessage() {
            return errorMessage;
        }

        public void setErrorMessage(String errorMessage) {
            this.errorMessage = errorMessage;
        }

        public Uri getUri() {
            return uri;
        }

        public void setUri(Uri uri) {
            this.uri = uri;
        }
    }
    }

Upvotes: 1

Views: 224

Answers (1)

Richard Fung
Richard Fung

Reputation: 1020

From http://docs.aws.amazon.com/AWSAndroidSDK/latest/javadoc/

setUserMetadata(Map) seems like it might be what you are looking for, although I don't think this is the best way to approach this problem.

Sets the custom user-metadata for the associated object.

Amazon S3 can store additional metadata on objects by internally representing it as HTTP headers prefixed with "x-amz-meta-". Use user-metadata to store arbitrary metadata alongside their data in Amazon S3. When setting user metadata, callers should not include the internal "x-amz-meta-" prefix; this library will handle that for them. Likewise, when callers retrieve custom user-metadata, they will not see the "x-amz-meta-" header prefix.

User-metadata keys are case insensitive and will be returned as lowercase strings, even if they were originally specified with uppercase strings.

Note that user-metadata for an object is limited by the HTTP request header limit. All HTTP headers included in a request (including user metadata headers and other standard HTTP headers) must be less than 8KB.

From your description, it sounds like this is an app where users can upload photos and view them from a browser as well as the phone. However, you want it so that when the user deletes a photo from the browser, the corresponding local copy will also be deleted on the phone.

I think you could probably accomplish this with the setUserMetadata method, by passing up metadata that describes the path. However, this would be very cumbersome because the metadata is only lower case, and unfortunately the path and file names are not guaranteed to be(I think?). So, you'd need your own way of mapping lower case strings to upper and lower case strings, which adds a lot of unnecessary complexity.

I feel like it would be better if you set up a database to hold the mappings from S3 file names to their respective local paths. You could do this either on the Android device, which would be much easier but could exhibit weird behavior if the user tries the data. You could also do this on your own back end, which probably makes more sense if you are going to be sending out push notifications to your users to handle deletion anyway, since you could just push the path to them.

An even bigger potential issue is that you have to differentiate between devices, although you could solve that by just keeping a unique id associated with each device in your table. There's also the problem of what to do when the user decides to move the picture.

In any case, those things aren't really relevant to the question at hand. To answer your question, you COULD do this with the metadata but it would be ill advised since it adds a lot of complexity with seeming little upside. If you are looking for the simplest approach(handling all this client side) then I would probably suggest you try to use Cognito and DynamoDB to directly add a row to some DynamoDB table from your app. Make sure you specify the correct permissions to both allow you to do this and also to not allow any extra power to the user if you do take this approach.

Hope some of that helped!

Upvotes: 1

Related Questions