Navigation

Twilio Android Tutorial

This tutorial demonstrates how to build a MongoDB Stitch app with the Android client that integrates with the Twilio SMS messaging service. The Quote of the Day app uses the Stitch Android SDK to anonymously log in users, then allows them to send a quote of their choice, stored in a MongoDB database, to any other Twilio-registered phone number.

You can apply the procedure used in this tutorial as a guideline for integrating the Stitch Android SDK and the Twilio service in your own applications.

Prerequisites

This tutorial assumes the the following:

  • Familiarity with Android Studio and Android SDK

  • Familiarity with Java

  • A Twilio account

  • An accessible MongoDB Atlas cluster using MongoDB version 3.4+. Quote of the Day stores its quotes in an Atlas Free Tier cluster. Each document in the qotd.quotes collection has the following schema:

    {
       "_id" : <ObjectID>,
       "quote" : <string>,
    }
    
  • A MongoDB Stitch app. See Getting Started for instructions.

  • A MongoDB service configured for your MongoDB Stitch app. See Integrate an Atlas Cluster with MongoDB Stitch for instructions.

Procedure

1

Add a Twilio service in the MongoDB Stitch Admin console.

Estimated Time to Complete: ~2 minutes

  1. Click Add Service in the left navigation pane.
  2. Select the Twilio box and enter tw1 in the Service Name box.
  3. Enter your Twilio account SID in the SID box and your Twilio Auth Token in the Auth Token box.
  4. Click Save.
2

Add values for your application phone numbers in the Stitch Admin console.

Estimated Time to Complete: ~5 minutes

  1. Click the Values view in the left navigation pane.

    Note

    See Values for more information about MongoDB Stitch values.

  2. Enter ourNumber in the New Value Name box.

  3. Enter your Twilio phone number in the box below the New Value Name box. The application sends the quote of the day SMS from this number. Your Twilio phone number can be found in your Twilio console. Your number must have the following format: "+16467981338".

    Tip

    You can modify Quote of the Day to allow multiple users to send quotes by modifying the source code to use their Twilio-registered phone numbers rather than this value.

  4. Click Save.

  5. Create a second value with the name confirmedNumbers.

  6. In the box below the New Value Name box, enter an array containing the phone number(s) to which you would like to send your quote of the day SMS. Your numbers must have the following format: "+16467981338".

  7. Click Save.

3

Configure Twilio Service

Estimated Time to Complete: ~3 minutes

  1. Click the tw1 Service view in the left navigation pane.
  2. Click the Rules tab.
  3. Click Add Rule.
  4. Check Send in the Actions row.

Note

The action attribute of a PipelineStage object must also appear in lower case in the source code when executing the pipeline. For example:

