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 tech.rithm.udacitysunwearable; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.util.TimeUtils; import android.support.v7.app.AppCompatActivity; 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 tech.rithm.udacitysunwearable.data.SunshinePreferences; import tech.rithm.udacitysunwearable.data.WeatherContract; import tech.rithm.udacitysunwearable.sync.SunshineSyncUtils; import tech.rithm.udacitysunwearable.utilities.SunshineWeatherUtils; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.vision.barcode.Barcode; import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.DataApi; import com.google.android.gms.wearable.DataMap; import com.google.android.gms.wearable.MessageApi; import com.google.android.gms.wearable.MessageEvent; import com.google.android.gms.wearable.Node; import com.google.android.gms.wearable.NodeApi; import com.google.android.gms.wearable.PutDataMapRequest; import com.google.android.gms.wearable.PutDataRequest; import com.google.android.gms.wearable.Wearable; import java.io.ByteArrayOutputStream; import java.util.Calendar; import java.util.List; import java.util.concurrent.TimeUnit; import static android.util.Log.i; public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>, ForecastAdapter.ForecastAdapterOnClickHandler, MessageApi.MessageListener, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { private final String TAG = MainActivity.class.getSimpleName(); /* * The columns of data that we are interested in displaying within our MainActivity's list of * weather data. */ public static final String[] MAIN_FORECAST_PROJECTION = { WeatherContract.WeatherEntry.COLUMN_DATE, WeatherContract.WeatherEntry.COLUMN_MAX_TEMP, WeatherContract.WeatherEntry.COLUMN_MIN_TEMP, WeatherContract.WeatherEntry.COLUMN_WEATHER_ID, }; /* * We store the indices of the values in the array of Strings above to more quickly be able to * access the data from our query. If the order of the Strings above changes, these indices * must be adjusted to match the order of the Strings. */ public static final int INDEX_WEATHER_DATE = 0; public static final int INDEX_WEATHER_MAX_TEMP = 1; public static final int INDEX_WEATHER_MIN_TEMP = 2; public static final int INDEX_WEATHER_CONDITION_ID = 3; /* * This ID will be used to identify the Loader responsible for loading our weather forecast. In * some cases, one Activity can deal with many Loaders. However, in our case, there is only one. * We will still use this ID to initialize the loader and create the loader for best practice. * Please note that 44 was chosen arbitrarily. You can use whatever number you like, so long as * it is unique and consistent. */ private static final int ID_FORECAST_LOADER = 44; /* private int CONNECTION_TIME_OUT_MS = 150000; private GoogleApiClient googleApiClient; private static final String PATH_WEATHER = "/weather"; private static final String PATH_WEATHER_REQUEST = "/weather-request"; private static final String KEY_MAX_TEMP = "com.sunshine.key.max_temp"; private static final String KEY_MIN_TEMP = "com.sunshine.key.min_temp"; private double maxTemp; private double minTemp; */ private static final String COUNT_KEY = "com.blab.key.count"; private static final String PATH_WEATHER = "/weather"; private static final String PATH_WEATHER_REQUEST = "/weather-request"; private static final String KEY_MAX_TEMP = "com.sunshine.key.max_temp"; private static final String KEY_MIN_TEMP = "com.sunshine.key.min_temp"; private static final String KEY_TIME_STAMP = "com.sunshine.key.stamp"; private static final String KEY_ASSET_IMAGE = "com.sunshine.key.asset_image"; private static final String KEY_WEATHER_DESC = "com.sunshine.key.weather_desc"; private String maxTemp; private String minTemp; private int weatherId; private int weatherImageId; private String weatherDescription; private long TIME_STAMP; private int CONNECTION_TIME_OUT_MS = 15000; private GoogleApiClient googleApiClient; private int count = 0; private ForecastAdapter mForecastAdapter; private RecyclerView mRecyclerView; private int mPosition = RecyclerView.NO_POSITION; private ProgressBar mLoadingIndicator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_forecast); if (getSupportActionBar() != null) { getSupportActionBar().setElevation(0f); } googleApiClient = new GoogleApiClient.Builder(this).addApi(Wearable.API).addConnectionCallbacks(this) .addOnConnectionFailedListener(this).build(); /* * 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); /* * 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); /* * 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. * * The third parameter (shouldReverseLayout) 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. */ LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); /* setLayoutManager associates the LayoutManager we created above with our RecyclerView */ 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. * * Although passing in "this" twice may seem strange, it is actually a sign of separation * of concerns, which is best programming practice. The ForecastAdapter requires an * Android Context (which all Activities are) as well as an onClickHandler. Since our * MainActivity implements the ForecastAdapter ForecastOnClickHandler interface, "this" * is also an instance of that type of handler. */ mForecastAdapter = new ForecastAdapter(this, this); /* Setting the adapter attaches it to the RecyclerView in our layout. */ mRecyclerView.setAdapter(mForecastAdapter); showLoading(); /* * 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(ID_FORECAST_LOADER, null, this); SunshineSyncUtils.initialize(this); } @Override protected void onStart() { super.onStart(); if (!googleApiClient.isConnected()) { googleApiClient.connect(); } } @Override public void onConnected(Bundle bundle) { Wearable.MessageApi.addListener(googleApiClient, this); Log.i("onConnected", "CALLED"); new Thread(new Runnable() { @Override public void run() { googleApiClient.blockingConnect(CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS); NodeApi.GetConnectedNodesResult result = Wearable.NodeApi.getConnectedNodes(googleApiClient) .await(); final List<Node> nodes = result.getNodes(); runOnUiThread(new Runnable() { public void run() { Log.i("onConnected", "Connected devices = " + nodes.size()); } }); } }).start(); } @Override public void onMessageReceived(MessageEvent event) { if (PATH_WEATHER_REQUEST.equals(event.getPath())) { sendWeather(); } Log.i("MSG", "MSG recieved!!"); } private void sendWeather() { Calendar calendar = Calendar.getInstance(); TIME_STAMP = calendar.getTimeInMillis(); PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(PATH_WEATHER); putDataMapRequest.setUrgent(); DataMap countToPut = putDataMapRequest.getDataMap(); countToPut.putString(KEY_MAX_TEMP, maxTemp); countToPut.putString(KEY_MIN_TEMP, minTemp); countToPut.putLong(KEY_TIME_STAMP, TIME_STAMP); Bitmap bitmap = BitmapFactory.decodeResource(getResources(), weatherImageId); Asset asset = createAssetFromBitmap(bitmap); countToPut.putAsset(KEY_ASSET_IMAGE, asset); countToPut.putString(KEY_WEATHER_DESC, weatherDescription); PutDataRequest putDataRequest = putDataMapRequest.asPutDataRequest(); PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi.putDataItem(googleApiClient, putDataRequest); pendingResult.setResultCallback(new ResultCallback<DataApi.DataItemResult>() { @Override public void onResult(@NonNull DataApi.DataItemResult dataItemResult) { if (dataItemResult.getStatus().isSuccess()) { Log.i("***", "*****:: _>> message SUCCESS "); } else { Log.i("***", "*****:: _>> message FAILED "); } } }); } private static Asset createAssetFromBitmap(Bitmap bitmap) { final ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream); return Asset.createFromBytes(byteStream.toByteArray()); } @Override public void onConnectionSuspended(int i) { // nodeConnected = false; } @Override public void onConnectionFailed(ConnectionResult connectionResult) { // nodeConnected = false; } /** * 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 openPreferredLocationInMap() { double[] coords = SunshinePreferences.getLocationCoordinates(this); String posLat = Double.toString(coords[0]); String posLong = Double.toString(coords[1]); Uri geoLocation = Uri.parse("geo:" + posLat + "," + posLong); 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!"); } } /** * Called by the {@link android.support.v4.app.LoaderManagerImpl} when a new Loader needs to be * created. This Activity only uses one loader, so we don't necessarily NEED to check the * loaderId, but this is certainly best practice. * * @param loaderId The loader ID for which we need to create a loader * @param bundle Any arguments supplied by the caller * @return A new Loader instance that is ready to start loading. */ @Override public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) { switch (loaderId) { case ID_FORECAST_LOADER: /* URI for all rows of weather data in our weather table */ Uri forecastQueryUri = WeatherContract.WeatherEntry.CONTENT_URI; /* Sort order: Ascending by date */ String sortOrder = WeatherContract.WeatherEntry.COLUMN_DATE + " ASC"; /* * A SELECTION in SQL declares which rows you'd like to return. In our case, we * want all weather data from today onwards that is stored in our weather table. * We created a handy method to do that in our WeatherEntry class. */ String selection = WeatherContract.WeatherEntry.getSqlSelectForTodayOnwards(); return new CursorLoader(this, forecastQueryUri, MAIN_FORECAST_PROJECTION, selection, null, sortOrder); default: throw new RuntimeException("Loader Not Implemented: " + loaderId); } } /** * Called when a Loader has finished loading its data. * * NOTE: There is one small bug in this code. If no data is present in the cursor do to an * initial load being performed with no access to internet, the loading indicator will show * indefinitely, until data is present from the ContentProvider. This will be fixed in a * future version of the course. * * @param loader The Loader that has finished. * @param data The data generated by the Loader. */ @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { double max_temp; double min_temp; if (data != null && data.moveToFirst()) { max_temp = data.getDouble(INDEX_WEATHER_MAX_TEMP); min_temp = data.getDouble(INDEX_WEATHER_MIN_TEMP); weatherId = data.getInt(INDEX_WEATHER_CONDITION_ID); maxTemp = SunshineWeatherUtils.formatTemperature(this, max_temp); minTemp = SunshineWeatherUtils.formatTemperature(this, min_temp); weatherImageId = SunshineWeatherUtils.getSmallArtResourceIdForWeatherCondition(weatherId); weatherDescription = SunshineWeatherUtils.getStringForWeatherCondition(this, weatherId); sendWeather(); } mForecastAdapter.swapCursor(data); if (mPosition == RecyclerView.NO_POSITION) mPosition = 0; mRecyclerView.smoothScrollToPosition(mPosition); if (data != null && data.getCount() != 0) 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<Cursor> loader) { /* * Since this Loader's data is now invalid, we need to clear the Adapter that is * displaying the data. */ mForecastAdapter.swapCursor(null); } /** * This method is for responding to clicks from our list. * * @param date Normalized UTC time that represents the local date of the weather in GMT time. * @see WeatherContract.WeatherEntry#COLUMN_DATE */ @Override public void onClick(long date) { Intent weatherDetailIntent = new Intent(MainActivity.this, DetailActivity.class); Uri uriForDateClicked = WeatherContract.WeatherEntry.buildWeatherUriWithDate(date); weatherDetailIntent.setData(uriForDateClicked); startActivity(weatherDetailIntent); } /** * This method will make the View for the weather data visible and hide the error message and * loading indicator. * <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, hide the loading indicator */ mLoadingIndicator.setVisibility(View.INVISIBLE); /* Finally, make sure the weather data is visible */ mRecyclerView.setVisibility(View.VISIBLE); } /** * This method will make the loading indicator visible and hide the weather View and 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 showLoading() { /* Then, hide the weather data */ mRecyclerView.setVisibility(View.INVISIBLE); /* Finally, show the loading indicator */ mLoadingIndicator.setVisibility(View.VISIBLE); } /** * This is where we inflate and set up the menu for this Activity. * * @param menu The options menu in which you place your items. * * @return You must return true for the menu to be displayed; * if you return false it will not be shown. * * @see #onPrepareOptionsMenu * @see #onOptionsItemSelected */ @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; } /** * Callback invoked when a menu item was selected from this Activity's menu. * * @param item The menu item that was selected by the user * * @return true if you handle the menu click here, false otherwise */ @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_settings) { startActivity(new Intent(this, SettingsActivity.class)); return true; } if (id == R.id.action_map) { openPreferredLocationInMap(); return true; } return super.onOptionsItemSelected(item); } }