Cloudinary Blog

Impressed by WhatsApp technology? Clone WhatsApp Technology to Build a File Upload Android App

Clone WhatsApp Technology to Build a File Upload Android App

With more than one billion people using WhatsApp, the platform is becoming a go-to for reliable and secure instant messaging. Having so many users means that data transfer processes must be optimized and scalable across all platforms. WhatsApp technology is touted for its ability to achieve significant media quality preservation when traversing the network from sender to receiver, and this is no easy feat to achieve.

In this post, we will build a simple clone of WhatsApp with a focus on showcasing the background image upload process using Cloudinary’s Android SDK. The app is built using Pusher to implement real-time features. We’ll do this in two parts, first we’ll build the app with primary focus on the file upload and delivery with Cloudinary. Then in the second part, we’ll show how to apply Cloudinary’s transformation and optimization features to the images. To continue with the project, we’ll work on the assumption that you’re not new to Android development and you’ve worked with custom layouts for CompoundViews(a ListView in this case). If you have not, then check out this tutorial.

Webinar
How to Optimize for Page Load Speed

Setting up an Android Studio Project

Follow the pictures below to set up your Android project.

Create a new Android project

select minimum sdk

select an empty activity

finish the creation with the default activity names

For this tutorial, we will be using a number of third-party libraries including:

Open up your app level build.gradle file, add the following lines and sync your project :

Copy to clipboard
implementation group: 'com.cloudinary', name: 'cloudinary-android', version: '1.22.0'
implementation 'com.pusher:pusher-java-client:1.5.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.picasso:picasso:2.5.2'

Before you proceed, create Cloudinary and Pusher accounts. You will need your API credentials to enable communication between your app and Cloudinary’s servers.

Open the AndroidManifest.xml file and add the following snippet:

Copy to clipboard
<application...>
      ....
      <meta-data android:name="CLOUDINARY_URL"
      android:value="cloudinary://@myCloudName"

</application>

The metadata tag will be used for a one-time lifecycle initialization of the Cloudinary SDK. Replace the myCloudName with your cloud name, which can be found on your Cloudinary dashboard.

Set Up A Simple API Server

Next, you need to create a web server with your Pusher credentials to handle your HTTP requests. You can get them from your account dashboard.

Here’s a breakdown of what the server should do:

  • The app sends a message via HTTP to the server
  • Server receives message and emits a pusher event
  • The app then subscribes to the Pusher event and updates view

Here’s a basic example using Node.js :

Copy to clipboard
// Import Dependencies
const Express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const low = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync');
const uuid = require('uuid/v4');
const Pusher = require('pusher');
const pusher = new Pusher({
  appId: 'APP_ID',
  key: 'APP_KEY',
  secret: 'APP_SECRET',
  cluster: 'us2',
  encrypted: true
});
// Create an Express app
const app = Express();
// Configure middleware
app.use(cors());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// Configure database and use a file adapter
const adapter = new FileSync('db.json');
const db = low(adapter);
// Choose a port
app.set('port', process.env.PORT || 8050);
app.post('/messages', (req, res) => {
  // Assemble data from the requesting client
  // Also assign an id and a creation time
  const post = Object.assign({}, req.body, {
    id: uuid(),
    created_at: new Date()
  });
  // Create post using `low`
  db
    .get('messages')
    .push(post)
    .write();
  // Respond with the last post that was created
  const newMessage = db
    .get('messages')
    .last()
    .value();
  pusher.trigger('messages', 'new-message', newMessage);
  res.json(newMessage);
});
// Listen to the chosen port
app.listen(app.get('port'), _ => console.log('App at ' + app.get('port')));

With that set, we are ready to start building the app. Let’s begin by customizing the xml files to suit our needs. Open activity_chat.xml file and change its content to the one in the repository

Since we’re using ListView to show our chats, we need to create a custom ListView layout. So create a new layout resource file `message_xml` and modify its content to feature the necessary view objects required to achieve the chat view.

Next, add two vector assets. We won’t be covering how to do that here. But you can check the official Android documentation on how to do it. Now our XML files are good to go. Next, we have to start adding the application logic.

Application logic

