This is the second tutorial in the Image Uploading with Retrofit series. In the first tutorial, we’d set up our NodeJS server on the localhost. Please refer to this tutorial before proceeding ahead to setup NodeJS server. In this tutorial, we’ll be implementing Image Uploading while showing the upload progress in our android application.
Retrofit MultiPart Image Upload Progress
We hope that you’ve successfully set up the Node JS server in the previous tutorial. In order to know the upload progress, we’ll use OkHttp.
OkHttp is handy in intercepting request and response calls. It has many recipes available here: OkHttp Recipes
We’ll be adapting one of the Recipes (Progress) in order to handle and display the Upload Progress.
The code for the ProgressRequestBody.java is given below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
package com.journaldev.androiduploadimageretrofitnodejs; import android.os.Handler; import android.os.Looper; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import okhttp3.MediaType; import okhttp3.RequestBody; import okio.BufferedSink; public class ProgressRequestBody extends RequestBody { private File mFile; private UploadCallbacks mListener; private static final int DEFAULT_BUFFER_SIZE = 2048; public interface UploadCallbacks { void onProgressUpdate(int percentage); void onError(); void onFinish(); void uploadStart(); } public ProgressRequestBody(final File file, final UploadCallbacks listener) { mFile = file; mListener = listener; mListener.uploadStart(); } @Override public MediaType contentType() { // i want to upload only images return MediaType.parse("image/*"); } @Override public long contentLength() throws IOException { return mFile.length(); } @Override public void writeTo(BufferedSink sink) throws IOException { long fileLength = mFile.length(); byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; FileInputStream in = new FileInputStream(mFile); long uploaded = 0; try { int read; Handler handler = new Handler(Looper.getMainLooper()); while ((read = in.read(buffer)) != -1) { uploaded += read; sink.write(buffer, 0, read); handler.post(new ProgressUpdater(uploaded, fileLength)); } } finally { in.close(); } } private class ProgressUpdater implements Runnable { private long mUploaded; private long mTotal; public ProgressUpdater(long uploaded, long total) { mUploaded = uploaded; mTotal = total; } @Override public void run() { try { int progress = (int) (100 * mUploaded / mTotal); if (progress == 100) mListener.onFinish(); else mListener.onProgressUpdate(progress); } catch (ArithmeticException e) { mListener.onError(); e.printStackTrace(); } } } } |
In the above code, we’ve defined UploadCallback
Interface which will be implemented in the MainActivity.java with the methods triggered on different events.
Inside the writeTo
function, we calculate the bytes uploaded. Each time, it invokes a runnable class where we trigger the callback methods by calculating the progress (based on current upload length and file length in bytes).
Now that our OKHttp RequestBody is ready, we’re ready to integrate it into our MainActivity inside the retrofit call.
Project Structure
Code
The code for the activity_main.xml layout is given below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="https://schemas.android.com/apk/res/android" xmlns:app="https://schemas.android.com/apk/res-auto" xmlns:dpv="https://schemas.android.com/apk/res-auto" xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <RelativeLayout android:id="@+id/content_main" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <TextView android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:gravity="center" android:textAppearance="@style/TextAppearance.AppCompat.Display1" /> <ImageView android:id="@+id/imageView" android:layout_width="250dp" android:layout_height="250dp" android:layout_centerInParent="true" android:adjustViewBounds="true" android:scaleType="centerCrop" /> </RelativeLayout> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" app:srcCompat="@android:drawable/ic_menu_camera" /> <FrameLayout android:id="@+id/frameLayout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal"> <android.support.design.widget.FloatingActionButton android:id="@+id/fabUpload" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" app:srcCompat="@drawable/ic_file_upload" /> </FrameLayout> </android.support.design.widget.CoordinatorLayout> |
We’ve enclosed the fabUpload
view inside a FrameLayout, since Android Support Design Library does not allow to toggle the visiblity of a FloatingActionButton present in a CoordinatorLayout due to layout anchors.
The code for the ApiService is the same as in the previous tutorial:
The code for the MainActivity.java is given below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
package com.journaldev.androiduploadimageretrofitnodejs; import android.annotation.TargetApi; import android.app.Activity; import android.content.ComponentName; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Parcelable; import android.provider.MediaStore; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.RequestBody; import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import static android.Manifest.permission.CAMERA; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; import static android.view.View.GONE; public class MainActivity extends AppCompatActivity implements View.OnClickListener, ProgressRequestBody.UploadCallbacks { ApiService apiService; Uri picUri; private ArrayList<String> permissionsToRequest; private ArrayList<String> permissionsRejected = new ArrayList<>(); private ArrayList<String> permissions = new ArrayList<>(); private final static int ALL_PERMISSIONS_RESULT = 107; private final static int IMAGE_RESULT = 200; FloatingActionButton fabCamera, fabUpload; Bitmap mBitmap; TextView textView; byte[] byteArray; FrameLayout frameLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); fabCamera = findViewById(R.id.fab); fabUpload = findViewById(R.id.fabUpload); textView = findViewById(R.id.textView); frameLayout = findViewById(R.id.frameLayout); fabCamera.setOnClickListener(this); fabUpload.setOnClickListener(this); askPermissions(); } private void askPermissions() { permissions.add(CAMERA); permissions.add(WRITE_EXTERNAL_STORAGE); permissions.add(READ_EXTERNAL_STORAGE); permissionsToRequest = findUnAskedPermissions(permissions); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (permissionsToRequest.size() > 0) requestPermissions(permissionsToRequest.toArray(new String[permissionsToRequest.size()]), ALL_PERMISSIONS_RESULT); } } private void initRetrofitClient() { OkHttpClient client = new OkHttpClient.Builder().build(); //change the ip to yours. apiService = new Retrofit.Builder().baseUrl("https://172.20.10.3:3000").client(client).build().create(ApiService.class); } public Intent getPickImageChooserIntent() { Uri outputFileUri = getCaptureImageOutputUri(); List<Intent> allIntents = new ArrayList<>(); PackageManager packageManager = getPackageManager(); Intent captureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); List<ResolveInfo> listCam = packageManager.queryIntentActivities(captureIntent, 0); for (ResolveInfo res : listCam) { Intent intent = new Intent(captureIntent); intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name)); intent.setPackage(res.activityInfo.packageName); if (outputFileUri != null) { intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri); } allIntents.add(intent); } Intent galleryIntent = new Intent(Intent.ACTION_GET_CONTENT); galleryIntent.setType("image/*"); List<ResolveInfo> listGallery = packageManager.queryIntentActivities(galleryIntent, 0); for (ResolveInfo res : listGallery) { Intent intent = new Intent(galleryIntent); intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name)); intent.setPackage(res.activityInfo.packageName); allIntents.add(intent); } Intent mainIntent = allIntents.get(allIntents.size() - 1); for (Intent intent : allIntents) { if (intent.getComponent().getClassName().equals("com.android.documentsui.DocumentsActivity")) { mainIntent = intent; break; } } allIntents.remove(mainIntent); Intent chooserIntent = Intent.createChooser(mainIntent, "Select source"); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, allIntents.toArray(new Parcelable[allIntents.size()])); return chooserIntent; } private Uri getCaptureImageOutputUri() { Uri outputFileUri = null; File getImage = getExternalFilesDir(""); if (getImage != null) { outputFileUri = Uri.fromFile(new File(getImage.getPath(), "profile.png")); } return outputFileUri; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) { ImageView imageView = findViewById(R.id.imageView); if (requestCode == IMAGE_RESULT) { String filePath = getImageFilePath(data); if (filePath != null) { frameLayout.setVisibility(GONE); mBitmap = BitmapFactory.decodeFile(filePath); getByteArrayInBackground(); imageView.setImageBitmap(mBitmap); } } } } private void getByteArrayInBackground() { Thread thread = new Thread() { @Override public void run() { ByteArrayOutputStream bos = new ByteArrayOutputStream(); mBitmap.compress(Bitmap.CompressFormat.PNG, 0, bos); byteArray = bos.toByteArray(); runOnUiThread(new Runnable() { @Override public void run() { frameLayout.setVisibility(View.VISIBLE); } }); } }; thread.start(); } private String getImageFromFilePath(Intent data) { boolean isCamera = data == null || data.getData() == null; if (isCamera) return getCaptureImageOutputUri().getPath(); else return getPathFromURI(data.getData()); } public String getImageFilePath(Intent data) { return getImageFromFilePath(data); } private String getPathFromURI(Uri contentUri) { String[] proj = {MediaStore.Audio.Media.DATA}; Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null); int column_index = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA); cursor.moveToFirst(); return cursor.getString(column_index); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable("pic_uri", picUri); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); // get the file url picUri = savedInstanceState.getParcelable("pic_uri"); } private ArrayList<String> findUnAskedPermissions(ArrayList<String> wanted) { ArrayList<String> result = new ArrayList<String>(); for (String perm : wanted) { if (!hasPermission(perm)) { result.add(perm); } } return result; } private boolean hasPermission(String permission) { if (canMakeSmores()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED); } } return true; } private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) { new AlertDialog.Builder(this) .setMessage(message) .setPositiveButton("OK", okListener) .setNegativeButton("Cancel", null) .create() .show(); } private boolean canMakeSmores() { return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1); } @TargetApi(Build.VERSION_CODES.M) @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { switch (requestCode) { case ALL_PERMISSIONS_RESULT: for (String perms : permissionsToRequest) { if (!hasPermission(perms)) { permissionsRejected.add(perms); } } if (permissionsRejected.size() > 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (shouldShowRequestPermissionRationale(permissionsRejected.get(0))) { showMessageOKCancel("These permissions are mandatory for the application. Please allow access.", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { requestPermissions(permissionsRejected.toArray(new String[permissionsRejected.size()]), ALL_PERMISSIONS_RESULT); } }); return; } } } break; } } private void multipartImageUpload() { initRetrofitClient(); try { if (byteArray != null) { File filesDir = getApplicationContext().getFilesDir(); File file = new File(filesDir, "image" + ".png"); FileOutputStream fos = new FileOutputStream(file); fos.write(byteArray); fos.flush(); fos.close(); textView.setTextColor(Color.BLUE); ProgressRequestBody fileBody = new ProgressRequestBody(file, this); MultipartBody.Part body = MultipartBody.Part.createFormData("upload", file.getName(), fileBody); RequestBody name = RequestBody.create(MediaType.parse("text/plain"), "upload"); Call<ResponseBody> req = apiService.postImage(body, name); req.enqueue(new Callback<ResponseBody>() { @Override public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) { Toast.makeText(getApplicationContext(), response.code() + " ", Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<ResponseBody> call, Throwable t) { textView.setText("Uploaded Failed!"); textView.setTextColor(Color.RED); Toast.makeText(getApplicationContext(), "Request failed", Toast.LENGTH_SHORT).show(); t.printStackTrace(); } }); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } @Override public void onClick(View view) { switch (view.getId()) { case R.id.fab: startActivityForResult(getPickImageChooserIntent(), IMAGE_RESULT); break; case R.id.fabUpload: if (mBitmap != null) multipartImageUpload(); else { Toast.makeText(getApplicationContext(), "Bitmap is null. Try again", Toast.LENGTH_SHORT).show(); } break; } } @Override public void onProgressUpdate(int percentage) { textView.setText(percentage + "%"); } @Override public void onError() { textView.setText("Uploaded Failed!"); textView.setTextColor(Color.RED); } @Override public void onFinish() { textView.setText("Uploaded Successfully"); } @Override public void uploadStart() { textView.setText("0%"); Toast.makeText(getApplicationContext(), "Upload started", Toast.LENGTH_SHORT).show(); } } |
In the above code,
We’ve used Runtime Permissions and Capturing Image From Camera And Gallery Using FileProvider.
Coming to the important differences, we’ve optimized the code such that the byte array that gets created from the Bitmap is done in a background thread in order to prevent freezing of the UI thread.
UploadCallbacks
interface is implemented and the text view updates its value while the image uploads.
The output of the above application in action is given below:
That brings an end to this tutorial. You can download the project from the link below: