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

Finished app intro, Refactored shared prefs

SettingsActivity is partially broken, 
Access a shared pref with its key and the Get/SetPref fct
Added editing & patch request for rfid, 
Changed strings to use translatable attribute
parent c7c602eb
"$schema":"http://json-schema.org/draft-04/schema#"
"additionalProperties":false
"title":"Additional Fields"
"type":"object"
"properties":{
"SBB_Abo":
{
"type":"string"
"enum":["None", "GA", "Halbtax", "Gleis 7"]
}
"Food":
{
"type":"string"
"enum":["Omnivor", "Vegi", "Vegan", "Other"]
}
"Special Food Requirements":
{
"type":"string"
}
}
"required":["SBB_Abo", "Food"]
......@@ -2,6 +2,7 @@ package ch.amiv.android_app.core;
import android.content.Context;
import android.content.Intent;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
......@@ -12,11 +13,14 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import ch.amiv.android_app.R;
import ch.amiv.android_app.ui.NonSwipeableViewPager;
import ch.amiv.android_app.ui.PrefDetailView;
import ch.amiv.android_app.util.Util;
public class IntroActivity extends AppCompatActivity {
......@@ -25,19 +29,39 @@ public class IntroActivity extends AppCompatActivity {
private LinearLayout dotsLayout;
private TextView[] dots;
private Button btnSkip, btnNext;
private EditText rfidField;
//page configs & layouts
private int[] layouts = {
R.layout.core_intro_slide_language,
R.layout.core_intro_slide_info,
R.layout.core_intro_slide_profile,
R.layout.core_intro_slide_event_prefs};
private boolean[] allowSkip = {false, false, true, false};
private int[] nextText = {0, R.string.next, 0, R.string.lets_go};//set to 0 to hide next button
R.layout.core_intro_slide_event_prefs,
R.layout.core_intro_slide_pref_detail};//Used for editing an enum pref
private static final class Page {
private static final int LANGUAGE = 0;
private static final int APP_INFO = 1;
private static final int EDIT_PROFILE = 2;
private static final int EVENT_PREF = 3;
private static final int PREF_DETAIL = 4;
}
private boolean[] allowSkip = {false, false, true, false, false};
private int[] nextText = {0, R.string.next, R.string.next, R.string.lets_go, 0};//set to 0 to hide next button
private boolean hasLoggedIn; //used when using back after the login page
private String langSetIntentKey = "lang_set";
private View.OnClickListener onNextClick = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (viewPager.getCurrentItem() == 2)//If we press next on the profile page, submit new data
UpdateProfile();
NextPage(true);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
......@@ -47,6 +71,7 @@ public class IntroActivity extends AppCompatActivity {
dotsLayout = findViewById(R.id.layoutDots);
btnSkip = findViewById(R.id.buttonSkip);
btnNext = findViewById(R.id.buttonNext);
rfidField = findViewById(R.id.rfidField);
boolean hasSetLang = false;
Intent intent = getIntent();
......@@ -66,12 +91,7 @@ public class IntroActivity extends AppCompatActivity {
}
});
btnNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NextPage(true);
}
});
btnNext.setOnClickListener(onNextClick);
if(hasSetLang)
NextPage(true);
......@@ -82,17 +102,17 @@ public class IntroActivity extends AppCompatActivity {
@Override
public void onBackPressed() {
int currentPos = viewPager.getCurrentItem();
if(currentPos == 2) {
if(currentPos == Page.EDIT_PROFILE) {
StartLoginActivity(true);
}
else if(currentPos == 3){
else if(currentPos == Page.EVENT_PREF){
if(hasLoggedIn)
viewPager.setCurrentItem(2);
SetPage(Page.EDIT_PROFILE, true);
else
StartLoginActivity(true);
}
else if(currentPos > 0)
viewPager.setCurrentItem(currentPos -1);
SetPage(currentPos -1, true);
//Dont call super.onBackPressed as we will otherwise leave the app
}
......@@ -113,7 +133,10 @@ public class IntroActivity extends AppCompatActivity {
* Will update the page dots at the bottom, to indicate which page we are on. note: the views are deleted and recreated
*/
private void RefreshPageDots(int currentPage) {
dots = new TextView[layouts.length +1]; //+1 for login acitivty
if(currentPage > layouts.length -2)//pref detail view
return;
dots = new TextView[layouts.length]; //+1 for login acitivty, -1 for pref detail
int inactive = ContextCompat.getColor(this, R.color.darkGrey);
dotsLayout.removeAllViews();
......@@ -126,17 +149,40 @@ public class IntroActivity extends AppCompatActivity {
}
if (dots.length > 0)//change indexes for login activity
dots[currentPage < 2 ? currentPage : currentPage +1].setTextColor(ContextCompat.getColor(this, R.color.lightGrey));
dots[currentPage < Page.EDIT_PROFILE ? currentPage : currentPage +1].setTextColor(ContextCompat.getColor(this, R.color.lightGrey));
}
private void NextPage(boolean success){
int nextPos = viewPager.getCurrentItem() + 1;
if (nextPos == 2)//go to login first
int currentPos = viewPager.getCurrentItem();
if(currentPos == Page.EDIT_PROFILE)
Util.HideKeyboard(this);
if (currentPos == Page.APP_INFO)//go to login first
StartLoginActivity(false);
else if (nextPos < layouts.length) {
viewPager.setCurrentItem(nextPos);
} else {
else if (currentPos == Page.EVENT_PREF)
StartMainAcivity();
else if (currentPos +1 < layouts.length)
SetPage(currentPos +1, true);
}
private void SetPage(int page, boolean setupPage){
viewPager.setCurrentItem(page);//This actually changes the page, otherwise we are just setting up the new page
if(!setupPage)
return;
//Setup Next Page
if(page == Page.EVENT_PREF){//Setup pref values XXXXX Check this
TextView foodLabel = findViewById(R.id.foodPrefText);
String foodValue = Settings.GetPref(Settings.foodPrefKey, getApplicationContext());
if(foodLabel != null)
foodLabel.setText(foodValue);
TextView sbbLabel = findViewById(R.id.sbbPrefText);
String sbbValue = Settings.GetPref(Settings.sbbPrefKey, getApplicationContext());
if(sbbLabel != null)
sbbLabel.setText(sbbValue);
}
}
......@@ -204,10 +250,23 @@ public class IntroActivity extends AppCompatActivity {
if(hasLoggedIn)
hasLoggedIn = Settings.HasToken(getApplicationContext());//check if user is only logged in by mail, or if we have no user profile to edit
if(resultCode == RESULT_OK)
viewPager.setCurrentItem(hasLoggedIn ? 2 : 3);
if(resultCode == RESULT_OK) {
if(hasLoggedIn){
SetPage(Page.EDIT_PROFILE, true);
Snackbar.make(viewPager, "Fetching Profile", 1000).show();
Requests.FetchUserData(getApplicationContext(), viewPager, new Requests.OnDataReceivedCallback() {
@Override
public void OnDataReceived() {
SetProfileUI();
}
});
}
else
SetPage(Page.EVENT_PREF, true);
}
else
viewPager.setCurrentItem(1);//means the user canceled using back, show the previous page
SetPage(Page.APP_INFO, true);//means the user canceled using back, show the previous page
}
}
......@@ -218,7 +277,7 @@ public class IntroActivity extends AppCompatActivity {
}
private void StartMainAcivity() {
Settings.SetIntroDone(true, this);
Settings.SetBoolPref(Settings.introDoneKey, true, this);
startActivity(new Intent(this, MainActivity.class));
finish();
}
......@@ -250,6 +309,90 @@ public class IntroActivity extends AppCompatActivity {
//region---Profile---
/**
* Use this to fill in the profile text fields once the userInfo has been received
*/
private void SetProfileUI(){
if(rfidField == null)
rfidField = findViewById(R.id.rfidField);
rfidField.setText(UserInfo.current.rfid);
}
/**
* Use this to update the profile info and submit to the server
*/
private void UpdateProfile(){
String newRfid = rfidField.getText().toString();
if(newRfid.isEmpty() || newRfid.equalsIgnoreCase(UserInfo.current.rfid))
return;
UserInfo.current.rfid = newRfid;
Requests.PatchUserData(getApplicationContext());
}
//endregion
//region---Prefs---
public void EditFoodPrefs(View view){
SetPage(Page.PREF_DETAIL, true);
final PrefDetailView.OnButtonIndexClicked onClick = new PrefDetailView.OnButtonIndexClicked() {
@Override
public void OnClick(int enumIndex) {//use length-1 when other is used
if(enumIndex < 0)
return;
SetPage(Page.EVENT_PREF, false);
//Set the event pref
String value = getResources().getStringArray(R.array.pref_food_list_values)[enumIndex];
Settings.SetPref(Settings.foodPrefKey, value, getApplicationContext());
if(enumIndex == getResources().getStringArray(R.array.pref_food_list_values).length -1 && findViewById(R.id.otherField) != null) {
Settings.SetPref(Settings.specialFoodPrefKey, ((EditText)findViewById(R.id.otherField)).getText().toString(), getApplicationContext());
}
TextView label = findViewById(R.id.foodPrefText);
if(label != null)
label.setText(getResources().getStringArray(R.array.pref_food_list_values)[enumIndex]);
btnNext.setOnClickListener(onNextClick);
}
};
PrefDetailView.InitialiseList(this, R.string.pref_food_title, onClick, getResources().getStringArray(R.array.pref_food_list_values), true);
btnNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onClick.OnClick(getResources().getStringArray(R.array.pref_food_list_values).length -1);
}
});
}
public void EditSBBPrefs(View view){
SetPage(Page.PREF_DETAIL, true);
final PrefDetailView.OnButtonIndexClicked onClick = new PrefDetailView.OnButtonIndexClicked() {
@Override
public void OnClick(int enumIndex) {//use length-1 when other is used
if(enumIndex < 0)
return;
SetPage(Page.EVENT_PREF, false);
String value = getResources().getStringArray(R.array.pref_sbb_list_values)[enumIndex];
Settings.SetPref(Settings.sbbPrefKey, value, getApplicationContext());
TextView label = findViewById(R.id.sbbPrefText);
if(label != null)
label.setText(getResources().getStringArray(R.array.pref_sbb_list_values)[enumIndex]);
btnNext.setOnClickListener(onNextClick);
}
};
PrefDetailView.InitialiseList(this, R.string.pref_sbb_title, onClick, getResources().getStringArray(R.array.pref_sbb_list_values), false);
btnNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onClick.OnClick(getResources().getStringArray(R.array.pref_sbb_list_values).length -1);
}
});
}
//endregion
//endregion
}
......@@ -46,7 +46,7 @@ public class LoginActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
isIntroLogin = !Settings.GetIntroDone(this);
isIntroLogin = !Settings.GetBoolPref(Settings.introDoneKey, getApplicationContext());
//Set for the keyboard to resize the window so the snackbars appear just above the keyboard
prevLayoutParams = getWindow().getAttributes().softInputMode;
......
......@@ -101,9 +101,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
PersistentStorage.LoadJobs(getApplicationContext());
InitialisePageView();
new Settings(getApplicationContext()); //creates the settings instance, so we can store/retrieve shared preferences
//fetch the user info if we are logged in, there exists a token from the previous session, should be cached.
if(!PersistentStorage.LoadUserInfo(getApplicationContext()) || UserInfo.current._id.isEmpty() && !Settings.IsEmailOnlyLogin(getApplicationContext())) {
Requests.FetchUserData(getApplicationContext(), drawerNavigation, new Requests.OnDataReceivedCallback() {
@Override
......
......@@ -32,10 +32,27 @@ import java.util.Map;
import ch.amiv.android_app.events.Events;
import ch.amiv.android_app.jobs.Jobs;
/**
* A static class to do backround http requests to the amiv api. Access these requests anywhere
* Most requests have a callback, so you can execute code when the request has returned or failed
* See API Docs to see what requests can be done: https://api.amiv.ethz.ch/docs or via github site https://github.com/amiv-eth/amivapi
*
* It is advised to test requests first with Postman or similar.
* To see the output of the requests setup Postman as a proxy server, and then set your computer's IP as the proxy in the phones wi-fi setting for the current connection
*
* Generally, call a FetchX function from anywhere, which will create and format the request then use SendRequest to submit the formatted request
* When creating your own, have a look at the override functions available for the StringRequest. Note the difference between getHeaders and getParams
*
* To add auth with a token, from settings see one of the functions as an example, eg. FetchEventSignups
*
* To load images, don't use a request directly, use a networkImageView, which will handle everything for you including caching
*
* Libary used for network stuff: volley, note: we use our own modified version of the libary as a git submodule
*/
public final class Requests {
private static RequestQueue requestQueue;
private static ImageLoader imageLoader;
private static final int MAX_CACHED_IMAGES = 50;
private static final int MAX_CACHED_IMAGES = 75;
public static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
......@@ -389,6 +406,64 @@ public final class Requests {
boolean hasSent = Requests.SendRequest(request, context);
}
/**
* Will update the user data in the amiv api
* XXX buffer and send request when internet is regained, and retry there is an error
*/
public static void PatchUserData(final Context context){
if(!Settings.HasToken(context) || !CheckConnection(context))
return;
//Do patch request to /user/{userId}
String url = Settings.API_URL + "users/" + UserInfo.current._id;
StringRequest request = new StringRequest(Request.Method.PATCH, url,null, null)
{
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) { //Note: the parseNetworkResponse is only called if the response was successful (codes 2xx), else parseNetworkError is called.
if(response != null) {
Log.e("request", "status Code: " + response.statusCode);
}
else
Log.e("request", "Request returned null response. fetch user data");
return super.parseNetworkResponse(response);
}
@Override
protected VolleyError parseNetworkError(final VolleyError volleyError) { //see comments at parseNetworkResponse()
if(volleyError != null && volleyError.networkResponse != null)
Log.e("request", "status code: " + volleyError.networkResponse.statusCode + "\n" + new String(volleyError.networkResponse.data));
else
Log.e("request", "Request returned null response. fetch user data");
return super.parseNetworkError(volleyError);
}
@Override
public Map<String, String> getHeaders() {
Map<String,String> headers = new HashMap<String, String>();
// Add basic auth with token
String credentials = Settings.GetToken(context) + ":";
String auth = "Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP);
headers.put("Authorization", auth);
headers.put("if-match", UserInfo.current._etag);
return headers;
}
@Override
protected Map<String, String> getParams() throws AuthFailureError {
Map<String,String> params = new HashMap<String, String>();
params.put("rfid", UserInfo.current.rfid);
return params;
}
};
boolean hasSent = Requests.SendRequest(request, context);
}
/**
* Will send a Delete request to delete the current session and token with it.
* XXX When there is no internet or an error the request is not completed. the session persists on the server. need to rerun request on next time we have a connection
......
......@@ -9,6 +9,8 @@ import android.util.Log;
import java.util.Locale;
import javax.xml.validation.Validator;
import ch.amiv.android_app.R;
/**
......@@ -17,7 +19,6 @@ import ch.amiv.android_app.R;
* Access the settings with the according get function from the static instance.
*/
public class Settings {
public static Settings instance;
//public static final String API_URL = "http://192.168.1.105:5000/";
public static final String API_URL = "https://api-dev.amiv.ethz.ch/";
......@@ -25,47 +26,21 @@ public class Settings {
//Whether to show hidden events, where the adverts should not have started yet, should later be set by user access group
public static final boolean showHiddenFeatures = true;
//Vars for saving/reading the url from shared prefs, to allow saving between sessions. For each variable, have a key to access it and a default value
//---PREF KEYS---- Vars for saving/reading the url from shared prefs, to allow saving between sessions. For each variable, have a key to access it and a default value
private static SharedPreferences sharedPrefs;
public static final String SHARED_PREFS_KEY = "ch.amiv.android_app";
private static final String apiUrlPrefKey = "ch.amiv.android_app.serverurl";
private static final String defaultApiUrl = "https://api-dev.amiv.ethz.ch";
private static final String themeKey = "ch.amiv.android_app.theme";
private static final boolean defaultTheme = false; //false for light
private static final String apiTokenKey = "ch.amiv.android_app.apitoken";
private static final String introDoneKey = "ch.amiv.android_app.introdone";
private static Vibrator vibrator;
public static final class VibrateTime {
public static final int SHORT = 50;
public static final int NORMAL = 100;
public static final int LONG = 250;
}
public static void Vibrate(int millisecs, Context context){
if(vibrator == null)
vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null)
vibrator.vibrate(millisecs);
}
//Keys are always two values, (Key for shared prefs, Default value)
//For boolean values true=1, false=0 (or anything else)
public static final String[] apiUrlPrefKey = {"ch.amiv.android_app.serverurl", "https://api-dev.amiv.ethz.ch"};
public static final String[] apiTokenKey = {"ch.amiv.android_app.apitoken", ""};
public static final String[] introDoneKey = {"ch.amiv.android_app.introdone", "0"};
public static void CancelVibrate (){
if(vibrator != null)
vibrator.cancel();
}
/*
* This constructor will set the instance created as the statically accessible instance, which can be accessed anywhere
* This means we can just create a settings instance to initialise this.
*/
public Settings(Context context) {
if(instance != null) {
Log.d("settings", "A Settings instance already exists. Will use existing instance.");
return;
}
instance = this;
CheckInitSharedPrefs(context);
}
public static final String[] foodPrefKey = {"ch.amiv.android_app.foodpref", ""};
public static final String[] specialFoodPrefKey = {"ch.amiv.android_app.specialfoodpref", ""};
public static final String[] sbbPrefKey = {"ch.amiv.android_app.sbbabo", ""};
//region ---SharedPrefs---
/**
* Will check that the shared prefs instance is set so we can edit/retrieve values
*/
......@@ -74,43 +49,62 @@ public class Settings {
sharedPrefs = context.getSharedPreferences(SHARED_PREFS_KEY, Context.MODE_PRIVATE);
}
//==========Get/Set Settings===============
/**
* Will store the value in sharedpreferences to be restored in another session
* @param key A string[2] in the format (prefs key, defValue), use the Settings public vars
*/
public static void SetApiURL(String value, Context context) {
public static void SetPref(String[] key, String value, Context context){
CheckInitSharedPrefs(context);
sharedPrefs.edit().putString(apiUrlPrefKey, value).apply();
sharedPrefs.edit().putString(key[0], value).apply();
}
/**
* Returns the saved url, so the url is saved between sessions
* Get the value stored in shared prefs with the given key
* @param key A string[2] in the format (prefs key, defValue), use the Settings public vars
*/
public static String GetApiURL(Context context)
public static String GetPref(String[] key, Context context)
{
CheckInitSharedPrefs(context);
return sharedPrefs.getString(apiUrlPrefKey, defaultApiUrl);
return sharedPrefs.getString(key[0], key[1]);
}
//Access Token
public static void SetToken(String value, Context context) {
//bool 'overloads'
public static void SetBoolPref (String[] key, boolean value, Context context){
CheckInitSharedPrefs(context);
sharedPrefs.edit().putString(apiTokenKey, value).apply();
sharedPrefs.edit().putBoolean(key[0], value).apply();
}
public static String GetToken(Context context) {
public static boolean GetBoolPref(String[] key, Context context)
{
CheckInitSharedPrefs(context);
return sharedPrefs.getString(apiTokenKey, "");
return sharedPrefs.getBoolean(key[0], key[1].equals("1"));
}
//Add token functions as they are commonly used, for ease of understanding in other code
public static void SetToken(String value, Context context){
SetPref(apiTokenKey, value, context);
}
public static String GetToken(Context context){
return GetPref(apiTokenKey, context);
}
/**
* Intended more for debug, will delete all saved data, which is stored in the shared prefs
*/
public static void ClearSharedPrefs(Context context){
CheckInitSharedPrefs(context);
sharedPrefs.edit().clear().commit();
}
//endregion
//region ---Auth---
/**
* Note: will only check if a token exists. This token may have expired but not have been refreshed/deleted.
* @return True if the user is logged into the api and has an access token.
*/
public static boolean HasToken(Context context){
CheckInitSharedPrefs(context);
String t = sharedPrefs.getString(apiTokenKey, "");
String t = GetPref(apiTokenKey, context);
return !t.isEmpty();
}
......@@ -128,8 +122,9 @@ public class Settings {
public static boolean IsLoggedIn(Context context){