Java tutorial
/* * Copyright 2014 WNCOutdoors.info * portions Copyright 2014 The Android Open Source Project * portions Copyright 2014 Google Inc. * portions Copyright 2012 StackOverflow user breceivemail * http://stackoverflow.com/questions/3695224/android-sqlite-getting-nearest-locations-with-latitude-and-longitude * * 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. * * AppInfoSettingsFragment.java * Fragment which controls the Settings tab under the App Info activity, * and provides information about the status of the Google Play Services * library on the user's device, as well as the status of offline maps * expansion file, and ability to download that if not completed yet. */ package info.wncwaterfalls.app; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.sqlite.SQLiteQueryBuilder; import android.location.Address; import android.location.Geocoder; import android.location.Location; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarActivity; import android.text.TextUtils; import android.widget.Toast; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesClient; import com.google.android.gms.location.LocationClient; import com.google.android.gms.location.LocationListener; import com.google.android.gms.location.LocationRequest; import info.wncwaterfalls.app.ResultsListFragment.OnWaterfallQueryListener; import info.wncwaterfalls.app.ResultsListFragment.OnWaterfallSelectListener; import info.wncwaterfalls.app.ResultsMapFragment.OnLocationQueryListener; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Locale; public class ResultsActivity extends ActionBarActivity implements OnWaterfallQueryListener, OnWaterfallSelectListener, OnLocationQueryListener, GooglePlayServicesClient.ConnectionCallbacks, GooglePlayServicesClient.OnConnectionFailedListener, LocationListener { public static final String PREFS_NAME = "AppSettingsPreferences"; private static final String USER_PREF_SKIP_PLAY_SERVICES = "UserPrefSkipPlayServices"; private static final String TAG = "ResultsActivity"; private ActionBar actionBar; private boolean showListTab = true; private Short mSearchMode; // 1: waterfall, 2: hike, 3: location private String searchTerm; private short searchTrailLength; private short searchTrailDifficulty; private short searchTrailClimb; private Short searchLocationDistance; private String searchLocationRelto; private String searchLocationReltoTxt; private Boolean searchOnlyShared; private boolean mFoundOrigin = false; private Address mOriginAddress; private Location mOriginLocation; private LocationClient mLocationClient; private LocationRequest mLocationRequest; private int mNumUpdates = 0; private Fragment mLocationRequestor; public static final String SELECTED_WATERFALL_ID = "info.wncwaterfalls.app.SELECTED_WATERFALL_ID"; // Called by Location Services when the request to connect the // client finishes successfully. This will happen when the user // has requested Current Location as the search point. @Override public void onConnected(Bundle dataBundle) { // Display the connection status /* Get our location * could use: * mOriginLocation = mLocationClient.getLastLocation(); * However, with that it's a pain to wait and try again if the returned location is stale/inaccurate * Instead: subscribe to a few 1-5 second location updates. * Updates will be turned off once an accurate-ish location is determined, on the assumption * that subsequent updates will be more accurate (esp. with GPS). * Only caveat to this is timing it out if we don't get any updates at all. */ mLocationRequest = LocationRequest.create(); // Use a balanced accuracy, approximately block level // This is fine for basing a query with a radius measured in miles mLocationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY); // Get updates every 5 seconds, but no more than once per second. mLocationRequest.setInterval(5000); mLocationRequest.setFastestInterval(1000); mLocationClient.requestLocationUpdates(mLocationRequest, this); // TODO: In rare instances, onLocationChanged may never get called // If, for example, settings->location->gps only and it can't get a fix: // Need to time it out if that happens. } @Override public void onLocationChanged(Location currentLocation) { mOriginLocation = currentLocation; // Yay! // If the reported accuracy is within 1km, we're good. if (mOriginLocation != null) { float accuracy = currentLocation.getAccuracy(); if (accuracy <= 1000.0) { // Good enough. mOriginAddress = new Address(new Locale("en", "US")); mOriginAddress.setFeatureName("Current Location"); mOriginAddress.setLatitude(mOriginLocation.getLatitude()); mOriginAddress.setLongitude(mOriginLocation.getLongitude()); mFoundOrigin = true; if (mLocationRequestor != null) { // Notify the fragment, which should be sitting there waiting for us, // that we're *finally* done getting the location, which will then // make IT initialize its loader, and call our onWaterfallQuery method. ((ResultsMapFragment) mLocationRequestor).onLocationDetermined(); } else { //Log.d(TAG, "Ooops, lol, requesting fragment is null."); } // Turn off updates. mLocationClient.removeLocationUpdates(this); } else { // Otherwise, we wait for more updates, up to 4. if (mNumUpdates >= 5) { // 5 inaccurate locations. Give up. mLocationClient.removeLocationUpdates(this); mOriginAddress = new Address(new Locale("en", "US")); ((ResultsMapFragment) mLocationRequestor).onLocationDetermined(); } mNumUpdates += 1; } } else { // Lame. mOriginAddress = new Address(new Locale("en", "US")); } } // Called by Location Services if the connection to the // location client drops because of an error. @Override public void onDisconnected() { // Display the connection status Toast.makeText(this, "Disconnected from location client. Please re-connect.", Toast.LENGTH_SHORT).show(); // TODO: Notify the map fragment? How do we reconnect? The user doesn't need to know this. } // Called by Location Services if the attempt to connect fails. @Override public void onConnectionFailed(ConnectionResult connectionResult) { // Google Play services can resolve some errors it detects. // If the error has a resolution, try sending an Intent to // start a Google Play services activity that can resolve the error. if (connectionResult.hasResolution()) { // TODO: Google Play Services needs installed. } else { // TODO: If no resolution is available, display a dialog to the // user with the error. // showErrorDialog(connectionResult.getErrorCode()); } } @Override protected void onCreate(Bundle savedInstanceState) { setTheme(R.style.Theme_AppCompat); super.onCreate(savedInstanceState); // See if Google Play Services - and thus the Map tab - should be available SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); boolean userPrefSkipPlayServices = settings.getBoolean(USER_PREF_SKIP_PLAY_SERVICES, false); // Restore or ingest our state from the saved state or the intent's extras Bundle instanceConfig; Intent intent = getIntent(); instanceConfig = savedInstanceState != null ? savedInstanceState : intent.getExtras(); Short defaultShort = 0; searchTerm = instanceConfig.getString(SearchActivity.EXTRA_SEARCH_TERM); searchTrailLength = instanceConfig.getShort(SearchActivity.EXTRA_SEARCH_TRAIL_LENGTH, defaultShort); searchTrailDifficulty = instanceConfig.getShort(SearchActivity.EXTRA_SEARCH_TRAIL_DIFFICULTY, defaultShort); searchTrailClimb = instanceConfig.getShort(SearchActivity.EXTRA_SEARCH_TRAIL_CLIMB, defaultShort); searchLocationDistance = instanceConfig.getShort(SearchActivity.EXTRA_SEARCH_LOCATION_DISTANCE, defaultShort); searchLocationRelto = instanceConfig.getString(SearchActivity.EXTRA_SEARCH_LOCATION_RELTO); searchLocationReltoTxt = instanceConfig.getString(SearchActivity.EXTRA_SEARCH_LOCATION_RELTO_TXT); searchOnlyShared = instanceConfig.getBoolean(SearchActivity.EXTRA_ONLY_SHARED, false); mSearchMode = instanceConfig.getShort(SearchActivity.EXTRA_SEARCH_MODE, defaultShort); if (mSearchMode == SearchActivity.SEARCH_MODE_LOCATION && !userPrefSkipPlayServices) { // Create a location client for getting the current location. mLocationClient = new LocationClient(this, this, this); // Display Map tab showListTab = false; } // Set up tabs actionBar = getSupportActionBar(); actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); actionBar.setDisplayHomeAsUpEnabled(false); ActionBar.Tab tab1 = actionBar.newTab(); tab1.setText("List"); tab1.setTabListener(new TabListener<ResultsListFragment>(this, "ResultsList", ResultsListFragment.class)); actionBar.addTab(tab1, showListTab); if (!userPrefSkipPlayServices) { ActionBar.Tab tab2 = actionBar.newTab(); tab2.setText("Map"); tab2.setTabListener(new TabListener<ResultsMapFragment>(this, "ResultsMap", ResultsMapFragment.class)); actionBar.addTab(tab2, !showListTab); } } // onCreate /* * Adapted from: http://stackoverflow.com/questions/3695224 * Calculates the end-point from a given source at a given range (meters) * and bearing (degrees). This methods uses simple geometry equations to * calculate the end-point. * TODO: Move this somewhere that makes sense. */ public static Location calculatePositionAtRange(Location origin, double range, double bearing) { double EarthRadius = 6371000; // m double latA = Math.toRadians(origin.getLatitude()); double lonA = Math.toRadians(origin.getLongitude()); double angularDistance = range / EarthRadius; double trueCourse = Math.toRadians(bearing); double lat = Math.asin(Math.sin(latA) * Math.cos(angularDistance) + Math.cos(latA) * Math.sin(angularDistance) * Math.cos(trueCourse)); double dlon = Math.atan2(Math.sin(trueCourse) * Math.sin(angularDistance) * Math.cos(latA), Math.cos(angularDistance) - Math.sin(latA) * Math.sin(lat)); double lon = ((lonA + dlon + Math.PI) % (Math.PI * 2)) - Math.PI; lat = Math.toDegrees(lat); lon = Math.toDegrees(lon); Location destination = new Location(""); destination.setLatitude((float) lat); destination.setLongitude((float) lon); return destination; } // OnWaterfallQueryListener interface methods // Called by fragments which want sql to query their loaders with // Will be called after location is determined if we're in location mode. @Override public Bundle onWaterfallQuery() { // Set up our query ArrayList<String> whereList = new ArrayList<String>(); // To hold chunks of the WHERE clause ArrayList<String> argList = new ArrayList<String>(); // To hold our args switch (mSearchMode) { // See which terms we're going to need to query case SearchActivity.SEARCH_MODE_WATERFALL: whereList.add("name like ?"); argList.add('%' + searchTerm.trim() + '%'); break; case SearchActivity.SEARCH_MODE_HIKE: whereList.add("trail_length <= ?"); argList.add(String.valueOf(searchTrailLength)); whereList.add("trail_difficulty_num <= ?"); argList.add(String.valueOf(searchTrailDifficulty)); whereList.add("trail_climb_num <= ?"); argList.add(String.valueOf(searchTrailClimb)); break; case SearchActivity.SEARCH_MODE_LOCATION: if (mFoundOrigin) { // Mi -> M double rangeMeters = searchLocationDistance * 1609.34; // Calculate our bounding box Location pn = calculatePositionAtRange(mOriginLocation, rangeMeters, 0); Location pe = calculatePositionAtRange(mOriginLocation, rangeMeters, 90); Location ps = calculatePositionAtRange(mOriginLocation, rangeMeters, 180); Location pw = calculatePositionAtRange(mOriginLocation, rangeMeters, 270); // Greater than S latitude whereList.add("geo_lat > ?"); argList.add(String.valueOf(ps.getLatitude())); // Less than N latitude whereList.add("geo_lat < ?"); argList.add(String.valueOf(pn.getLatitude())); // Less than E longitude whereList.add("geo_lon < ?"); argList.add(String.valueOf(pe.getLongitude())); // Greater than W longitude whereList.add("geo_lon > ?"); argList.add(String.valueOf(pw.getLongitude())); } else { // Make sure no results are returned, and a toast should happen // on the map side notifying the user as such. whereList.add("_id = ?"); argList.add(""); } // Requester can filter results to actual radius using // android.location.Location.distanceTo() break; } // Restrict to only shared falls if box checked if (searchOnlyShared != null && searchOnlyShared) { whereList.add("shared=1"); } String tables = "waterfalls"; // Select all String[] columns = AttrDatabase.COLUMNS.toArray(new String[AttrDatabase.COLUMNS.size()]); String and = " AND "; // To join our where clause String whereClause = TextUtils.join(and, whereList); String query = SQLiteQueryBuilder.buildQueryString(false, tables, columns, whereClause, null, null, "name ASC", null); Bundle qBundle = new Bundle(); qBundle.putString("query", query); String[] args = argList.toArray(new String[argList.size()]); qBundle.putStringArray("args", args); return qBundle; } // A subclass of AsyncTask which uses the built-in Android platform geocoder // to return the lat/lon of a given address. private class GetLocationTask extends AsyncTask<String, Void, Address> { Context mContext; Fragment mCaller; public GetLocationTask(Context context) { super(); mContext = context; } @Override protected Address doInBackground(String... params) { List<Address> addrList = null; // Get the coords for the given location Geocoder geocoder = new Geocoder(mContext); Address originAddress = new Address(new Locale("en", "US")); // Default null address try { addrList = geocoder.getFromLocationName(searchLocationReltoTxt, 1); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } if (addrList != null && addrList.size() > 0) { // Copy over default null address. originAddress = addrList.get(0); mFoundOrigin = true; } else { //Log.d(TAG, "Unable to geocode location: " + searchLocationReltoTxt); } return originAddress; } @Override protected void onPostExecute(Address originAddress) { mOriginAddress = originAddress; if (originAddress.hasLatitude() && originAddress.hasLongitude()) { mOriginLocation = new Location(""); mOriginLocation.setLatitude(mOriginAddress.getLatitude()); mOriginLocation.setLongitude(mOriginAddress.getLongitude()); } else { //Log.d(TAG, "Oops...your origin location has not latitude/longitude."); } // Now invoke our fragment callback... if (mLocationRequestor != null) { ((ResultsMapFragment) mLocationRequestor).onLocationDetermined(); } } } public void determineLocation(Fragment requestor) { mLocationRequestor = requestor; // Either start the geocoding in an AsnycTask, or connect the location client, which will // invoke onConnected when it's done. If the search mode wasn't by location, this does // nothing. if (mSearchMode == SearchActivity.SEARCH_MODE_LOCATION && searchLocationRelto.equals("Current Location")) { startLocationClient(); // Gets the current location when done } else if (mSearchMode == SearchActivity.SEARCH_MODE_LOCATION) { (new GetLocationTask(this)).execute(); } else { //Search was not location based; not necessary to geocode ((ResultsMapFragment) mLocationRequestor).onLocationDetermined(); } } private void startLocationClient() { if (mLocationClient != null) { mLocationClient.connect(); } } public void stopLocationClient() { // TODO: Disconnecting the client invalidates it. If we do this, // and then need to use it again later, we'll need to create // a new one. After onStop() is called, we'll always // run through onCreate or onRestart. Take heed. if (mLocationClient != null) { mLocationClient.disconnect(); } } public Address getOriginAddress() { // Return the origin address this search was conducted with. return mOriginAddress; } public Location getOriginLocation() { // Return the origin address this search was conducted with. return mOriginLocation; } public short getSearchLocationDistance() { // Return the origin address this search was conducted with. return searchLocationDistance; } // OnWaterfallQueryListener interface methods // Called by fragments which want sql to query their loaders with @Override public void onWaterfallSelected(long waterfallId) { // Create new intent Intent intent = new Intent(this, InformationActivity.class); // Pack it with message containing search data intent.putExtra(SELECTED_WATERFALL_ID, waterfallId); // Start the Information activity startActivity(intent); } @Override public void onSaveInstanceState(Bundle savedInstanceState) { savedInstanceState.putShort(SearchActivity.EXTRA_SEARCH_MODE, mSearchMode); savedInstanceState.putString(SearchActivity.EXTRA_SEARCH_TERM, searchTerm); savedInstanceState.putShort(SearchActivity.EXTRA_SEARCH_TRAIL_LENGTH, searchTrailLength); savedInstanceState.putShort(SearchActivity.EXTRA_SEARCH_TRAIL_DIFFICULTY, searchTrailDifficulty); savedInstanceState.putShort(SearchActivity.EXTRA_SEARCH_TRAIL_CLIMB, searchTrailClimb); savedInstanceState.putShort(SearchActivity.EXTRA_SEARCH_LOCATION_DISTANCE, searchLocationDistance); savedInstanceState.putString(SearchActivity.EXTRA_SEARCH_LOCATION_RELTO, searchLocationRelto); savedInstanceState.putString(SearchActivity.EXTRA_SEARCH_LOCATION_RELTO_TXT, searchLocationReltoTxt); savedInstanceState.putBoolean(SearchActivity.EXTRA_ONLY_SHARED, searchOnlyShared); super.onSaveInstanceState(savedInstanceState); } }