Skip to content

Commit

Permalink
Merge pull request #3 from JasonChong96/milestone3
Browse files Browse the repository at this point in the history
Milestone3
  • Loading branch information
JasonChong96 authored Jul 27, 2018
2 parents 6e4c094 + 408fd01 commit 3e34ee3
Show file tree
Hide file tree
Showing 118 changed files with 6,118 additions and 2,237 deletions.
4 changes: 2 additions & 2 deletions .idea/assetWizardSettings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified .idea/caches/build_file_checksums.ser
Binary file not shown.
5 changes: 5 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
language: android
android:
components:
- tools
- tools
- platform-tools
- build-tools-28.0.0
- android-28
before_install:
- openssl aes-256-cbc -K $encrypted_f2da1cf98853_key -iv $encrypted_f2da1cf98853_iv
-in Mugger.jks.enc -out Mugger.jks -d
- yes | sdkmanager "platforms;android-28"
- chmod +x gradlew
before_script:
script:
- "./gradlew build"
jdk:
- oraclejdk8
39 changes: 39 additions & 0 deletions DeveloperGuide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,44 @@ Profile data is cached in a MuggerUser instance. This instance can be fetched us
Data Structure of Module Cache
Modules are cached in the same MuggerUser instance as a TreeMap of String keys and nested TreeMaps of values. This data structure is shown in the figure above. TreeMaps were chosen as the semesters and module codes should be sorted alphabetically in the UI so that the user can easily choose his/her desired module. This structure allows us to easily fetch modules in a given semester as well as the user’s role in the module efficiently in O(lg(n)) time.

=== 2.3 View Profile
Profile is loaded with the ProfileViewModel class which ProfileFragment instances are linked to. Each text view in profile fragment is linked to a LiveData reference in ProfileViewModel. The ViewModel listens to changes to the user’s profile data on the Firestore database and posts a new value to the LiveData reference when it changes. The active ProfileFragment will be observing such these LiveData objects for changes and the respective TextView will be have it’s display text changed when a new value is posted to the LiveData object. The ViewModel will check if the given field of the profile has been changed from the original value before posting the new value into the LiveData objects as changing UI text is a relatively expensive operation that shouldn’t be called unecessarily. This also applies to the modules taken display in the profile screen, if a new semester or module is added, the UI will be automatically adjusted accordingly.

=== 2.4 The Mute Function
When trying to create a listing or send a new message in listing chats, a check is done against the client cache to check if the user has been muted. And if he/she has been, checks if the current time is past the mute end time. Muted users are disallowed from creating listings and sending messages. When an admin mutes a user, a notification is sent to their client which will show the user that he/she has been muted for the given duration. In addition, the client will automatically update the cache with the end time of the mute. This end time will also be added to the database so simply restarting the client will not reset the mute status. The same process is carried out for unmuting the user. These ensure that mute/unmute operations will be applied in real time.

=== 2.5 Push Notifications
A node.js function is deployed on Firebase Functions that listens to the notifications collection of our Firestore database (This function can be found under the server folder in the main directory). Whenever a new document is added to the collection, the function sends the notification to the relevant users created from the document data. The notifications are sent through the Firebase Cloud Messaging service. For listing chat/delete notifications, they will be sent to users who are subscribed to the channel that is identified by the listing’s unique id (which is automatically generated by firebase when the listing is made). When users join/unjoin a given study session, they will be automatically subscribed/unsubscribed to this channel. Similarly, users will be automatically subscribed to channels for modules that they are currently taking, these channels are identified by the module codes. Notifications will be sent to the module code channel whenever a listing is created. For users who have disabled certain categories of notifications, the filtering is done on the client side and the client will not show the user notifications which they have not enabled. For personal notifications such as mute/unmute notifications, the notification is sent to the user’s device only through the device’s instance id which is stored in the Firestore database on login.

