일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- alarmanager
- Android
- firebase
- shceduler
- jobdispatcher
- schedule
- 빈
- Library
- Job
- workmanager
- Background
- PHP
- 검사
- livedatam
- epmty
- jobschduler
- Service
- Today
- Total
에몽이
place api 웹 쿼리 버젼 with auto complete 본문
Recently in one of my android applications I wanted to obtain the user’s location in terms of city and country that they can feed from their (edit) profile section. So one way to do this is basically have two dropdowns or dialogs (or even open an entirely new activity where the user can search through entities and select one). One of them would display all the countries in which, once a selection is made, the other one will show a restricted set of cities based on the prior country selection. Now the number of cities in the world is large, so to do this we’ll need to get a database that contains all the countries and cities and then make sure we can query that over HTTP to get the cities based on what the user types into the app (autocomplete box). We’ve to make sure the response is really quick and doesn’t cause lags. We can also bundle all the city and country data into our app but then that’ll blow up the apk size.
Anyway, while searching the web and trying out apps like Airbnb and Couchsurfing I figured Google has the Place Autocomplete service using which I can allow the user to type in a city name which’ll fetch all the cities containing that string from Google’s servers that can be presented in an AutoCompleteTextView.
So let’s see how to execute this.
Getting an API Key
The first thing to do is to get an API key from Google Developers Console. More on this here. You’ll find four different key options – server, browser, android and iOS – you should go for the browser one as according to their documentation Places API doesn’t work with Android or iOS API key and since the app is like a browser client, a server key doesn’t make sense.
Know your Limits
You should know about the usage limitation. All the information pertaining to that is available here.
Place Autocomplete Requests and Responses
You should go through the Place Autocomplete service documentationwhere you can learn everything about the requests and responses. Basically you’ll have to make a request to https://maps.googleapis.com/maps/api/place/autocomplete/output?parameters
where output
can be either json
or xml
indicating the type of expected response. There are 2 required GET
parameters which are input
that should contain the string (that the user will type) to use as the search term and key
which should contain your API key. So then the URL will look something like this:
There are other optional parameters that you can read up on but the most important one that you’ll mostly use is the types
parameter. You can pass various values to types
like:
geocode
– Sort of like broad-level (generic) addresses.address
– Fully specified addresses.establishment
– Business results.(regions)
– This will return results matchinglocality
,sublocality
,postal_code
,country
,administrative_area_level_1
,administrative_area_level_2
. Specific details for each of them can be found here.(cities)
– Return results matchinglocality
,administrative_area_level_3
.
You can try a JavaScript based Place Autocomplete demo here. In our case we only want cities, so here’s a sample request:
1 | https://maps.googleapis.com/maps/api/place/autocomplete/json?input=ban&types=(cities)&key=API_KEY |
Here’s the type of response that you should expect:
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 | { "predictions" : [ { "description" : "Bangalore, Karnataka, India", "id" : "0862832923832bfb1e46cbe843cdaa03a9ee8aa1", "matched_substrings" : [ { "length" : 3, "offset" : 0 } ], "place_id" : "ChIJbU60yXAWrjsR4E9-UejD3_g", "reference" : "CkQzAAAAUY01eTC-f7Z9vOzDiHFtEL0rLEXfgae0MfOPR8bE26gDDFceZ3AH0SNE44HK7v27noYrRKobbNiWQQ2E4HjRPBIQAM_qIcQgybTqiyKiGyZtkhoUiFs2XXjLEJ3jWUU-p_PGcbm8JH4", "terms" : [ { "offset" : 0, "value" : "Bangalore" }, { "offset" : 11, "value" : "Karnataka" }, { "offset" : 22, "value" : "India" } ], "types" : [ "locality", "political", "geocode" ] }, { "description" : "Bangarapet, Karnataka, India", "id" : "7496e1dc83b719d83d1f64dcacc1b0592bc1149b", "matched_substrings" : [ { "length" : 3, "offset" : 0 } ], "place_id" : "ChIJu4YuxDfprTsRHSqs7ubXIaw", "reference" : "CkQ0AAAAGsgimTlmv97VHo27L3dRXg0ieWui67PGx84buBY3byBdBecoLmV-IwnW93-RxFRWgQkYRsM3NCQ38UEoKG_VQBIQe7TArn4qHgds5g_Y1Jhl0hoUe0sawRU7Hazq-4jJ8_0RT5coqBY", "terms" : [ { "offset" : 0, "value" : "Bangarapet" }, { "offset" : 12, "value" : "Karnataka" }, { "offset" : 23, "value" : "India" } ], "types" : [ "locality", "political", "geocode" ] }, { "description" : "Bande Nalla Sandra, Karnataka, India", "id" : "bbdabbca92990bd63dcc6a300d54fdfdcc3f5199", "matched_substrings" : [ { "length" : 3, "offset" : 0 } ], "place_id" : "ChIJseY83P9rrjsRj2k7EToO7Qs", "reference" : "CkQ8AAAA88Q9tpa3SB4oz5t00LapY--mX1kMNOhzO6yQxATNcRTsJvseqWNvu_6xwQm15r55XjV13uvElBa-FHLisNJv-BIQ7jt7wr2lw1Cf4kYuPCXH5BoU3d_k-Oyyh75-uYd-cqsEHZjuCKA", "terms" : [ { "offset" : 0, "value" : "Bande Nalla Sandra" }, { "offset" : 20, "value" : "Karnataka" }, { "offset" : 31, "value" : "India" } ], "types" : [ "locality", "political", "geocode" ] }, { "description" : "Bandipur, Karnataka, India", "id" : "ba9f9c4fb47c1e65e8b8584a80b05c024c9abab5", "matched_substrings" : [ { "length" : 3, "offset" : 0 } ], "place_id" : "ChIJDQkpSv2tqDsRIBeczAHjVq0", "reference" : "CkQyAAAAEwbLyLxCGnxREVqs067O7MIHJuvr_gUTkBxWoGmZ4XX1xOi8x3krYrL5YelYwn-KdFCQwZw2Usrdr_14loGIfRIQc4CkJu7jq8t0K9VHO7viGxoUacOrVQYFfAS5x8s5bkQbZAQJVXQ", "terms" : [ { "offset" : 0, "value" : "Bandipur" }, { "offset" : 10, "value" : "Karnataka" }, { "offset" : 21, "value" : "India" } ], "types" : [ "locality", "political", "geocode" ] }, { "description" : "Bangkok Thailand", "id" : "56ef65d942d42054613887fd09cee596d5949359", "matched_substrings" : [ { "length" : 3, "offset" : 0 } ], "place_id" : "ChIJ82ENKDJgHTERIEjiXbIAAQE", "reference" : "CjQoAAAALKjoDM_fiySdIqLK1_B-lWKsH0CyTmaXChd-XYUs6hejfoflIwFfFvUcj12e9Ae3EhC4qhXFaJMMqAIy7-_TJvKKGhQQ7VifQF1Cpn8-bMXCceX0MaQa9A", "terms" : [ { "offset" : 0, "value" : "Bangkok" }, { "offset" : 8, "value" : "Thailand" } ], "types" : [ "locality", "political", "geocode" ] } ], "status" : "OK" } |
You’ll note that the description
key has the place name which is sort of in the format “city, state, country” or “city, country”. The place_id
data can be used to make Place Details Requests in which you can get the lat/long any several other details regarding the place.
Adding Place Autocomplete to the Android App
Adding Place Autocomplete to an Android app is fairly easy. All we have to do is issue search requests to the Place API over HTTP and parse the JSON/XML response to display the results in an AutoCompleteTextView
. The requests issued are just like any other request from any type of client like a web browser or a server-side programming language.
Creating the Request Class
We’ll create a class called PlaceAPI
that will send requests to the API over HTTP and then parse the response JSON to build a list of strings containing the description
key which contains the city/state/country names (like “Bangalore, Karnataka, India”). Here’s the class code:
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 | public class PlaceAPI { private static final String TAG = PlaceAPI. class .getSimpleName(); private static final String TYPE_AUTOCOMPLETE = "/autocomplete" ; private static final String OUT_JSON = "/json" ; private static final String API_KEY = "YOUR_API_KEY" ; public ArrayList<String> autocomplete (String input) { ArrayList<String> resultList = null ; HttpURLConnection conn = null ; StringBuilder jsonResults = new StringBuilder(); try { StringBuilder sb = new StringBuilder(PLACES_API_BASE + TYPE_AUTOCOMPLETE + OUT_JSON); sb.append( "?key=" + API_KEY); sb.append( "&types=(cities)" ); sb.append( "&input=" + URLEncoder.encode(input, "utf8" )); URL url = new URL(sb.toString()); conn = (HttpURLConnection) url.openConnection(); InputStreamReader in = new InputStreamReader(conn.getInputStream()); // Load the results into a StringBuilder int read; char [] buff = new char [ 1024 ]; while ((read = in.read(buff)) != - 1 ) { jsonResults.append(buff, 0 , read); } } catch (MalformedURLException e) { Log.e(TAG, "Error processing Places API URL" , e); return resultList; } catch (IOException e) { Log.e(TAG, "Error connecting to Places API" , e); return resultList; } finally { if (conn != null ) { conn.disconnect(); } } try { // Log.d(TAG, jsonResults.toString()); // Create a JSON object hierarchy from the results JSONObject jsonObj = new JSONObject(jsonResults.toString()); JSONArray predsJsonArray = jsonObj.getJSONArray( "predictions" ); // Extract the Place descriptions from the results resultList = new ArrayList<String>(predsJsonArray.length()); for ( int i = 0 ; i < predsJsonArray.length(); i++) { resultList.add(predsJsonArray.getJSONObject(i).getString( "description" )); } } catch (JSONException e) { Log.e(TAG, "Cannot process JSON results" , e); } return resultList; } } |
Next, make sure your layout file (for instance res/layout/activity_main.xml
) has an AutoCompleteTextView:
1 2 3 4 5 6 | <!-- City and Country Selector --> < AutoCompleteTextView android:layout_width = "match_parent" android:layout_height = "wrap_content" android:id = "@+id/autocomplete" android:hint = "Type in your Location" /> |
Also create another layout file (res/layout/autocomplete_list_item.xml
) that’ll hold the views representing each entry inside the AutoCompleteTextView:
1 2 3 4 5 6 | <? xml version = "1.0" encoding = "utf-8" ?> android:layout_width = "match_parent" android:layout_height = "wrap_content" android:padding = "10dp" android:id = "@+id/autocompleteText" /> |
Pretty cool! Now your Activity/Fragment class must set the adapter for the AutoCompleteTextView:
1 2 | AutoCompleteTextView autocompleteView = (AutoCompleteTextView) rootView.findViewById(R.id.autocomplete); autocompleteView.setAdapter( new PlacesAutoCompleteAdapter(getActivity(), R.layout.autocomplete_list_item)); |
It’s time to code the PlacesAutoCompleteAdapter
class now which should be subtype of ListAdapter
(extends) and Filterable
(implements), if you check the type of parameter specified for the setAdapter()
method in the docs.
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 | class PlacesAutoCompleteAdapter extends ArrayAdapter<String> implements Filterable { ArrayList<String> resultList; Context mContext; int mResource; PlaceAPI mPlaceAPI = new PlaceAPI(); public PlacesAutoCompleteAdapter(Context context, int resource) { super (context, resource); mContext = context; mResource = resource; } @Override public int getCount() { // Last item will be the footer return resultList.size(); } @Override public String getItem( int position) { return resultList.get(position); } @Override public Filter getFilter() { Filter filter = new Filter() { @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults filterResults = new FilterResults(); if (constraint != null ) { resultList = mPlaceAPI.autocomplete(constraint.toString()); filterResults.values = resultList; filterResults.count = resultList.size(); } return filterResults; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { if (results != null && results.count > 0 ) { notifyDataSetChanged(); } else { notifyDataSetInvalidated(); } } }; return filter; } } |
As the user types something inside the AutoCompleteTextView, the filter()
method on the Filter
object returned by getFilter()
will be called that’ll trigger performFiltering()
(asynchronously in a background thread). This method calls the autocomplete()
method on the `PlaceAPI` class which makes the HTTP calls to the Places API returning results for the string/text entered by the user. The API does a substring match against its database of places and returns a result containing predictions which are basically a list of the places found. The predictions are stored in an ArrayList
which are then used to populate the AutoCompleteTextView internally by the getView()
method of ArrayAdapter
.
You should go through the AutoCompleteTextView source code if you feel like. Also I’ve written an article on the Filter class and Filterable interface before that you should consider reading to get a deep understanding of the filtering works in the piece of code shown above.
Getting the Selection Made
When an item from the AutoCompleteTextView’s list is selected, we can get the data associated with that list item, i.e., the selection made (from the ArrayList used as the data source by the adapter).
1 2 3 4 5 6 7 8 9 | autocompleteView.setOnItemClickListener( new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // Get data associated with the specified position // in the list (AdapterView) String description = (String) parent.getItemAtPosition(position); Toast.makeText(getActivity(), description, Toast.LENGTH_SHORT).show(); } }); |
Show Powered by Google Logo
According to the Places API Policies, if you’re using the Place Autocomplete service without Google Maps, then a powered by Google logo has to be shown. Generally this logo is shown right at the end of the AutoCompleteTextView list items. The logos for various platforms are available in the policies page that you can download and use.
So how to show up the logo in the autocomplete list ? AutoCompleteTextView doesn’t have addHeaderView()
and addFooterView()
methods like the ListView
ViewGroup. So for now, one way to achieve this is by adding an extra element to the ArrayList used as the data source by the adapter and then overriding the getView()
method in the Adapter. In the getView()
if the position
is the last index (equals list.size() - 1
) then inflate a layout which contains an ImageView
whose src
is set to the google logo’s drawable file.
In practise, this is how a quick modification to our adapter implementation would look like:
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 | @Override public View getView( int position, View convertView, ViewGroup parent) { View view; //if (convertView == null) { LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (position != (resultList.size() - 1 )) view = inflater.inflate(R.layout.autocomplete_list_item, null ); else view = inflater.inflate(R.layout.autocomplete_google_logo, null ); //} //else { // view = convertView; //} if (position != (resultList.size() - 1 )) { TextView autocompleteTextView = (TextView) view.findViewById(R.id.autocompleteText); autocompleteTextView.setText(resultList.get(position)); } else { ImageView imageView = (ImageView) view.findViewById(R.id.imageView); // not sure what to do <img draggable="false" class="emoji" alt="" src="https://s.w.org/images/core/emoji/72x72/1f600.png"> } return view; } |
This looks really dirty and doesn’t make use of cache. You should implement the ViewHolder pattern for a better efficient implementation. Also a small line of code will have to be added in the performFiltering()
method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults filterResults = new FilterResults(); if (constraint != null ) { resultList = mPlaceAPI.autocomplete(constraint.toString()); // Footer resultList.add( "footer" ); filterResults.values = resultList; filterResults.count = resultList.size(); } return filterResults; } |
Notice the // Footer
comment and the line next to it. This should be enough to abide by their logo requirement policies.
Setting Timeouts for Requests
In the current situation, when the user types something in the box, our code will make API requests to Places Autocomplete service with every character typed (or removed). This can quickly eat up our usage limit. So to reduce the total number of requests made, we’ve to set a timeout for each request as something is typed into the input box. What this means is that when the user is done typing wait for a short time like 500ms or 1000ms (1s) and then make API requests.
So initially I was a little stuck on how to do this. I thought the filtering logic will have to be modified. but then that is sort of not possible. Internally, AutoCompleteTextView attaches a TextWatcher to itself whose afterTextChanged()
method keeps on calling the filter()
method on the Filter object we supply. Now the autocomplete()
method on PlaceAPI
must not be called on the UI/Main thread (since it’s a Network operation) and performFiltering()
already gets called on a background thread which must also return a FilterResults
instantly. So I guess one way to modify the performFiltering()
logic and making it work might be to create a new Handler object inside that (which’ll attach to the background HandlerThread created internally) and then post Runnables to it with a delay. Then return null or an empty FilterResults
object which will then call publishResults
where you pretty much do nothing. When the internal created handler is done executing, post it to a Handler on the main thread. But I think after waiting for 3 seconds or so, the HandlerThread will quit, so this can be a little messy.
Instead what I did was empty the filtering logic and add my own TextWatcher to achieve all of this. The TextWatcher would post the autocomplete operation to a HandlerThread that would do the job in a worker thread asynchronously. First we’ll initialize the HandlerThread and its associated Handler in the constructor.
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 | private static String TAG = MainActivity. class .getSimpleName(); private PlacesAutoCompleteAdapter mAdapter; HandlerThread mHandlerThread; Handler mThreadHandler; public ProfileFragment() { // Required empty public constructor if (mThreadHandler == null ) { // Initialize and start the HandlerThread // which is basically a Thread with a Looper // attached (hence a MessageQueue) mHandlerThread = new HandlerThread(TAG, android.os.Process.THREAD_PRIORITY_BACKGROUND); mHandlerThread.start(); // Initialize the Handler mThreadHandler = new Handler(mHandlerThread.getLooper()) { @Override public void handleMessage(Message msg) { if (msg.what == 1 ) { ArrayList<String> results = mAdapter.resultList; if (results != null && results.size() > 0 ) { mAdapter.notifyDataSetChanged(); } else { mAdapter.notifyDataSetInvalidated(); } } } }; } } |
Then add a custom text watcher:
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 | autocompleteView.addTextChangedListener( new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { final String value = s.toString(); // Remove all callbacks and messages mThreadHandler.removeCallbacksAndMessages( null ); // Now add a new one mThreadHandler.postDelayed( new Runnable() { @Override public void run() { // Background thread mAdapter.resultList = mAdapter.mPlaceAPI.autocomplete(value); // Footer if (mAdapter.resultList.size() > 0 ) mAdapter.resultList.add( "footer" ); // Post to Main Thread mThreadHandler.sendEmptyMessage( 1 ); } }, 500 ); } @Override public void afterTextChanged(Editable s) { doAfterTextChanged(); } }); |
Then finally in onDestroy()
we should quit the HandlerThread:
1 2 3 4 5 6 7 8 9 10 | @Override public void onDestroy() { super .onDestroy(); // Get rid of our Place API Handlers if (mThreadHandler != null ) { mThreadHandler.removeCallbacksAndMessages( null ); mHandlerThread.quit(); } } |
To learn more about HandlerThreads, you should read this article that I wrote sometime back.
Make sure the filtering logic, i.e., performFiltering()
and publishResults()
are empty. You can return null or an empty FilterResults
object from performFiltering()
.
Now both the internal TextWatcher and the one you add will keep on executing. The internal one will try to filter but since the filtering logic is empty, nothing will happen. The custom one that we added will do the actual filtering by modifying the data source (ArrayList) of the Adapter and triggering the notifyDataSetChanged()
.
API Keys
There’s some information regarding where you should store your API Key at the end of this article that you should go through once.
Summary
The Google Place Autocomplete service makes it really simple to integrate a widget where the user can select his location that the app can store and later use for various purposes. If you think about it carefully, Google’s service is only being used to fetch a set of results in JSON format. So replacing that with your own API service will be fairly easy in terms of Android code, i.e., you’ll just need to make few changes in the PlaceAPI
class. This is the power of separation and abstraction. If you think you’ll surpass Google’s request limit or cannot afford them, then consider building your own service using some free/paid databases provided by various vendors online.
'android' 카테고리의 다른 글
구글 마커 클러스터링 (0) | 2016.12.24 |
---|---|
구글 마커 커스터마이징 (0) | 2016.12.24 |
resource->arrays->2차배열 만들기 (0) | 2016.12.24 |
SQLITE (0) | 2016.12.24 |
페이스북 로그인 (0) | 2016.12.24 |