diff --git a/README.md b/README.md index 028c614a..d925595d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,99 @@ # Squawker Code -This is a exercise repository for the Squawker example app which is part of Udacity's Advanced Android course. The Squawker example app uses Firebase Cloud Messenger to receive Twitter-like messages, sent from [this server](https://squawkerfcmserver.udacity.com/), in real time. You can learn more about how to use this repository [here](https://classroom.udacity.com/courses/ud857/lessons/8b2a9d63-0ff5-48ff-90d3-a9855b701dae/concepts/41b82e3c-2797-46e5-8a66-684098ca8cbb). \ No newline at end of file +This is a exercise repository for the Squawker example app which is part of Udacity's Advanced Android course. The Squawker example app uses Firebase Cloud Messenger to receive Twitter-like messages, sent from [this server](https://squawkerfcmserver.udacity.com/), in real time. + +This is the toy app for the FCM lesson of the [Advanced Android App Development course on Udacity](https://www.udacity.com/course/advanced-android-app-development--ud855). + +## How to use this repo while taking the course + +Each code repository in this class has a chain of commits that looks like this: + +![listofcommits](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58befe2e_listofcommits/listofcommits.png) + +These commits show every step you'll take to create the app. They include **Exercise** commits and **Solution** commits. + +Exercise commits contain instructions for completing the exercise, while solution commits show the completed exercise. You can tell what a commit is by looking at its commit message. + +For example, **TFCM.01-Exercise-AddGradleDependencies** is the first code step in the Firebase Cloud Messaging (FCM) lesson. This is the exercise commit, and the exercise is called Add Gradle Dependencies. + +Each commit also has a **branch** associated with it of the same name as the commit message, seen below: + +![branches](https://d17h27t6h515a5.cloudfront.net/topher/2017/April/590390fe_branches-ud855/branches-ud855.png +) +Access all branches from this tab + +![listofbranches](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58befe76_listofbranches/listofbranches.png +) + + +![branchesdropdown](https://d17h27t6h515a5.cloudfront.net/topher/2017/April/590391a3_branches-dropdown-ud855/branches-dropdown-ud855.png +) + + +The branches are also accessible from the drop-down in the "Code" tab + + +## Working with the Course Code + +Here are the basic steps for working with and completing exercises in the repo. This information is linked whenever you start a new exercise project, so don't feel you need to memorize all of this! In fact, skim it now, make sure that you know generally how to do the different tasks, and then come back when you start your first exercise. + +The basic steps are: + +1. Clone the repo +2. Checkout the exercise branch +3. Find and complete the TODOs +4. Optionally commit your code changes +5. Compare with the solution + + +**Step 1: Clone the repo** + +As you go through the course, you'll be instructed to clone the different exercise repositories, so you don't need to set these up now. You can clone a repository from github in a folder of your choice with the command: + +```bash +git clone https://github.com/udacity/REPOSITORY_NAME.git +``` + +**Step 2: Checkout the exercise branch** + +As you do different exercises in the code, you'll be told which exercise you're on, as seen below: +![exerciseexample](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf0087_exerciseexample/exerciseexample.png +) + +To complete an exercise, you'll want to check out the branch associated with that exercise. For the exercise above, the command to check out that branch would be: + +```bash +git checkout TFCM.01-Exercise-AddGradleDependencies +``` + +**Step 3: Find and complete the TODOs** + +This branch should always have **Exercise** in the title. Once you've checked out the branch, you'll have the code in the exact state you need. You'll even have TODOs, which are special comments that tell you all the steps you need to complete the exercise. You can easily navigate to all the TODOs using Android Studio's TODO tool. To open the TODO tool, click the button at the bottom of the screen that says TODO. This will display a list of all comments with TODO in the project. + +We've numbered the TODO steps so you can do them in order: +![todos](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf00e7_todos/todos.png +) + +**Step 4: Optionally commit your code changes** + +After You've completed the TODOs, you can optionally commit your changes. This will allow you to see the code you wrote whenever you return to the branch. The following git code will add and save **all** your changes. + +```bash +git add . +git commit -m "Your commit message" +``` + +**Step 5: Compare with the solution** + +Most exercises will have a list of steps for you to check off in the classroom. Once you've checked these off, you'll see a pop up window with a link to the solution code. Note the **Diff** link: + +![solutionwindow](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf00f9_solutionwindow/solutionwindow.png +) + +The **Diff** link will take you to a Github diff as seen below: +![diff](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf0108_diffsceenshot/diffsceenshot.png +) + +All of the code that was added in the solution is in green, and the removed code (which will usually be the TODO comments) is in red. +## Report Issues +Notice any issues with a repository? Please file a github issue in the repository. \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 7d9b9a76..9b01c07c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,13 +1,12 @@ apply plugin: 'com.android.application' -apply plugin: 'android-apt' android { - compileSdkVersion 25 - buildToolsVersion "25.0.2" + compileSdkVersion 27 + buildToolsVersion "27.0.1" defaultConfig { applicationId "android.example.com.squawker" minSdkVersion 16 - targetSdkVersion 25 + targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -25,22 +24,25 @@ dependencies { androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) - compile 'com.android.support:appcompat-v7:25.1.0' + compile 'com.android.support:appcompat-v7:27.0.1' testCompile 'junit:junit:4.12' // RecyclerView - compile 'com.android.support:recyclerview-v7:25.1.0' + compile 'com.android.support:recyclerview-v7:27.0.1' // Schematic dependencies for ContentProvider - apt 'net.simonvt.schematic:schematic-compiler:0.6.3' + annotationProcessor 'net.simonvt.schematic:schematic-compiler:0.6.3' compile 'net.simonvt.schematic:schematic:0.6.3' // Preferences Dependencies - compile 'com.android.support:preference-v7:25.1.0' + compile 'com.android.support:preference-v7:27.0.1' - // TODO (3) Add the Firebase libraries and apply the GPS plugin in this file. Make sure to save the // google.services.json file in the app folder. After syncing the changes, you should be able // to send a notification to your app using the Firebase console. The app must be in the // background! - + compile 'com.google.firebase:firebase-messaging:11.6.2' } + +// Apply the Google Services plugin. Make sure to add the google-services.json file in the app +// folder. You download it from the Firebase console +apply plugin: 'com.google.gms.google-services' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 39412b52..021d37a0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,6 +33,20 @@ android:value=".MainActivity" /> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/android/example/com/squawker/MainActivity.java b/app/src/main/java/android/example/com/squawker/MainActivity.java index fa53a850..04c9dec4 100644 --- a/app/src/main/java/android/example/com/squawker/MainActivity.java +++ b/app/src/main/java/android/example/com/squawker/MainActivity.java @@ -35,6 +35,8 @@ import android.view.MenuInflater; import android.view.MenuItem; +import com.google.firebase.iid.FirebaseInstanceId; + public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks { @@ -61,6 +63,7 @@ public class MainActivity extends AppCompatActivity implements @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + Log.v(LOG_TAG, "-> onCreate"); setContentView(R.layout.activity_main); mRecyclerView = (RecyclerView) findViewById(R.id.squawks_recycler_view); @@ -86,6 +89,11 @@ protected void onCreate(Bundle savedInstanceState) { // Start the loader getSupportLoaderManager().initLoader(LOADER_ID_MESSAGES, null, this); + // Get token from the ID Service you created and show it in a log + String token = FirebaseInstanceId.getInstance().getToken(); + String msg = getString(R.string.message_token_format, token); + Log.d(LOG_TAG, "-> " + msg); + } @Override diff --git a/app/src/main/java/android/example/com/squawker/fcm/SquawkFirebaseInstanceIdService.java b/app/src/main/java/android/example/com/squawker/fcm/SquawkFirebaseInstanceIdService.java new file mode 100644 index 00000000..fd088a21 --- /dev/null +++ b/app/src/main/java/android/example/com/squawker/fcm/SquawkFirebaseInstanceIdService.java @@ -0,0 +1,59 @@ +/* +* Copyright (C) 2017 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 android.example.com.squawker.fcm; + +import android.util.Log; + +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.FirebaseInstanceIdService; + +/** + * Listens for changes in the InstanceID + */ +public class SquawkFirebaseInstanceIdService extends FirebaseInstanceIdService { + + private static String LOG_TAG = SquawkFirebaseInstanceIdService.class.getSimpleName(); + + /** + * Called if InstanceID token is updated. This may occur if the security of + * the previous token had been compromised. Note that this is called when the InstanceID token + * is initially generated so this is where you would retrieve the token. + */ + @Override + public void onTokenRefresh() { + // Get updated InstanceID token. + String refreshedToken = FirebaseInstanceId.getInstance().getToken(); + Log.d(LOG_TAG, "Refreshed token: " + refreshedToken); + + // If you want to send messages to this application instance or + // manage this apps subscriptions on the server side, send the + // Instance ID token to your app server. + sendRegistrationToServer(refreshedToken); + } + + /** + * Persist token to third-party servers. + *