=== 2.6 Custom Filters
As mentioned in section 4.1.1, the listing data fetched from Firestore cannot be filtered/sorted by different fields on their end, hence the filtering has to be done on the client side in an efficient manner that will not cause a significant decrease in performance.
The data fetched from the Firestore database depends on the category they choose, either “All listings”, “Listings I have joined” and “My Listings”, all these correspond to an existing query already used for other features. As for the other filter settings, each correspond to a Predicate of their own which are composed into a single predicate using the composePredicate method (The predicate#and method introduced in Java 8 cannot be used here as it is not supported by older versions of Android). When a listing is loaded, it is tested using the generated predicate and if it does not pass, the view for the listing is not processed and is hidden from the user. To keep the functionality of getItemCount in the FirestoreRecyclerAdapter class, these filtered listings are added in a HashSet and removed when they no longer exist in the database. These operations are done in O(1) time and a HashSet disallows duplicates by design, making it efficient. The item count can then be calculated by the number of items fetched by the query minus the number of items in the hashset. This getItemCount method is important in showing the user a message instead of an empty screen when there are no listings that match

==== 2.6.1 Custom Filter Settings Storage
The custom filter settings have many groups of options and on-off filter switches, e.g category, show/hide student listings, show/hide TA listings, show/hide Professor’s listings. Creating a new field in the database for each switch would be inefficient and a waste of memory. Hence all such switches are stored in a single long object and can be fetched by applying a bitmask to the number, e.g applying bitwise AND with the number 4 will give the number 4 if student listings are to be shown and give 0 if they are to be hidden. String filters, however, cannot be stored this way and still have to be stored in separate fields.

=== 2.7 Presentation of Listing Dates
If the date is within a day of the current date, it’ll be represented as either “Today”, “Tomorrow”, “Yesterday”. If not, it’ll be represented as last (day of the week) or this (day of the week) wherever possible. If neither of those representations are possible, the raw date is shown based on the device’s date format. Next (day of the week) is not used as it can lead to some ambiguity, e.g if today is monday and we say next sunday, the user would not be 100% sure if it means the coming sunday 6 days later or the following sunday 13 days later. Although it might be obvious to some of us that it is the former, this slight ambiguity should still be avoided to prevent confusion.

=== 2.8 My Schedule
The app iterates through all listings that the user is currently joining and marks the dates that are involved in them. When a date is clicked, a predicate is created to check if a listing starts before the date and ends after the start of the date or if a listing starts during the date. This will predicate will filter out listings that are unrelated to the date chosen in a similar manner to the custom filter implementation in 10.1.1.

== 3. Dev Ops
=== 3.1 Build Automation
By default, Android Studio uses Gradle for build automation. Gradle automatically downloads and imports relevant dependencies listed under build.gradle, along with the required Android SDK version and build tools.

=== 3.2 Continuous Integration
The github repository is linked to Travis CI. Unit tests will be automatically carried out by Travis CI and the result will be shown on the README. If the build or tests fail, there will be an icon to indicate as such on the top of the README. The build on Travis CI’s side will be automatically signed with the same production signature being used on our Play Store builds. Integration tests are not carried out by Travis CI at the moment due to issues with Firebase not being available on the Travis CI android emulators. Due to time constraints, we have decided to prioritize bug fixing and improving the frontend of the application before trying to fix this.

== 4. Architecture
Mugger makes use of the Android Architecture Components and largely follows the Model-View-ViewModel architecture. This creates a separation of logic and UI code with Activity/Fragment classes focusing mainly on the UI and the ViewModel classes focusing on the logic and communicating with the Model and database.

=== 4.1 Model
The model represents the data and logic of the application. For our application, an example of this would be the MuggerUserCache class which stores a cached local copy of the user data.

=== 4.2 ViewModel
The ViewModel interacts with the model and its main role is to contain the logic separately from the UI code. LiveData objects are also used for the views to observe changes in data for data that are volatile, e.g the number of people attending a study session. Refer to section 4.3 for an example of how LiveData is used. The view model also persists through reconfigurations such as screen rotations, unlike activity/fragment classes, this ensures that the data loaded persists throughout reconfigurations. Without the ViewModel, activity/fragment classes have to reload data every time a rotation happens, which is very inefficient. This life cycle is represented by the picture below, showing the lifecycle of an activity on the left and the ViewModel on the right. (Taken from Google documentation)
image::https://i.imgur.com/5g2xwcO.png[LifeCycle of Activity vs ViewModel]

=== 4.3 View
The View is simply the UI shown to the user, handled by the Fragment/Activity classes.
Binary file added Mugger.jks.enc
Binary file not shown.
12 changes: 11 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ ifdef::env-github,env-browser[:relfileprefix:]
:toc:
:toc-title: User Guide
:toc-placement: preamble
image:https://travis-ci.org/JasonChong96/Mugger.svg?branch=milestone3["Build Status", link="https://travis-ci.org/JasonChong96/Mugger"]

= Mugger

Expand Down Expand Up @@ -89,6 +90,15 @@ This page allows users to send feedback that is viewable only by the admins. Thi
image::https://i.imgur.com/DPTzhdx.png[Screenshot of Prof/TA request form]
This page allows users to request to be registered as a Professor or TA for the module that they are teaching. They'll be required to type in the module code along with proof of their position. These requests can only be viewed by admins who can approve such requests and give them their respective roles(section 3.3) so that they can make specially tagged listings as mentioned in section 3.1.

=== 3.8 Logout
=== 3.8 Custom Filters
Users can now customise their filter settings using ‘Custom Filters’ from the navigation drawer. This allows for advanced filtering options. Under ‘Settings’, they can also enable unrelated modules to be shown to them, in case they want to attend study sessions for modules they are interested in but not officially enrolled in. These filter settings are stored in the database so that users would not have to.

=== 3.9 In-App Tutorial
This brief tutorial describing the purpose and usage of the app will be shown to first-time app users, and can subsequently be accessed anytime from ‘View Introduction’ under the three dots action bar at the top right corner.

=== 3.10 My Schedule
This intuitive display of the listings that a user is attending will help keep track of all such appointments in a way that is already familiar to the average user who uses any other calendar app. A red dot under a date indicates that there is at least one study session, and tapping on the date will reveal the listing(s) scheduled for that day. Pulling down the screen will refresh the dates that are marked with the red dot.

=== 3.11 Logout

After the user logs out, he/she will be brought back to the main login screen until the next time the app is opened again.
41 changes: 36 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,17 +1,42 @@
apply plugin: 'com.android.application'

android {
dataBinding {
enabled = true
}
compileSdkVersion 28
defaultConfig {
applicationId "com.bojio.mugger"
minSdkVersion 19
targetSdkVersion 28
versionCode 12
versionName "1.5.6"
versionCode 26
versionName "1.10.7"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
multiDexEnabled true
}
lintOptions {
warning 'InvalidPackage'
}
signingConfigs {
release {

}
}
def isRunningOnTravis = System.getenv("CI") == "true"

if (isRunningOnTravis) {
// configure keystore
signingConfigs.release.storeFile = file("../Mugger.jks")
signingConfigs.release.storePassword = System.getenv("keystore_password")
signingConfigs.release.keyAlias = System.getenv("keystore_alias")
signingConfigs.release.keyPassword = System.getenv("keystore_alias_password")
signingConfigs.debug.storeFile = file("../Mugger.jks")
signingConfigs.debug.storePassword = System.getenv("keystore_password")
signingConfigs.debug.keyAlias = System.getenv("keystore_alias")
signingConfigs.debug.keyPassword = System.getenv("keystore_alias_password")
}

buildTypes {
release {
minifyEnabled false
Expand All @@ -22,6 +47,7 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

buildToolsVersion '28.0.0'
}

Expand All @@ -34,16 +60,16 @@ dependencies {
implementation 'com.google.firebase:firebase-auth:16.0.2'
implementation 'com.android.support:design:28.0.0-alpha3'
implementation 'com.android.support:support-v4:28.0.0-alpha3'
implementation 'com.android.support:recyclerview-v7:28.0.0-alpha3'
implementation 'com.android.support:support-vector-drawable:28.0.0-alpha3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.google.firebase:firebase-core:16.0.1'
implementation 'com.google.android.gms:play-services-auth:15.0.1'
implementation 'com.google.firebase:firebase-firestore:17.0.2'
implementation 'com.google.firebase:firebase-firestore:17.0.3'
implementation 'com.android.support:recyclerview-v7:28.0.0-alpha3'
implementation 'com.google.firebase:firebase-messaging:17.0.0'
implementation 'com.google.firebase:firebase-messaging:17.1.0'
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
implementation 'com.firebaseui:firebase-ui-firestore:4.0.1'
Expand All @@ -55,6 +81,11 @@ dependencies {
implementation 'com.github.GrenderG:Toasty:1.3.0'
implementation 'com.github.matecode:Snacky:1.0.3'
implementation 'com.mikepenz:fastadapter:3.2.7'
//noinspection GradleDependency
implementation 'com.heinrichreimersoftware:material-intro:-SNAPSHOT'
implementation 'com.annimon:stream:1.2.0'
implementation 'com.github.prolificinteractive:material-calendarview:1.6.0'
implementation 'android.arch.lifecycle:extensions:1.1.1'
}

apply plugin: 'com.google.gms.google-services'
Binary file added app/release/Mugger 1.10.7.apk
Binary file not shown.
Binary file modified app/release/app.aab
Binary file not shown.
1 change: 1 addition & 0 deletions app/release/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":26,"versionName":"1.10.7","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.bojio.mugger;

import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import android.widget.TextView;

import com.bojio.mugger.authentication.GoogleLoginActivity;
import com.bojio.mugger.authentication.MuggerUserCache;
import com.bojio.mugger.database.MuggerDatabase;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.auth.AuthCredential;
import com.google.firebase.auth.EmailAuthCredential;
import com.google.firebase.auth.EmailAuthProvider;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.GoogleAuthCredential;
import com.google.firebase.auth.UserProfileChangeRequest;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FirebaseFirestore;

import org.hamcrest.core.IsNull;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.concurrent.ExecutionException;

import static org.junit.Assert.assertEquals;

@RunWith(AndroidJUnit4.class)
public class Main2ActivityInstrumentalTest {
private FirebaseFirestore db;
private FirebaseAuth mAuth;
private MuggerUserCache muggerUserCache;

@Rule
public final ActivityTestRule<Main2Activity> mActivityRule = new ActivityTestRule<>(
Main2Activity.class, true, true);

@Before
public void setup() {
FirebaseApp.initializeApp(InstrumentationRegistry.getContext());
mAuth = FirebaseAuth.getInstance();
db = FirebaseFirestore.getInstance();
muggerUserCache = MuggerUserCache.getInstance();
TestUser.login(mAuth, db);
}

@Test
@SmallTest
public void testSetup() {
Assert.assertNotNull(db);
Assert.assertNotNull(mAuth);
Assert.assertNotNull(muggerUserCache);
}

@Test
@SmallTest
public void testModules() {
Assert.assertNotNull(muggerUserCache.getModules());
}

@Test
@SmallTest
public void testAllModules() {
Assert.assertNotNull(muggerUserCache.getAllModules());
}

@Test
@SmallTest
public void testDisplayNameView() {
Assert.assertNotNull(mAuth.getCurrentUser().getDisplayName());
Assert.assertEquals(((TextView) mActivityRule.getActivity().findViewById(R.id.username))
.getText(), mAuth.getCurrentUser().getDisplayName());
Assert.assertEquals(((TextView) mActivityRule.getActivity().findViewById(R.id.email))
.getText(), mAuth.getCurrentUser().getEmail());
}
}
Loading

0 comments on commit 3e34ee3

Please sign in to comment.