To achieve the desired functionalities of the app, we’ll create two Java Classes Message and ListMessagesAdapter and an interface called Constants

So create a new java class called Message and modify its contents as:

Copy to clipboard
public class Message {
    public String messageType, message, messageTime, user, image;
}

Once that's done, create the Adapter Class and modify its contents as well :

Copy to clipboard
public class ListMessagesAdapter extends BaseAdapter {
private Context context;
private List<Message> messages;
public ListMessagesAdapter(Context context, List<Message> messages){
    this.context = context;
    this.messages = messages;
}
@Override
public int getCount() {
    return messages.size();
}
@Override
public Message getItem(int position) {
    return messages.get(position);
}
@Override
public long getItemId(int position) {
    return position;
}
public void add(Message message){
    messages.add(message);
    notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null){
        convertView = LayoutInflater.from(context).inflate
        (R.layout.message_layout, parent, false);
    }
    TextView messageContent = convertView.findViewById(R.id.message_content);
    TextView timeStamp = convertView.findViewById(R.id.time_stamp);
    ImageView imageSent = convertView.findViewById(R.id.image_sent);
    View layoutView = convertView.findViewById(R.id.view_layout);
    Message message = messages.get(position);
    if (message.messageType.equals(Constants.IMAGE)){
        imageSent.setVisibility(View.VISIBLE);
        messageContent.setVisibility(View.GONE);
        layoutView.setBackgroundColor(context.getResources().getColor
        (android.R.color.transparent));
        timeStamp.setTextColor(context.getResources().getColor
        (android.R.color.black));
        Picasso.with(context)
                .load(message.image)
                .placeholder(R.mipmap.ic_launcher)
                .into(imageSent);
    } else {
        imageSent.setVisibility(View.GONE);
        messageContent.setVisibility(View.VISIBLE);
    }
    timeStamp.setText(message.user);
    messageContent.setText(message.message);
    return convertView;
}
}
// updating the ListView.
public void add(Message message){
    messages.add(message);
    notifyDataSetChanged();
}
/**
This method adds a new item to our List<Messages> container and subsequently
notifies the ListView holding the adapter of the change by calling the
“notifyDataSetChanged()” method.
**/

Finally, let’s create an interface for our constant values:

Copy to clipboard
public interface Constants {
    String PUSHER_KEY = "*******************";
    String PUSHER_CLUSTER_TYPE = "us2";
    String MESSAGE_ENDPOINT = "https://fast-temple-83483.herokuapp.com/";
    String IMAGE = "image";
    String TEXT = "text";
    int IMAGE_CHOOSER_INTENT = 10001;
}

The interface file contains variables we will make reference to later in other classes. Having your constant values in the same class eases access to them. One crucial thing to note is that you need your own PUSHERKEY (you can get it from your profile dashboard on Pusher) and MESSAGEEND_POINT(representing your server link). Next, open your MainActivity.java file. Add the following method to your onCreate() method:

Copy to clipboard
@Override
protected void onCreate(Bundle savedInstanceState){
  ...
  MediaManager.init(this)
}

The entry point of the Cloudinary Android SDK is the MediaManager class. MediaManager.init(this) initiates a one-time initialization of the project with the parameters specified in our metadata tag earlier on. Suffice to say, this initialization can only be executed once per application lifecycle.

Another way to achieve this without modifying the AndroidManifest.xml file is to pass an HashMap with the necessary configuration details as the second parameter of the MediaManager.init() method:

Copy to clipboard
Map config = new HashMap();
config.put("cloud_name", "myCloudName");
MediaManager.init(this, config);

For this project, we will be sticking with the former method since we already modified our AndroidManifest.xml file.

Configure Pusher library

It’s time to configure our Pusher library. Add the following lines of code to your onCreate() method below.

Copy to clipboard
PusherOptions options = new PusherOptions();
options.setCluster(Constants.PUSHER_CLUSTER_TYPE);
Pusher pusher = new Pusher(Constants.PUSHER_KEY, options);
Channel channel = pusher.subscribe("messages");

The snippet above is self explanatory.

messages is the name of the channel you created in your server. Now, we need to subscribe to an event in the messages channel. Hence, we’ll subscribe to the new-message event.

Copy to clipboard
channel.bind("new-message", new SubscriptionEventListener() {
    @Override
    public void onEvent(String channelName, String eventName, final String data) {
        /..../
    }
});
pusher.connect();

Now, we have successfully tagged to our messages channel and subscribed to the new-message event. So, each time we send an HTTP request to the server, it redirects it to Pusher and we get notified of this “event” in our app, and we can then react to it appropriately in the onEvent(…) method.

Set Up Server Communication with Retrofit

Before we continue, we need to initialize the Retrofit library to communicate with our server.

To do this, we will create to Java files:

  • RetrofitUtils
  • Upload(an Interface)

Modify the the RetrofitUtils.java file :

Copy to clipboard
public class RetrofitUtils {
    private static Retrofit retrofit;
    public static Retrofit getRetrofit(){
        if (retrofit != null){
            return retrofit;
        }
        retrofit = new Retrofit.Builder()
                .baseUrl(Constants.MESSAGE_ENDPOINT)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        return retrofit;
    }
}

In the Upload.java file, we also set it up as so:

Copy to clipboard
public interface Upload {
    @FormUrlEncoded
    @POST("messages")
    Call<Void> message(@Field("message") String message, @Field("user")
    String user);
    @FormUrlEncoded
    @POST("messages")
    Call<Void> picture(@Field("message") String message, @Field("user")
    String user, @Field("image") String imageLink);
}

This is not a Retrofit tutorial, so I won’t be covering the basics of using the library. There are a number of Medium articles that provide those details. But you can check this article from Vogella or this one by Code TutsPlus. What you need to know, however, is the reason we are making two POST requests. The first POST request will be triggered in the case the user sends only a text. The second will be triggered in the case of picture upload.

Hence we’ll use this second POST request to handle this part of the tutorial for Image Upload and Delivery using Cloudinary.

Handling Android File Upload and Delivery with Cloudinary

Now we’ll start adding the logic and discussing how to achieve the Android file upload features using the Cloudinary account we’ve set up. Given the code complexity of this part, we’ll be walking through it with snippets and providing explanations as we go. To handle the image upload features, we’ll head back to the MainActivity.java file and set it up the onClick() method :

Copy to clipboard
case R.id.load_image:
  Intent  chooseImage = new Intent();
  chooseImage.setType("image/*");
  chooseImage.setAction(Intent.ACTION_GET_CONTENT);
  startActivityForResult(Intent.createChooser(chooseImage, "Select Picture"), Constants.IMAGE_CHOOSER_INTENT);
  break;

Here we sent an implicit intent for images upload. This would pop up a new activity to select pictures from your phone. Once that is done, we want to get the details of the selected image. This is handled in the onActivityResult(…) method. Here’s how we set up the method:

Copy to clipboard
@override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == Constants.IMAGE_CHOOSER_INTENT && resultCode == RESULT_OK){
    if (data != null && data.getData() != null){
        uri = data.getData();
        hasUploadedPicture = true;
        String localImagePath = getRealPathFromURI(uri);
        Bitmap bitmap;
        try {
            InputStream stream = getContentResolver().openInputStream(uri);
            bitmap = BitmapFactory.decodeStream(stream);
            localImage.setVisibility(View.VISIBLE);
            localImage.setImageBitmap(bitmap);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        imagePath = MediaManager.get().url().generate(getFileName(uri));
        typedMessage.setText(localImagePath);
    }
}
}

Next we set up a method that will be triggered whenever the user selects an image. Once this method executes, we will have the URI of the selected image stored in the “uri” variable. We monitor the image upload with hasUploadedPicture variable. This will be useful in determining which upload interface method to trigger. so we set it up as:

Copy to clipboard
@Override
public void onClick(View v) {
    switch (v.getId()){
        case R.id.send:
//                makeToast("Send clicked");
            if (hasUploadedPicture){
//                unsigned upload
                String requestId = MediaManager.get()
                  .upload(uri)
                  .unsigned("sample_preset")
                  .option("resource_type", "image")
                  .callback(new UploadCallback() {
                    @Override
                    public void onStart(String requestId) {
                        makeToast("Uploading...");
                    }
                    @Override
                    public void onProgress(String requestId, long bytes,
                                           long totalBytes) {
                    }
                    @Override
                    public void onSuccess(String requestId, Map resultData) {
                        makeToast("Upload finished");
                        imagePath = MediaManager.get().url()
                        .generate(resultData.get("public_id").toString()
                        .concat(".jpg"));
                        uploadToPusher();
                    }
                    @Override
                     public void onError(String requestId, ErrorInfo error) {
                        makeToast("An error occurred.\n" + error
                        .getDescription());
                    }
                    @Override
                     public void onReschedule(String requestId,
                                              ErrorInfo error) {
                        makeToast("Upload rescheduled\n" + error
                        .getDescription());
                }).dispatch();

            } else {
                upload.message(typedMessage.getText().toString(), "Eipeks"
                ).enqueue(new Callback<Void>() {
                @Override
                public void onResponse(@NonNull Call<Void> call,
                                       @NonNull Response<Void> response) {
                     switch (response.code()){
                      case 200:
                      typedMessage.setText("");
                      break;
                }
            }
                @Override
                public void onFailure(@NonNull Call<Void> call,
                @NonNull Throwable t) {
                   Toast.makeText(Chat.this, "Error uploading message",
                   Toast.LENGTH_SHORT).show();
                }
            });
         }
         break;
      case R.id.load_image:
         Intent  chooseImage = new Intent();
         chooseImage.setType("image/*");
         chooseImage.setAction(Intent.ACTION_GET_CONTENT);
         startActivityForResult(Intent.createChooser(
         chooseImage, "Select Picture"),
         Constants.IMAGE_CHOOSER_INTENT);
         break;
  }
}

At this point we have the URI of the selected image stored in the uri variable and the hasUploaded variable should now let us know whether the image upload was successful or not. With this information, we can head on back to the onClick method and upload the selected image to pusher:

Copy to clipboard
@Override
public void onClick(View v) {
    switch (v.getId()){
        case R.id.send:
//                makeToast("Send clicked");
            if (hasUploadedPicture){
                String requestId = MediaManager.get()
                .upload(uri)
                .unsigned("myPreset")
                .option("resource_type", "image")
                .callback(new UploadCallback() {
                      @Override
public void onStart(String requestId) {
   makeToast("Uploading...");
}
.........

To further explain what went on here, it is worth noting that this method is used for building our image upload request. It contains five synchronized methods:

  • get()
  • upload()
  • option()
  • callback()
  • dispatch()
  • upload() is an overloaded method however, we’ll be using upload(Uri uri) since we already have the uri of the image we want to upload.

We need to set an unsigned upload preset to upload images to our cloud without a secret key.

  • option() takes in two parameters: name and value If you are uploading a video, then your value will be video instead of image.
  • callback() method. This method is used in tracking the progress of the upload. We are using the UploadCallback(){…} as its method
  • onSuccess() method is triggered upon successful completion of the the media upload. This method contains two parameters: String requestId and Map resultData

The resultData contains information about the uploaded picture. The information we need is the uniquely generated picture name, which can be accessed from the resultData using the public_id as the key. Cloudinary also enables unique url() generation for easy access of the uploaded picture. That’s what we achieved with this bit of code

Copy to clipboard
imagePath = MediaManager.get().url().format("webp"). generate(resultData.get("public_id"));

The line ensures that we can access the image we’ve uploaded using the Retrofit library. With this, we then call the uploadToPusher() method.

Copy to clipboard
private void uploadToPusher(){
    upload.picture(typedMessage.getText().toString(), "Eipeks", imagePath)
        .enqueue(new Callback<Void>() {
            @Override
            public void onResponse(@NonNull Call<Void> call,
                                   @NonNull Response<Void> response) {
                switch (response.code()){
                    case 200:
                        localImage.setVisibility(View.GONE);
                        typedMessage.setText("");
                        break;
                }
            }
            @Override
           public void onFailure(@NonNull Call<Void> call, @NonNull Throwable t) {
               Toast.makeText(Chat.this, "Failed to upload picture\n" +
               t.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
           }
        });
}

Once this method executes, our HTTP requests reaches the server, which in turn redirects the information we’ve uploaded to Pusher. This information goes to the “messages” channel. Since, we have subscribed to the “new-messages” event, our application is notified of this event. All that’s left is for our app to react appropriately to this event. Next, we will modify our onEvent() method.

Copy to clipboard
@Override
public void onEvent(String channelName, String eventName, final String data) {
    Gson gson = new Gson();
    final Message message = gson.fromJson(data, Message.class);
    if (hasUploadedPicture){
        message.messageType = Constants.IMAGE;
    } else {
        message.messageType = Constants.TEXT;
    }
    hasUploadedPicture = false;
    messages.add(message);
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            messagesList.setSelection(messagesList.getAdapter().getCount() - 1);
        }
    });
}

This brings us to the end of this part of this tutorial, on the next part we’ll be discussing how to manipulate our uploaded images with Cloudinary to add transformations and optimizations. Here’s an image showing how the image upload works thus far:

Build a WhatsApp clone with image and video upload

Feel free to check the official documentation here. The source code for the project is on GitHub. In the next part of this article, we will cover how uploaded images can be transformed and what we can get from Cloudinary’s optimization features.

About Cloudinary

Cloudinary provides easy-to-use, cloud-based media management solutions for the world’s top brands. With offices in the US, UK and Israel, Cloudinary has quickly become the de facto solution used by developers and marketers at major companies around the world to streamline rich media management and deliver optimal end-user experiences.

For more information, visit www.cloudinary.com or follow us on Twitter


Want to Learn More About File Uploads?

Recent Blog Posts

Maya Shavin: How I Built My Website

Besides working as a senior front-end developer at Cloudinary, I'm also a content creator, a blogger, and an open-source developer. Follow me at @mayashavin and on mayashavin.com.

In the beginning, my website, mayashavin.com, was mainly for showcasing the status of my development projects and keeping me organized with my speaking schedule. Initially, I built it with Vue.js, later on switching to Nuxt.js (aka Nuxt) for a higher SEO score, and deployed it with Netlify. After some time, I added a blog section with Netlify CMS as the content management system (CMS). Everything was fine until I added more content and features, which led to a significant decline in the site’s performance. Also, the site design needed a modern look. So, I gave the site a makeover.

Read more
Automation Frees Up PetRescue’s Staff to Help Pets Find Their Forever Homes

As we spend more time at home, many of us are adopting pets for the joy, companionship and a surprising range of health benefits. In Australia, where our nonprofit customer PetRescue is located, there’s a shortage of pets to adopt. Last August, the Guardian reported that dog shelters in Australia emptied and adoption fees for puppies were running as high as $AUS1800.

Read more
Cloudinary and Contentful Make Modern Content Management Easier

I am pleased to share that Cloudinary and Contentful have joined forces to further streamline the creation, processing, and delivery of online content through Cloudinary’s digital asset management (DAM) solution and advanced transformation and delivery capabilities for images and video. What’s more, the partnership delivers a headless approach to DAM. By leveraging APIs for media management tasks, marketers and developers alike benefit from an integrated stack of optimized assets for optimization and automation. As a result, page loads are fast and beautiful, and at scale—with less overhead and effort.

Read more
Introducing Cloudinary's Nuxt Module

Since its initial release in October 2016 by the Chopin brothers as a server-side framework that runs on top of Vue.js, Nuxt (aka Nuxt.js) has gained prominence in both intuitiveness and performance. The framework offers numerous built-in features based on a modular architecture, bringing ease and simplicity to web development. Not surprisingly, Nuxt.js has seen remarkable growth in adoption by the developer community along with accolades galore. At this writing, Nuxt has earned over 30K stars on GitHub and 96 active modules with over a million downloads per month. And the upward trend is ongoing.

Read more
How Quality and Quantity can go Hand in Hand

When it comes to quality versus quantity, you’ll often hear people say, “It’s the quality that counts, not the quantity”. While that’s true in many situations, there are also cases where you want both quality and quantity. You may have thousands of images on your website and you want them all to look great. This is especially important if your website allows users to upload their own content, for example, to sell their own products or services. You don't want their poor quality images to reflect badly on your brand.

Read more