Skip to content

Commit 3e34ee3

Browse files
authored
Merge pull request #3 from JasonChong96/milestone3
Milestone3
2 parents 6e4c094 + 408fd01 commit 3e34ee3

File tree

118 files changed

+6118
-2237
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+6118
-2237
lines changed

.idea/assetWizardSettings.xml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/caches/build_file_checksums.ser

0 Bytes
Binary file not shown.

.idea/misc.xml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.travis.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
language: android
2+
android:
3+
components:
4+
- tools
5+
- tools
6+
- platform-tools
7+
- build-tools-28.0.0
8+
- android-28
9+
before_install:
10+
- openssl aes-256-cbc -K $encrypted_f2da1cf98853_key -iv $encrypted_f2da1cf98853_iv
11+
-in Mugger.jks.enc -out Mugger.jks -d
12+
- yes | sdkmanager "platforms;android-28"
13+
- chmod +x gradlew
14+
before_script:
15+
script:
16+
- "./gradlew build"
17+
jdk:
18+
- oraclejdk8

DeveloperGuide.adoc

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,44 @@ Profile data is cached in a MuggerUser instance. This instance can be fetched us
5454
Data Structure of Module Cache
5555
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.
5656

57+
=== 2.3 View Profile
58+
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.
59+
60+
=== 2.4 The Mute Function
61+
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.
62+
63+
=== 2.5 Push Notifications
64+
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.
65+
66+
=== 2.6 Custom Filters
67+
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.
68+
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
69+
70+
==== 2.6.1 Custom Filter Settings Storage
71+
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.
72+
73+
=== 2.7 Presentation of Listing Dates
74+
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.
75+
76+
=== 2.8 My Schedule
77+
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.
78+
5779
== 3. Dev Ops
80+
=== 3.1 Build Automation
5881
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.
82+
83+
=== 3.2 Continuous Integration
84+
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.
85+
86+
== 4. Architecture
87+
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.
88+
89+
=== 4.1 Model
90+
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.
91+
92+
=== 4.2 ViewModel
93+
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)
94+
image::https://i.imgur.com/5g2xwcO.png[LifeCycle of Activity vs ViewModel]
95+
96+
=== 4.3 View
97+
The View is simply the UI shown to the user, handled by the Fragment/Activity classes.

Mugger.jks.enc

2.22 KB
Binary file not shown.

README.adoc

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ ifdef::env-github,env-browser[:relfileprefix:]
22
:toc:
33
:toc-title: User Guide
44
:toc-placement: preamble
5+
image:https://travis-ci.org/JasonChong96/Mugger.svg?branch=milestone3["Build Status", link="https://travis-ci.org/JasonChong96/Mugger"]
56

67
= Mugger
78

@@ -89,6 +90,15 @@ This page allows users to send feedback that is viewable only by the admins. Thi
8990
image::https://i.imgur.com/DPTzhdx.png[Screenshot of Prof/TA request form]
9091
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.
9192

92-
=== 3.8 Logout
93+
=== 3.8 Custom Filters
94+
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.
95+
96+
=== 3.9 In-App Tutorial
97+
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.
98+
99+
=== 3.10 My Schedule
100+
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.
101+
102+
=== 3.11 Logout
93103

94104
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.

app/build.gradle

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,42 @@
11
apply plugin: 'com.android.application'
22

