From ff96dde9dc28706aa00b9300fe7a22664f6b5402 Mon Sep 17 00:00:00 2001 From: "gu.martinm@gmail.com" Date: Thu, 4 Sep 2014 20:42:40 +0200 Subject: [PATCH] WeatherInformation Android: map looking good, WIP --- AndroidManifest.xml | 11 - res/values/strings.xml | 2 +- src/de/example/exampletdd/MapActivity.java | 385 +++++++++++++++++---- .../gms/GPlayServicesErrorDialogFragment.java | 39 +++ .../exampletdd/model/WeatherLocationDbQueries.java | 46 ++- 5 files changed, 405 insertions(+), 78 deletions(-) create mode 100644 src/de/example/exampletdd/gms/GPlayServicesErrorDialogFragment.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 9d74883..9da7fb2 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -42,18 +42,7 @@ android:theme="@style/AppTheme" android:name="de.example.exampletdd.WeatherInformationApplication" android:supportsRtl="false"> - Connection error timeout Impossible to receive weather data. Impossible to save current location. - Impossible to receive city and country values. + Can not get address. Icon weather City,country weather_preferences_units diff --git a/src/de/example/exampletdd/MapActivity.java b/src/de/example/exampletdd/MapActivity.java index d18af04..f273d1f 100644 --- a/src/de/example/exampletdd/MapActivity.java +++ b/src/de/example/exampletdd/MapActivity.java @@ -5,18 +5,28 @@ import java.util.List; import java.util.Locale; import android.app.ActionBar; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; import android.location.Address; import android.location.Geocoder; +import android.location.Location; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentActivity; import android.util.Log; import android.view.View; import android.widget.TextView; +import android.widget.Toast; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesClient; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.android.gms.location.LocationClient; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMap.OnMapLongClickListener; @@ -26,36 +36,54 @@ import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import de.example.exampletdd.fragment.ErrorDialogFragment; +import de.example.exampletdd.fragment.ProgressDialogFragment; +import de.example.exampletdd.gms.GPlayServicesErrorDialogFragment; import de.example.exampletdd.model.WeatherLocation; -import de.example.exampletdd.model.WeatherLocationContract; import de.example.exampletdd.model.WeatherLocationDbHelper; import de.example.exampletdd.model.WeatherLocationDbQueries; -public class MapActivity extends FragmentActivity { +/** + * {@link http://developer.android.com/training/location/retrieve-current.html} + * + */ +public class MapActivity extends FragmentActivity implements + GooglePlayServicesClient.ConnectionCallbacks, + GooglePlayServicesClient.OnConnectionFailedListener { + private static final String TAG = "MapActivity"; + private static final int CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000; + private WeatherLocation mRestoreUI; + + // Google Play Services Map private GoogleMap mMap; // TODO: read and store from different threads private Marker mMarker; - private WeatherLocation mRestoreUI; + + // Google Play Services Location + // Stores the current instantiation of the location client in this object + private LocationClient mLocationClient; @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(R.layout.weather_map); + // Google Play Services Location + /* + * Create a new location client, using the enclosing class to + * handle callbacks. + */ + mLocationClient = new LocationClient(this, this, this); + + + + // Google Play Services Map final MapFragment mapFragment = (MapFragment) this.getFragmentManager() .findFragmentById(R.id.map); this.mMap = mapFragment.getMap(); this.mMap.setMyLocationEnabled(false); this.mMap.getUiSettings().setCompassEnabled(false); - this.mMap.setOnMapLongClickListener(new OnMapLongClickListener() { - - @Override - public void onMapLongClick(final LatLng point) { - final LocationAsyncTask geocoderAsyncTask = new LocationAsyncTask(); - geocoderAsyncTask.execute(point.latitude, point.longitude); - } - }); + this.mMap.setOnMapLongClickListener(new MapActivityOnMapLongClickListener(this)); } @Override @@ -90,7 +118,7 @@ public class MapActivity extends FragmentActivity { } if (weatherLocation != null) { - this.updateMap(weatherLocation); + this.updateUI(weatherLocation); } } @@ -122,56 +150,42 @@ public class MapActivity extends FragmentActivity { } public void onClickGetLocation(final View v) { - + // If Google Play Services is available + if (servicesConnected()) { + + if (mLocationClient.isConnected()) { + // Get the current location + // TODO: DO NOT USE IT!!! http://stackoverflow.com/questions/17265722/locationclient-does-not-update-getlastlocation-on-emulator + // USE THIS INSTEAD: http://developer.android.com/reference/com/google/android/gms/location/LocationClient.html#requestLocationUpdates(com.google.android.gms.location.LocationRequest, com.google.android.gms.location.LocationListener) + final Location currentLocation = mLocationClient.getLastLocation(); + + if (currentLocation != null) { + // Display the current location in the UI + this.getAddressAndUpdateUI(currentLocation.getLatitude(), currentLocation.getLongitude()); + } else { + // Location is not available + // TODO: string resource + Toast.makeText(this, "Location is not available.", Toast.LENGTH_LONG).show(); + } + } else { + // TODO: string resource + Toast.makeText(this, "You are not yet connected to Google Play Services.", Toast.LENGTH_LONG).show(); + } + } } - + private WeatherLocation queryDataBase() { - final String selection = WeatherLocationContract.WeatherLocation.COLUMN_NAME_IS_SELECTED + " = ?"; - final String[] selectionArgs = { "1" }; - final String[] projection = { - WeatherLocationContract.WeatherLocation._ID, - WeatherLocationContract.WeatherLocation.COLUMN_NAME_CITY, - WeatherLocationContract.WeatherLocation.COLUMN_NAME_COUNTRY, - WeatherLocationContract.WeatherLocation.COLUMN_NAME_IS_SELECTED, - WeatherLocationContract.WeatherLocation.COLUMN_NAME_LAST_CURRENT_UI_UPDATE, - WeatherLocationContract.WeatherLocation.COLUMN_NAME_LAST_FORECAST_UI_UPDATE, - WeatherLocationContract.WeatherLocation.COLUMN_NAME_LATITUDE, - WeatherLocationContract.WeatherLocation.COLUMN_NAME_LONGITUDE - }; final WeatherLocationDbHelper dbHelper = new WeatherLocationDbHelper(this); try { - final WeatherLocationDbQueries queryDb = new WeatherLocationDbQueries(dbHelper); - final WeatherLocationDbQueries.DoQuery doQuery = new WeatherLocationDbQueries.DoQuery() { - - @Override - public WeatherLocation doQuery(final Cursor cursor) { - String city = cursor.getString(cursor. - getColumnIndexOrThrow(WeatherLocationContract.WeatherLocation.COLUMN_NAME_CITY)); - String country = cursor.getString(cursor. - getColumnIndexOrThrow(WeatherLocationContract.WeatherLocation.COLUMN_NAME_COUNTRY)); - double latitude = cursor.getDouble(cursor. - getColumnIndexOrThrow(WeatherLocationContract.WeatherLocation.COLUMN_NAME_LATITUDE)); - double longitude = cursor.getDouble(cursor. - getColumnIndexOrThrow(WeatherLocationContract.WeatherLocation.COLUMN_NAME_LONGITUDE)); - - return new WeatherLocation.Builder(). - setCity(city).setCountry(country). - setLatitude(latitude).setLongitude(longitude). - build(); - } - - }; - - return queryDb.queryDataBase( - WeatherLocationContract.WeatherLocation.TABLE_NAME, projection, - selectionArgs, selection, doQuery); + final WeatherLocationDbQueries queryDb = new WeatherLocationDbQueries(dbHelper); + return queryDb.queryDataBase(this); } finally { dbHelper.close(); } } - private void updateMap(final WeatherLocation weatherLocation) { + private void updateUI(final WeatherLocation weatherLocation) { final TextView city = (TextView) this.findViewById(R.id.weather_map_city); final TextView country = (TextView) this.findViewById(R.id.weather_map_country); @@ -191,9 +205,26 @@ public class MapActivity extends FragmentActivity { this.mMap.animateCamera(CameraUpdateFactory.zoomTo(8), 2000, null); } - private class LocationAsyncTask extends AsyncTask { - private static final String TAG = "LocationAsyncTask"; + private class GetAddressTask extends AsyncTask { + private static final String TAG = "GetAddressTask"; + // Store the context passed to the AsyncTask when the system instantiates it. + private final Context localContext; + private final DialogFragment newFragment; + private GetAddressTask(final Context context) { + this.localContext = context; + this.newFragment = ProgressDialogFragment.newInstance( + R.string.progress_dialog_get_remote_data, + this.localContext.getString(R.string.progress_dialog_generic_message)); + } + + @Override + protected void onPreExecute() { + // TODO: The same with Overview and Current? I guess so... + final FragmentActivity activity = (FragmentActivity) this.localContext; + this.newFragment.show(activity.getSupportFragmentManager(), "progressDialog"); + } + @Override protected WeatherLocation doInBackground(final Object... params) { final double latitude = (Double) params[0]; @@ -203,7 +234,7 @@ public class MapActivity extends FragmentActivity { try { weatherLocation = this.getLocation(latitude, longitude); } catch (final IOException e) { - Log.e(TAG, "LocationAsyncTask doInBackground exception: ", e); + Log.e(TAG, "GetAddressTask doInBackground exception: ", e); } return weatherLocation; @@ -211,26 +242,30 @@ public class MapActivity extends FragmentActivity { @Override protected void onPostExecute(final WeatherLocation weatherLocation) { + this.newFragment.dismiss(); + // TODO: Is AsyncTask calling this method even when RunTimeException in doInBackground method? // I hope so, otherwise I must catch(Throwable) in doInBackground method :( if (weatherLocation == null) { + final FragmentActivity activity = (FragmentActivity) this.localContext; // TODO: if user changed activity, where is this going to appear? final DialogFragment newFragment = ErrorDialogFragment.newInstance(R.string.error_dialog_location_error); - newFragment.show(MapActivity.this.getSupportFragmentManager(), - "errorDialog"); + newFragment.show(activity.getSupportFragmentManager(), "errorDialog"); return; } - MapActivity.this.updateMap(weatherLocation); + final MapActivity activity = (MapActivity) this.localContext; + activity.updateUI(weatherLocation); } - + private WeatherLocation getLocation(final double latitude, final double longitude) throws IOException { - final Geocoder geocoder = new Geocoder(MapActivity.this, Locale.US); + // TODO: i18n Locale.getDefault() + final Geocoder geocoder = new Geocoder(this.localContext, Locale.US); final List
addresses = geocoder.getFromLocation(latitude, longitude, 1); // Default values - String city = MapActivity.this.getString(R.string.city_not_found); - String country = MapActivity.this.getString(R.string.country_not_found); + String city = this.localContext.getString(R.string.city_not_found); + String country = this.localContext.getString(R.string.country_not_found); if (addresses == null || addresses.size() <= 0) { if (addresses.get(0).getLocality() != null) { city = addresses.get(0).getLocality(); @@ -247,4 +282,226 @@ public class MapActivity extends FragmentActivity { } } + + private class MapActivityOnMapLongClickListener implements OnMapLongClickListener { + // Store the context passed to the AsyncTask when the system instantiates it. + private final Context localContext; + + private MapActivityOnMapLongClickListener(final Context context) { + this.localContext = context; + } + + @Override + public void onMapLongClick(final LatLng point) { + final MapActivity activity = (MapActivity) this.localContext; + activity.getAddressAndUpdateUI(point.latitude, point.longitude); + } + + } + + /************************ Google Play Services ************************/ + + /** + * Getting the address of the current location, using reverse geocoding only works if + * a geocoding service is available. + * + */ + private void getAddressAndUpdateUI(final double latitude, final double longitude) { + + // In Gingerbread and later, use Geocoder.isPresent() to see if a geocoder is available. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD && Geocoder.isPresent()) { + if (servicesConnected()) { + + // Start the background task + final GetAddressTask geocoderAsyncTask = new GetAddressTask(this); + geocoderAsyncTask.execute(latitude, longitude); + } + } else { + // No geocoder is present. Issue an error message. + // TODO: string resource + Toast.makeText(this, "Cannot get address. No geocoder available.", Toast.LENGTH_LONG).show(); + } + + // Default values + final String city = this.getString(R.string.city_not_found); + final String country = this.getString(R.string.country_not_found); + final WeatherLocation weatherLocation = new WeatherLocation.Builder(). + setLatitude(latitude).setLongitude(longitude). + setCity(city).setCountry(country). + build(); + + updateUI(weatherLocation); + } + + /** + * Verify that Google Play services is available before making a request. + * + * @return true if Google Play services is available, otherwise false + */ + private boolean servicesConnected() { + + // Check that Google Play services is available + final int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); + + // If Google Play services is available + if (ConnectionResult.SUCCESS == resultCode) { + // In debug mode, log the status + Log.d(TAG, "Google Play Services is available."); + + // Continue + return true; + // Google Play services was not available for some reason + } else { + // Display an error dialog + final Dialog dialog = GooglePlayServicesUtil.getErrorDialog(resultCode, this, 0); + if (dialog != null) { + final GPlayServicesErrorDialogFragment errorFragment = new GPlayServicesErrorDialogFragment(); + errorFragment.setDialog(dialog); + errorFragment.show(getSupportFragmentManager(), TAG); + } + return false; + } + } + + /************************ Google Play Services Location ************************/ + + /* + * Called when the Activity is started/restarted, even before it becomes visible. + */ + @Override + protected void onStart() { + super.onStart(); + + /* + * Connect the client. Don't re-start any requests here; + * instead, wait for onResume() + */ + mLocationClient.connect(); + } + + /* + * Called when the Activity is no longer visible at all. + * Disconnect. + */ + @Override + public void onStop() { + + // Disconnecting the client invalidates it. + // After disconnect() is called, the client is considered "dead". + mLocationClient.disconnect(); + + super.onStop(); + } + + @Override + public void onConnected(final Bundle bundle) { + // TODO: Why should some user care about this? Could I skip this message? + // TODO: string resource + Toast.makeText(this, "Connected to Google Play Services.", Toast.LENGTH_SHORT).show(); + } + + @Override + public void onDisconnected() { + // TODO: What should the user do in this case? Perhaps this method is called after onStop() by mLocationClient.disconnect() + // TODO: string resource + Toast.makeText(this, "Client disconnected from Google Play Services.", Toast.LENGTH_SHORT).show(); + } + + /* + * Called by Location Services if the attempt to + * Location Services fails. + */ + @Override + public void onConnectionFailed(final 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 + * error. + */ + if (connectionResult.hasResolution()) { + try { + // Start an Activity that tries to resolve the error + connectionResult.startResolutionForResult( + this, CONNECTION_FAILURE_RESOLUTION_REQUEST); + /* + * Thrown if Google Play services canceled the original + * PendingIntent + */ + } catch (final IntentSender.SendIntentException e) { + // Log the error + } + } else { + /* + * If no resolution is available, display a dialog to the + * user with the error. + */ + showErrorDialog(connectionResult.getErrorCode()); + } + } + + /* + * Handle results returned to this Activity by other Activities started with + * startActivityForResult(). In particular, the method onConnectionFailed() in + * LocationUpdateRemover and LocationUpdateRequester may call startResolutionForResult() to + * start an Activity that handles Google Play services problems. The result of this + * call returns here, to onActivityResult. + */ + @Override + protected void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { + + // Choose what to do based on the request code + switch (requestCode) { + + // If the request code matches the code sent in onConnectionFailed + case CONNECTION_FAILURE_RESOLUTION_REQUEST : + switch (resultCode) { + // If Google Play services resolved the problem + case Activity.RESULT_OK: + // Log the result + Log.d(TAG, "Error resolved. Please re-try operation"); + + // TODO: Should I call mLocationClient.connect() ? I GUESS SO!!!! + // TODO: Display the result + //mConnectionState.setText("Client connected"); + //mConnectionStatus.setText("Error resolved. Please re-try operation."); + break; + // If any other result was returned by Google Play services + default: + // Log the result + Log.d(TAG, "Google Play services: unable to resolve connection error."); + + // TODO: Display the result + //mConnectionState.setText("Client disconnected"; + //mConnectionStatus.setText("Google Play services: unable to resolve connection error."); + break; + } + // If any other request code was received + default: + // TODO: show some error? + // Report that this Activity received an unknown requestCode + Log.d(TAG, "Received an unknown activity request code onActivityResult. Request code: " + requestCode); + break; + } + } + + private void showErrorDialog(final int errorCode) { + + // Get the error dialog from Google Play services + final Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog( + errorCode, this, CONNECTION_FAILURE_RESOLUTION_REQUEST); + + // If Google Play services can provide an error dialog + if (errorDialog != null) { + + // Create a new DialogFragment in which to show the error dialog + final GPlayServicesErrorDialogFragment GPSErrorFragment = new GPlayServicesErrorDialogFragment(); + + // Set the dialog in the DialogFragment + GPSErrorFragment.setDialog(errorDialog); + + // Show the error dialog in the DialogFragment + GPSErrorFragment.show(this.getSupportFragmentManager(), TAG); + } + } } diff --git a/src/de/example/exampletdd/gms/GPlayServicesErrorDialogFragment.java b/src/de/example/exampletdd/gms/GPlayServicesErrorDialogFragment.java new file mode 100644 index 0000000..0d76616 --- /dev/null +++ b/src/de/example/exampletdd/gms/GPlayServicesErrorDialogFragment.java @@ -0,0 +1,39 @@ +package de.example.exampletdd.gms; + +import android.app.Dialog; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; + +/** + * Define a DialogFragment to display the error dialog generated in + * showErrorDialog. + */ +public class GPlayServicesErrorDialogFragment extends DialogFragment { + + // Global field to contain the error dialog + private Dialog mDialog; + + /** + * Default constructor. Sets the dialog field to null + */ + public GPlayServicesErrorDialogFragment() { + mDialog = null; + } + + /** + * Set the dialog to display + * + * @param dialog An error dialog + */ + public void setDialog(final Dialog dialog) { + mDialog = dialog; + } + + /* + * This method must return a Dialog to the DialogFragment. + */ + @Override + public Dialog onCreateDialog(final Bundle savedInstanceState) { + return mDialog; + } +} diff --git a/src/de/example/exampletdd/model/WeatherLocationDbQueries.java b/src/de/example/exampletdd/model/WeatherLocationDbQueries.java index 82f7d1a..da96165 100644 --- a/src/de/example/exampletdd/model/WeatherLocationDbQueries.java +++ b/src/de/example/exampletdd/model/WeatherLocationDbQueries.java @@ -1,5 +1,6 @@ package de.example.exampletdd.model; +import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; @@ -16,8 +17,48 @@ public class WeatherLocationDbQueries { this.mDbHelper = dbHelper; } - // TODO: right now it can be used just once - public WeatherLocation queryDataBase(String table, + public WeatherLocation queryDataBase(final Context context) { + final String selection = WeatherLocationContract.WeatherLocation.COLUMN_NAME_IS_SELECTED + " = ?"; + final String[] selectionArgs = { "1" }; + final String[] projection = { + WeatherLocationContract.WeatherLocation._ID, + WeatherLocationContract.WeatherLocation.COLUMN_NAME_CITY, + WeatherLocationContract.WeatherLocation.COLUMN_NAME_COUNTRY, + WeatherLocationContract.WeatherLocation.COLUMN_NAME_IS_SELECTED, + WeatherLocationContract.WeatherLocation.COLUMN_NAME_LAST_CURRENT_UI_UPDATE, + WeatherLocationContract.WeatherLocation.COLUMN_NAME_LAST_FORECAST_UI_UPDATE, + WeatherLocationContract.WeatherLocation.COLUMN_NAME_LATITUDE, + WeatherLocationContract.WeatherLocation.COLUMN_NAME_LONGITUDE + }; + + + final WeatherLocationDbQueries.DoQuery doQuery = new WeatherLocationDbQueries.DoQuery() { + + @Override + public WeatherLocation doQuery(final Cursor cursor) { + String city = cursor.getString(cursor. + getColumnIndexOrThrow(WeatherLocationContract.WeatherLocation.COLUMN_NAME_CITY)); + String country = cursor.getString(cursor. + getColumnIndexOrThrow(WeatherLocationContract.WeatherLocation.COLUMN_NAME_COUNTRY)); + double latitude = cursor.getDouble(cursor. + getColumnIndexOrThrow(WeatherLocationContract.WeatherLocation.COLUMN_NAME_LATITUDE)); + double longitude = cursor.getDouble(cursor. + getColumnIndexOrThrow(WeatherLocationContract.WeatherLocation.COLUMN_NAME_LONGITUDE)); + + return new WeatherLocation.Builder(). + setCity(city).setCountry(country). + setLatitude(latitude).setLongitude(longitude). + build(); + } + }; + + return this.queryDataBase( + WeatherLocationContract.WeatherLocation.TABLE_NAME, projection, + selectionArgs, selection, doQuery); + } + + // TODO: May I perform another query after this method (after closing almost everything but mDbHelper) + private WeatherLocation queryDataBase(final String table, final String[] projection, final String[] selectionArgs, final String selection, final DoQuery doQuery) { // TODO: execute around idiom? I miss try/finally with resources @@ -38,4 +79,5 @@ public class WeatherLocationDbQueries { db.close(); } } + } -- 2.1.4