_client.executePipeline(new PipelineStage("send", "tw1", map))
   .addOnCompleteListener(this, new OnCompleteListener<List<Object>>() {
   ...
}
  1. Enter the following rule in the When box:

    {
       "from": "%%values.ourNumber",
       "to" : { "%in": "%%values.confirmedNumbers" }
    }
    

    This rule requires that the from argument always be our Twilio number and limits the to argument to those numbers entered in the confirmedNumbers array value.

  2. Click Save.

Note

See Twilio Service for more information about this Stitch service.

4
5

Add required import statements to your Android project.

Estimated Time to Complete: ~2 minutes

The following import statements include Stitch APIs used for anonymous authentication, retrieving documents from a MongoDB database, and constructing and executing a Stitch pipeline:

import android.content.Context;
import com.mongodb.stitch.android.StitchClient;
import com.mongodb.stitch.android.PipelineStage;
import com.mongodb.stitch.android.services.mongodb.MongoClient;
import com.mongodb.stitch.android.services.mongodb.MongoClient.Collection;
// Used to represent MongoDB Documents
import org.bson.Document;

// used for authentication and anonymous auth
import com.mongodb.stitch.android.auth.Auth;
import com.mongodb.stitch.android.auth.AvailableAuthProviders;
import com.mongodb.stitch.android.auth.anonymous.AnonymousAuthProvider;
6

Create and instantiate StitchClient and MongoClient objects.

Estimated Time to Complete: ~5 minutes

The StitchClient object handles interactions between your application and all Stitch services except MongoDB. The MongoClient object handles interactions between your application and the MongoDB service.

Create and instantiate both classes in the onCreate() method of your application in the MainActivity.java file. Substitute your Stitch application ID for the APP_ID variable.

    // Substitute your Stitch application ID for APP_ID
    private static final String APP_ID = "APP_ID";
    private static String TAG = "MainActivity";
    private static final String MONGODB_SERVICE_NAME = "mongodb-atlas";

    private StitchClient _client;
    private MongoClient _mongoClient;

    private Iterator<String> quotes;
    private Prefs cache;

    private static Context appContext;


    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        _client = new StitchClient(this, APP_ID);
        _mongoClient = new MongoClient(_client, MONGODB_SERVICE_NAME);

        appContext = getApplicationContext();

        doAnonymousAuthentication();
7

Configure authentication in the Stitch Admin console.

Estimated Time to Complete: ~1 minute

  1. Click the Authentication view in the Stitch Admin console.

  2. Verify that Allow users to log in anonymously is enabled.

    Tip

    You can your modify Quote of the Day app to require Facebook or Google authentication. See Authentication for more information or follow the Android Todo Tutorial for a step-by-step example.

8

Configure anonymous user authentication in your application.

Estimated Time to Complete: ~3 minutes

The doAnonymousAuthentication() method retrieves the authentication providers for the Stitch application and, if anonymous authentication is enabled, anonymously authenticates the user.

private void doAnonymousAuthentication() {
    final Task<AvailableAuthProviders> task = _client.getAuthProviders();
    task.addOnSuccessListener(new OnSuccessListener<AvailableAuthProviders>() {
        @Override
        public void onSuccess(final @NonNull AvailableAuthProviders availableAuthProviders) {
            if (!task.isSuccessful()) {
                Log.e(TAG, "Could not retrieve authentication providers", task.getException());
            }
            if (!availableAuthProviders.hasAnonymous())
                Log.i(TAG,"Anonymous login not allowed");
        }

    }).addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(final @NonNull Exception e) {
            Log.e(TAG, "Could not retrieve authentication providers", new Throwable(e.getMessage()));
        }
    });
    _client.logInWithProvider(new AnonymousAuthProvider());
}
9

Retrieve quotes from MongoDB and cache them.

Quote of the Day uses an instance of the Prefs class to cache the quotes retrieved from the MongoDB service.

