diff --git a/app/build.gradle b/app/build.gradle index 88499c1d..ccce4f83 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'com.google.gms.google-services' android { compileSdkVersion 21 @@ -29,5 +30,5 @@ dependencies { compile 'com.android.support:support-annotations:22.1.0' compile 'com.android.support:gridlayout-v7:22.1.0' compile 'com.android.support:cardview-v7:22.1.1' - compile 'com.google.android.gms:play-services-gcm:7.0.0' + compile 'com.google.android.gms:play-services-gcm:7.5.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7b2ef03f..bb2aff21 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,17 +21,21 @@ - - - + + + - + + - @@ -43,7 +47,7 @@ android:supportsRtl="true"> + android:label="@string/app_name" > @@ -62,24 +66,22 @@ android:name=".SettingsActivity" android:label="@string/title_activity_settings" android:parentActivityName=".MainActivity" - android:theme="@style/SettingsTheme" > + android:theme="@style/SettingsTheme"> - - + - @@ -88,25 +90,45 @@ + android:exported="true" + > - - + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/example/android/sunshine/app/GcmBroadcastReceiver.java b/app/src/main/java/com/example/android/sunshine/app/GcmBroadcastReceiver.java deleted file mode 100644 index 2b748805..00000000 --- a/app/src/main/java/com/example/android/sunshine/app/GcmBroadcastReceiver.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2015 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.example.android.sunshine.app; - -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.NotificationCompat; -import android.util.Log; - -import com.google.android.gms.gcm.GoogleCloudMessaging; - -public class GcmBroadcastReceiver extends BroadcastReceiver { - private final String LOG_TAG = BroadcastReceiver.class.getSimpleName(); - - private static final String EXTRA_SENDER = "from"; - private static final String EXTRA_WEATHER = "weather"; - private static final String EXTRA_LOCATION = "location"; - - public static final int NOTIFICATION_ID = 1; - private NotificationManager mNotificationManager; - - public GcmBroadcastReceiver() { - super(); - } - - @Override - public void onReceive(Context context, Intent intent) { - Bundle extras = intent.getExtras(); - GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(context); - String messageType = gcm.getMessageType(intent); - - if (!extras.isEmpty()) { // has effect of unparcelling Bundle - /* - * Filter messages based on message type. Since it is likely that GCM - * will be extended in the future with new message types, just ignore - * any message types you're not interested in, or that you don't - * recognize. - */ - if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)) { - // Is this our message?? Better be if you're going to act on it! - if (MainActivity.PROJECT_NUMBER.equals(extras.getString(EXTRA_SENDER))) { - // Process message and then post a notification of the received message. - String weather = extras.getString(EXTRA_WEATHER); - String location = extras.getString(EXTRA_LOCATION); - String alert = "Heads up: " + weather + " in " + location + "!"; - - sendNotification(context, alert); - } - - Log.i(LOG_TAG, "Received: " + extras.toString()); - } - } - } - - // Put the message into a notification and post it. - // This is just one simple example of what you might choose to do with a GCM message. - private void sendNotification(Context context, String msg) { - mNotificationManager = (NotificationManager) - context.getSystemService(Context.NOTIFICATION_SERVICE); - - PendingIntent contentIntent = - PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0); - - NotificationCompat.Builder mBuilder = - new NotificationCompat.Builder(context) - .setSmallIcon(R.drawable.art_storm) - .setContentTitle("Weather Alert!") - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(msg)) - .setContentText(msg) - .setPriority(NotificationCompat.PRIORITY_HIGH); - - mBuilder.setContentIntent(contentIntent); - mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build()); - } -} diff --git a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java index 77bccfbc..f3f99376 100644 --- a/app/src/main/java/com/example/android/sunshine/app/MainActivity.java +++ b/app/src/main/java/com/example/android/sunshine/app/MainActivity.java @@ -15,45 +15,31 @@ */ package com.example.android.sunshine.app; -import android.app.AlertDialog; -import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import com.example.android.sunshine.app.gcm.RegistrationIntentService; import com.example.android.sunshine.app.sync.SunshineSyncAdapter; import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GooglePlayServicesUtil; -import com.google.android.gms.gcm.GoogleCloudMessaging; - -import java.io.IOException; +import com.google.android.gms.common.GoogleApiAvailability; public class MainActivity extends AppCompatActivity implements ForecastFragment.Callback { private final String LOG_TAG = MainActivity.class.getSimpleName(); private static final String DETAILFRAGMENT_TAG = "DFTAG"; private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000; - public static final String PROPERTY_REG_ID = "registration_id"; - private static final String PROPERTY_APP_VERSION = "appVersion"; - - /** - * Substitute you own project number here. This project number comes - * from the Google Developers Console. - */ - static final String PROJECT_NUMBER = "Your Project Number"; + public static final String SENT_TOKEN_TO_SERVER = "sentTokenToServer"; private boolean mTwoPane; private String mLocation; - private GoogleCloudMessaging mGcm; @Override protected void onCreate(Bundle savedInstanceState) { @@ -89,25 +75,21 @@ protected void onCreate(Bundle savedInstanceState) { SunshineSyncAdapter.initializeSyncAdapter(this); - // If Google Play Services is not available, some features, such as GCM-powered weather - // alerts, will not be available. + // If Google Play Services is up to date, we'll want to register GCM. If it is not, we'll + // skip the registration and this device will not receive any downstream messages from + // our fake server. Because weather alerts are not a core feature of the app, this should + // not affect the behavior of the app, from a user perspective. if (checkPlayServices()) { - mGcm = GoogleCloudMessaging.getInstance(this); - String regId = getRegistrationId(this); - - if (PROJECT_NUMBER.equals("Your Project Number")) { - new AlertDialog.Builder(this) - .setTitle("Needs Project Number") - .setMessage("GCM will not function in Sunshine until you set the Project Number to the one from the Google Developers Console.") - .setPositiveButton(android.R.string.ok, null) - .create().show(); - } else if (regId.isEmpty()) { - registerInBackground(this); + // Because this is the initial creation of the app, we'll want to be certain we have + // a token. If we do not, then we will start the IntentService that will register this + // application with GCM. + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(this); + boolean sentToken = sharedPreferences.getBoolean(SENT_TOKEN_TO_SERVER, false); + if (!sentToken) { + Intent intent = new Intent(this, RegistrationIntentService.class); + startService(intent); } - } else { - Log.i(LOG_TAG, "No valid Google Play Services APK. Weather alerts will be disabled."); - // Store regID as null - storeRegistrationId(this, null); } } @@ -130,22 +112,16 @@ public boolean onOptionsItemSelected(MenuItem item) { startActivity(new Intent(this, SettingsActivity.class)); return true; } + return super.onOptionsItemSelected(item); } @Override protected void onResume() { super.onResume(); - - // If Google Play Services is not available, some features, such as GCM-powered weather - // alerts, will not be available. - if (!checkPlayServices()) { - // Store regID as null - } - - String location = Utility.getPreferredLocation(this); + String location = Utility.getPreferredLocation( this ); // update the location in our second pane using the fragment manager - if (location != null && !location.equals(mLocation)) { + if (location != null && !location.equals(mLocation)) { ForecastFragment ff = (ForecastFragment)getSupportFragmentManager().findFragmentById(R.id.fragment_forecast); if ( null != ff ) { ff.onLocationChanged(); @@ -186,10 +162,11 @@ public void onItemSelected(Uri contentUri) { * the Google Play Store or enable it in the device's system settings. */ private boolean checkPlayServices() { - int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); + GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance(); + int resultCode = apiAvailability.isGooglePlayServicesAvailable(this); if (resultCode != ConnectionResult.SUCCESS) { - if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) { - GooglePlayServicesUtil.getErrorDialog(resultCode, this, + if (apiAvailability.isUserResolvableError(resultCode)) { + apiAvailability.getErrorDialog(this, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST).show(); } else { Log.i(LOG_TAG, "This device is not supported."); @@ -199,113 +176,4 @@ private boolean checkPlayServices() { } return true; } - - /** - * Gets the current registration ID for application on GCM service. - *

- * If result is empty, the app needs to register. - * - * @return registration ID, or empty string if there is no existing - * registration ID. - */ - private String getRegistrationId(Context context) { - final SharedPreferences prefs = getGCMPreferences(context); - String registrationId = prefs.getString(PROPERTY_REG_ID, ""); - if (registrationId.isEmpty()) { - Log.i(LOG_TAG, "GCM Registration not found."); - return ""; - } - - // Check if app was updated; if so, it must clear the registration ID - // since the existing registration ID is not guaranteed to work with - // the new app version. - int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE); - int currentVersion = getAppVersion(context); - if (registeredVersion != currentVersion) { - Log.i(LOG_TAG, "App version changed."); - return ""; - } - return registrationId; - } - - /** - * @return Application's {@code SharedPreferences}. - */ - private SharedPreferences getGCMPreferences(Context context) { - // Sunshine persists the registration ID in shared preferences, but - // how you store the registration ID in your app is up to you. Just make sure - // that it is private! - return getSharedPreferences(MainActivity.class.getSimpleName(), Context.MODE_PRIVATE); - } - - /** - * @return Application's version code from the {@code PackageManager}. - */ - private static int getAppVersion(Context context) { - try { - PackageInfo packageInfo = context.getPackageManager() - .getPackageInfo(context.getPackageName(), 0); - return packageInfo.versionCode; - } catch (PackageManager.NameNotFoundException e) { - // Should never happen. WHAT DID YOU DO?!?! - throw new RuntimeException("Could not get package name: " + e); - } - } - - /** - * Registers the application with GCM servers asynchronously. - *

- * Stores the registration ID and app versionCode in the application's - * shared preferences. - */ - private void registerInBackground(final Context context) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - String msg = ""; - try { - if (mGcm == null) { - mGcm = GoogleCloudMessaging.getInstance(context); - } - String regId = mGcm.register(PROJECT_NUMBER); - msg = "Device registered, registration ID=" + regId; - - // You should send the registration ID to your server over HTTP, - // so it can use GCM/HTTP or CCS to send messages to your app. - // The request to your server should be authenticated if your app - // is using accounts. - //sendRegistrationIdToBackend(); - // For this demo: we don't need to send it because the device - // will send upstream messages to a server that echo back the - // message using the 'from' address in the message. - - // Persist the registration ID - no need to register again. - storeRegistrationId(context, regId); - } catch (IOException ex) { - msg = "Error :" + ex.getMessage(); - // TODO: If there is an error, don't just keep trying to register. - // Require the user to click a button again, or perform - // exponential back-off. - } - return null; - } - }.execute(null, null, null); - } - - /** - * Stores the registration ID and app versionCode in the application's - * {@code SharedPreferences}. - * - * @param context application's context. - * @param regId registration ID - */ - private void storeRegistrationId(Context context, String regId) { - final SharedPreferences prefs = getGCMPreferences(context); - int appVersion = getAppVersion(context); - Log.i(LOG_TAG, "Saving regId on app version " + appVersion); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(PROPERTY_REG_ID, regId); - editor.putInt(PROPERTY_APP_VERSION, appVersion); - editor.commit(); - } } diff --git a/app/src/main/java/com/example/android/sunshine/app/gcm/MyGcmListenerService.java b/app/src/main/java/com/example/android/sunshine/app/gcm/MyGcmListenerService.java new file mode 100644 index 00000000..14627478 --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/gcm/MyGcmListenerService.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2015 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.example.android.sunshine.app.gcm; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Bundle; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.widget.Toast; + +import com.example.android.sunshine.app.MainActivity; +import com.example.android.sunshine.app.R; +import com.google.android.gms.gcm.GcmListenerService; + +import org.json.JSONException; +import org.json.JSONObject; + +public class MyGcmListenerService extends GcmListenerService { + + private static final String TAG = "MyGcmListenerService"; + + private static final String EXTRA_DATA = "data"; + private static final String EXTRA_WEATHER = "weather"; + private static final String EXTRA_LOCATION = "location"; + + public static final int NOTIFICATION_ID = 1; + + /** + * Called when message is received. + * + * @param from SenderID of the sender. + * @param data Data bundle containing message data as key/value pairs. + * For Set of keys use data.keySet(). + */ + @Override + public void onMessageReceived(String from, Bundle data) { + // Time to unparcel the bundle! + if (!data.isEmpty()) { + // TODO: gcm_default sender ID comes from the API console + String senderId = getString(R.string.gcm_defaultSenderId); + if (senderId.length() == 0) { + Toast.makeText(this, "SenderID string needs to be set", Toast.LENGTH_LONG).show(); + } + // Not a bad idea to check that the message is coming from your server. + if ((senderId).equals(from)) { + // Process message and then post a notification of the received message. + try { + JSONObject jsonObject = new JSONObject(data.getString(EXTRA_DATA)); + String weather = jsonObject.getString(EXTRA_WEATHER); + String location = jsonObject.getString(EXTRA_LOCATION); + String alert = + String.format(getString(R.string.gcm_weather_alert), weather, location); + sendNotification(alert); + } catch (JSONException e) { + // JSON parsing failed, so we just let this message go, since GCM is not one + // of our critical features. + } + } + Log.i(TAG, "Received: " + data.toString()); + } + } + + /** + * Put the message into a notification and post it. + * This is just one simple example of what you might choose to do with a GCM message. + * + * @param message The alert message to be posted. + */ + private void sendNotification(String message) { + NotificationManager mNotificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + PendingIntent contentIntent = + PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0); + + // Notifications using both a large and a small icon (which yours should!) need the large + // icon as a bitmap. So we need to create that here from the resource ID, and pass the + // object along in our notification builder. Generally, you want to use the app icon as the + // small icon, so that users understand what app is triggering this notification. + Bitmap largeIcon = BitmapFactory.decodeResource(this.getResources(), R.drawable.art_storm); + NotificationCompat.Builder mBuilder = + new NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.art_clear) + .setLargeIcon(largeIcon) + .setContentTitle("Weather Alert!") + .setStyle(new NotificationCompat.BigTextStyle().bigText(message)) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_HIGH); + mBuilder.setContentIntent(contentIntent); + mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android/sunshine/app/gcm/MyInstanceIDListenerService.java b/app/src/main/java/com/example/android/sunshine/app/gcm/MyInstanceIDListenerService.java new file mode 100644 index 00000000..862bc476 --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/gcm/MyInstanceIDListenerService.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 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.example.android.sunshine.app.gcm; + +import android.content.Intent; +import com.google.android.gms.iid.InstanceIDListenerService; + +public class MyInstanceIDListenerService extends InstanceIDListenerService { + private static final String TAG = "MyInstanceIDLS"; + + /** + * Called if InstanceID token is updated. This may occur if the security of + * the previous token had been compromised. This call is initiated by the + * InstanceID provider. + */ + @Override + public void onTokenRefresh() { + // Fetch updated Instance ID token. + Intent intent = new Intent(this, RegistrationIntentService.class); + startService(intent); + } +} diff --git a/app/src/main/java/com/example/android/sunshine/app/gcm/RegistrationIntentService.java b/app/src/main/java/com/example/android/sunshine/app/gcm/RegistrationIntentService.java new file mode 100644 index 00000000..fae7157f --- /dev/null +++ b/app/src/main/java/com/example/android/sunshine/app/gcm/RegistrationIntentService.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2015 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.example.android.sunshine.app.gcm; + +import android.app.IntentService; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; +import android.widget.Toast; + +import com.example.android.sunshine.app.MainActivity; +import com.example.android.sunshine.app.R; +import com.google.android.gms.gcm.GoogleCloudMessaging; +import com.google.android.gms.iid.InstanceID; + + +public class RegistrationIntentService extends IntentService { + private static final String TAG = "RegIntentService"; + + public RegistrationIntentService() { + super(TAG); + } + + @Override + protected void onHandleIntent(Intent intent) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + try { + // In the (unlikely) event that multiple refresh operations occur simultaneously, + // ensure that they are processed sequentially. + synchronized (TAG) { + // Initially this call goes out to the network to retrieve the token, subsequent calls + // are local. + InstanceID instanceID = InstanceID.getInstance(this); + + // TODO: gcm_default sender ID comes from the API console + String senderId = getString(R.string.gcm_defaultSenderId); + if ( senderId.length() != 0 ) { + String token = instanceID.getToken(senderId, + GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); + sendRegistrationToServer(token); + } + + // You should store a boolean that indicates whether the generated token has been + // sent to your server. If the boolean is false, send the token to your server, + // otherwise your server should have already received the token. + sharedPreferences.edit().putBoolean(MainActivity.SENT_TOKEN_TO_SERVER, true).apply(); + } + } catch (Exception e) { + Log.d(TAG, "Failed to complete token refresh", e); + + // If an exception happens while fetching the new token or updating our registration data + // on a third-party server, this ensures that we'll attempt the update at a later time. + sharedPreferences.edit().putBoolean(MainActivity.SENT_TOKEN_TO_SERVER, false).apply(); + } + } + + /** + * Normally, you would want to persist the registration to third-party servers. Because we do + * not have a server, and are faking it with a website, you'll want to log the token instead. + * That way you can see the value in logcat, and note it for future use in the website. + * + * @param token The new token. + */ + private void sendRegistrationToServer(String token) { + Log.i(TAG, "GCM Registration Token: " + token); + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1263e484..06bdb4d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -211,4 +211,9 @@ Hurricane Unknown (%1$s) - \ No newline at end of file + + + Heads up: %1$s in %2$s! + // TODO: Get the SenderID from the Developer Console + + diff --git a/build.gradle b/build.gradle index 6356aabd..cfad1887 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'com.google.gms:google-services:1.3.0-beta1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files