diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a54b409 --- /dev/null +++ b/.gitignore @@ -0,0 +1,86 @@ +# Built application files +*.apk +*.aar +*.ap_ +*.aab +.idea + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ diff --git a/AppLogin.zip b/AppLogin.zip deleted file mode 100644 index 8ba92c6..0000000 Binary files a/AppLogin.zip and /dev/null differ diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..897d30f --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.application' +apply plugin: 'com.google.gms.google-services' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + defaultConfig { + applicationId "com.example.applogin" + minSdkVersion 15 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' + implementation 'com.google.firebase:firebase-messaging:17.3.4' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + implementation 'com.android.volley:volley:1.1.1' +} diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..21b99d7 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,48 @@ +{ + "project_info": { + "project_number": "151349644451", + "firebase_url": "https://ev-scheduler.firebaseio.com", + "project_id": "ev-scheduler", + "storage_bucket": "ev-scheduler.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:151349644451:android:27259748c7c3922d09c316", + "android_client_info": { + "package_name": "com.example.applogin" + } + }, + "oauth_client": [ + { + "client_id": "151349644451-mnpur4ea4fv7g18h6uu9p8o5b00bs5jb.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.example.applogin", + "certificate_hash": "c19a4d379632e4ade113d04317186ef6c1591f41" + } + }, + { + "client_id": "151349644451-n754jogqbrosf21cs7i8ji39ovnedutl.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBVS72Cex9u-8iCIgM4ap_1KiqPRrmuJ_M" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "151349644451-n754jogqbrosf21cs7i8ji39ovnedutl.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/example/applogin/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/applogin/ExampleInstrumentedTest.java new file mode 100644 index 0000000..c078543 --- /dev/null +++ b/app/src/androidTest/java/com/example/applogin/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package com.example.applogin; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.example.applogin", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a792adc --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/applogin/ChargingActivity.java b/app/src/main/java/com/example/applogin/ChargingActivity.java new file mode 100644 index 0000000..d455129 --- /dev/null +++ b/app/src/main/java/com/example/applogin/ChargingActivity.java @@ -0,0 +1,53 @@ +package com.example.applogin; + +import androidx.appcompat.app.AppCompatActivity; + +import android.content.Intent; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.widget.TextView; + +import java.util.Timer; + +public class ChargingActivity extends AppCompatActivity { + + TextView coundownTime; + TextView chargeState; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_charging); + + coundownTime = findViewById(R.id.charge_time); + chargeState = findViewById(R.id.charge_state); + + new CountDownTimer(7200000, 1000) { + + public void onTick(long millisUntilFinished) { + int secondsLeft = (int) millisUntilFinished / 1000; + int hoursLeft = secondsLeft / 3600; + int minutesLeft = (secondsLeft % 3600)/60; + secondsLeft %= 60; + + coundownTime.setText("Time remaining: " + String.format("%d:%d:%02d", hoursLeft, minutesLeft,secondsLeft)); + } + + public void onFinish() { + End_criteria(); + } + }.start(); + } + //Move vehicle if timer runs out or the car is full (need to add the car full situation) + private void End_criteria() + { + chargeState.setText("Move Vehicle"); + } + //If someone just unplugs their car then they go back to the join queue screen, need to add this unplug data + private void Unplug() + { + Intent myIntent = new Intent(ChargingActivity.this, JoinQueueActivity.class); + startActivity(myIntent); + } + +} diff --git a/app/src/main/java/com/example/applogin/EVFirebase.java b/app/src/main/java/com/example/applogin/EVFirebase.java new file mode 100644 index 0000000..910e3ba --- /dev/null +++ b/app/src/main/java/com/example/applogin/EVFirebase.java @@ -0,0 +1,72 @@ +package com.example.applogin; + +import android.content.Context; +import android.content.SharedPreferences; +import android.widget.Toast; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; +import com.google.firebase.messaging.FirebaseMessagingService; + +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +public class EVFirebase extends FirebaseMessagingService { + + @Override + public void onNewToken(String token) { + + // If you want to send messages to this application instance or + // manage this apps subscriptions on the server side, send the + // Instance ID token to your app server. + sendRegistrationToServer(token); + + } + + public void sendRegistrationToServer(String token){ + + // Instantiate the RequestQueue. + RequestQueue queue = Volley.newRequestQueue(this); + String url ="https://ev-scheduler.appspot.com/api/user"; + + HashMap dict = new HashMap<>(); + dict.put("notification_token", token); + JSONObject json = new JSONObject(dict); + + // Request a string response from the provided URL. + final JsonObjectRequest stringRequest = new JsonObjectRequest(Request.Method.PUT, url, json, + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + Toast.makeText(EVFirebase.this, Integer.toString(error.networkResponse.statusCode), Toast.LENGTH_SHORT).show(); + + } + }){ + + @Override + public Map getHeaders() throws AuthFailureError { + + HashMap headers = new HashMap<>(); + SharedPreferences context = getApplicationContext().getSharedPreferences("scheduler_preferences", Context.MODE_PRIVATE); + headers.put("Authorization", "Bearer " + context.getString("api_key", "")); + return headers; + + } + + }; + + } + +} diff --git a/app/src/main/java/com/example/applogin/JoinQueueActivity.java b/app/src/main/java/com/example/applogin/JoinQueueActivity.java new file mode 100644 index 0000000..11b2e00 --- /dev/null +++ b/app/src/main/java/com/example/applogin/JoinQueueActivity.java @@ -0,0 +1,105 @@ +package com.example.applogin; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; +import com.example.applogin.ui.login.LoginActivity; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +public class JoinQueueActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_join_queue); + + FirebaseInstanceId.getInstance().getInstanceId() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (!task.isSuccessful()) { + return; + } + + // Get new Instance ID token + String token = task.getResult().getToken(); + + // Instantiate the RequestQueue. + RequestQueue network_queue = Volley.newRequestQueue(JoinQueueActivity.this); + String url ="https://ev-scheduler.appspot.com/api/user"; + + HashMap dict = new HashMap<>(); + dict.put("notification_token", token); + JSONObject json = new JSONObject(dict); + + // Request a string response from the provided URL. + final JsonObjectRequest stringRequest = new JsonObjectRequest(Request.Method.PUT, url, json, + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + Toast.makeText(JoinQueueActivity.this, "token sent", Toast.LENGTH_SHORT).show(); + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + Toast.makeText(JoinQueueActivity.this, Integer.toString(error.networkResponse.statusCode), Toast.LENGTH_SHORT).show(); + //Put error message (failed login, 3 different messages) + //if error.networkResponse.statusCode == 401 , message wrong password (auth_blueprint.py) + } + }){ + + @Override + public Map getHeaders() throws AuthFailureError { + + HashMap headers = new HashMap<>(); + SharedPreferences context = getApplicationContext().getSharedPreferences("scheduler_preferences", Context.MODE_PRIVATE); + headers.put("Authorization", "Bearer " + context.getString("api_key", "")); + return headers; + + } + + }; + network_queue.add(stringRequest); + + } + }); + + + + final Button joinButton = findViewById(R.id.join_queue); + joinButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + //Toast.makeText(JoinQueueActivity.this, "testtttt", Toast.LENGTH_SHORT).show(); + + Intent myIntent = new Intent(JoinQueueActivity.this, WaitingActivity.class); + startActivity(myIntent); + } + }); + } +} diff --git a/app/src/main/java/com/example/applogin/Login2Activity.java b/app/src/main/java/com/example/applogin/Login2Activity.java new file mode 100644 index 0000000..ad8eedb --- /dev/null +++ b/app/src/main/java/com/example/applogin/Login2Activity.java @@ -0,0 +1,141 @@ +package com.example.applogin; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.StringRequest; +import com.android.volley.toolbox.Volley; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.ConnectException; +import java.util.HashMap; +import java.util.Map; + +public class Login2Activity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login2); + + if (ContextCompat.checkSelfPermission(this, + Manifest.permission.INTERNET) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.INTERNET}, + 1); + + } + if (ContextCompat.checkSelfPermission(this, + Manifest.permission.ACCESS_NETWORK_STATE) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.ACCESS_NETWORK_STATE}, + 1); + + } + + final Button signInButton = findViewById(R.id.login); + final EditText usernameEditText = findViewById(R.id.username); + final EditText passwordEditText = findViewById(R.id.password); + signInButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + tryLogin(usernameEditText.getText().toString(), + passwordEditText.getText().toString()); + + + } + }); + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String[] permissions, int[] grantResults) { + switch (requestCode) { + case 1: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // permission was granted, yay! Do the + // contacts-related task you need to do. + } else { + // permission denied, boo! Disable the + // functionality that depends on this permission. + System.exit(1); + } + } + } + } + + private void tryLogin(String username, String password) { + + // Instantiate the RequestQueue. + RequestQueue queue = Volley.newRequestQueue(this); + String url ="https://ev-scheduler.appspot.com/api/auth/login"; + + HashMap dict = new HashMap<>(); + dict.put("username", username); + dict.put("password", password); + JSONObject json = new JSONObject(dict); + + // Request a string response from the provided URL. + final JsonObjectRequest stringRequest = new JsonObjectRequest(Request.Method.POST, url, json, + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + try { + //Toast.makeText(Login2Activity.this, response.getString("token"), Toast.LENGTH_SHORT).show(); + //System.out.println(response.toString()); + SharedPreferences context = getApplicationContext().getSharedPreferences("scheduler_preferences", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = context.edit(); + editor.putString("api_key", response.getString("token")); + editor.apply(); + Intent myIntent = new Intent(Login2Activity.this, JoinQueueActivity.class); + startActivity(myIntent); + }catch(JSONException e){ + //could put an error message here + } + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + //Toast.makeText(Login2Activity.this, Integer.toString(error.networkResponse.statusCode), Toast.LENGTH_SHORT).show(); + //Put error message (failed login, 3 different messages) + //if error.networkResponse.statusCode == 401 , message wrong password (auth_blueprint.py) + } + }); + +// Add the request to the RequestQueue. + queue.add(stringRequest); + + } + +} + diff --git a/app/src/main/java/com/example/applogin/PluginWait_Activity.java b/app/src/main/java/com/example/applogin/PluginWait_Activity.java new file mode 100644 index 0000000..79548ed --- /dev/null +++ b/app/src/main/java/com/example/applogin/PluginWait_Activity.java @@ -0,0 +1,37 @@ +package com.example.applogin; + +import androidx.appcompat.app.AppCompatActivity; + +import android.content.Intent; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.widget.TextView; + +import com.example.applogin.ui.login.ConfirmationActivity; + +public class PluginWait_Activity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_plugin_wait_); + + final TextView acceptCoundown = findViewById(R.id.accept_timer); + + new CountDownTimer(900000, 1000) { + + public void onTick(long millisUntilFinished) { + int secondsLeft = (int) millisUntilFinished / 1000; + int minutesLeft = secondsLeft / 60; + secondsLeft %= 60; + + acceptCoundown.setText("Time remaining: " + String.format("%d:%02d", minutesLeft,secondsLeft)); + } + + public void onFinish() { + Intent myIntent = new Intent(PluginWait_Activity.this, JoinQueueActivity.class); + startActivity(myIntent); + } + }.start(); + } +} diff --git a/app/src/main/java/com/example/applogin/WaitingActivity.java b/app/src/main/java/com/example/applogin/WaitingActivity.java new file mode 100644 index 0000000..1ccec3a --- /dev/null +++ b/app/src/main/java/com/example/applogin/WaitingActivity.java @@ -0,0 +1,45 @@ +package com.example.applogin; + +import androidx.appcompat.app.AppCompatActivity; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.Button; +import android.widget.TextView; + +import com.example.applogin.ui.login.ConfirmationActivity; + +import java.util.ArrayList; +import java.util.List; + +public class WaitingActivity extends AppCompatActivity { + + List queue; + TextView queuePosition; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_waiting); + + queuePosition = findViewById(R.id.queue_position_text); + queue = new ArrayList(); + queue.add("Test"); + + //do initial update + UpdateQueue(); + } + //Need to fill in queue data, currently assuming we are first position in queue + private void UpdateQueue() { + //get our new position in queue + int pos = queue.indexOf("Test"); + queuePosition.setText("You are number " + (pos+1) + " in the queue"); + + //check if first in queue + if (pos == 0) + { + Intent myIntent = new Intent(WaitingActivity.this, ConfirmationActivity.class); + startActivity(myIntent); + } + } +} diff --git a/app/src/main/java/com/example/applogin/data/LoginDataSource.java b/app/src/main/java/com/example/applogin/data/LoginDataSource.java new file mode 100644 index 0000000..7dd0af6 --- /dev/null +++ b/app/src/main/java/com/example/applogin/data/LoginDataSource.java @@ -0,0 +1,29 @@ +package com.example.applogin.data; + +import com.example.applogin.data.model.LoggedInUser; + +import java.io.IOException; + +/** + * Class that handles authentication w/ login credentials and retrieves user information. + */ +public class LoginDataSource { + + public Result login(String username, String password) { + + try { + // TODO: handle loggedInUser authentication + LoggedInUser fakeUser = + new LoggedInUser( + java.util.UUID.randomUUID().toString(), + "Jane Doe"); + return new Result.Success<>(fakeUser); + } catch (Exception e) { + return new Result.Error(new IOException("Error logging in", e)); + } + } + + public void logout() { + // TODO: revoke authentication + } +} diff --git a/app/src/main/java/com/example/applogin/data/LoginRepository.java b/app/src/main/java/com/example/applogin/data/LoginRepository.java new file mode 100644 index 0000000..baf7d7a --- /dev/null +++ b/app/src/main/java/com/example/applogin/data/LoginRepository.java @@ -0,0 +1,54 @@ +package com.example.applogin.data; + +import com.example.applogin.data.model.LoggedInUser; + +/** + * Class that requests authentication and user information from the remote data source and + * maintains an in-memory cache of login status and user credentials information. + */ +public class LoginRepository { + + private static volatile LoginRepository instance; + + private LoginDataSource dataSource; + + // If user credentials will be cached in local storage, it is recommended it be encrypted + // @see https://developer.android.com/training/articles/keystore + private LoggedInUser user = null; + + // private constructor : singleton access + private LoginRepository(LoginDataSource dataSource) { + this.dataSource = dataSource; + } + + public static LoginRepository getInstance(LoginDataSource dataSource) { + if (instance == null) { + instance = new LoginRepository(dataSource); + } + return instance; + } + + public boolean isLoggedIn() { + return user != null; + } + + public void logout() { + user = null; + dataSource.logout(); + } + + private void setLoggedInUser(LoggedInUser user) { + this.user = user; + // If user credentials will be cached in local storage, it is recommended it be encrypted + // @see https://developer.android.com/training/articles/keystore + } + + public Result login(String username, String password) { + // handle login + Result result = dataSource.login(username, password); + if (result instanceof Result.Success) { + setLoggedInUser(((Result.Success) result).getData()); + } + return result; + } +} diff --git a/app/src/main/java/com/example/applogin/data/Result.java b/app/src/main/java/com/example/applogin/data/Result.java new file mode 100644 index 0000000..f2f6fea --- /dev/null +++ b/app/src/main/java/com/example/applogin/data/Result.java @@ -0,0 +1,48 @@ +package com.example.applogin.data; + +/** + * A generic class that holds a result success w/ data or an error exception. + */ +public class Result { + // hide the private constructor to limit subclass types (Success, Error) + private Result() { + } + + @Override + public String toString() { + if (this instanceof Result.Success) { + Result.Success success = (Result.Success) this; + return "Success[data=" + success.getData().toString() + "]"; + } else if (this instanceof Result.Error) { + Result.Error error = (Result.Error) this; + return "Error[exception=" + error.getError().toString() + "]"; + } + return ""; + } + + // Success sub-class + public final static class Success extends Result { + private T data; + + public Success(T data) { + this.data = data; + } + + public T getData() { + return this.data; + } + } + + // Error sub-class + public final static class Error extends Result { + private Exception error; + + public Error(Exception error) { + this.error = error; + } + + public Exception getError() { + return this.error; + } + } +} diff --git a/app/src/main/java/com/example/applogin/data/model/LoggedInUser.java b/app/src/main/java/com/example/applogin/data/model/LoggedInUser.java new file mode 100644 index 0000000..aef5160 --- /dev/null +++ b/app/src/main/java/com/example/applogin/data/model/LoggedInUser.java @@ -0,0 +1,23 @@ +package com.example.applogin.data.model; + +/** + * Data class that captures user information for logged in users retrieved from LoginRepository + */ +public class LoggedInUser { + + private String userId; + private String displayName; + + public LoggedInUser(String userId, String displayName) { + this.userId = userId; + this.displayName = displayName; + } + + public String getUserId() { + return userId; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/app/src/main/java/com/example/applogin/ui/login/ConfirmationActivity.java b/app/src/main/java/com/example/applogin/ui/login/ConfirmationActivity.java new file mode 100644 index 0000000..cb90606 --- /dev/null +++ b/app/src/main/java/com/example/applogin/ui/login/ConfirmationActivity.java @@ -0,0 +1,65 @@ +package com.example.applogin.ui.login; + +import androidx.appcompat.app.AppCompatActivity; + +import android.content.Intent; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.applogin.ChargingActivity; +import com.example.applogin.JoinQueueActivity; +import com.example.applogin.R; +import com.example.applogin.WaitingActivity; + +public class ConfirmationActivity extends AppCompatActivity { + + CountDownTimer countdownTimer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_confirmation); + + final TextView acceptCountdown = findViewById(R.id.accept_timer); + + countdownTimer = new CountDownTimer(300000, 1000) { + + public void onTick(long millisUntilFinished) { + int secondsLeft = (int) millisUntilFinished / 1000; + int minutesLeft = secondsLeft / 60; + secondsLeft %= 60; + + acceptCountdown.setText("Time Remaining: " + minutesLeft + ":" + secondsLeft); + } + + public void onFinish() { + Intent myIntent = new Intent(ConfirmationActivity.this, JoinQueueActivity.class); + startActivity(myIntent); + } + }.start(); + + final Button acceptButton = findViewById(R.id.Accept_btn); + acceptButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + countdownTimer.cancel(); + Intent myIntent = new Intent(ConfirmationActivity.this, ChargingActivity.class); + startActivity(myIntent); + } + }); + + final Button declineButton = findViewById(R.id.Decline_btn); + declineButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + countdownTimer.cancel(); + Intent myIntent = new Intent(ConfirmationActivity.this, JoinQueueActivity.class); + startActivity(myIntent); + } + }); + } +} diff --git a/app/src/main/java/com/example/applogin/ui/login/LoggedInUserView.java b/app/src/main/java/com/example/applogin/ui/login/LoggedInUserView.java new file mode 100644 index 0000000..25e6ccd --- /dev/null +++ b/app/src/main/java/com/example/applogin/ui/login/LoggedInUserView.java @@ -0,0 +1,17 @@ +package com.example.applogin.ui.login; + +/** + * Class exposing authenticated user details to the UI. + */ +class LoggedInUserView { + private String displayName; + //... other data fields that may be accessible to the UI + + LoggedInUserView(String displayName) { + this.displayName = displayName; + } + + String getDisplayName() { + return displayName; + } +} diff --git a/app/src/main/java/com/example/applogin/ui/login/LoginActivity.java b/app/src/main/java/com/example/applogin/ui/login/LoginActivity.java new file mode 100644 index 0000000..8392946 --- /dev/null +++ b/app/src/main/java/com/example/applogin/ui/login/LoginActivity.java @@ -0,0 +1,139 @@ +package com.example.applogin.ui.login; + +import android.app.Activity; + +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProviders; + +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AppCompatActivity; + +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.applogin.JoinQueueActivity; +import com.example.applogin.R; +import com.example.applogin.ui.login.LoginViewModel; +import com.example.applogin.ui.login.LoginViewModelFactory; + +public class LoginActivity extends AppCompatActivity { + + private LoginViewModel loginViewModel; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + loginViewModel = ViewModelProviders.of(this, new LoginViewModelFactory()) + .get(LoginViewModel.class); + + final EditText usernameEditText = findViewById(R.id.username); + final EditText passwordEditText = findViewById(R.id.password); + final Button loginButton = findViewById(R.id.login); + final ProgressBar loadingProgressBar = findViewById(R.id.loading); + + loginViewModel.getLoginFormState().observe(this, new Observer() { + @Override + public void onChanged(@Nullable LoginFormState loginFormState) { + if (loginFormState == null) { + return; + } + loginButton.setEnabled(loginFormState.isDataValid()); + if (loginFormState.getUsernameError() != null) { + usernameEditText.setError(getString(loginFormState.getUsernameError())); + } + if (loginFormState.getPasswordError() != null) { + passwordEditText.setError(getString(loginFormState.getPasswordError())); + } + } + }); + + loginViewModel.getLoginResult().observe(this, new Observer() { + @Override + public void onChanged(@Nullable LoginResult loginResult) { + if (loginResult == null) { + return; + } + loadingProgressBar.setVisibility(View.GONE); + if (loginResult.getError() != null) { + showLoginFailed(loginResult.getError()); + } + if (loginResult.getSuccess() != null) { + updateUiWithUser(loginResult.getSuccess()); + } + setResult(Activity.RESULT_OK); + + //Complete and destroy login activity once successful + finish(); + } + }); + + TextWatcher afterTextChangedListener = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // ignore + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // ignore + } + + @Override + public void afterTextChanged(Editable s) { + loginViewModel.loginDataChanged(usernameEditText.getText().toString(), + passwordEditText.getText().toString()); + } + }; + usernameEditText.addTextChangedListener(afterTextChangedListener); + passwordEditText.addTextChangedListener(afterTextChangedListener); + passwordEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + loginViewModel.login(usernameEditText.getText().toString(), + passwordEditText.getText().toString()); + } + return false; + } + }); + + loginButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + //Toast.makeText(LoginActivity.this, "testtttt", Toast.LENGTH_SHORT).show(); + loadingProgressBar.setVisibility(View.VISIBLE); + //HTTP CALL + loginViewModel.login(usernameEditText.getText().toString(), + passwordEditText.getText().toString()); + } + }); + } + + private void updateUiWithUser(LoggedInUserView model) { + //String welcome = getString(R.string.welcome) + model.getDisplayName(); + // TODO : initiate successful logged in experience + //Toast.makeText(getApplicationContext(), welcome, Toast.LENGTH_LONG).show(); + + Intent myIntent = new Intent(LoginActivity.this, JoinQueueActivity.class); + //myIntent.putExtra("intVariableName", intValue); + startActivity(myIntent); + } + + private void showLoginFailed(@StringRes Integer errorString) { + Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_SHORT).show(); + } +} diff --git a/app/src/main/java/com/example/applogin/ui/login/LoginFormState.java b/app/src/main/java/com/example/applogin/ui/login/LoginFormState.java new file mode 100644 index 0000000..a2cdc33 --- /dev/null +++ b/app/src/main/java/com/example/applogin/ui/login/LoginFormState.java @@ -0,0 +1,40 @@ +package com.example.applogin.ui.login; + +import androidx.annotation.Nullable; + +/** + * Data validation state of the login form. + */ +class LoginFormState { + @Nullable + private Integer usernameError; + @Nullable + private Integer passwordError; + private boolean isDataValid; + + LoginFormState(@Nullable Integer usernameError, @Nullable Integer passwordError) { + this.usernameError = usernameError; + this.passwordError = passwordError; + this.isDataValid = false; + } + + LoginFormState(boolean isDataValid) { + this.usernameError = null; + this.passwordError = null; + this.isDataValid = isDataValid; + } + + @Nullable + Integer getUsernameError() { + return usernameError; + } + + @Nullable + Integer getPasswordError() { + return passwordError; + } + + boolean isDataValid() { + return isDataValid; + } +} diff --git a/app/src/main/java/com/example/applogin/ui/login/LoginResult.java b/app/src/main/java/com/example/applogin/ui/login/LoginResult.java new file mode 100644 index 0000000..a0dce17 --- /dev/null +++ b/app/src/main/java/com/example/applogin/ui/login/LoginResult.java @@ -0,0 +1,31 @@ +package com.example.applogin.ui.login; + +import androidx.annotation.Nullable; + +/** + * Authentication result : success (user details) or error message. + */ +class LoginResult { + @Nullable + private LoggedInUserView success; + @Nullable + private Integer error; + + LoginResult(@Nullable Integer error) { + this.error = error; + } + + LoginResult(@Nullable LoggedInUserView success) { + this.success = success; + } + + @Nullable + LoggedInUserView getSuccess() { + return success; + } + + @Nullable + Integer getError() { + return error; + } +} diff --git a/app/src/main/java/com/example/applogin/ui/login/LoginViewModel.java b/app/src/main/java/com/example/applogin/ui/login/LoginViewModel.java new file mode 100644 index 0000000..09d3b35 --- /dev/null +++ b/app/src/main/java/com/example/applogin/ui/login/LoginViewModel.java @@ -0,0 +1,70 @@ +package com.example.applogin.ui.login; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import android.util.Patterns; + +import com.example.applogin.data.LoginRepository; +import com.example.applogin.data.Result; +import com.example.applogin.data.model.LoggedInUser; +import com.example.applogin.R; + +public class LoginViewModel extends ViewModel { + + private MutableLiveData loginFormState = new MutableLiveData<>(); + private MutableLiveData loginResult = new MutableLiveData<>(); + private LoginRepository loginRepository; + + LoginViewModel(LoginRepository loginRepository) { + this.loginRepository = loginRepository; + } + + LiveData getLoginFormState() { + return loginFormState; + } + + LiveData getLoginResult() { + return loginResult; + } + + public void login(String username, String password) { + // can be launched in a separate asynchronous job + Result result = loginRepository.login(username, password); + + if (result instanceof Result.Success) { + LoggedInUser data = ((Result.Success) result).getData(); + loginResult.setValue(new LoginResult(new LoggedInUserView(data.getDisplayName()))); + } else { + loginResult.setValue(new LoginResult(R.string.login_failed)); + } + } + + public void loginDataChanged(String username, String password) { + if (!isUserNameValid(username)) { + loginFormState.setValue(new LoginFormState(R.string.invalid_username, null)); + } else if (!isPasswordValid(password)) { + loginFormState.setValue(new LoginFormState(null, R.string.invalid_password)); + } else { + loginFormState.setValue(new LoginFormState(true)); + } + } + + // A placeholder username validation check + private boolean isUserNameValid(String username) { + if (username == null) { + return false; + } + if (username.contains("@")) { + return Patterns.EMAIL_ADDRESS.matcher(username).matches(); + } else { + return !username.trim().isEmpty(); + } + } + + // A placeholder password validation check + private boolean isPasswordValid(String password) { + return password != null && password.trim().length() > 5; + } +} diff --git a/app/src/main/java/com/example/applogin/ui/login/LoginViewModelFactory.java b/app/src/main/java/com/example/applogin/ui/login/LoginViewModelFactory.java new file mode 100644 index 0000000..f92f17c --- /dev/null +++ b/app/src/main/java/com/example/applogin/ui/login/LoginViewModelFactory.java @@ -0,0 +1,26 @@ +package com.example.applogin.ui.login; + +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; +import androidx.annotation.NonNull; + +import com.example.applogin.data.LoginDataSource; +import com.example.applogin.data.LoginRepository; + +/** + * ViewModel provider factory to instantiate LoginViewModel. + * Required given LoginViewModel has a non-empty constructor + */ +public class LoginViewModelFactory implements ViewModelProvider.Factory { + + @NonNull + @Override + @SuppressWarnings("unchecked") + public T create(@NonNull Class modelClass) { + if (modelClass.isAssignableFrom(LoginViewModel.class)) { + return (T) new LoginViewModel(LoginRepository.getInstance(new LoginDataSource())); + } else { + throw new IllegalArgumentException("Unknown ViewModel class"); + } + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/buttonbackground.xml b/app/src/main/res/drawable/buttonbackground.xml new file mode 100644 index 0000000..8042037 --- /dev/null +++ b/app/src/main/res/drawable/buttonbackground.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gradientbackground.xml b/app/src/main/res/drawable/gradientbackground.xml new file mode 100644 index 0000000..2e13254 --- /dev/null +++ b/app/src/main/res/drawable/gradientbackground.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_charging.xml b/app/src/main/res/layout/activity_charging.xml new file mode 100644 index 0000000..7a597c2 --- /dev/null +++ b/app/src/main/res/layout/activity_charging.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_confirmation.xml b/app/src/main/res/layout/activity_confirmation.xml new file mode 100644 index 0000000..799031f --- /dev/null +++ b/app/src/main/res/layout/activity_confirmation.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_join_queue.xml b/app/src/main/res/layout/activity_join_queue.xml new file mode 100644 index 0000000..0e97dbd --- /dev/null +++ b/app/src/main/res/layout/activity_join_queue.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..9040150 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,78 @@ + + + + + + + +