public Task<Object> onGetQuote(final View view) {
    // Call database only once and store quotes for better performance
    final Document query = new Document();
    final Collection collection = _mongoClient.getDatabase("qotd").getCollection("quotes");
    final String quote = new String("quote");

    return collection.find(query).continueWith(new Continuation<List<Document>, Object>() {
        @Override
        public Object then(final @NonNull Task<List<Document>> task) throws Exception {
            if (task.isSuccessful()) {
                if (cache == null) {
                    String newQuote = new String();
                    cache = new Prefs(appContext);

                    // Convert quotes to a Set of Strings and cache in Prefs object
                    final List<Document> quotes = task.getResult();
                    Set<String> quoteSet = new HashSet<String>();

                    for (int i = 0; i < quotes.size(); i++) {
                        quoteSet.add(quotes.get(i).get(quote).toString());
                    }

                    cache.putQuotes(quoteSet);
10

Send a Twilio SMS.

Quote of the Day executes a pipeline to send the current quote to the specified Twilio number when a user clicks the Send button. The code calls the addOnCompleteListener() method on the returned Task object rather than continueWith() because onSendQuote() does not return the Task object to the caller.

    final Map<String, Object> map = new HashMap<>();

    if (findViewById(R.id.sms) != null) {
        final TextView smsButton = (TextView) findViewById(R.id.sms);
        String sms = (String) smsButton.getText().toString();
        map.put("to", sms);
    }
    if (findViewById(R.id.quote) != null) {
        final TextView buttonText = (TextView) findViewById(R.id.quote);
        final String quote = (String) buttonText.getText().toString();
        map.put("body", quote);
    }

    // The following value is specified in the Values view in the Stitch console.
    map.put("from","%%values.ourNumber");

    // The "send" action must appear in a string in lower case.
    _client.executePipeline(new PipelineStage("send", "tw1", map))
        .addOnCompleteListener(this, new OnCompleteListener<List<Object>>() {
        @Override
        public void onComplete(final @NonNull Task<List<Object>> task) {
            if (task.isSuccessful()) {
                Log.i(TAG, "Successfully executed pipeline.");
            } else {
                Log.e(TAG, "Failed to execute pipeline.");
            }
        }
    });
}

Quote of the Day Application Code

MainActivity.java

This activity first performs anonymous authentication of the user, then caches the quotes from a MongoDB database. The onSendQuote method executes a MongoDB Stitch pipeline when the user clicks the associated button in the user interface. See Pipelines for more information about MongoDB Stitch pipelines.

  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
package com.example.mongoqotd;

import android.app.Application;
import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import com.google.android.gms.tasks.OnCompleteListener;
import com.mongodb.stitch.android.StitchClient;
import com.mongodb.stitch.android.PipelineStage;
import com.mongodb.stitch.android.services.mongodb.MongoClient;
import com.mongodb.stitch.android.services.mongodb.MongoClient.Collection;

import android.support.annotation.NonNull;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.OnFailureListener;
import android.util.Log;
import android.view.View;
import android.widget.TextView;


// Used to represent MongoDB Documents
import org.bson.Document;

// used for authentication and anonymous auth
import com.mongodb.stitch.android.auth.Auth;
import com.mongodb.stitch.android.auth.AvailableAuthProviders;
import com.mongodb.stitch.android.auth.anonymous.AnonymousAuthProvider;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
import java.util.Iterator;
import java.util.HashMap;
import java.util.Map;

public class MainActivity extends AppCompatActivity {

    // Substitute your Stitch application ID for APP_ID
    private static final String APP_ID = "APP_ID";
    private static String TAG = "MainActivity";
    private static final String MONGODB_SERVICE_NAME = "mongodb-atlas";

    private StitchClient _client;
    private MongoClient _mongoClient;

    private Iterator<String> quotes;
    private Prefs cache;

    private static Context appContext;


    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        _client = new StitchClient(this, APP_ID);
        _mongoClient = new MongoClient(_client, MONGODB_SERVICE_NAME);

        appContext = getApplicationContext();

        doAnonymousAuthentication();
    }

    private void doAnonymousAuthentication() {
        final Task<AvailableAuthProviders> task = _client.getAuthProviders();
        task.addOnSuccessListener(new OnSuccessListener<AvailableAuthProviders>() {
            @Override
            public void onSuccess(final @NonNull AvailableAuthProviders availableAuthProviders) {
                if (!task.isSuccessful()) {
                    Log.e(TAG, "Could not retrieve authentication providers", task.getException());
                }
                if (!availableAuthProviders.hasAnonymous())
                    Log.i(TAG,"Anonymous login not allowed");
            }

        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(final @NonNull Exception e) {
                Log.e(TAG, "Could not retrieve authentication providers", new Throwable(e.getMessage()));
            }
        });
        _client.logInWithProvider(new AnonymousAuthProvider());
    }

    public Task<Object> onGetQuote(final View view) {
        // Call database only once and store quotes for better performance
        final Document query = new Document();
        final Collection collection = _mongoClient.getDatabase("qotd").getCollection("quotes");
        final String quote = new String("quote");

        return collection.find(query).continueWith(new Continuation<List<Document>, Object>() {
            @Override
            public Object then(final @NonNull Task<List<Document>> task) throws Exception {
                if (task.isSuccessful()) {
                    if (cache == null) {
                        String newQuote = new String();
                        cache = new Prefs(appContext);

                        // Convert quotes to a Set of Strings and cache in Prefs object
                        final List<Document> quotes = task.getResult();
                        Set<String> quoteSet = new HashSet<String>();

                        for (int i = 0; i < quotes.size(); i++) {
                            quoteSet.add(quotes.get(i).get(quote).toString());
                        }

                        cache.putQuotes(quoteSet);

                        if (findViewById(R.id.quote) != null) {
                            final TextView buttonText = (TextView) findViewById(R.id.quote);
                            // Setting the text with the first cached quote:
                            newQuote = cache.getQuote();
                            buttonText.setText(newQuote);
                        } else {
                            // Don't continue if view cannot be found
                            Log.i(TAG,"Cannot find view");
                            return task;
                        }
                        return newQuote;
                    } else {
                        // Cache is not null
                        if (findViewById(R.id.quote) != null) {
                            final TextView buttonText = (TextView) findViewById(R.id.quote);
                            // Setting the text:
                            final String newQuote = cache.getQuote();
                            buttonText.setText(newQuote);
                            return newQuote;
                        } else {
                            // Don't continue if view cannot be found
                            Log.i(TAG,"Cannot find view");
                            return task;
                        }
                    }
                }
                // Task failed
                Log.e(TAG, "Error retrieving quotes" + task.getException().getMessage(), task.getException());
                return Tasks.forException(task.getException());
            }
        });
    }

    public void onSendQuote(final View view) {
        final Map<String, Object> map = new HashMap<>();

        if (findViewById(R.id.sms) != null) {
            final TextView smsButton = (TextView) findViewById(R.id.sms);
            String sms = (String) smsButton.getText().toString();
            map.put("to", sms);
        }
        if (findViewById(R.id.quote) != null) {
            final TextView buttonText = (TextView) findViewById(R.id.quote);
            final String quote = (String) buttonText.getText().toString();
            map.put("body", quote);
        }

        // The following value is specified in the Values view in the Stitch console.
        map.put("from","%%values.ourNumber");

        // The "send" action must appear in a string in lower case.
        _client.executePipeline(new PipelineStage("send", "tw1", map))
            .addOnCompleteListener(this, new OnCompleteListener<List<Object>>() {
            @Override
            public void onComplete(final @NonNull Task<List<Object>> task) {
                if (task.isSuccessful()) {
                    Log.i(TAG, "Successfully executed pipeline.");
                } else {
                    Log.e(TAG, "Failed to execute pipeline.");
                }
            }
        });
    }
}

Prefs.java

Prefs.java acts as a cache for quotes retrieved from the MongoDB database so that the app doesn’t have to call the database more than once.

 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
package com.example.mongoqotd;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Iterator;

public class Prefs {
    private SharedPreferences prefs;
    private Iterator iterator;
    private static String TAG = "Prefs";
    final static String prefsName = new String("Quote of the Day");

    public Prefs(final Context context) {
        prefs = context.getSharedPreferences(prefsName, Context.MODE_PRIVATE);
    }

    public boolean putQuotes(final Set<String> quotes) {
        final SharedPreferences.Editor editor = prefs.edit();
        editor.putStringSet("quotes", quotes);
        return editor.commit();
    }

    public String getQuote() {

        final Set<String> set = prefs.getStringSet("quotes", new HashSet<String>());
        final List<String> liststrings = new ArrayList<String>(set);

        if (liststrings.size() > 0) {
            final String retVal = liststrings.get(0);
            try {
                liststrings.remove(retVal);
            } catch (final UnsupportedOperationException uoe) {
                Log.e(TAG, uoe.getMessage());
            } catch (final IndexOutOfBoundsException ioe) {
                Log.e(TAG,ioe.getMessage());
            }

            putQuotes(new HashSet<>(liststrings));

            return retVal;
        } else {
            return "Out of quotes!";
        }

    }
}

activity_main.xml

This is the xml file used to render the front-end application view.

 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
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.mongoqotd.MainActivity">

    <TextView
        android:id="@+id/quote"
        android:layout_width="214dp"
        android:layout_height="29dp"
        android:ems="10"
        android:text="@string/quote_default"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="@+id/button1"
        app:layout_constraintLeft_toRightOf="@+id/button1" />

    <Button
        android:id="@+id/button1"
        android:layout_width="116dp"
        android:layout_height="48dp"
        android:text="@string/button1_text"
        app:layout_constraintBaseline_toBaselineOf="@+id/quote"
        app:layout_constraintRight_toLeftOf="@+id/sms"
        android:onClick="onGetQuote"/>

    <EditText
        android:id="@+id/sms"
        android:layout_width="197dp"
        android:layout_height="42dp"
        android:ems="10"
        android:hint="@string/phone_default"
        android:inputType="phone"
        app:layout_constraintBottom_toBottomOf="@+id/button2"
        app:layout_constraintLeft_toRightOf="@+id/button2" />

    <Button
        android:id="@+id/button2"
        android:layout_width="115dp"
        android:layout_height="48dp"
        android:text="@string/button2_text"
        android:onClick="onSendQuote"
        tools:layout_constraintTop_creator="1"
        android:layout_marginStart="16dp"
        app:layout_constraintTop_toBottomOf="@+id/quote"
        tools:layout_constraintLeft_creator="1"
        app:layout_constraintLeft_toLeftOf="parent" />

</android.support.constraint.ConstraintLayout>

build.gradle

This is the build file used to compile the source code.

Important

The minimum supported Android API level for MongoDB Stitch is 19.

 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
apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.example.quoteoftheday"
        minSdkVersion 19
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

repositories {

    // TODO: Remove once BSON 3.5.0 is released
    maven {
        url "https://oss.sonatype.org/content/repositories/snapshots"
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    compile 'org.mongodb:stitch:0.1.0-SNAPSHOT'
}