33
android {
4+
dataBinding {
5+
enabled = true
6+
}
47
compileSdkVersion 28
58
defaultConfig {
69
applicationId "com.bojio.mugger"
710
minSdkVersion 19
811
targetSdkVersion 28
9-
versionCode 12
10-
versionName "1.5.6"
12+
versionCode 26
13+
versionName "1.10.7"
1114
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
1215
vectorDrawables.useSupportLibrary = true
1316
multiDexEnabled true
1417
}
18+
lintOptions {
19+
warning 'InvalidPackage'
20+
}
21+
signingConfigs {
22+
release {
23+
24+
}
25+
}
26+
def isRunningOnTravis = System.getenv("CI") == "true"
27+
28+
if (isRunningOnTravis) {
29+
// configure keystore
30+
signingConfigs.release.storeFile = file("../Mugger.jks")
31+
signingConfigs.release.storePassword = System.getenv("keystore_password")
32+
signingConfigs.release.keyAlias = System.getenv("keystore_alias")
33+
signingConfigs.release.keyPassword = System.getenv("keystore_alias_password")
34+
signingConfigs.debug.storeFile = file("../Mugger.jks")
35+
signingConfigs.debug.storePassword = System.getenv("keystore_password")
36+
signingConfigs.debug.keyAlias = System.getenv("keystore_alias")
37+
signingConfigs.debug.keyPassword = System.getenv("keystore_alias_password")
38+
}
39+
1540
buildTypes {
1641
release {
1742
minifyEnabled false
@@ -22,6 +47,7 @@ android {
2247
sourceCompatibility JavaVersion.VERSION_1_8
2348
targetCompatibility JavaVersion.VERSION_1_8
2449
}
50+
2551
buildToolsVersion '28.0.0'
2652
}
2753

@@ -34,16 +60,16 @@ dependencies {
3460
implementation 'com.google.firebase:firebase-auth:16.0.2'
3561
implementation 'com.android.support:design:28.0.0-alpha3'
3662
implementation 'com.android.support:support-v4:28.0.0-alpha3'
37-
implementation 'com.android.support:recyclerview-v7:28.0.0-alpha3'
3863
implementation 'com.android.support:support-vector-drawable:28.0.0-alpha3'
3964
testImplementation 'junit:junit:4.12'
65+
androidTestImplementation 'com.android.support.test:rules:1.0.2'
4066
androidTestImplementation 'com.android.support.test:runner:1.0.2'
4167
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
4268
implementation 'com.google.firebase:firebase-core:16.0.1'
4369
implementation 'com.google.android.gms:play-services-auth:15.0.1'
44-
implementation 'com.google.firebase:firebase-firestore:17.0.2'
70+
implementation 'com.google.firebase:firebase-firestore:17.0.3'
4571
implementation 'com.android.support:recyclerview-v7:28.0.0-alpha3'
46-
implementation 'com.google.firebase:firebase-messaging:17.0.0'
72+
implementation 'com.google.firebase:firebase-messaging:17.1.0'
4773
implementation 'com.jakewharton:butterknife:8.8.1'
4874
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
4975
implementation 'com.firebaseui:firebase-ui-firestore:4.0.1'
@@ -55,6 +81,11 @@ dependencies {
5581
implementation 'com.github.GrenderG:Toasty:1.3.0'
5682
implementation 'com.github.matecode:Snacky:1.0.3'
5783
implementation 'com.mikepenz:fastadapter:3.2.7'
84+
//noinspection GradleDependency
85+
implementation 'com.heinrichreimersoftware:material-intro:-SNAPSHOT'
86+
implementation 'com.annimon:stream:1.2.0'
87+
implementation 'com.github.prolificinteractive:material-calendarview:1.6.0'
88+
implementation 'android.arch.lifecycle:extensions:1.1.1'
5889
}
5990

6091
apply plugin: 'com.google.gms.google-services'

app/release/Mugger 1.10.7.apk

5.99 MB
Binary file not shown.

app/release/app.aab

522 KB
Binary file not shown.

app/release/output.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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":{}}]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.bojio.mugger;
2+
3+
import android.content.Context;
4+
import android.support.test.InstrumentationRegistry;
5+
import android.support.test.filters.SmallTest;
6+
import android.support.test.rule.ActivityTestRule;
7+
import android.support.test.runner.AndroidJUnit4;
8+
import android.util.Log;
9+
import android.widget.TextView;
10+
11+
import com.bojio.mugger.authentication.GoogleLoginActivity;
12+
import com.bojio.mugger.authentication.MuggerUserCache;
13+
import com.bojio.mugger.database.MuggerDatabase;
14+
import com.google.android.gms.tasks.Task;
15+
import com.google.android.gms.tasks.Tasks;
16+
import com.google.firebase.FirebaseApp;
17+
import com.google.firebase.auth.AuthCredential;
18+
import com.google.firebase.auth.EmailAuthCredential;
19+
import com.google.firebase.auth.EmailAuthProvider;
20+
import com.google.firebase.auth.FirebaseAuth;
21+
import com.google.firebase.auth.GoogleAuthCredential;
22+
import com.google.firebase.auth.UserProfileChangeRequest;
23+
import com.google.firebase.firestore.DocumentReference;
24+
import com.google.firebase.firestore.DocumentSnapshot;
25+
import com.google.firebase.firestore.FirebaseFirestore;
26+
27+
import org.hamcrest.core.IsNull;
28+
import org.junit.Assert;
29+
import org.junit.Before;
30+
import org.junit.Rule;
31+
import org.junit.Test;
32+
import org.junit.runner.RunWith;
33+
34+
import java.util.concurrent.ExecutionException;
35+
36+
import static org.junit.Assert.assertEquals;
37+
38+
@RunWith(AndroidJUnit4.class)
39+
public class Main2ActivityInstrumentalTest {
40+
private FirebaseFirestore db;
41+
private FirebaseAuth mAuth;
42+
private MuggerUserCache muggerUserCache;
43+
44+
@Rule
45+
public final ActivityTestRule<Main2Activity> mActivityRule = new ActivityTestRule<>(
46+
Main2Activity.class, true, true);
47+
48+
@Before
49+
public void setup() {
50+
FirebaseApp.initializeApp(InstrumentationRegistry.getContext());
51+
mAuth = FirebaseAuth.getInstance();
52+
db = FirebaseFirestore.getInstance();
53+
muggerUserCache = MuggerUserCache.getInstance();
54+
TestUser.login(mAuth, db);
55+
}
56+
57+
@Test
58+
@SmallTest
59+
public void testSetup() {
60+
Assert.assertNotNull(db);
61+
Assert.assertNotNull(mAuth);
62+
Assert.assertNotNull(muggerUserCache);
63+
}
64+
65+
@Test
66+
@SmallTest
67+
public void testModules() {
68+
Assert.assertNotNull(muggerUserCache.getModules());
69+
}
70+
71+
@Test
72+
@SmallTest
73+
public void testAllModules() {
74+
Assert.assertNotNull(muggerUserCache.getAllModules());
75+
}
76+
77+
@Test
78+
@SmallTest
79+
public void testDisplayNameView() {
80+
Assert.assertNotNull(mAuth.getCurrentUser().getDisplayName());
81+
Assert.assertEquals(((TextView) mActivityRule.getActivity().findViewById(R.id.username))
82+
.getText(), mAuth.getCurrentUser().getDisplayName());
83+
Assert.assertEquals(((TextView) mActivityRule.getActivity().findViewById(R.id.email))
84+
.getText(), mAuth.getCurrentUser().getEmail());
85+
}
86+
}

0 commit comments

Comments
 (0)