Android Dynamic Autocompletion Using the Google Places API

I just started learning the Android platform. I find that the best way to learn something is just diving in. It's my first time using Java as a language. It seems pretty familiar since I've done a bit of Processing and JavaScript, but throw in an SDK and it can get down right confusing. I had this idea for an app and it was basically a Google mashup. Naively, I thought that everything Google has to offer would be baked in. Not so much for the Google Places API. Using Places on the web is stupid easy so I was a bit disappointed when I wanted to incorporate that autocomplete into my app. Searching the interwebs, I was able to piece it together but I was really surprised that a tutorial for this specific example doesn't exist. The following code will show an editable textfield that will dynamically update the autocomplete list based on JSON from the Google Places API.

**Update, thanks to guinetik for the asynchronous suggestion. The code has been updated to reflect it.

So here we go...

First things first, in your XML layout you need to put an AutoCompleteTextField.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#dddddd">
<AutoCompleteTextView android:id="@+id/autoCompleteTextView1" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_below="@+id/textView1" android:layout_alignLeft="@+id/textView2" android:layout_marginTop="10dp" android:layout_alignRight="@+id/mapButton">
<requestFocus></requestFocus>
</AutoCompleteTextView>
</RelativeLayout>

Your AutoCompleteTextField likes a layout file for the items, so go ahead and add an XML flle to called item_list.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="10dp"
    android:textSize="16sp"
    android:textColor="#000">
</TextView>

The AutoCompleteTextField needs an ArrayAdapter for the list, so when in the OnCreate method we declare it and put some initial data in it. Here are the important parts in your java file:

        adapter = new ArrayAdapter<String>(this,R.layout.list_item);
        textView = (AutoCompleteTextView) findViewById(R.id.autoCompleteTextView1);
        adapter.setNotifyOnChange(true);
        textView.setAdapter(adapter);

The code above declares the ArrayAdapter and points it to the item_list.xml file for styling. Then we declare textView and reference the AutoCompleteTextField in our XML layout. Since we want to get updates, we tell the adapter to notify on changes. Last but not least, we set the adapter for our AutoCompleteTextField to the ArrayAdapter we created.

Right below that, we setup our text watcher. This will run every time the text changes. This is pretty crude on my part, but to limit the amount of calls to the webservice, I'm checking every 3 characters. We call GetPlaces and run it as an asynchronous task.

