Java tutorial
/* * Copyright (c) 2014 Yehezkel (Zack) Yovel * * 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 zack.yovel.clear.controller.foreground; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; import android.content.SharedPreferences; import android.location.Location; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentTransaction; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.view.MenuItemCompat; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarActivity; import android.support.v7.widget.SearchView; import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; 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.location.LocationListener; import com.google.android.gms.location.LocationRequest; import java.util.ArrayList; import java.util.Locale; import zack.yovel.clear.R; import zack.yovel.clear.infrastructure.model.datapoints.WeatherReport; import zack.yovel.clear.infrastructure.model.geonames.GeoName; import zack.yovel.clear.infrastructure.networking.geocoding.GeoCoderTask; import zack.yovel.clear.infrastructure.networking.geocoding.LatLng; import zack.yovel.clear.infrastructure.networking.geonames.GeoNamesApiClientFactory; import zack.yovel.clear.infrastructure.networking.weather.WeatherApiClientFactory; import zack.yovel.clear.infrastructure.networking.weather.WeatherApiClientInterface; import zack.yovel.clear.infrastructure.persist.AWeatherPersistAgent; import zack.yovel.clear.infrastructure.persist.IWeatherPersist; import zack.yovel.clear.util.Constants; /* * TODO: Check this dilemma: * Implementing so many interfaces seems to be a violation of the single responsibility * prinsiple. It may be good to provide separate managing instances instead. Still, since * android can be a limited-resources environment, it me be inevitable to mix responsibility * in order to cut down on resource consumption. Needs further research. */ public class MainActivity extends ActionBarActivity implements ActionBar.TabListener, IWeatherPersist.IWeatherInflatedListener, SearchView.OnQueryTextListener, SuggestionsDialogFragment.OnSelectionMade, GooglePlayServicesClient.ConnectionCallbacks, GooglePlayServicesClient.OnConnectionFailedListener, GeoCoderTask.OnAddressReady, LocationListener, WeatherFragment.IDataProvider<WeatherReport> { private static final int REQUEST_PGS_ERROR_DIALOG = 9000; private static final int REQUEST_GPS_CONNECTION_RESOLUTION = 9001; private static final String TAG = "MainActivity"; /** * The {@link android.support.v4.view.PagerAdapter} that will provide * fragments for each of the sections. We use a * {@link android.support.v4.app.FragmentPagerAdapter} derivative, which will keep every * loaded fragment in memory. If this becomes too memory intensive, it * may be best to switch to a * {@link android.support.v4.app.FragmentStatePagerAdapter}. */ SectionsPagerAdapter mSectionsPagerAdapter; /** * The {@link android.support.v4.view.ViewPager} that will host the section contents. */ ViewPager mViewPager; Handler handler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.arg1) { case Constants.Flags.SUCCESS: handlerReportsSuccess((ArrayList<GeoName>) msg.obj); break; case Constants.Flags.FAILURE: handlerReportsFailure(msg.arg2); } } }; private ArrayList<IWeatherPersist.IWeatherInflatedListener> listeners = new ArrayList<IWeatherPersist.IWeatherInflatedListener>(); private WeatherReport weatherReport; private LocationClient locationClient; private ProgressDialog progress; private boolean needCurrentLocation; private void handlerReportsFailure(int status) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.title_server_error).setMessage(getString(R.string.message_server_error) + status); builder.create().show(); } private void handlerReportsSuccess(ArrayList<GeoName> suggestions) { // This is a bit dangerous. Throughout method definitions this // object has been up-casted to List, but knowing that it originated // as an array list I use an array list here. I don't know of any usage // of non-array lists, but if such a thing happens a class cast exception // may rise here. I do not catch it because this should not remain un-fixed // at run-time, this should be noticed and taken care of by the developer. // Yes, that huge comment referred to the above line only. Bundle args = new Bundle(2); SuggestionsDialogFragment suggestionsDialog = new SuggestionsDialogFragment(); args.putString(SuggestionsDialogFragment.EXTRA_TITLE, getString(R.string.title_suggestions_dialog)); args.putParcelableArrayList(SuggestionsDialogFragment.EXTRA_SUGGESTIONS_LIST, suggestions); suggestionsDialog.setArguments(args); suggestionsDialog.show(getSupportFragmentManager(), Constants.Tags.SUGGESTIONS); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setLocationClient(); initTitle(); final ActionBar actionBar = initActionBar(); initViewPager(actionBar); initTabs(actionBar); registerReceiverForPersistingCompleteAction(); attemptReadWeatherReport(); } private void registerReceiverForPersistingCompleteAction() { // Register a broadcast receiver to listen to weather report persisting complete events. LocalBroadcastManager.getInstance(this).registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { attemptReadWeatherReport(); } }, new IntentFilter(IWeatherPersist.ACTION_PERSISTING_COMPLETE)); } private void initTabs(ActionBar actionBar) { // For each of the sections in the app, add a tab to the action bar. for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) { // Create a tab with text corresponding to the page title defined by // the adapter. Also specify this Activity object, which implements // the TabListener interface, as the callback (listener) for when // this tab is selected. actionBar .addTab(actionBar.newTab().setText(mSectionsPagerAdapter.getPageTitle(i)).setTabListener(this)); } } private void initViewPager(final ActionBar actionBar) { // Create the adapter that will return a fragment for each of the three // primary sections of the activity. mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); // Set up the ViewPager with the sections adapter. mViewPager = (ViewPager) findViewById(R.id.pager); mViewPager.setAdapter(mSectionsPagerAdapter); // When swiping between different sections, select the corresponding // tab. We can also use ActionBar.Tab#select() to do this if we have // a reference to the Tab. mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { actionBar.setSelectedNavigationItem(position); } }); } private ActionBar initActionBar() { // Set up the action bar. final ActionBar actionBar = getSupportActionBar(); actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); return actionBar; } private void initTitle() { // Set page title to the chosen location. SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); String defValue = getString(R.string.app_name); String locationName = preferences.getString(Constants.Keys.PREF_LOCATION_NAME, defValue); if (locationName.equals(defValue)) { setLocationToMyLocation(); } setTitle(locationName); } private void setLocationClient() { // Connect location client locationClient = new LocationClient(this, this, this); } @Override protected void onStart() { super.onStart(); locationClient.connect(); } @Override protected void onStop() { locationClient.disconnect(); super.onStop(); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); } private void attemptReadWeatherReport() { // Trigger the read WeatherUpdate inflation process IWeatherPersist persistenceAgent = AWeatherPersistAgent.getInstance(); persistenceAgent.readWeatherReport(this, this); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.search)); searchView.setQueryHint(getString(R.string.hint_search)); searchView.setOnQueryTextListener(this); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); switch (id) { case R.id.action_my_location: setLocationToMyLocation(); break; case R.id.action_help: showHelpDialogFragment(); break; } return super.onOptionsItemSelected(item); } private void showHelpDialogFragment() { HelpFragment fragment = new HelpFragment(); fragment.show(getSupportFragmentManager(), Constants.Tags.HELP); } private void setLocationToMyLocation() { showCheckingLocationProgressDialog(); int available = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); if (available == ConnectionResult.SUCCESS) { if (locationClient.isConnected()) { findCurrentLocation(); } else { needCurrentLocation = true; } } else { launchGpsErrorDialog(available); } } private void findCurrentLocation() { Location lastLocation = locationClient.getLastLocation(); if (lastLocation != null) { setNewLocation(lastLocation); } else { LocationRequest request = LocationRequest.create(); request.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY); int expirationDuration = 30 * 1000; request.setExpirationDuration(expirationDuration); request.setInterval(1000); request.setNumUpdates(1); locationClient.requestLocationUpdates(request, this); scheduleProgressDismiss(expirationDuration); } } private void showCheckingLocationProgressDialog() { String title = getString(R.string.title_checking_location); String message = getString(R.string.message_checking_location); progress = ProgressDialog.show(this, title, message, true, true); } private void scheduleProgressDismiss(int expirationDuration) { Handler dismissHandler; try { dismissHandler = getWindow().getDecorView().getHandler(); } catch (NullPointerException e) { Log.e(TAG, "scheduleProgressDismiss failed with error: " + e.getMessage()); dismissHandler = handler; } if (dismissHandler != null) { dismissHandler.postDelayed(new Runnable() { @Override public void run() { if (progress != null && progress.isShowing()) { progress.dismiss(); progress = null; Log.d("test", "scheduleProgressDismiss"); getCannotFindLocationDialog().show(); } } }, expirationDuration); } } private void setNewLocation(Location location) { dismissProgressDialog(); if (null != location) { String newLatLng = location.getLatitude() + "," + location.getLongitude(); triggerWeatherUpdate(newLatLng); GeoCoderTask geoCoderTask = new GeoCoderTask(this, this); geoCoderTask.execute(new LatLng(location)); removeSetting(Constants.Keys.PREF_LOCATION_NAME, Constants.Keys.PREF_LATLNG); } else { Log.d("test", "setNewLocation"); getCannotFindLocationDialog().show(); } } private void dismissProgressDialog() { if (progress != null) { progress.dismiss(); progress = null; } } private Dialog getCannotFindLocationDialog() { Dialog result; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.title_cannot_find_location).setMessage(R.string.message_cannot_find_location) .setIcon(R.drawable.sad); result = builder.create(); return result; } private void removeSetting(String... keys) { SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); for (String key : keys) { editor.remove(key); } editor.apply(); } private void launchGpsErrorDialog(int available) { Dialog dialog = GooglePlayServicesUtil.getErrorDialog(available, this, REQUEST_PGS_ERROR_DIALOG); GPSErrorDialogFragment errorDialogFragment = new GPSErrorDialogFragment(); errorDialogFragment.setDialog(dialog); errorDialogFragment.show(getSupportFragmentManager(), Constants.Tags.GPSERROR); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_PGS_ERROR_DIALOG && resultCode == RESULT_OK) { setLocationToMyLocation(); } } @Override public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { // When the given tab is selected, switch to the corresponding page in // the ViewPager. mViewPager.setCurrentItem(tab.getPosition()); } @Override public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { } @Override public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { } /** * Adds a listener to the WeatherReport ready event, returns the index of this listener in the list. * * @param listener * @return */ public int addListener(IWeatherPersist.IWeatherInflatedListener listener) { int position = listeners.size(); listeners.add(listener); return position; } public void removeListener(IWeatherPersist.IWeatherInflatedListener listener) { listeners.remove(listener); } @Override public void onWeatherReportReady(WeatherReport weatherReport) { if (weatherReport != null) { for (IWeatherPersist.IWeatherInflatedListener listener : listeners) { listener.onWeatherReportReady(weatherReport); } this.weatherReport = weatherReport; } } @Override public void onReadFailed() { triggerWeatherUpdate(); } private void triggerWeatherUpdate(String latlng) { WeatherApiClientInterface client = WeatherApiClientFactory.getInstance(); if (latlng != null) { client.makeAsyncRequest(this, latlng); } } private void triggerWeatherUpdate() { String latlng = PreferenceManager.getDefaultSharedPreferences(this).getString(Constants.Keys.PREF_LATLNG, null); triggerWeatherUpdate(latlng); } @Override public boolean onQueryTextSubmit(String query) { GeoNamesApiClientFactory.getInstance().makeAsyncRequest(MainActivity.this, query, handler); return false; } @Override public boolean onQueryTextChange(String newText) { return false; } public void setNewLocation(String locationName, String latlng) { if (TextUtils.isEmpty(locationName)) { locationName = getString(R.string.app_name); } setTitle(locationName); changeSetting(Constants.Keys.PREF_LOCATION_NAME, locationName); changeSetting(Constants.Keys.PREF_LATLNG, latlng); } // Only get the editor private void changeSetting(String key, String value) { SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); editor.putString(key, value); editor.apply(); } @Override public void onSelectionMade(String locationName, String latlng) { triggerWeatherUpdate(latlng); setNewLocation(locationName, latlng); } @Override public void onConnected(Bundle bundle) { Log.i(TAG, "Connected to location services."); if (needCurrentLocation) { findCurrentLocation(); } } @Override public void onDisconnected() { Log.d(TAG, "Disconnected from location services."); } @Override public void onConnectionFailed(ConnectionResult connectionResult) { if (connectionResult.hasResolution()) { try { connectionResult.startResolutionForResult(this, REQUEST_GPS_CONNECTION_RESOLUTION); } catch (IntentSender.SendIntentException e) { e.printStackTrace(); } } else { GPSErrorDialogFragment errorDialogFragment = new GPSErrorDialogFragment(); if (getTitle().equals(getString(R.string.app_name))) { errorDialogFragment.setDialog(getCannotFindLocationDialog()); errorDialogFragment.show(getSupportFragmentManager(), Constants.Tags.CONNECTION_FAILED); } } } @Override public void onAddressReady(String address) { setTitle(address); } @Override public void onLocationChanged(Location location) { setNewLocation(location); } @Override public WeatherReport getData() { return weatherReport; } /** * A {@link android.support.v4.app.FragmentPagerAdapter} that returns a fragment corresponding to * one of the sections/tabs/pages. */ public class SectionsPagerAdapter extends FragmentPagerAdapter { public SectionsPagerAdapter(FragmentManager fm) { super(fm); } @Override public Fragment getItem(int position) { // getItem is called to instantiate the fragment for the given page. // Return a PlaceholderFragment (defined as a static inner class below). switch (position) { case 0: return new CurrentFragment(); case 1: return new HourlyFragment(); case 2: return new DailyFragment(); default: return new CurrentFragment(); } } @Override public int getCount() { // Show 3 total pages. return 3; } @Override public CharSequence getPageTitle(int position) { Locale l = Locale.getDefault(); switch (position) { case 0: return getString(R.string.title_currently).toUpperCase(l); case 1: return getString(R.string.title_hourly).toUpperCase(l); case 2: return getString(R.string.title_daily).toUpperCase(l); } return null; } } }