Commit c9485bd0 authored by Roger Barton's avatar Roger Barton
Browse files

Merge branch 'checkin'

parents 9d3d3e10 d9d785e7
......@@ -17,6 +17,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
}
dependencies {
......@@ -29,6 +33,7 @@ dependencies {
implementation project(path: ':volley')
implementation 'com.google.zxing:core:3.2.1'
implementation 'com.journeyapps:zxing-android-embedded:3.2.0@aar'
implementation 'com.google.android.gms:play-services-vision:15.0.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
......
......@@ -17,19 +17,19 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:screenOrientation="portrait"
android:name=".MyApplication"
android:name=".core.MyApplication"
android:theme="@style/AppThemeLight">
<activity
android:name=".SettingsActivity"
android:name=".core.SettingsActivity"
android:label=""
android:configChanges="layoutDirection|locale"
android:parentActivityName=".MainActivity">
android:parentActivityName=".core.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="ch.amiv.android_app.MainActivity" />
android:value="ch.amiv.android_app.core.MainActivity" />
</activity>
<activity
android:name=".MainActivity"
android:name=".core.MainActivity"
android:configChanges="keyboardHidden|orientation|layoutDirection|screenSize|locale"
android:label="@string/app_name"
android:theme="@style/AppThemeLight"
......@@ -42,28 +42,79 @@
</activity>
<activity
android:name=".LoginActivity"
android:name=".core.LoginActivity"
android:configChanges="keyboardHidden|orientation|layoutDirection|screenSize|locale"
android:label=""
android:windowSoftInputMode="stateVisible">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="ch.amiv.android_app.MainActivity" />
android:value="ch.amiv.android_app.core.MainActivity" />
</activity>
<activity
android:name=".EventDetailActivity"
android:name=".core.EventDetailActivity"
android:configChanges="orientation|layoutDirection|locale"
android:theme="@style/AppThemeLight">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="ch.amiv.android_app.MainActivity" />
android:value="ch.amiv.android_app.core.MainActivity" />
</activity>
<activity android:name=".BarcodeIdActivity"
<activity android:name=".checkin.BarcodeIdActivity"
android:screenOrientation="portrait"
android:configChanges="layoutDirection|locale"
android:parentActivityName=".MainActivity">
android:parentActivityName=".core.MainActivity">
</activity>
<!-- Checkin -->
<activity
android:name=".checkin.MainActivity"
android:screenOrientation="portrait"
android:label="@string/app_name"
android:theme="@style/CheckinTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".checkin.ScanActivity"
android:screenOrientation="portrait"
android:theme="@style/CheckinTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".checkin.MainActivity" />
</activity>
<activity android:name=".checkin.SettingsActivity"
android:screenOrientation="portrait"
android:label="@string/app_name"
android:theme="@style/CheckinTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".checkin.MainActivity" />
</activity>
<activity android:name=".checkin.MemberListActivity"
android:theme="@style/CheckinTheme"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/checkin_searchable"/>
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".checkin.ScanActivity" />
</activity>
<activity android:name=".checkin.SearchMembersActivity"
android:theme="@style/CheckinTheme"
android:label="@string/app_name">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".checkin.MemberListActivity" />
</activity>
</application>
</manifest>
\ No newline at end of file
package ch.amiv.android_app;
package ch.amiv.android_app.checkin;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
......@@ -17,6 +17,10 @@ import com.google.zxing.common.BitMatrix;
import com.journeyapps.barcodescanner.BarcodeEncoder;
import java.lang.reflect.Field;
import java.nio.file.attribute.UserDefinedFileAttributeView;
import ch.amiv.android_app.R;
import ch.amiv.android_app.core.UserInfo;
public class BarcodeIdActivity extends AppCompatActivity {
......@@ -28,7 +32,7 @@ public class BarcodeIdActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_barcode_id);
setContentView(R.layout.checkin_activity_barcode_id);
//Set up toolbar and back button
Toolbar toolbar = findViewById(R.id.toolbar);
......@@ -72,14 +76,14 @@ public class BarcodeIdActivity extends AppCompatActivity {
* Will generate a barcode bitmap and apply it to the barcodeImageview. Note: barcode will not fill whole constraint as the width of the bars is determined in pixels, to prevent distortion
*/
public void GenerateBarcode(){
if(UserInfo.current == null || (UserInfo.current.nethz.isEmpty() && UserInfo.current.email.isEmpty())){
if(UserInfo.current == null || (UserInfo.current.legi.isEmpty() && UserInfo.current.nethz.isEmpty() && UserInfo.current.email.isEmpty())){
Snackbar.make(swipeRefreshLayout, R.string.not_logged_in, Snackbar.LENGTH_LONG).show();
swipeRefreshLayout.setRefreshing(false);
return;
}
//prioritise nethz as this is shorter
String encode = UserInfo.current.nethz.isEmpty() ? UserInfo.current.email : UserInfo.current.nethz;
//prioritise legi > nethz > email
String encode = UserInfo.current.legi.isEmpty() ? (UserInfo.current.nethz.isEmpty() ? UserInfo.current.email : UserInfo.current.nethz) : UserInfo.current.legi;
encode = encode.replace('@',' ').toUpperCase();
MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
try {
......
package ch.amiv.android_app.checkin;
import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.List;
import ch.amiv.android_app.R;
/**
* Created by Roger on 06-Feb-18.
*/
public class CustomListAdapter extends ArrayAdapter<Member> {
private final Activity context;
private final List<Member> members;
public CustomListAdapter(Activity context, List<Member> _members){
super(context, R.layout.checkin_list_item_member, _members);
this.context = context;
this.members = _members;
}
public View getView(int position, View view, ViewGroup parent) {
LayoutInflater inflater = context.getLayoutInflater();
View rowView = inflater.inflate(R.layout.checkin_list_item_member, null,true);
TextView nameField = rowView.findViewById(R.id.nameField);
TextView infoField = rowView.findViewById(R.id.infoField);
TextView checkinField = rowView.findViewById(R.id.checkinStatus);
TextView membershipField = rowView.findViewById(R.id.infoField2);
Member m = members.get(position);
nameField.setText(m.firstname + " " + m.lastname);
infoField.setText(m.legi);
checkinField.setText((m.checkedIn ? "In" : "Out"));
if(EventDatabase.instance != null && EventDatabase.instance.eventData.eventType == EventData.EventType.GV && m.membership.length() > 1)
membershipField.setText(m.membership.substring(0,1).toUpperCase() + m.membership.substring(1));
else
membershipField.setText("");
return rowView;
}
}
package ch.amiv.android_app.checkin;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* This stores all event data received by /get_event_data get request. It can be accessed by the static instance. The instance should be deleted when a new session is started
* Created by Roger on 28-Feb-18.
*/
public class EventData {
public String serverId = "0";
public enum EventType {NotSet, Event, PVK, GV, Counter, Unknown}
public EventType eventType = EventType.NotSet;
public enum CheckinType {InOut, Counter}
public CheckinType checkinType = CheckinType.InOut;
public String name = "";
public String description;
public int signupCount;
public int spots; //number of expected people/preregistered members
public String startTime = "";
public boolean Update(String _serverId, String _eventType, String _checkinType, String _name, String _description, int _signupCount, int _spots, String _startTime)
{
serverId = _serverId;
name = _name;
description = _description;
signupCount = _signupCount;
spots = _spots;
startTime = _startTime;
/*
if(_checkinType .equalsIgnoreCase("counter"))
checkinType = CheckinType.Counter;
else if(_checkinType.equalsIgnoreCase("in_out"))
checkinType = CheckinType.InOut;*/
return SetEventType(_eventType);
}
public boolean SetEventType(String s)
{
checkinType = CheckinType.InOut; //XXX set the checkin type in a better way, get from json or use a fkt to determine it
if(s.equalsIgnoreCase("AMIV Events")) //Matches the human readable string in the dropdown on the checkin login website
eventType = EventType.Event;
else if(s.equalsIgnoreCase("AMIV PVK")) //May need to be adjusted to what is given
eventType = EventType.PVK;
else if(s.equalsIgnoreCase("AMIV General Assemblies"))
eventType = EventType.GV;
else if(s.equalsIgnoreCase("AMIV Freebies")) {
eventType = EventType.Counter;
checkinType = CheckinType.Counter;
}
else {
eventType = EventType.Unknown;
Log.d("SetEventType()", "Parsing event type unsuccessful, will attempt to imply event type else restrict functionality.");
return false;
}
return true;
}
public List<KeyValuePair> GetInfosAsKeyValuePairs ()
{
List<KeyValuePair> list = new ArrayList<KeyValuePair>();
list.add(new KeyValuePair("Event", name));
list.add(new KeyValuePair("Event Type", eventType.toString()));
list.add(new KeyValuePair("Sign-ups", "" + signupCount));
list.add(new KeyValuePair("Description", description));
list.add(new KeyValuePair("Start Time", startTime));
return list;
}
}
package ch.amiv.android_app.checkin;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Created by Roger on 06-Feb-18.
* Contains all the data received by the server with GET /checkin_update_data in a android friendly format. The JSON file received by the server fills this.
* See README_API on the server side repo
*/
public class EventDatabase {
public static EventDatabase instance;
final List<Member> members = new ArrayList<Member>();
public EventData eventData = new EventData();
public List<KeyValuePair> stats = new ArrayList<KeyValuePair>();
public Comparator<Member> memberComparator;
public enum MemberComparator {None, Name, Membership, Status, Legi}
private MemberComparator currentSorting = MemberComparator.None;
private boolean invertSorting = false;
public EventDatabase()
{
if(instance != null)
Log.e("memberDatabase", "A Member Database already exists, cannot create another. Should delete old MemberDB if this is wanted");
instance = this;
}
public void UpdateMemberData(JSONArray _members)
{
members.clear();
for (int i = 0; i < _members.length(); i++) {
try {
Member m = new Member(_members.getJSONObject(i));
members.add(m);
} catch (JSONException e) {
e.printStackTrace();
}
}
SortMembers();
}
public void UpdateStats(JSONArray statSource, boolean hasEventInfos)
{
stats.clear();
try {
for (int i = 0; i < statSource.length(); i++) {
JSONObject j = statSource.getJSONObject(i);
stats.add(new KeyValuePair(j.getString("key"), j.get("value").toString()));
//also imply event type if it has not been parsed from the json
if ((!hasEventInfos || EventDatabase.instance.eventData.eventType == EventData.EventType.NotSet || EventDatabase.instance.eventData.eventType == EventData.EventType.Unknown)
&& j.getString("key").equalsIgnoreCase("Extraordinary Members"))
EventDatabase.instance.eventData.eventType = EventData.EventType.GV;
}
if (hasEventInfos && EventDatabase.instance.eventData.eventType == EventData.EventType.NotSet) //imply event type if we do not have the event infos
EventDatabase.instance.eventData.eventType = EventData.EventType.Event;
}
catch (JSONException e) {
Log.e("postrequest", "Error parsing received JsonObject in GetStats().");
e.printStackTrace();
}
}
public void SetMemberSortingType(Comparator<Member> comparator)
{
memberComparator = comparator;
SortMembers();
}
public void SetMemberSortingType(final MemberComparator sortingType)
{
if(sortingType == MemberComparator.None)
return;
if(currentSorting == sortingType)
invertSorting = !invertSorting;
else
invertSorting = false;
currentSorting = sortingType;
Comparator<Member> comparator;
comparator = new Comparator<Member>() {
@Override
public int compare(Member a, Member b) {
switch (sortingType)
{
case Name:
return a.firstname.compareTo(b.firstname);
case Membership:
return a.membership.compareTo(b.membership);
case Status:
if(eventData.checkinType == EventData.CheckinType.InOut)
return (b.checkedIn == a.checkedIn ? 0 : (a.checkedIn ? 1 : -1));
else if(eventData.checkinType == EventData.CheckinType.Counter)
return a.checkinCount.compareTo(b.checkinCount);
case Legi:
return a.legi.compareTo(b.legi);
}
return 0;
}
};
if(invertSorting && android.os.Build.VERSION.SDK_INT >= 24)
SetMemberSortingType(comparator.reversed());
else
SetMemberSortingType(comparator);
}
/**
* Way of sorting the array of members using a custom defined comparator
*/
private void SortMembers()
{
if(memberComparator == null)
return;
Collections.sort(members, memberComparator);
}
}
package ch.amiv.android_app.checkin;
/**
* Created by roger on 01/03/2018.
*/
public class KeyValuePair {
public String name;
public String value;
public KeyValuePair(String name, String value) {
this.name = name;
this.value = value;
}
}
package ch.amiv.android_app.checkin;
/**
* Author: Roger Barton, rbarton@ethz.ch
* Date Created: 2/12/17
*/
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Vibrator;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.EditText;
import android.widget.TextView;
import ch.amiv.android_app.R;
public class MainActivity extends AppCompatActivity {
public static String CurrentPin;
private boolean mWaitingOnServer = false;
private EditText mPinField;
private TextView mInvalidPinLabel;
public static Vibrator vibrator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.checkin_activity_main);
InitialiseUI();
CheckPermissions();
}
@Override
public void onPause()
{
super.onPause();
if(vibrator != null)
vibrator.cancel();
}
private void InitialiseUI()
{
mPinField = findViewById(R.id.PinField);
mInvalidPinLabel = findViewById(R.id.InvalidPinLabel);
vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
mPinField.setOnKeyListener(new View.OnKeyListener() {
public boolean onKey(View view, int keyCode, KeyEvent keyevent) {
if ((keyevent.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
SubmitPin(view);
return true;
}
return false;
}
});
View logo = findViewById(R.id.LogoImage);
if(logo != null) {
Animation animation = AnimationUtils.loadAnimation(this, R.anim.item_anim_pop);
animation.setDuration(150);
logo.startAnimation(animation);
}
}
private void CheckPermissions()
{
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { //Get permission for camera
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
//Add popup
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 0);
}
else
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 0);
}
}
/**
* Submit a pin for an event to the server and act on response accondingly, ie open scanActivity if valid, or request pin entry again
* @param view
*/
public void SubmitPin(View view)
{
if(vibrator != null)
vibrator.vibrate(50);
View button = findViewById(R.id.SubmitPin);
if(button != null)
button.startAnimation(AnimationUtils.loadAnimation(this, R.anim.item_anim_pop));
if(mWaitingOnServer || mPinField.getText().toString().isEmpty()) //prevents submitting a second pin while still waiting on the response for the first pin
return;
mWaitingOnServer = true;
if(!ServerRequests.CheckConnection(getApplicationContext())) {
ApplyServerResponse(true, 0, getResources().getString(R.string.no_internet));
return;
}
CurrentPin = mPinField.getText().toString();
//Create a callback, this is what happens when we get the response
ServerRequests.OnCheckPinReceivedCallback callback = new ServerRequests.OnCheckPinReceivedCallback() {
@Override
public void OnStringReceived(final boolean validResponse, final int statusCode, final String data) {
mPinField.post(new Runnable() { //delay to other thread by using a ui element, as this is in a callback on another thread
public void run() {
ApplyServerResponse(validResponse, statusCode, data);
}});
}
};
ServerRequests.CheckPin(this, callback);
//StartScanActivity(); //NOTE: Uncomment for debugging without valid pin
}
/**
* Submit a server response to the function, will apply UI feedback or start the scan activity
* @param statusCode http status code from the response, eg 200 or 400
* @param responseText the text received from the server about our post request
*/
private void ApplyServerResponse(boolean validResponse, int statusCode, String responseText)