public void onTextChanged(CharSequence s, int start, int before, int count) {
if (count%3 == 1) {
     //we don't want to make an insanely large array, so we clear it each time
                                     adapter.clear();
                                    //create the task
     GetPlaces task = new GetPlaces();
    //now pass the argument in the textview to the task
                                    task.execute(textView.getText().toString());

}

In the asynchronous task, we do the actual JSON parsing. Here's that code:

class GetPlaces extends AsyncTask<String, Void, ArrayList<String>>
{

@Override
               // three dots is java for an array of strings
protected ArrayList<String> doInBackground(String... args)
{

Log.d("gottaGo", "doInBackground");

ArrayList<String> predictionsArr = new ArrayList<String>();

try
{

            URL googlePlaces = new URL(
            // URLEncoder.encode(url,"UTF-8");
                    "https://maps.googleapis.com/maps/api/place/autocomplete/json?input="+ URLEncoder.encode(s.toString(), "UTF-8") +"&types=geocode&language=en&sensor=true&key=<yourapikeygoeshere>");
            URLConnection tc = googlePlaces.openConnection();
            BufferedReader in = new BufferedReader(new InputStreamReader(
                    tc.getInputStream()));

            String line;
            StringBuffer sb = new StringBuffer();
                            //take Google's legible JSON and turn it into one big string.
            while ((line = in.readLine()) != null) {
            sb.append(line);
            }
                            //turn that string into a JSON object
            JSONObject predictions = new JSONObject(sb.toString());
                           //now get the JSON array that's inside that object           
            JSONArray ja = new JSONArray(predictions.getString("predictions"));

                for (int i = 0; i < ja.length(); i++) {
                    JSONObject jo = (JSONObject) ja.get(i);
                                    //add each entry to our array
                    predictionsArr.add(jo.getString("description"));
                }
} catch (IOException e)
{

Log.e("YourApp", "GetPlaces : doInBackground", e);

} catch (JSONException e)
{

Log.e("YourApp", "GetPlaces : doInBackground", e);

}

return predictionsArr;

}

//then our post

@Override
protected void onPostExecute(ArrayList<String> result)
{

Log.d("YourApp", "onPostExecute : " + result.size());
//update the adapter
adapter = new ArrayAdapter<String>(getBaseContext(), R.layout.list_item);
adapter.setNotifyOnChange(true);
//attach the adapter to textview
textView.setAdapter(adapter);

for (String string : result)
{

Log.d("YourApp", "onPostExecute : result = " + string);
adapter.add(string);
adapter.notifyDataSetChanged();

}

Log.d("YourApp", "onPostExecute : autoCompleteAdapter" + adapter.getCount());

}

}

You might notice there's some trickery there with JSONArray and JSONObject. I found this tutorial for parsing the public twitter feed. After scratching my head a bit on why that code wouldn't automagically work with Google's JSON I realized two things: Twitter gives you just one garbled line of JSON (which is fine for machines) while Google gives you beautiful multiline JSON (great for humans) and second, the Twitter feed is an array of objects, while Google Places returns an object with an array.

For those of you who like cutting and pasting, here's the whole thing:

package com.yourco.yourapp;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.ArrayList;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class Main extends Activity {
    /** Called when the activity is first created. */
public ArrayAdapter<String> adapter;
public AutoCompleteTextView textview;

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,R.layout.list_item);
        AutoCompleteTextView textView = (AutoCompleteTextView)
                findViewById(R.id.autoCompleteTextView1);
        adapter.setNotifyOnChange(true);
        textView.setAdapter(adapter);
         textView.addTextChangedListener(new TextWatcher() {

public void onTextChanged(CharSequence s, int start, int before, int count) {
if (count%3 == 1) {
adapter.clear();
                GetPlaces task = new GetPlaces();
                        //now pass the argument in the textview to the task
                                task.execute(textView.getText().toString());
        }
}

public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// TODO Auto-generated method stub

}

public void afterTextChanged(Editable s) {

}
});
       
class GetPlaces extends AsyncTask<String, Void, ArrayList<String>>
{

@Override
               // three dots is java for an array of strings
protected ArrayList<String> doInBackground(String... args)
{

Log.d("gottaGo", "doInBackground");

ArrayList<String> predictionsArr = new ArrayList<String>();

try
{

            URL googlePlaces = new URL(
            // URLEncoder.encode(url,"UTF-8");
                    "https://maps.googleapis.com/maps/api/place/autocomplete/json?input="+ URLEncoder.encode(s.toString(), "UTF-8") +"&types=geocode&language=en&sensor=true&key=<yourapikeygoeshere>");
            URLConnection tc = googlePlaces.openConnection();
            BufferedReader in = new BufferedReader(new InputStreamReader(
                    tc.getInputStream()));

            String line;
            StringBuffer sb = new StringBuffer();
                            //take Google's legible JSON and turn it into one big string.
            while ((line = in.readLine()) != null) {
            sb.append(line);
            }
                            //turn that string into a JSON object
            JSONObject predictions = new JSONObject(sb.toString());
                           //now get the JSON array that's inside that object           
            JSONArray ja = new JSONArray(predictions.getString("predictions"));

                for (int i = 0; i < ja.length(); i++) {
                    JSONObject jo = (JSONObject) ja.get(i);
                                    //add each entry to our array
                    predictionsArr.add(jo.getString("description"));
                }
} catch (IOException e)
{

Log.e("YourApp", "GetPlaces : doInBackground", e);

} catch (JSONException e)
{

Log.e("YourApp", "GetPlaces : doInBackground", e);

}

return predictionsArr;

}

//then our post

@Override
protected void onPostExecute(ArrayList<String> result)
{

Log.d("YourApp", "onPostExecute : " + result.size());
//update the adapter
adapter = new ArrayAdapter<String>(getBaseContext(), R.layout.list_item);
adapter.setNotifyOnChange(true);
//attach the adapter to textview
textView.setAdapter(adapter);

for (String string : result)
{

Log.d("YourApp", "onPostExecute : result = " + string);
adapter.add(string);
adapter.notifyDataSetChanged();

}

Log.d("YourApp", "onPostExecute : autoCompleteAdapter" + adapter.getCount());

}

}

}