Java tutorial
/* * Copyright 2011 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package edu.utexas.quietplaces.services; import android.app.IntentService; import android.content.*; import android.content.SharedPreferences.Editor; import android.location.Location; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.BatteryManager; import android.os.Bundle; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import edu.utexas.quietplaces.Config; import edu.utexas.quietplaces.PlacesConstants; import edu.utexas.quietplaces.content_providers.PlaceDetailsContentProvider; import edu.utexas.quietplaces.content_providers.PlacesContentProvider; import edu.utexas.quietplaces.utils.PlatformSpecificImplementationFactory; import edu.utexas.quietplaces.utils.base.SharedPreferenceSaver; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import javax.net.ssl.HttpsURLConnection; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; /** * Service that requests a list of nearby locations from the underlying web service. */ public class PlacesUpdateService extends IntentService { protected static String TAG = Config.PACKAGE_NAME + ".PlacesUpdateService"; protected ContentResolver contentResolver; protected SharedPreferences prefs; private SharedPreferenceSaver sharedPreferenceSaver; protected Editor prefsEditor; protected ConnectivityManager cm; protected boolean lowBattery = false; protected boolean mobileData = false; protected int prefetchCount = 0; public PlacesUpdateService() { super(TAG); setIntentRedeliveryMode(false); } /** * Set the Intent Redelivery mode to true to ensure the Service starts "Sticky" * Defaults to "true" on legacy devices. */ protected void setIntentRedeliveryMode(boolean enable) { } /** * Returns battery status. True if less than 10% remaining. * * @param battery Battery Intent * @return Battery is low */ protected boolean getIsLowBattery(Intent battery) { float pctLevel = (float) battery.getIntExtra(BatteryManager.EXTRA_LEVEL, 1) / battery.getIntExtra(BatteryManager.EXTRA_SCALE, 1); return pctLevel < 0.15; } @Override public void onCreate() { super.onCreate(); cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); contentResolver = getContentResolver(); prefs = PreferenceManager.getDefaultSharedPreferences(this); prefsEditor = prefs.edit(); sharedPreferenceSaver = PlatformSpecificImplementationFactory.getSharedPreferenceSaver(this); } /** * {@inheritDoc} * Checks the battery and connectivity state before removing stale venues * and initiating a server poll for new venues around the specified * location within the given radius. */ @Override protected void onHandleIntent(Intent intent) { // Check if we're running in the foreground, if not, check if // we have permission to do background updates. //noinspection deprecation boolean backgroundAllowed = cm.getBackgroundDataSetting(); boolean inBackground = prefs.getBoolean(PlacesConstants.EXTRA_KEY_IN_BACKGROUND, true); if (!backgroundAllowed && inBackground) return; // Extract the location and radius around which to conduct our search. Location location = new Location(PlacesConstants.CONSTRUCTED_LOCATION_PROVIDER); int radius = PlacesConstants.PLACES_SEARCH_RADIUS; Bundle extras = intent.getExtras(); if (intent.hasExtra(PlacesConstants.EXTRA_KEY_LOCATION)) { location = (Location) (extras.get(PlacesConstants.EXTRA_KEY_LOCATION)); radius = extras.getInt(PlacesConstants.EXTRA_KEY_RADIUS, PlacesConstants.PLACES_SEARCH_RADIUS); } // Check if we're in a low battery situation. IntentFilter batIntentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent battery = registerReceiver(null, batIntentFilter); lowBattery = getIsLowBattery(battery); // Check if we're connected to a data network, and if so - if it's a mobile network. NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting(); mobileData = activeNetwork != null && activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE; // If we're not connected, enable the connectivity receiver and disable the location receiver. // There's no point trying to poll the server for updates if we're not connected, and the // connectivity receiver will turn the location-based updates back on once we have a connection. if (!isConnected) { Log.w(TAG, "Not connected!"); } else { // If we are connected check to see if this is a forced update (typically triggered // when the location has changed). boolean doUpdate = intent.getBooleanExtra(PlacesConstants.EXTRA_KEY_FORCEREFRESH, false); // If it's not a forced update (for example from the Activity being restarted) then // check to see if we've moved far enough, or there's been a long enough delay since // the last update and if so, enforce a new update. if (!doUpdate) { // Retrieve the last update time and place. long lastTime = prefs.getLong(PlacesConstants.SP_KEY_LAST_LIST_UPDATE_TIME, Long.MIN_VALUE); float lastLat; try { lastLat = prefs.getFloat(PlacesConstants.SP_KEY_LAST_LIST_UPDATE_LAT, Float.MIN_VALUE); } catch (ClassCastException e) { // handle legacy version lastLat = Float.MIN_VALUE; } float lastLng; try { lastLng = prefs.getFloat(PlacesConstants.SP_KEY_LAST_LIST_UPDATE_LNG, Float.MIN_VALUE); } catch (ClassCastException e) { lastLng = Float.MIN_VALUE; } Location lastLocation = new Location(PlacesConstants.CONSTRUCTED_LOCATION_PROVIDER); lastLocation.setLatitude(lastLat); lastLocation.setLongitude(lastLng); long currentTime = System.currentTimeMillis(); float distanceMovedSinceLast = lastLocation.distanceTo(location); long elapsedTime = currentTime - lastTime; Log.i(TAG, "Last Location in places update service: " + lastLocation + " distance to current: " + distanceMovedSinceLast + " - current: " + location); if (location == null) { Log.w(TAG, "Location is null..."); } // If update time and distance bounds have been passed, do an update. else if (lastTime < currentTime - PlacesConstants.MAX_TIME_BETWEEN_PLACES_UPDATE) { Log.i(TAG, "Time bounds passed on places update, " + elapsedTime / 1000 + "s elapsed"); doUpdate = true; } else if (distanceMovedSinceLast > PlacesConstants.MIN_DISTANCE_TRIGGER_PLACES_UPDATE) { Log.i(TAG, "Distance bounds passed on places update, moved: " + distanceMovedSinceLast + " meters, time elapsed: " + elapsedTime / 1000 + "s"); doUpdate = true; } else { Log.d(TAG, "Time/distance bounds not passed on places update. Moved: " + distanceMovedSinceLast + " meters, time elapsed: " + elapsedTime / 1000 + "s"); } } if (location == null) { Log.e(TAG, "null location in onHandleIntent"); } else if (doUpdate) { // Refresh the prefetch count for each new location. prefetchCount = 0; // Remove the old locations - TODO: we flush old locations, but if the next request // fails we are left high and dry removeOldLocations(location, radius); // Hit the server for new venues for the current location. refreshPlaces(location, radius, null); // Tell the main activity about the new results. Intent placesUpdatedIntent = new Intent(Config.ACTION_PLACES_UPDATED); LocalBroadcastManager.getInstance(this).sendBroadcast(placesUpdatedIntent); } else { Log.i(TAG, "Place List is fresh: Not refreshing"); } // Retry any queued checkins. /* Intent checkinServiceIntent = new Intent(this, PlaceCheckinService.class); startService(checkinServiceIntent); */ } Log.d(TAG, "Place List Download Service Complete"); } /** * Polls the underlying service to return a list of places within the specified * radius of the specified Location. * * @param location Location * @param radius Radius */ protected void refreshPlaces(Location location, int radius, String page_token) { if (location == null) { Log.e(TAG, "Null location in refreshPlaces"); return; } // Log to see if we'll be prefetching the details page of each new place. if (mobileData) { Log.d(TAG, "Not prefetching due to being on mobile"); } else if (lowBattery) { Log.d(TAG, "Not prefetching due to low battery"); } long currentTime = System.currentTimeMillis(); URL url; String placeTypes = prefs.getString(PlacesConstants.SP_KEY_API_PLACE_TYPES, PlacesConstants.SP_KEY_API_PLACE_TYPES_DEFAULT); Log.v(TAG, "Doing places search with types: " + placeTypes); try { // Should be at most 6 decimal places for max cache usage, but 5 is fine too // https://developers.google.com/maps/documentation/business/articles/usage_limits String locationStr = String.format("%.5f,%.5f", location.getLatitude(), location.getLongitude()); String baseURI = PlacesConstants.PLACES_LIST_BASE_URI; String placesFeed; if (page_token != null && page_token.length() > 0) { // Other params are actually ignored here. placesFeed = baseURI + PlacesConstants.getPlacesAPIKey(this, true) + "&pagetoken=" + page_token; } else { placesFeed = baseURI + "&types=" + placeTypes + "&location=" + locationStr + "&radius=" + radius + PlacesConstants.getPlacesAPIKey(this, true); } url = new URL(placesFeed); Log.w(TAG, "HTTP request: " + url); // Open the connection URLConnection connection = url.openConnection(); HttpsURLConnection httpConnection = (HttpsURLConnection) connection; int responseCode = httpConnection.getResponseCode(); /* if (connection.getUseCaches()) { Log.v(TAG, "Using HTTPS cache"); } else { Log.v(TAG, "HTTPS cache is disabled"); } */ if (responseCode == HttpURLConnection.HTTP_OK) { // Use the XML Pull Parser to extract each nearby location. // TODO Replace the XML parsing to extract your own place list. InputStream in = httpConnection.getInputStream(); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); XmlPullParser xpp = factory.newPullParser(); int placesAddedThisRequest = 0; String next_page_token = ""; xpp.setInput(in, null); int eventType = xpp.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("next_page_token")) { next_page_token = xpp.nextText(); } else if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("result")) { eventType = xpp.next(); String id = ""; String name = ""; String vicinity = ""; String types = ""; String locationLat = ""; String locationLng = ""; String viewport = ""; String icon = ""; String reference = ""; while (!(eventType == XmlPullParser.END_TAG && xpp.getName().equals("result"))) { if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("name")) name = xpp.nextText(); else if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("vicinity")) vicinity = xpp.nextText(); else if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("type")) types = types.equals("") ? xpp.nextText() : types + " " + xpp.nextText(); else if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("lat")) locationLat = xpp.nextText(); else if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("lng")) locationLng = xpp.nextText(); else if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("icon")) icon = xpp.nextText(); else if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("reference")) reference = xpp.nextText(); else if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("id")) id = xpp.nextText(); else if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("next_page_token")) next_page_token = xpp.nextText(); eventType = xpp.next(); } Location placeLocation = new Location(PlacesConstants.CONSTRUCTED_LOCATION_PROVIDER); placeLocation.setLatitude(Double.valueOf(locationLat)); placeLocation.setLongitude(Double.valueOf(locationLng)); Log.v(TAG, "Found place: " + " location: " + location + " id: " + id + " name: " + name + " vicinity: " + vicinity + " types: " + types + " ref: " + reference); if (!next_page_token.equals("")) { Log.e(TAG, "WARNING: unhandled next_page_token from Places search " + next_page_token); } // Add each new place to the Places Content Provider addPlace(location, id, name, vicinity, types, placeLocation, viewport, icon, reference, currentTime); placesAddedThisRequest++; } eventType = xpp.next(); } if (placesAddedThisRequest > 0) { Log.i(TAG, "Found " + placesAddedThisRequest + " places this request."); if (!next_page_token.equals("")) { // TODO: we should check for INVALID_RESULT and retry after a shorter wait // Currently, if this wait is too long, we waste time, but if it's too short, we don't get the next page. Log.d(TAG, "Sleeping before fetching next page. Sleep interval (ms): " + PlacesConstants.PLACES_NEXT_PAGE_INTERVAL_MS); SystemClock.sleep(PlacesConstants.PLACES_NEXT_PAGE_INTERVAL_MS); Log.i(TAG, "Fetching next page of places results."); refreshPlaces(location, radius, next_page_token); } } else { Log.w(TAG, "Found 0 places this request."); } // Remove places from the PlacesDetailsContentProvider that aren't from this update. String where = PlaceDetailsContentProvider.KEY_LAST_UPDATE_TIME + " < " + currentTime; contentResolver.delete(PlacesContentProvider.CONTENT_URI, where, null); // Save the last update time and place to the Shared Preferences. prefsEditor.putFloat(PlacesConstants.SP_KEY_LAST_LIST_UPDATE_LAT, (float) location.getLatitude()); prefsEditor.putFloat(PlacesConstants.SP_KEY_LAST_LIST_UPDATE_LNG, (float) location.getLongitude()); prefsEditor.putLong(PlacesConstants.SP_KEY_LAST_LIST_UPDATE_TIME, System.currentTimeMillis()); sharedPreferenceSaver.savePreferences(prefsEditor, false); //prefsEditor.apply(); //prefsEditor.commit(); } else Log.e(TAG, responseCode + ": " + httpConnection.getResponseMessage()); } catch (MalformedURLException e) { Log.e(TAG, e.getMessage()); } catch (IOException e) { Log.e(TAG, e.getMessage()); } catch (XmlPullParserException e) { Log.e(TAG, e.getMessage()); } finally { } } /** * Adds the new place to the {@link PlacesContentProvider} using the values passed in. * TODO Update this method to accept and persist the place information your service provides. * * @param currentLocation Current location * @param id Unique identifier * @param name Name * @param vicinity Vicinity * @param types Types * @param location Location * @param viewport Viewport * @param icon Icon * @param reference Reference * @param currentTime Current time * @return Successfully added */ protected boolean addPlace(Location currentLocation, String id, String name, String vicinity, String types, Location location, String viewport, String icon, String reference, long currentTime) { // Contruct the Content Values ContentValues values = new ContentValues(); values.put(PlacesContentProvider.KEY_ID, id); values.put(PlacesContentProvider.KEY_NAME, name); double lat = location.getLatitude(); double lng = location.getLongitude(); values.put(PlacesContentProvider.KEY_LOCATION_LAT, lat); values.put(PlacesContentProvider.KEY_LOCATION_LNG, lng); values.put(PlacesContentProvider.KEY_VICINITY, vicinity); values.put(PlacesContentProvider.KEY_TYPES, types); values.put(PlacesContentProvider.KEY_VIEWPORT, viewport); values.put(PlacesContentProvider.KEY_ICON, icon); values.put(PlacesContentProvider.KEY_REFERENCE, reference); values.put(PlacesContentProvider.KEY_LAST_UPDATE_TIME, currentTime); // Calculate the distance between the current location and the venue's location float distance = 0; if (currentLocation != null && location != null) distance = currentLocation.distanceTo(location); values.put(PlacesContentProvider.KEY_DISTANCE, distance); // Update or add the new place to the PlacesContentProvider String where = PlacesContentProvider.KEY_ID + " = '" + id + "'"; boolean result = false; try { if (contentResolver.update(PlacesContentProvider.CONTENT_URI, values, where, null) == 0) { if (contentResolver.insert(PlacesContentProvider.CONTENT_URI, values) != null) result = true; } else result = true; } catch (Exception ex) { Log.e("PLACES", "Adding " + name + " failed."); } // If we haven't yet reached our prefetching limit, and we're either // on WiFi or don't have a WiFi-only prefetching restriction, and we // either don't have low batter or don't have a low battery prefetching // restriction, then prefetch the details for this newly added place. /* if ((prefetchCount < PlacesConstants.PREFETCH_LIMIT) && (!PlacesConstants.PREFETCH_ON_WIFI_ONLY || !mobileData) && (!PlacesConstants.DISABLE_PREFETCH_ON_LOW_BATTERY || !lowBattery)) { prefetchCount++; // Start the PlaceDetailsUpdateService to prefetch the details for this place. // As we're prefetching, don't force the refresh if we already have data. Intent updateServiceIntent = new Intent(this, PlaceDetailsUpdateService.class); updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_REFERENCE, reference); updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_ID, id); updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_FORCEREFRESH, false); startService(updateServiceIntent); } */ return result; } /** * Remove stale place detail records unless we've set the persistent cache flag to true. * This is typically the case where a place has actually been viewed rather than prefetched. * * @param location Location * @param radius Radius */ protected void removeOldLocations(Location location, int radius) { // Stale Detail Pages long minTime = System.currentTimeMillis() - PlacesConstants.MAX_DETAILS_UPDATE_LATENCY; String where = PlaceDetailsContentProvider.KEY_LAST_UPDATE_TIME + " < " + minTime + " AND " + PlaceDetailsContentProvider.KEY_FORCE_CACHE + " = 0"; contentResolver.delete(PlaceDetailsContentProvider.CONTENT_URI, where, null); Log.d(TAG, "removeOldLocations"); } }