Java tutorial
/* * Copyright (C) 2016 The Android Open Source Project * * 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 com.delaroystudios.weatherapp; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.Loader; import android.support.v7.app.AppCompatActivity; import android.support.v7.preference.PreferenceManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.ProgressBar; import android.widget.TextView; import com.delaroystudios.weatherapp.R; import com.delaroystudios.weatherapp.data.WeatherPreferences; import com.delaroystudios.weatherapp.utilities.NetworkUtils; import com.delaroystudios.weatherapp.utilities.OpenWeatherJsonUtils; import java.net.URL; public class MainActivity extends AppCompatActivity implements ForecastAdapter.ForecastAdapterOnClickHandler, LoaderCallbacks<String[]>, SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = MainActivity.class.getSimpleName(); private RecyclerView mRecyclerView; private ForecastAdapter mForecastAdapter; private TextView mErrorMessageDisplay; private ProgressBar mLoadingIndicator; private static final int FORECAST_LOADER_ID = 0; private static boolean PREFERENCES_HAVE_BEEN_UPDATED = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_forecast); /* * Using findViewById, we get a reference to our RecyclerView from xml. This allows us to * do things like set the adapter of the RecyclerView and toggle the visibility. */ mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview_forecast); /* This TextView is used to display errors and will be hidden if there are no errors */ mErrorMessageDisplay = (TextView) findViewById(R.id.tv_error_message_display); /* * A LinearLayoutManager is responsible for measuring and positioning item views within a * RecyclerView into a linear list. This means that it can produce either a horizontal or * vertical list depending on which parameter you pass in to the LinearLayoutManager * constructor. In our case, we want a vertical list, so we pass in the constant from the * LinearLayoutManager class for vertical lists, LinearLayoutManager.VERTICAL. * * There are other LayoutManagers available to display your data in uniform grids, * staggered grids, and more! See the developer documentation for more details. */ int recyclerViewOrientation = LinearLayoutManager.VERTICAL; /* * This value should be true if you want to reverse your layout. Generally, this is only * true with horizontal lists that need to support a right-to-left layout. */ boolean shouldReverseLayout = false; LinearLayoutManager layoutManager = new LinearLayoutManager(this, recyclerViewOrientation, shouldReverseLayout); mRecyclerView.setLayoutManager(layoutManager); /* * Use this setting to improve performance if you know that changes in content do not * change the child layout size in the RecyclerView */ mRecyclerView.setHasFixedSize(true); /* * The ForecastAdapter is responsible for linking our weather data with the Views that * will end up displaying our weather data. */ mForecastAdapter = new ForecastAdapter(this); /* Setting the adapter attaches it to the RecyclerView in our layout. */ mRecyclerView.setAdapter(mForecastAdapter); /* * The ProgressBar that will indicate to the user that we are loading data. It will be * hidden when no data is loading. * * Please note: This so called "ProgressBar" isn't a bar by default. It is more of a * circle. We didn't make the rules (or the names of Views), we just follow them. */ mLoadingIndicator = (ProgressBar) findViewById(R.id.pb_loading_indicator); /* * This ID will uniquely identify the Loader. We can use it, for example, to get a handle * on our Loader at a later point in time through the support LoaderManager. */ int loaderId = FORECAST_LOADER_ID; /* * From MainActivity, we have implemented the LoaderCallbacks interface with the type of * String array. (implements LoaderCallbacks<String[]>) The variable callback is passed * to the call to initLoader below. This means that whenever the loaderManager has * something to notify us of, it will do so through this callback. */ LoaderCallbacks<String[]> callback = MainActivity.this; /* * The second parameter of the initLoader method below is a Bundle. Optionally, you can * pass a Bundle to initLoader that you can then access from within the onCreateLoader * callback. In our case, we don't actually use the Bundle, but it's here in case we wanted * to. */ Bundle bundleForLoader = null; /* * Ensures a loader is initialized and active. If the loader doesn't already exist, one is * created and (if the activity/fragment is currently started) starts the loader. Otherwise * the last created loader is re-used. */ getSupportLoaderManager().initLoader(loaderId, bundleForLoader, callback); Log.d(TAG, "onCreate: registering preference changed listener"); /* * Register MainActivity as an OnPreferenceChangedListener to receive a callback when a * SharedPreference has changed. Please note that we must unregister MainActivity as an * OnSharedPreferenceChanged listener in onDestroy to avoid any memory leaks. */ PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this); } /** * Instantiate and return a new Loader for the given ID. * * @param id The ID whose loader is to be created. * @param loaderArgs Any arguments supplied by the caller. * * @return Return a new Loader instance that is ready to start loading. */ @Override public Loader<String[]> onCreateLoader(int id, final Bundle loaderArgs) { return new AsyncTaskLoader<String[]>(this) { /* This String array will hold and help cache our weather data */ String[] mWeatherData = null; /** * Subclasses of AsyncTaskLoader must implement this to take care of loading their data. */ @Override protected void onStartLoading() { if (mWeatherData != null) { deliverResult(mWeatherData); } else { mLoadingIndicator.setVisibility(View.VISIBLE); forceLoad(); } } /** * This is the method of the AsyncTaskLoader that will load and parse the JSON data * from OpenWeatherMap in the background. * * @return Weather data from OpenWeatherMap as an array of Strings. * null if an error occurs */ @Override public String[] loadInBackground() { URL weatherRequestUrl = NetworkUtils.getUrl(MainActivity.this); try { String jsonWeatherResponse = NetworkUtils.getResponseFromHttpUrl(weatherRequestUrl); String[] simpleJsonWeatherData = OpenWeatherJsonUtils .getSimpleWeatherStringsFromJson(MainActivity.this, jsonWeatherResponse); return simpleJsonWeatherData; } catch (Exception e) { e.printStackTrace(); return null; } } /** * Sends the result of the load to the registered listener. * * @param data The result of the load */ public void deliverResult(String[] data) { mWeatherData = data; super.deliverResult(data); } }; } /** * Called when a previously created loader has finished its load. * * @param loader The Loader that has finished. * @param data The data generated by the Loader. */ @Override public void onLoadFinished(Loader<String[]> loader, String[] data) { mLoadingIndicator.setVisibility(View.INVISIBLE); mForecastAdapter.setWeatherData(data); if (null == data) { showErrorMessage(); } else { showWeatherDataView(); } } /** * Called when a previously created loader is being reset, and thus * making its data unavailable. The application should at this point * remove any references it has to the Loader's data. * * @param loader The Loader that is being reset. */ @Override public void onLoaderReset(Loader<String[]> loader) { /* * We aren't using this method in our example application, but we are required to Override * it to implement the LoaderCallbacks<String> interface */ } /** * This method is used when we are resetting data, so that at one point in time during a * refresh of our data, you can see that there is no data showing. */ private void invalidateData() { mForecastAdapter.setWeatherData(null); } /** * This method uses the URI scheme for showing a location found on a map in conjunction with * an implicit Intent. This super-handy intent is detailed in the "Common Intents" page of * Android's developer site: * * @see "http://developer.android.com/guide/components/intents-common.html#Maps" * <p> * Protip: Hold Command on Mac or Control on Windows and click that link to automagically * open the Common Intents page */ private void openLocationInMap() { String addressString = WeatherPreferences.getPreferredWeatherLocation(this); Uri geoLocation = Uri.parse("geo:0,0?q=" + addressString); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(geoLocation); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); } else { Log.d(TAG, "Couldn't call " + geoLocation.toString() + ", no receiving apps installed!"); } } /** * This method is for responding to clicks from our list. * * @param weatherForDay String describing weather details for a particular day */ @Override public void onClick(String weatherForDay) { Context context = this; Class destinationClass = DetailActivity.class; Intent intentToStartDetailActivity = new Intent(context, destinationClass); intentToStartDetailActivity.putExtra(Intent.EXTRA_TEXT, weatherForDay); startActivity(intentToStartDetailActivity); } /** * This method will make the View for the weather data visible and * hide the error message. * <p> * Since it is okay to redundantly set the visibility of a View, we don't * need to check whether each view is currently visible or invisible. */ private void showWeatherDataView() { /* First, make sure the error is invisible */ mErrorMessageDisplay.setVisibility(View.INVISIBLE); /* Then, make sure the weather data is visible */ mRecyclerView.setVisibility(View.VISIBLE); } /** * This method will make the error message visible and hide the weather * View. * <p> * Since it is okay to redundantly set the visibility of a View, we don't * need to check whether each view is currently visible or invisible. */ private void showErrorMessage() { /* First, hide the currently visible data */ mRecyclerView.setVisibility(View.INVISIBLE); /* Then, show the error */ mErrorMessageDisplay.setVisibility(View.VISIBLE); } /** * OnStart is called when the Activity is coming into view. This happens when the Activity is * first created, but also happens when the Activity is returned to from another Activity. We * are going to use the fact that onStart is called when the user returns to this Activity to * check if the location setting or the preferred units setting has changed. If it has changed, * we are going to perform a new query. */ @Override protected void onStart() { super.onStart(); /* * If the preferences for location or units have changed since the user was last in * MainActivity, perform another query and set the flag to false. * * This isn't the ideal solution because there really isn't a need to perform another * GET request just to change the units, but this is the simplest solution that gets the * job done for now. Later in this course, we are going to show you more elegant ways to * handle converting the units from celsius to fahrenheit and back without hitting the * network again by keeping a copy of the data in a manageable format. */ if (PREFERENCES_HAVE_BEEN_UPDATED) { Log.d(TAG, "onStart: preferences were updated"); getSupportLoaderManager().restartLoader(FORECAST_LOADER_ID, null, this); PREFERENCES_HAVE_BEEN_UPDATED = false; } } @Override protected void onDestroy() { super.onDestroy(); /* Unregister MainActivity as an OnPreferenceChangedListener to avoid any memory leaks. */ PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { /* Use AppCompatActivity's method getMenuInflater to get a handle on the menu inflater */ MenuInflater inflater = getMenuInflater(); /* Use the inflater's inflate method to inflate our menu layout to this menu */ inflater.inflate(R.menu.forecast, menu); /* Return true so that the menu is displayed in the Toolbar */ return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_refresh) { invalidateData(); getSupportLoaderManager().restartLoader(FORECAST_LOADER_ID, null, this); return true; } if (id == R.id.action_map) { openLocationInMap(); return true; } if (id == R.id.action_settings) { Intent startSettingsActivity = new Intent(this, SettingsActivity.class); startActivity(startSettingsActivity); return true; } return super.onOptionsItemSelected(item); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { /* * Set this flag to true so that when control returns to MainActivity, it can refresh the * data. * * This isn't the ideal solution because there really isn't a need to perform another * GET request just to change the units, but this is the simplest solution that gets the * job done for now. Later in this course, we are going to show you more elegant ways to * handle converting the units from celsius to fahrenheit and back without hitting the * network again by keeping a copy of the data in a manageable format. */ PREFERENCES_HAVE_BEEN_UPDATED = true; } }