Java tutorial
/* Montral Just in Case Copyright (C) 2011 Mudar Noufal <mn@mudar.ca> Geographic locations of public safety services. A Montral Open Data project. This file is part of Montral Just in Case. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.mudar.mtlaucasou; import com.google.android.maps.GeoPoint; import ca.mudar.mtlaucasou.BaseListFragment.OnPlacemarkSelectedListener; import ca.mudar.mtlaucasou.BaseMapFragment.OnMyLocationChangedListener; import ca.mudar.mtlaucasou.utils.ActivityHelper; import ca.mudar.mtlaucasou.utils.AppHelper; import ca.mudar.mtlaucasou.utils.ConnectionHelper; import ca.mudar.mtlaucasou.utils.Const; import ca.mudar.mtlaucasou.utils.Helper; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.res.Resources; import android.location.Location; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentMapActivity; import android.support.v4.app.FragmentTransaction; import android.support.v4.view.Menu; import android.support.v4.view.MenuInflater; import android.support.v4.view.MenuItem; import android.text.InputType; import android.util.Log; import android.view.View; import android.widget.EditText; import android.widget.Toast; import java.io.IOException; public class BaseMapActivity extends FragmentMapActivity implements OnPlacemarkSelectedListener, OnMyLocationChangedListener, Runnable { protected static final String TAG = "BaseMapActivity"; /** * Used to save/restore display of the fragmentList onResume and when * rotating device. If list is shown in portrait, Xlarge-land layouts * default to show both map and list. This also toggles the icon used for * R.id.actionbar_toggle_list. */ protected boolean isHiddenList; // protected MenuItem btnActionbarToggleList; private String postalCode; private ProgressDialog pd; /** * Must be initialized by constructor. */ protected int indexSection; public BaseMapActivity(int indexSection) { this.indexSection = indexSection; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((AppHelper) getApplicationContext()).updateUiLanguage(); /** * By default, show map and hide list. */ isHiddenList = true; if ((savedInstanceState != null) && savedInstanceState.containsKey(Const.KEY_INSTANCE_LIST_IS_HIDDEN)) { /** * For visible/hidden fragments and actionbar icon */ isHiddenList = savedInstanceState.getBoolean(Const.KEY_INSTANCE_LIST_IS_HIDDEN); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { getActionBar().setHomeButtonEnabled(true); } } /** * Default (and restore) of hidden/visible fragments. {@inheritDoc} */ @Override public void onResume() { super.onResume(); if (!ConnectionHelper.hasConnection(this)) { ConnectionHelper.showDialogNoConnection(this); } View root = findViewById(R.id.map_root_landscape); boolean isTablet = (root != null); FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); Fragment fragmentMap = fm.findFragmentByTag(Const.TAG_FRAGMENT_MAP); Fragment fragmentList = fm.findFragmentByTag(Const.TAG_FRAGMENT_LIST); // TODO Bug: onResume after device (Nook!) shutdown or memory problems. // Temporary solution is the use of ft.show() in the following lines. /** * By default, both fragments are shown. No need to use * FragmentTransaction.show(). */ if (isTablet) { if (isHiddenList) { /** * List was hidden, we'll hide it again. */ ft.show(fragmentMap); ft.hide(fragmentList); // isHiddenList = true; } else { /** * List was not hidden, nothing to do here. */ ft.show(fragmentMap); ft.show(fragmentList); // isHiddenList = false; } } else if (!isTablet) { if (isHiddenList) { /** * List was hidden, we'll hide it again. */ ft.show(fragmentMap); ft.hide(fragmentList); // isHiddenList = true; } else if (!isHiddenList) { /** * List was not hidden. Hide the map since this is a portrait * layout. */ ft.hide(fragmentMap); ft.show(fragmentList); // isHiddenList = false; } } ft.commit(); } @Override protected boolean isRouteDisplayed() { return false; } /** * Creates the activity's options menu. It contains items common to both * fragments which will also load their own items. */ @Override public boolean onCreateOptionsMenu(Menu menu) { // Log.v(TAG, "onCreateOptionsMenu"); /** * Manual detection of Android version: This is because of a * ActionBarSherlock/compatibility package issue with the MenuInflater. * Also, versions earlier than Honeycomb don't manage SHOW_AS_ACTION_* * options other than ALWAYS. */ if (Const.SUPPORTS_HONEYCOMB) { /** * Honeycomb drawables are different (white instead of grey) because * the items are in the actionbar. Order is: toggle (1), kml (2), * list sort (3), postal code (4), my position (5). */ menu.add(Menu.NONE, R.id.actionbar_toggle_list, 1, R.string.menu_view_list) .setIcon(getResources().getDrawable(R.drawable.ic_actionbar_view_list)) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); menu.add(Menu.NONE, R.id.menu_link_kml, 2, R.string.menu_link_kml) .setIcon(getResources().getDrawable(R.drawable.ic_actionbar_directions)); } else { MenuInflater inflater = (MenuInflater) getMenuInflater(); inflater.inflate(R.menu.menu_activity_map, menu); } MenuItem btnActionbarToggleList = menu.findItem(R.id.actionbar_toggle_list); if (!isHiddenList) { /** * Activity/Fragments Lifecycle issues. The clean solution would be * to detect orientation and fragmentList.isVisible() here to decide * which button should be displayed in the actionbar. */ if (btnActionbarToggleList != null) { btnActionbarToggleList.setIcon(getResources().getDrawable(R.drawable.ic_actionbar_view_map)); } } return true; } /** * Save the list isHidden() status. {@inheritDoc} */ @Override public void onSaveInstanceState(Bundle outState) { FragmentManager fm = getSupportFragmentManager(); Fragment fragmentList = fm.findFragmentByTag(Const.TAG_FRAGMENT_LIST); if (fragmentList != null) { /** * For visible/hidden fragments and actionbar icon. */ outState.putBoolean(Const.KEY_INSTANCE_LIST_IS_HIDDEN, fragmentList.isHidden()); } super.onSaveInstanceState(outState); } /** * Toggle display of both fragments, depending on landscape/portrait * layouts. Also toggle the actionbar button icon. {@inheritDoc} */ @Override public boolean onOptionsItemSelected(MenuItem item) { FragmentManager fm = getSupportFragmentManager(); BaseMapFragment fragmentMap = (BaseMapFragment) fm.findFragmentByTag(Const.TAG_FRAGMENT_MAP); Fragment fragmentList = fm.findFragmentByTag(Const.TAG_FRAGMENT_LIST); if (item.getItemId() == R.id.actionbar_toggle_list) { View root = findViewById(R.id.map_root_landscape); boolean isTablet = (root != null); FragmentTransaction ft = fm.beginTransaction(); if ((fragmentMap == null) || (fragmentList == null)) { return false; } if (fragmentList.isVisible()) { /** * List is visible: hide it. */ ft.hide(fragmentList); isHiddenList = true; if (!isTablet) { /** * In portrait layout, we also have to show the hidden map. */ ft.show(fragmentMap); } /** * List is now hidden: Set the actionbar button to view_list. */ item.setIcon(getResources().getDrawable(R.drawable.ic_actionbar_view_list)); item.setTitle(R.string.menu_view_list); } else { /** * List is not visible: show it. */ ft.show(fragmentList); isHiddenList = false; if (!isTablet) { /** * In portrait layout, we also have to hide the visible map. */ ft.hide(fragmentMap); } /** * Map is now hidden: Set the actionbar button to view_map. */ item.setIcon(getResources().getDrawable(R.drawable.ic_actionbar_view_map)); item.setTitle(R.string.menu_view_map); } ft.commit(); return true; } else if (item.getItemId() == R.id.menu_map_mylocation) { /** * Center map on user location. */ fragmentMap.setMapCenterOnLocation(((AppHelper) getApplicationContext()).getLocation()); return true; } else if (item.getItemId() == R.id.menu_map_find_from_name) { /** * Search location by postal code (or address) and center map on * location if found) by Geocode. */ showPostalCodeDialog(); return true; } else { ActivityHelper mActivityHelper = ActivityHelper.createInstance(this); return mActivityHelper.onOptionsItemSelected(item, indexSection) || super.onOptionsItemSelected(item); } } /** * Implement the fragmentList listener interface, to pass the selected item * to the map. This will center map and display balloon. Actionbar button is * toggled to view_map. */ @Override public void onPlacemarkSelected(GeoPoint geoPoint) { FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); BaseMapFragment fragmentMap = (BaseMapFragment) fm.findFragmentByTag(Const.TAG_FRAGMENT_MAP); Fragment fragmentList = fm.findFragmentByTag(Const.TAG_FRAGMENT_LIST); if (fragmentMap.isHidden()) { /** * Toggle hide/show of the fragments and the Actionbar button */ ft.hide(fragmentList); ft.show(fragmentMap); isHiddenList = true; ft.commit(); } fragmentMap.setMapCenter(geoPoint); } /** * Implement the fragmentMap listener interface, to get the user's location, * send it to the AppHelper and enable the My Location item in the menu. */ @Override public void OnMyLocationChanged(final GeoPoint geoPoint) { /** * Following code allows the background listener to modify the UI's * menu. */ runOnUiThread(new Runnable() { public void run() { ((AppHelper) getApplicationContext()).setLocation(Helper.geoPointToLocation(geoPoint)); invalidateOptionsMenu(); } }); } /** * Show dialog to type postal code for Geocode search. */ private void showPostalCodeDialog() { AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle(R.string.dialog_postal_code_title); alert.setMessage(R.string.dialog_postal_code_summary); final EditText input = new EditText(this); input.setInputType(InputType.TYPE_CLASS_TEXT); input.setHint(R.string.input_hint_postal_code); alert.setView(input); alert.setPositiveButton(R.string.dialog_btn_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { /** * Store value and show processing dialog. Geocode search will * be done in a thread. */ postalCode = input.getText().toString(); showDialogProcessing(); } }); alert.setNegativeButton(R.string.dialog_btn_cancel, null); alert.show(); } /** * Show the Processing dialog and start the Geocode search thread. */ public void showDialogProcessing() { Resources res = getResources(); String message = res.getString(R.string.toast_postal_code_processing); pd = ProgressDialog.show(this, "", message, true); Thread thread = new Thread(this); thread.start(); } /** * This runnable thread gets the Geocode search value in the background * (front activity is the Processing dialog). Results are sent to the * handler. */ @Override public void run() { Location loc = null; try { /** * Geocode search. Takes time and not very reliable! */ loc = Helper.findLocatioFromName(getApplicationContext(), postalCode); } catch (IOException e) { Log.e(TAG, e.getMessage()); } Message msg = handler.obtainMessage(); Bundle b = new Bundle(); if (loc == null) { /** * Send error message to handler. */ b.putInt(Const.KEY_BUNDLE_SEARCH_ADDRESS, Const.BUNDLE_SEARCH_ADDRESS_ERROR); } else { /** * Send success message to handler with the found geocoordinates. */ b.putInt(Const.KEY_BUNDLE_SEARCH_ADDRESS, Const.BUNDLE_SEARCH_ADDRESS_SUCCESS); b.putDouble(Const.KEY_BUNDLE_ADDRESS_LAT, loc.getLatitude()); b.putDouble(Const.KEY_BUNDLE_ADDRESS_LNG, loc.getLongitude()); } msg.setData(b); handler.sendMessage(msg); } /** * Handle the runnable thread results. This closes the processing dialog * then centers map on found location or displays error message. */ private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { pd.dismiss(); Bundle b = msg.getData(); if (b.getInt(Const.KEY_BUNDLE_SEARCH_ADDRESS) == Const.BUNDLE_SEARCH_ADDRESS_SUCCESS) { /** * Address is found, center map on location. */ Location loc = new Location(Const.LOCATION_PROVIDER); loc.setLatitude(b.getDouble(Const.KEY_BUNDLE_ADDRESS_LAT)); loc.setLongitude(b.getDouble(Const.KEY_BUNDLE_ADDRESS_LNG)); FragmentManager fm = getSupportFragmentManager(); ((BaseMapFragment) fm.findFragmentByTag(Const.TAG_FRAGMENT_MAP)).setMapCenterOnLocation(loc); } else { /** * Address not found! Display error message. */ String errorMsg = String.format(getResources().getString(R.string.toast_search_error, postalCode)); ((AppHelper) getApplicationContext()).showToastText(errorMsg, Toast.LENGTH_LONG); } } }; }