+ * Modify this method to associate the user's FCM InstanceID token with any server-side account + * maintained by your application. + * + * @param token The new token. + */ + private void sendRegistrationToServer(String token) { + // This method is blank, but if you were to build a server that stores users token + // information, this is where you'd send the token to the server. + } +} \ No newline at end of file diff --git a/app/src/main/java/android/example/com/squawker/fcm/SquawkFirebaseMessageService.java b/app/src/main/java/android/example/com/squawker/fcm/SquawkFirebaseMessageService.java new file mode 100644 index 00000000..bc117fc5 --- /dev/null +++ b/app/src/main/java/android/example/com/squawker/fcm/SquawkFirebaseMessageService.java @@ -0,0 +1,164 @@ +/* +* Copyright (C) 2017 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 android.example.com.squawker.fcm; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.example.com.squawker.MainActivity; +import android.example.com.squawker.R; +import android.example.com.squawker.provider.SquawkContract; +import android.example.com.squawker.provider.SquawkProvider; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; + +import java.util.Map; + +/** + * Listens for squawk FCM messages both in the background and the foreground and responds + * appropriately + * depending on type of message + */ +public class SquawkFirebaseMessageService extends FirebaseMessagingService { + + private static final String JSON_KEY_AUTHOR = SquawkContract.COLUMN_AUTHOR; + private static final String JSON_KEY_AUTHOR_KEY = SquawkContract.COLUMN_AUTHOR_KEY; + private static final String JSON_KEY_MESSAGE = SquawkContract.COLUMN_MESSAGE; + private static final String JSON_KEY_DATE = SquawkContract.COLUMN_DATE; + + private static final int NOTIFICATION_MAX_CHARACTERS = 30; + private static String LOG_TAG = SquawkFirebaseMessageService.class.getSimpleName(); + + /** + * Called when message is received. + * + * @param remoteMessage Object representing the message received from Firebase Cloud Messaging + */ + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + // There are two types of messages data messages and notification messages. Data messages + // are handled + // here in onMessageReceived whether the app is in the foreground or background. Data + // messages are the type + // traditionally used with FCM. Notification messages are only received here in + // onMessageReceived when the app + // is in the foreground. When the app is in the background an automatically generated + // notification is displayed. + // When the user taps on the notification they are returned to the app. Messages + // containing both notification + // and data payloads are treated as notification messages. The Firebase console always + // sends notification + // messages. For more see: https://firebase.google.com/docs/cloud-messaging/concept-options\ + + // The Squawk server always sends just *data* messages, meaning that onMessageReceived when + // the app is both in the foreground AND the background + + Log.d(LOG_TAG, "-> From: " + remoteMessage.getFrom()); + + // Check if message contains a data payload. + + Map data = remoteMessage.getData(); + + if (data.size() > 0) { + Log.d(LOG_TAG, "-> Message data payload: " + data); + + // Send a notification that you got a new message + sendNotification(data); + insertSquawk(data); + + } + } + + /** + * Inserts a single squawk into the database; + * + * @param data Map which has the message data in it + */ + private void insertSquawk(final Map data) { + + // Database operations should not be done on the main thread + AsyncTask insertSquawkTask = new InsertSquawkTask(this, data); + insertSquawkTask.execute(); + } + + private static class InsertSquawkTask extends AsyncTask { + + private Context context; + private Map data; + + public InsertSquawkTask(Context context, Map data) { + this.context = context; + this.data = data; + } + + @Override + protected Void doInBackground(Void... voids) { + ContentValues newMessage = new ContentValues(); + newMessage.put(SquawkContract.COLUMN_AUTHOR, data.get(JSON_KEY_AUTHOR)); + newMessage.put(SquawkContract.COLUMN_MESSAGE, data.get(JSON_KEY_MESSAGE).trim()); + newMessage.put(SquawkContract.COLUMN_DATE, data.get(JSON_KEY_DATE)); + newMessage.put(SquawkContract.COLUMN_AUTHOR_KEY, data.get(JSON_KEY_AUTHOR_KEY)); + context.getContentResolver().insert(SquawkProvider.SquawkMessages.CONTENT_URI, newMessage); + context = null; + return null; + } + } + + + /** + * Create and show a simple notification containing the received FCM message + * + * @param data Map which has the message data in it + */ + private void sendNotification(Map data) { + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + // Create the pending intent to launch the activity + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent, + PendingIntent.FLAG_ONE_SHOT); + + String author = data.get(JSON_KEY_AUTHOR); + String message = data.get(JSON_KEY_MESSAGE); + + // If the message is longer than the max number of characters we want in our + // notification, truncate it and add the unicode character for ellipsis + if (message.length() > NOTIFICATION_MAX_CHARACTERS) { + message = message.substring(0, NOTIFICATION_MAX_CHARACTERS) + "\u2026"; + } + + Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.ic_duck) + .setContentTitle(String.format(getString(R.string.notification_message), author)) + .setContentText(message) + .setAutoCancel(true) + .setSound(defaultSoundUri) + .setContentIntent(pendingIntent); + + NotificationManager notificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + notificationManager.notify(0 /* ID of notification */, notificationBuilder.build()); + } +} \ No newline at end of file diff --git a/app/src/main/java/android/example/com/squawker/following/FollowingPreferenceFragment.java b/app/src/main/java/android/example/com/squawker/following/FollowingPreferenceFragment.java index 682f000c..a5cc48b1 100644 --- a/app/src/main/java/android/example/com/squawker/following/FollowingPreferenceFragment.java +++ b/app/src/main/java/android/example/com/squawker/following/FollowingPreferenceFragment.java @@ -15,15 +15,23 @@ */ package android.example.com.squawker.following; +import android.content.SharedPreferences; import android.example.com.squawker.R; import android.os.Bundle; +import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceFragmentCompat; +import android.support.v7.preference.SwitchPreferenceCompat; +import android.util.Log; + +import com.google.firebase.messaging.FirebaseMessaging; /** * Shows the list of instructors you can follow */ -public class FollowingPreferenceFragment extends PreferenceFragmentCompat { +// TODO COMPLETED (1) Implement onSharedPreferenceChangeListener +public class FollowingPreferenceFragment extends PreferenceFragmentCompat implements + SharedPreferences.OnSharedPreferenceChangeListener { private final static String LOG_TAG = FollowingPreferenceFragment.class.getSimpleName(); @@ -32,4 +40,62 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { // Add visualizer preferences, defined in the XML file in res->xml->preferences_squawker addPreferencesFromResource(R.xml.following_squawker); } + + // TODO COMPLETED (2) When a SharedPreference changes, check which preference it is and subscribe or + // un-subscribe to the correct topics. + + // Ex. FirebaseMessaging.getInstance().subscribeToTopic("key_lyla"); + // subscribes to Lyla's squawks. + + // HINT: Checkout res->xml->following_squawker.xml. Note how the keys for each of the + // preferences matches the topic to subscribe to for each instructor. + + /** + * Triggered when shared preferences changes. This will be triggered when a person is followed + * or un-followed + * + * @param sharedPreferences SharedPreferences file + * @param key The key of the preference which was changed + */ + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + + Preference preference = findPreference(key); + if (preference != null && preference instanceof SwitchPreferenceCompat) { + // Get the current state of the switch preference + boolean isOn = sharedPreferences.getBoolean(key, false); + if (isOn) { + // The preference key matches the following key for the associated instructor in + // FCM. For example, the key for Lyla is key_lyla (as seen in + // following_squawker.xml). The topic for Lyla's messages is /topics/key_lyla + + // Subscribe + FirebaseMessaging.getInstance().subscribeToTopic(key); + Log.d(LOG_TAG, "-> Subscribing to " + key); + } else { + // Un-subscribe + FirebaseMessaging.getInstance().unsubscribeFromTopic(key); + Log.d(LOG_TAG, "-> Un-subscribing to " + key); + } + } + } + + // TODO COMPLETED (3) Make sure to register and unregister this as a Shared Preference Change listener, in + // onCreate and onDestroy. + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Add the shared preference change listener + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + // Remove the shared preference change listener + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + } } diff --git a/build.gradle b/build.gradle index fddab040..1c04c2d1 100644 --- a/build.gradle +++ b/build.gradle @@ -5,13 +5,8 @@ buildscript { jcenter() } dependencies { - classpath 'com.neenbedankt.gradle.plugins:android-apt:1.2' - classpath 'com.android.tools.build:gradle:2.2.3' - - // TODO (1) Make a new Firebase project in the Firebase Console and follow the instructions. - // The console is here: https://console.firebase.google.com/ - - // TODO (2) Add Google Services plugin here + classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.google.gms:google-services:3.1.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -22,6 +17,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" // Google's Maven repository + } } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 04e285f3..98377102 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Dec 28 10:00:20 PST 2015 +#Thu Nov 30 20:19:40 IST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip