diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 982344e..5328d2e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -14,6 +14,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 1188f7e..11987c1 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ The following instructions are tested for Intellij on Mac OS X and Windows:
1. Ensure you have
[the latest Android SDKs and build tools installed](https://developer.android.com/sdk/index.html).
+1. Install the [Google Play Services SDK](http://developer.android.com/google/play-services/setup.html#Install).
1. Create a file in the project root directory called `local.properties` and add the line `sdk.dir=/path/to/your/sdk`.
1. Download [Gradle](http://www.gradle.org/downloads) (tested with version 1.10).
1. Open Intellij (or Android Studio). Ensure you have the Gradle Intellij plugin.
diff --git a/build.gradle b/build.gradle
index f80ef7c..03612f0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -42,6 +42,7 @@ dependencies {
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.1'
compile "com.mixpanel.android:mixpanel-android:4.0.0@aar"
compile 'com.crashlytics.android:crashlytics:1.+'
+ compile 'com.google.android.gms:play-services:4.0.30'
}
android {
diff --git a/proguard-project.txt b/proguard-project.txt
index 492ca2c..c7f9bf7 100644
--- a/proguard-project.txt
+++ b/proguard-project.txt
@@ -42,3 +42,22 @@
# We're open-sourced anyway... let's make life easier :)
-dontobfuscate
+
+# Don't strip away required classes from Google Play Services SDK.
+# From http://developer.android.com/google/play-services/setup.html
+-keep class * extends java.util.ListResourceBundle {
+ protected Object[][] getContents();
+}
+
+-keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable {
+ public static final *** NULL;
+}
+
+-keepnames @com.google.android.gms.common.annotation.KeepName class *
+-keepclassmembernames class * {
+ @com.google.android.gms.common.annotation.KeepName *;
+}
+
+-keepnames class * implements android.os.Parcelable {
+ public static final ** CREATOR;
+}
diff --git a/res/drawable-xhdpi/ic_add_alert.png b/res/drawable-xhdpi/ic_add_alert.png
new file mode 100644
index 0000000..e1ea093
Binary files /dev/null and b/res/drawable-xhdpi/ic_add_alert.png differ
diff --git a/res/drawable-xhdpi/ic_notification.png b/res/drawable-xhdpi/ic_notification.png
new file mode 100644
index 0000000..c835319
Binary files /dev/null and b/res/drawable-xhdpi/ic_notification.png differ
diff --git a/res/layout/course_class_row_item.xml b/res/layout/course_class_row_item.xml
index 73039ab..55a57ca 100644
--- a/res/layout/course_class_row_item.xml
+++ b/res/layout/course_class_row_item.xml
@@ -1,6 +1,46 @@
-
-
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="3dp"
+ android:paddingBottom="3dp"
+ android:paddingLeft="10dp"
+ android:paddingRight="10dp">
+
+
+
+
+
+
diff --git a/res/layout/course_schedule.xml b/res/layout/course_schedule.xml
index 5e9f1ae..eecdda9 100644
--- a/res/layout/course_schedule.xml
+++ b/res/layout/course_schedule.xml
@@ -24,26 +24,6 @@
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp" >
-
-
-
-
-
-
-
-
-
-
diff --git a/src/com/uwflow/flow_android/MainFlowActivity.java b/src/com/uwflow/flow_android/MainFlowActivity.java
index 422ef1a..41cafec 100644
--- a/src/com/uwflow/flow_android/MainFlowActivity.java
+++ b/src/com/uwflow/flow_android/MainFlowActivity.java
@@ -34,6 +34,8 @@
import com.uwflow.flow_android.network.FlowAsyncClient;
import com.uwflow.flow_android.nfc.SharableURL;
import com.uwflow.flow_android.util.FacebookUtilities;
+import com.uwflow.flow_android.util.RegistrationIdUtil;
+import org.apache.commons.lang3.StringUtils;
import org.json.JSONException;
import org.json.JSONObject;
@@ -154,8 +156,12 @@ public void onDrawerOpened(View view) {
mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
if (savedInstanceState == null) {
- Fragment initialFragment;
- if (isUserLoggedIn) {
+ Bundle bundle = getIntent().getExtras();
+ String courseId = (bundle == null ? null : bundle.getString(Constants.COURSE_ID_KEY));
+
+ if (StringUtils.isNotEmpty(courseId)) {
+ replaceWithFragment(CourseFragment.newInstance(courseId), false);
+ } else if (isUserLoggedIn) {
selectItem(Constants.NAV_DRAWER_PROFILE_INDEX, false);
} else {
selectItem(Constants.NAV_DRAWER_EXPLORE_INDEX, false);
@@ -198,6 +204,8 @@ public NdefMessage createNdefMessage(NfcEvent event) {
}, this);
}
+ // Register this device with GCM to receive push notifications, if possible.
+ RegistrationIdUtil.init(getApplicationContext());
}
/**
@@ -339,6 +347,18 @@ private void selectItem(int itemID, boolean addToBackStack) {
return;
}
+ replaceWithFragment(fragment, addToBackStack);
+ int selectedPosition = mNavDrawerAdapter.getPositionFromId(itemID);
+ if (selectedPosition >= 0) {
+ mDrawerList.setItemChecked(selectedPosition, true);
+ }
+ mDrawerLayout.closeDrawer(mDrawerContainer);
+ }
+
+ /**
+ * Adds the given fragment to the back stack and replaces the current one with it.
+ */
+ private void replaceWithFragment(Fragment fragment, boolean addToBackStack) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction()
.replace(R.id.content_frame, fragment);
@@ -346,13 +366,9 @@ private void selectItem(int itemID, boolean addToBackStack) {
transaction.addToBackStack(null);
}
transaction.commit();
- int selectedPosition = mNavDrawerAdapter.getPositionFromId(itemID);
- if (selectedPosition >= 0) {
- mDrawerList.setItemChecked(selectedPosition, true);
- }
- mDrawerLayout.closeDrawer(mDrawerContainer);
}
+
private void handleNfcIntent(Intent intent) {
if (intent == null) return;
diff --git a/src/com/uwflow/flow_android/adapters/CourseClassListAdapter.java b/src/com/uwflow/flow_android/adapters/CourseClassListAdapter.java
index 9f96824..53bd44c 100644
--- a/src/com/uwflow/flow_android/adapters/CourseClassListAdapter.java
+++ b/src/com/uwflow/flow_android/adapters/CourseClassListAdapter.java
@@ -2,23 +2,39 @@
import android.content.Context;
import android.text.Html;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
+import android.widget.ImageButton;
import android.widget.TextView;
+import android.widget.Toast;
+import com.crashlytics.android.Crashlytics;
import com.uwflow.flow_android.R;
import com.uwflow.flow_android.db_object.Meeting;
import com.uwflow.flow_android.db_object.Section;
+import com.uwflow.flow_android.network.FlowApiRequestCallback;
+import com.uwflow.flow_android.network.FlowApiRequests;
+import com.uwflow.flow_android.util.CourseUtil;
+import com.uwflow.flow_android.util.RegistrationIdUtil;
import com.uwflow.flow_android.util.StringHelper;
+import com.uwflow.flow_android.util.UserUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.json.JSONObject;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.LinkedList;
import java.util.List;
/**
* Created by jasperfung on 2/22/14.
*/
public class CourseClassListAdapter extends BaseAdapter {
+ private static final String TAG = CourseClassListAdapter.class.getSimpleName();
+
private List mClasses;
private Context mContext;
@@ -51,70 +67,117 @@ public View getView(int position, View convertView, ViewGroup parent) {
}
// Fill view with appropriate data
- TextView column1, column2;
+ TextView column1, column2, seatsAvailableTextView;
column1 = (TextView) convertView.findViewById(R.id.col1);
column2 = (TextView) convertView.findViewById(R.id.col2);
+ seatsAvailableTextView = (TextView) convertView.findViewById(R.id.seats_available_textview);
+ final ImageButton addAlertButton = (ImageButton) convertView.findViewById(R.id.add_alert_button);
- Section currClass = mClasses.get(position);
-
+ final Section currClass = mClasses.get(position);
// Populate first column (class info)
String sectionType = currClass.getSectionType();
String sectionNumber = currClass.getSectionNum();
- Meeting meeting = currClass.getMeetings().get(0); // TODO: find out if we should ever handle >1 meeting
- String professor = meeting.getProfId();
- if (professor != null) {
- professor = StringHelper.capitalize(professor.replaceAll("_", " "));
- }
- int enrollmentTotal = currClass.getEnrollmentTotal();
- int enrollmentCapacity = currClass.getEnrollmentCapacity();
+ Meeting meeting = currClass.getMeetings().get(0); // TODO: Handle more than 1 meeting
// Make sure we don't have nulls
- if (sectionType == null) sectionType = "---";
- if (sectionNumber == null) sectionNumber = "---";
- if (professor == null) professor = "---";
-
- String string1 = String.format("%s %s\n%s\nSeats: %d/%d",
- sectionType,
- sectionNumber,
- professor,
- enrollmentTotal,
- enrollmentCapacity);
- column1.setText(string1);
+ if (sectionType == null) sectionType = "";
+ if (sectionNumber == null) sectionNumber = "";
+ column1.setText(String.format("%s\n%s", sectionType, sectionNumber));
- // Populate second column (time and location)
- String string2;
+ // Populate second column (class details)
+ List detailsList = new LinkedList();
String building = meeting.getBuilding();
String room = meeting.getRoom();
String campus = currClass.getCampus();
+ if (campus == null) campus = "";
+ String professor = meeting.getProfId();
- String time;
long startSeconds = meeting.getStartSeconds();
long endSeconds = meeting.getEndSeconds();
- if (startSeconds == 0 || endSeconds == 0) {
- time = "N/A";
- } else {
- time = String.format("%s - %s",
+ if (startSeconds != 0 && endSeconds != 0) {
+ detailsList.add(String.format("%s - %s",
getTimeFromSeconds(startSeconds),
- getTimeFromSeconds(endSeconds));
+ getTimeFromSeconds(endSeconds)));
}
- if (campus == null) campus = "";
- if (building == null || room == null) {
- string2= String.format("%s
%s
%s",
- time,
- getFormattedDays(meeting.getDays()),
- campus);
+ if (meeting.getDays() != null && !meeting.getDays().isEmpty()) {
+ detailsList.add(getFormattedDays(meeting.getDays()));
+ }
+ if (StringUtils.isNotEmpty(building) || StringUtils.isNotEmpty(room)) {
+ detailsList.add(String.format("%s %s - %s", building, room, campus));
+ } else if (StringUtils.isNotEmpty(campus)) {
+ detailsList.add(campus);
+ }
+ if (StringUtils.isNotEmpty(professor)) {
+ professor = StringHelper.capitalize(professor.replaceAll("_", " "));
+ detailsList.add(professor);
+ }
+ column2.setText(Html.fromHtml(StringUtils.join(detailsList, "
")));
+
+ // Populate seats available column
+ int enrollmentTotal = currClass.getEnrollmentTotal();
+ int enrollmentCapacity = currClass.getEnrollmentCapacity();
+ String fractionHtml = String.format("%s/%s
seats", enrollmentTotal, enrollmentCapacity);
+ seatsAvailableTextView.setText(Html.fromHtml(fractionHtml));
+
+ // Enable notification subscription button for at-capacity classes
+ // TODO(david): Would be good to change button styling if alert already added
+ // TODO(david): Change back to checkbox (and style it properly) if above is done.
+ final String registrationId = RegistrationIdUtil.getRegistrationId(mContext);
+ if (RegistrationIdUtil.supportsGcm(mContext) && StringUtils.isNotEmpty(registrationId) &&
+ enrollmentTotal >= enrollmentCapacity) {
+ addAlertButton.setVisibility(View.VISIBLE);
} else {
- string2= String.format("%s
%s
%s %s - %s",
- time,
- getFormattedDays(meeting.getDays()),
- building,
- room,
- campus);
+ addAlertButton.setVisibility(View.INVISIBLE);
}
- column2.setText(Html.fromHtml(string2));
+
+ final String finalSectionType = sectionType;
+ final String finalSectionNumber = sectionNumber;
+ final String userId = UserUtil.getLoggedInUserId(mContext);
+
+ addAlertButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final String courseId = currClass.getCourseId();
+ final String humanizedCourseId = CourseUtil.humanizeCourseId(courseId);
+ final String termId = currClass.getTermId();
+ String registrationId = RegistrationIdUtil.getRegistrationId(mContext);
+
+ addAlertButton.setEnabled(false);
+
+ FlowApiRequests.addCourseAlert(registrationId, courseId, termId, finalSectionType, finalSectionNumber,
+ userId, new FlowApiRequestCallback() {
+ @Override
+ public void onSuccess(JSONObject response) {
+ String message = String.format(
+ "You will be notified when a seat opens up for %s: %s %s.",
+ humanizedCourseId, finalSectionType, finalSectionNumber);
+ Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onFailure(String error) {
+ String message;
+
+ // TODO(david): Server should return error code instead
+ if (StringUtils.containsIgnoreCase(error, "already exists")) {
+ message = String.format("You've already added an alert for %s: %s %s",
+ humanizedCourseId, finalSectionType, finalSectionNumber);
+ } else {
+ message = String.format("Uh oh, could not add a course alert for %s. Error: %s",
+ humanizedCourseId, error);
+ Log.d(TAG, message);
+ Crashlytics.log(Log.ERROR, TAG, message);
+ }
+
+ Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
+ addAlertButton.setEnabled(true);
+ }
+ });
+ }
+ });
return convertView;
}
@@ -122,12 +185,11 @@ public View getView(int position, View convertView, ViewGroup parent) {
private String getTimeFromSeconds(long seconds) {
int hours = (int)(seconds / 3600);
int minutes = (int)(seconds % 3600) / 60;
- if (hours >= 12 && minutes >= 0) {
- if (hours > 12) hours -= 12;
- return String.format("%d:%dpm", hours, minutes);
- } else {
- return String.format("%d:%dam", hours, minutes);
- }
+ Calendar cal = Calendar.getInstance();
+ cal.set(Calendar.HOUR_OF_DAY, hours);
+ cal.set(Calendar.MINUTE, minutes);
+
+ return new SimpleDateFormat("h:mma").format(cal.getTime());
}
private String getFormattedDays(ArrayList days) {
diff --git a/src/com/uwflow/flow_android/adapters/ProfilePagerAdapter.java b/src/com/uwflow/flow_android/adapters/ProfilePagerAdapter.java
index c8eff1d..2bc5bdf 100644
--- a/src/com/uwflow/flow_android/adapters/ProfilePagerAdapter.java
+++ b/src/com/uwflow/flow_android/adapters/ProfilePagerAdapter.java
@@ -17,8 +17,8 @@ public class ProfilePagerAdapter extends FragmentStatePagerAdapter {
private static final int USER_FRIEND_PROFILE_TAB_NUMBER = 3;
private static final String[] TITLES = new String[]{
- "Schedule",
"Courses",
+ "Schedule",
"Exams",
"Friends",
};
diff --git a/src/com/uwflow/flow_android/adapters/ProfileScheduleAdapter.java b/src/com/uwflow/flow_android/adapters/ProfileScheduleAdapter.java
index 0eefc8d..eebf8ab 100644
--- a/src/com/uwflow/flow_android/adapters/ProfileScheduleAdapter.java
+++ b/src/com/uwflow/flow_android/adapters/ProfileScheduleAdapter.java
@@ -5,17 +5,17 @@
import android.content.Context;
import android.graphics.Typeface;
import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
-import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.*;
import com.uwflow.flow_android.FlowApplication;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.ImageButton;
+import android.widget.TableLayout;
+import android.widget.TextView;
import com.uwflow.flow_android.R;
-import com.uwflow.flow_android.constant.Constants;
-import com.uwflow.flow_android.db_object.*;
-import com.uwflow.flow_android.fragment.CourseFragment;
+import com.uwflow.flow_android.db_object.ScheduleCourse;
+import com.uwflow.flow_android.db_object.ScheduleCourses;
import com.uwflow.flow_android.util.CalendarHelper;
import com.uwflow.flow_android.util.CourseUtil;
@@ -155,7 +155,6 @@ public View getChildView(final int groupPosition, final int childPosition,
addAlarmButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- // TODO: Add an alarm for the current course
mContext.startActivity(CalendarHelper.getAddAlarmIntent(title, location, startDate));
}
});
diff --git a/src/com/uwflow/flow_android/broadcast_receiver/GcmBroadcastReceiver.java b/src/com/uwflow/flow_android/broadcast_receiver/GcmBroadcastReceiver.java
new file mode 100644
index 0000000..778d364
--- /dev/null
+++ b/src/com/uwflow/flow_android/broadcast_receiver/GcmBroadcastReceiver.java
@@ -0,0 +1,110 @@
+package com.uwflow.flow_android.broadcast_receiver;
+
+import android.app.Activity;
+import android.app.Notification;
+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.support.v4.app.TaskStackBuilder;
+import android.util.Log;
+import com.crashlytics.android.Crashlytics;
+import com.google.android.gms.gcm.GoogleCloudMessaging;
+import com.uwflow.flow_android.MainFlowActivity;
+import com.uwflow.flow_android.R;
+import com.uwflow.flow_android.constant.Constants;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Random;
+
+/**
+ * Receives push notifications from GCM.
+ *
+ * This does not use a WakefulBroadcastReceiver and thus cannot take long to process. If more processing time
+ * is needed, take a look at http://developer.android.com/google/gcm/client.html for extending WakefulBroadcastReceiver.
+ */
+public class GcmBroadcastReceiver extends BroadcastReceiver {
+ private final String TAG = GcmBroadcastReceiver.class.getSimpleName();
+
+ @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()) {
+ if (GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR.equals(messageType)) {
+ String errorMessage = "Push notification send error: " + extras.toString();
+ Crashlytics.log(Log.ERROR, TAG, errorMessage);
+ Log.e(TAG, errorMessage);
+ } else if (GoogleCloudMessaging.MESSAGE_TYPE_DELETED.equals(messageType)) {
+ String errorMessage = "Deleted push notifications on server: " + extras.toString();
+ Crashlytics.log(Log.ERROR, TAG, errorMessage);
+ Log.e(TAG, errorMessage);
+ } else if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)) {
+ // Actually a regular message. Handle it.
+ Log.i(TAG, "Received push notification " + extras.toString());
+ displayNotification(context, extras);
+ }
+ }
+
+ setResultCode(Activity.RESULT_OK);
+ }
+
+ /**
+ * Notify a push notification received from GCM.
+ * This is currently only configured to receive course class seat opening alerts.
+ * @param data
+ */
+ private void displayNotification(Context context, Bundle data) {
+ String courseName = "";
+ String courseId = "";
+ String courseCode = "";
+
+ try {
+ JSONObject course = new JSONObject(data.getString("course"));
+ courseName = course.getString("name");
+ courseId = course.getString("id");
+ courseCode = course.getString("code");
+ } catch (JSONException e) {
+ e.printStackTrace();
+ Crashlytics.logException(e);
+ }
+
+ // TODO(david): Look into why this doesn't go to the right course if Flow is already started
+ Intent resultIntent = new Intent(context, MainFlowActivity.class);
+ resultIntent.putExtra(Constants.COURSE_ID_KEY, courseId);
+
+ // The stack builder object will contain an artificial back stack for the started Activity.
+ // This ensures that navigating backward from the Activity leads out of the app to the Home screen.
+ TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
+ // Adds the back stack for the Intent (but not the Intent itself)
+ stackBuilder.addParentStack(MainFlowActivity.class);
+ // Adds the Intent that starts the Activity to the top of the stack
+ stackBuilder.addNextIntent(resultIntent);
+
+ PendingIntent contentIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ String title = String.format("%s: spots open!", courseCode);
+ String text = String.format("%s: %s has seats open now.", courseCode, courseName);
+
+ Notification notification = new NotificationCompat.Builder(context)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setContentIntent(contentIntent)
+ .setAutoCancel(true)
+ .setPriority(Notification.PRIORITY_HIGH)
+ .setDefaults(Notification.DEFAULT_ALL)
+ .build();
+
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(new Random().nextInt(), notification);
+ }
+
+}
diff --git a/src/com/uwflow/flow_android/constant/Constants.java b/src/com/uwflow/flow_android/constant/Constants.java
index ffb64ac..c9aa6a2 100644
--- a/src/com/uwflow/flow_android/constant/Constants.java
+++ b/src/com/uwflow/flow_android/constant/Constants.java
@@ -28,6 +28,7 @@ public class Constants {
public static final String API_USERS_FRIENDS = "api/v1/users/%s/friends";
public static final String API_USER_SHORTLIST_COURSE = "api/v1/user/shortlist/%s";
public static final String API_SEARCH_COURSES = "api/v1/search/courses?%s";
+ public static final String API_GCM_COURSE_ALERTS = "api/v1/alerts/course/gcm";
public static class DatabaseColumnName{
public static final String USER_ID = "user_id";
@@ -84,8 +85,8 @@ public static class BroadcastActionId {
public static final int COURSE_ABOUT_PAGE_INDEX = 1;
public static final int COURSE_REVIEWS_PAGE_INDEX = 2;
- public static final int PROFILE_SCHEDULE_PAGE_INDEX = 0;
- public static final int PROFILE_COURSES_PAGE_INDEX = 1;
+ public static final int PROFILE_COURSES_PAGE_INDEX = 0;
+ public static final int PROFILE_SCHEDULE_PAGE_INDEX = 1;
public static final int PROFILE_EXAMS_PAGE_INDEX = 2;
public static final int PROFILE_FRIENDS_PAGE_INDEX = 3;
@@ -103,4 +104,6 @@ public static class BroadcastActionId {
public static final String SHORTLIST_TERM_ID = "9999_99";
+ public static final String APP_VERSION_KEY = "app_version";
+
}
diff --git a/src/com/uwflow/flow_android/fragment/CourseScheduleFragment.java b/src/com/uwflow/flow_android/fragment/CourseScheduleFragment.java
index 4cad7b9..90c9d31 100644
--- a/src/com/uwflow/flow_android/fragment/CourseScheduleFragment.java
+++ b/src/com/uwflow/flow_android/fragment/CourseScheduleFragment.java
@@ -127,13 +127,14 @@ private View createScheduleTermHeader(String heading) {
TextView textView = new TextView(getActivity());
textView.setText(heading);
textView.setTypeface(null, Typeface.BOLD);
+ textView.setTextColor(getResources().getColor(R.color.white));
textView.setLayoutParams(new TableLayout.LayoutParams(
TableLayout.LayoutParams.MATCH_PARENT,
TableLayout.LayoutParams.WRAP_CONTENT,
1f));
float scale = getResources().getDisplayMetrics().density; // scale for converting dp to px
- textView.setPadding((int)(10 * scale + 0.5f), 0, 0, 0);
- textView.setBackgroundResource(R.color.flow_light_blue);
+ textView.setPadding((int)(10 * scale + 0.5f), 8, 0, 8);
+ textView.setBackgroundResource(R.color.flow_blue);
return textView;
}
diff --git a/src/com/uwflow/flow_android/network/FlowApiRequestCallback.java b/src/com/uwflow/flow_android/network/FlowApiRequestCallback.java
index 284d01c..8a8616a 100644
--- a/src/com/uwflow/flow_android/network/FlowApiRequestCallback.java
+++ b/src/com/uwflow/flow_android/network/FlowApiRequestCallback.java
@@ -3,8 +3,6 @@
import com.uwflow.flow_android.db_object.*;
import org.json.JSONObject;
-import java.util.ArrayList;
-
public abstract class FlowApiRequestCallback {
abstract public void onSuccess(JSONObject response);
abstract public void onFailure(String error);
diff --git a/src/com/uwflow/flow_android/network/FlowApiRequests.java b/src/com/uwflow/flow_android/network/FlowApiRequests.java
index b71e19b..56b8be8 100644
--- a/src/com/uwflow/flow_android/network/FlowApiRequests.java
+++ b/src/com/uwflow/flow_android/network/FlowApiRequests.java
@@ -17,6 +17,7 @@
import com.uwflow.flow_android.constant.Constants;
import com.uwflow.flow_android.db_object.*;
import com.uwflow.flow_android.util.JsonToDbUtil;
+import com.uwflow.flow_android.util.RegistrationIdUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.json.JSONException;
@@ -77,6 +78,59 @@ public void onFailure(Throwable e, JSONObject errorResponse) {
});
}
+ public static void addCourseAlert(String registrationId, final String courseId, String termId, String sectionType,
+ String sectionNum, String userId, final FlowApiRequestCallback callback) {
+ RequestParams params = new RequestParams();
+ params.put("registration_id", registrationId);
+ params.put("course_id", courseId);
+
+ if (StringUtils.isNotEmpty(termId)) {
+ params.put("term_id", termId);
+ }
+ if (StringUtils.isNotEmpty(sectionType)) {
+ params.put("section_type", sectionType);
+ }
+ if (StringUtils.isNotEmpty(sectionNum)) {
+ params.put("section_num", sectionNum);
+ }
+ if (StringUtils.isNotEmpty(userId)) {
+ params.put("user_id", userId);
+ }
+
+ FlowAsyncClient.post(Constants.API_GCM_COURSE_ALERTS, params, new JsonHttpResponseHandler() {
+ @Override
+ public void onSuccess(JSONObject response) {
+ Log.d(TAG, "Add alert for course " + courseId + " success.");
+ callback.onSuccess(response);
+ }
+
+ @Override
+ public void onFailure(String responseBody, Throwable error) {
+ Log.d(TAG, "Add alert for course " + courseId + " failed.");
+ callback.onFailure(responseBody);
+ }
+
+ @Override
+ public void onFailure(Throwable e, JSONObject errorResponse) {
+ Log.d(TAG, "Add alert for course " + courseId + " failed.");
+
+ if (errorResponse == null) {
+ callback.onFailure(null);
+ return;
+ }
+
+ String errorMessage = "";
+ try {
+ errorMessage = errorResponse.getString("error");
+ } catch (JSONException e1) {
+ // Ignore could not parse error message
+ }
+
+ callback.onFailure(errorMessage);
+ }
+ });
+ }
+
public static void searchCourses(String keywords, String sortMode, Boolean excludeTakenCourses, Integer count,
Integer offset, final FlowApiRequestCallback callback) {
// Build the search query string
diff --git a/src/com/uwflow/flow_android/network/FlowDatabaseLoader.java b/src/com/uwflow/flow_android/network/FlowDatabaseLoader.java
index 9ea6b56..b82063a 100644
--- a/src/com/uwflow/flow_android/network/FlowDatabaseLoader.java
+++ b/src/com/uwflow/flow_android/network/FlowDatabaseLoader.java
@@ -1,23 +1,23 @@
package com.uwflow.flow_android.network;
import android.content.Context;
-import android.content.SharedPreferences;
import android.os.AsyncTask;
-import android.preference.PreferenceManager;
import com.crashlytics.android.Crashlytics;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.stmt.QueryBuilder;
-import com.uwflow.flow_android.adapters.ProfileScheduleAdapter;
import com.uwflow.flow_android.broadcast_receiver.BroadcastFactory;
-import com.uwflow.flow_android.constant.Constants;
import com.uwflow.flow_android.dao.FlowDatabaseHelper;
import com.uwflow.flow_android.db_object.*;
import com.uwflow.flow_android.util.CalendarHelper;
import com.uwflow.flow_android.util.JsonToDbUtil;
+import com.uwflow.flow_android.util.UserUtil;
import org.json.JSONObject;
import java.sql.SQLException;
-import java.util.*;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
import java.util.concurrent.Callable;
/**
@@ -29,13 +29,11 @@ public class FlowDatabaseLoader {
protected FlowDatabaseHelper flowDatabaseHelper;
protected Context context;
protected FlowImageLoader flowImageLoader;
- protected SharedPreferences sp;
public FlowDatabaseLoader(Context context, FlowDatabaseHelper flowDatabaseHelper) {
this.context = context;
this.flowDatabaseHelper = flowDatabaseHelper;
this.flowImageLoader = new FlowImageLoader(context);
- sp = PreferenceManager.getDefaultSharedPreferences(context);
}
/**
@@ -88,9 +86,7 @@ protected Void doInBackground(JSONObject... jsonObjects) {
if (user != null && user.getProfilePicUrls() != null)
flowImageLoader.preloadImage(user.getProfilePicUrls().getLarge());
if (user != null) {
- SharedPreferences.Editor editor = sp.edit();
- editor.putString(Constants.PROFILE_ID_KEY, user.getId());
- editor.commit();
+ UserUtil.saveLoggedInUserId(context, user.getId());
userDao.createOrUpdate(user);
}
} catch (SQLException e) {
@@ -127,11 +123,12 @@ public void updateOrCreateUserScheduleImage(ScheduleImage scheduleImage) {
return;
}
if (scheduleImage.getId() == null) {
- String id = sp.getString(Constants.PROFILE_ID_KEY, null);
- if (id == null)
+ String id = UserUtil.getLoggedInUserId(context);
+ if (id == null) {
return;
- else
- scheduleImage.setId(id);
+ }
+
+ scheduleImage.setId(id);
}
new AsyncTask() {
@Override
@@ -390,7 +387,7 @@ protected ScheduleImage doInBackground(String... strings) {
try {
String arg = strings[0];
if (arg == null) {
- arg = sp.getString(Constants.PROFILE_ID_KEY, null);
+ arg = UserUtil.getLoggedInUserId(context);
if (arg == null) return null;
}
Dao scheduleImageStringDao = flowDatabaseHelper.getUserSchduleImageDao();
diff --git a/src/com/uwflow/flow_android/util/RegistrationIdUtil.java b/src/com/uwflow/flow_android/util/RegistrationIdUtil.java
new file mode 100644
index 0000000..f8222e3
--- /dev/null
+++ b/src/com/uwflow/flow_android/util/RegistrationIdUtil.java
@@ -0,0 +1,135 @@
+package com.uwflow.flow_android.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.AsyncTask;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import com.crashlytics.android.Crashlytics;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+import com.google.android.gms.gcm.GoogleCloudMessaging;
+import com.uwflow.flow_android.constant.Constants;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.IOException;
+
+/**
+ * Helper methods for dealing with registration IDs used for push notifications from GCM.
+ * Mostly adapted from http://developer.android.com/google/gcm/client.html
+ */
+public class RegistrationIdUtil {
+ private static final String TAG = RegistrationIdUtil.class.getSimpleName();
+
+ // Since this has to be shipped with APK, there is no need to keep this secret.
+ // Also http://stackoverflow.com/questions/18196292/what-are-consequences-of-having-gcm-sender-id-being-exposed
+ private static final String GCM_SENDER_ID = "914611125417";
+
+ private static final String REGISTRATION_ID_KEY = "registration_id";
+
+ private static GoogleCloudMessaging mGcm = null;
+ private static String mRegistrationId = null;
+
+ /**
+ * Register this device with GCM to receive push notifications, if possible.
+ * @param context
+ */
+ public static void init(Context context) {
+ if (supportsGcm(context)) {
+ mGcm = GoogleCloudMessaging.getInstance(context);
+ mRegistrationId = getRegistrationId(context);
+
+ if (StringUtils.isEmpty(mRegistrationId)) {
+ registerInBackground(context);
+ }
+ Log.i(TAG, "Registration ID is " + mRegistrationId);
+ } else {
+ Log.w(TAG, "Google Play Services SDK not available. Cannot receive push notifications.");
+ }
+ }
+
+ public static boolean supportsGcm(Context context) {
+ return GooglePlayServicesUtil.isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS;
+ }
+
+ /**
+ * Get the current registration ID from shared preferences.
+ *
+ * @return The registration ID if available and valid; null otherwise.
+ */
+ public static String getRegistrationId(Context context) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String registrationId = preferences.getString(REGISTRATION_ID_KEY, null);
+
+ if (StringUtils.isEmpty(registrationId)) {
+ return registrationId;
+ }
+
+ // Check if app was updated; if so, it must clear the registration ID
+ // since the existing regID is not guaranteed to work with the new app version.
+ int registeredVersion = preferences.getInt(Constants.APP_VERSION_KEY, Integer.MIN_VALUE);
+ int appVersion = getAppVersion(context);
+ if (registeredVersion != appVersion) {
+ return null;
+ }
+
+ return registrationId;
+ }
+
+ /**
+ * @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) {
+ Crashlytics.logException(e);
+ Log.e(TAG, "Could not get package name");
+ return Integer.MIN_VALUE;
+ }
+ }
+
+ /**
+ * Registers the application with the GCM servers asynchronously. Stores registration ID and app version code
+ * in shared preferences.
+ */
+ private static void registerInBackground(final Context context) {
+ if (mGcm == null) {
+ return;
+ }
+
+ new AsyncTask() {
+ @Override
+ protected Object doInBackground(Object[] params) {
+ try {
+ mRegistrationId = mGcm.register(GCM_SENDER_ID);
+ storeRegistrationId(context, mRegistrationId);
+ Log.i(TAG, "New registration ID obtained: " + mRegistrationId);
+ } catch (IOException e) {
+ e.printStackTrace();
+ Crashlytics.logException(e);
+
+ // TODO(david): Enter exponential backoff to retry register. Grr why can't Google just do this.
+ }
+
+ return null;
+ }
+ }.execute();
+ }
+
+ /**
+ * Stores the registration ID and app versionCode in the application's {@code SharedPreferences}.
+ */
+ private static void storeRegistrationId(Context context, String registrationId) {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ int appVersion = getAppVersion(context);
+ preferences.edit()
+ .putString(REGISTRATION_ID_KEY, registrationId)
+ .putInt(Constants.APP_VERSION_KEY, appVersion)
+ .commit();
+ }
+
+}
diff --git a/src/com/uwflow/flow_android/util/UserUtil.java b/src/com/uwflow/flow_android/util/UserUtil.java
new file mode 100644
index 0000000..4d3f5d8
--- /dev/null
+++ b/src/com/uwflow/flow_android/util/UserUtil.java
@@ -0,0 +1,25 @@
+package com.uwflow.flow_android.util;
+
+import android.content.Context;
+import android.preference.PreferenceManager;
+import com.uwflow.flow_android.constant.Constants;
+
+public class UserUtil {
+
+ /**
+ * Save the logged in user's ID in shared preferences.
+ */
+ public static void saveLoggedInUserId(Context context, String userId) {
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .putString(Constants.PROFILE_ID_KEY, userId)
+ .commit();
+ }
+
+ /**
+ * Get the logged in user's ID from shared preferences.
+ */
+ public static String getLoggedInUserId(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getString(Constants.PROFILE_ID_KEY, null);
+ }
+
+}