Android Image Multi-Part Upload

This tutorial will guide you through building an app component for uploading image to a server. I'm gonna use PHP as my server side script to receive the image from the client.

tl;dr Github source

Prerequisite

  • OkHttp 3.2.0

Client side

First, let's create a simple UI for this purpose. A page which have two buttons; a button to choose image from gallery or taking photo via camera intent, and the upload to server button.

<?xml version="1.0" encoding="utf-8"?>  
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.aimanbaharum.camerademo.MainActivity">

        <Button
            android:id="@+id/btn_camera"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_alignParentTop="true"
            android:text="Take Picture" />

        <ImageView
            android:id="@+id/iv_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/btn_camera" />

        <Button
            android:id="@+id/btn_upload"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_toEndOf="@+id/btn_camera"
            android:layout_toRightOf="@+id/btn_camera"
            android:text="Upload Photo" />

        <TextView
            android:id="@+id/tv_url"
            android:visibility="gone"
            android:clickable="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/iv_image"/>

        <EditText
            android:id="@+id/et_response"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/tv_url"
            android:textIsSelectable="true"
            android:inputType="none"
            android:hint="Take photo, upload, response will appear here"
            android:maxHeight="200dp"
            android:textSize="14sp"
            android:typeface="monospace" />

    </RelativeLayout>
</ScrollView>

Of course, an ImageView to preview the image chosen. Also, I'm putting an EditText in the page to display reponse from the server, though this is optional.

Now, for the Java part, inflate these views in the onCreate() method of an Activity. I'm also registering a click listener for the camera button. Upload button have to be disabled to avoid any issues.

iv_image = (ImageView) findViewById(R.id.iv_image);  
btn_upload = (Button) findViewById(R.id.btn_upload);  
btn_camera = (Button) findViewById(R.id.btn_camera);  
et_response = (EditText) findViewById(R.id.et_response);  
tv_url = (TextView) findViewById(R.id.tv_url);  
btn_camera.setOnClickListener(new View.OnClickListener() {  
    @Override
    public void onClick(View v) {
        openFileChooserDialog();
    }
});
if (selectedImageUri == null) {  
    btn_upload.setClickable(false);
}

Camera permission as per Android 6.0

Now that the recent Android Marshmallow has restricted access to permission, we have to explicitly ask for users to grant for permission. This approach will not affect Android 6.0 and below.

When the user clicks on the camera button, a popup will be shown asking user to choose image from gallery or taking from camera.

private void openFileChooserDialog() {  
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle("Add Picture");
    builder.setItems(items, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            switch (which) {
                case 0:
                    initCameraPermission();
                    break;
                case 1:
                    initGalleryIntent();
                    break;
                default:
            }
        }
    });
    builder.show();
}

The first time user choose to take a photo from camera, the app will ask to access camera permission. If it has already granted for this particular permission, the app will automatically direct user to use the camera. A simple if-else statement is used in this scenario.

@TargetApi(Build.VERSION_CODES.M)
private void initCameraPermission() {  
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
            != PackageManager.PERMISSION_GRANTED) {
        if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
            Toast.makeText(this, "Permission to use Camera", Toast.LENGTH_SHORT).show();
        }
        requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
    } else {
        initCameraIntent();
    }
}

requestPermissions() is used to passed the required permission. onRequestPermissionResult() will be called for the permission result.

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {  
    if (requestCode == REQUEST_CAMERA_PERMISSION) {
        if (grantResults.length == 1 &&
                grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            initCameraIntent();
        } else {
            Toast.makeText(this, "Permission denied by user", Toast.LENGTH_SHORT).show();
        }
    } else {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

I've set a toast to show up if the user denied the camera permission access, or if granted, the app will direct user to use the camera app.

private void initCameraIntent() {  
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    File file = getOutputMediafile(1);
    selectedImageUri = Uri.fromFile(file);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, selectedImageUri);
    if (intent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(intent, REQUEST_CAMERA);
    }
}

private File getOutputMediafile(int type) {  
    File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES), getResources().getString(R.string.app_name)
    );
    if (!mediaStorageDir.exists()) {
        if (!mediaStorageDir.mkdirs()) {
            return null;
        }
    }
    String timeStamp = new SimpleDateFormat("yyyyHHdd_HHmmss").format(new Date());
    File mediaFile;
    if (type == 1) {
        mediaFile = new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timeStamp + ".png");
    } else {
        return null;
    }

    return mediaFile;
}

The selectedImageUri is a variable to store the URI of the output image file stored in device local storage; specifically inside Pictures directory as defined in getOutputMediaFile() method. This URI will be used to display the preview image in the UI I have made earlier.

Displaying a preview image

The camera and gallery intent will invoke onActivityResult() method to capture the data result of the intent. Displaying the preview will be done here.

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {  
    if (resultCode == Activity.RESULT_OK) {
        if (requestCode == REQUEST_CAMERA) {
            //
        } else if (requestCode == REQUEST_GALLERY) {
            selectedImageUri = data.getData();
        }
        mBitmap = ImageUtils.getScaledImage(selectedImageUri, this);
        setImageBitmap(mBitmap);
    }

}

private void setImageBitmap(Bitmap bm) {  
    iv_image.setImageBitmap(bm);
    btn_upload.setClickable(true);
    btn_upload.setOnClickListener(upload);
}

Don't forget to re-enable the upload button click listener.

Upload image using multi-part form

Register the btn_upload to an event listener.

View.OnClickListener upload = new View.OnClickListener() {  
    @Override
    public void onClick(View v) {
        /** Multipart upload */
        try {
            execMultipartPost();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
};

Upload the image together with other information that we are going to pass to the server in a multi-part form using OkHttp library.

This snippet code shows a request body to be passed to the server.

RequestBody requestBody = new MultipartBody.Builder()  
        .setType(MultipartBody.FORM)
        .addFormDataPart("fileUploadType", "1")
        .addFormDataPart("miniType", contentType)
        .addFormDataPart("ext", file.getAbsolutePath().substring(file.getAbsolutePath().lastIndexOf(".")))
        .addFormDataPart("fileTypeName", "img")
        .addFormDataPart("clientFilePath", selectedImageUri.getPath())
        .addFormDataPart("filedata", filename + ".png", fileBody)
        .build();

Making a HTTP request has never been this easy!

    Request request = new Request.Builder()
            .url(API_URL)
            .post(requestBody)
            .build();

Calling the server is a piece of cake. Listen to the response carefully. The invisible soundwave from the server sounds so sweet! Mama mia

OkHttpClient okHttpClient = new OkHttpClient();  
okHttpClient.newCall(request).enqueue(new Callback() {  
    @Override
    public void onFailure(Call call, final IOException e) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                et_response.setText(e.getMessage());
                Toast.makeText(MainActivity.this, "nah", Toast.LENGTH_SHORT).show();
            }
        });
    }

    @Override
    public void onResponse(Call call, final Response response) throws IOException {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    et_response.setText(response.body().string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
                tv_url.setVisibility(View.VISIBLE);
                tv_url.setText(Html.fromHtml("<a href=\"http://api.aimanbaharum.com/shoppermate-test/uploads/" + filename + ".png\">See image in browser</a>"));
                tv_url.setMovementMethod(LinkMovementMethod.getInstance());
                Toast.makeText(MainActivity.this, "response: " + response, Toast.LENGTH_LONG).show();
            }
        });
    }
});

Also, don't forget to set permission in manifest file.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />  
<uses-permission android:name="android.permission.CAMERA" />  
<uses-permission android:name="android.permission.INTERNET" />  

Server side

Take this code. Eat it. Upload those naughty picture of yours to my server over here. Hehe, jk. Also, chmod 755 your uploads/ dir to make it functional.

<?php

$data = array();
$target_dir = "uploads/";

$target_file = $target_dir . basename($_FILES['filedata']['name']);
$imageFileType = pathinfo($target_file, PATHINFO_EXTENSION);
$uploadOk = 1;
$check = getimagesize($_FILES['filedata']['tmp_name']);

header('Content-Type: application/json');  
if ($check !== false) {  
//  echo json_encode(array('status' => $check['mime']));
  $uploadOk = 1;
} else {  
//  echo json_encode(array('status' => 'upload failed'));
  $uploadOk = 0;
}

if (file_exists($target_file)) {  
  echo json_encode(array('status' => 'file already exists'));
  $uploadOk = 0;
}

if ($uploadOk == 0) {  
  //
} else {
  if (move_uploaded_file($_FILES['filedata']['tmp_name'], $target_file)) {  
    echo json_encode(array('status' => basename($_FILES['filedata']['name']) . " has been uploaded",
    'file_url' => 'http://api.aimanbaharum.com/uploads/' . $_FILES['filedata']['name']));
  } else {
    echo json_encode(array('status' => 'upload failed. check permission please.'));
  }

}

Those sweet response will show up in the EditText in client's app. Enjoy your shitty app!

Aiman Baharum

More about this blog https://github.com/aimanbaharum/random-wiki/wiki

Kuala Lumpur, Malaysia http://www.aimanbaharum.com

Subscribe to Knowledge Log

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!