From 3b66ed2721077fdfa2cebd2eb630872a9cc11254 Mon Sep 17 00:00:00 2001 From: Adam Hansen Date: Wed, 9 May 2018 10:55:31 +0200 Subject: [PATCH 01/21] Implemented login --- .idea/assetWizardSettings.xml | 5 +- .idea/caches/build_file_checksums.ser | Bin 536 -> 536 bytes app/build.gradle | 8 +- app/src/main/AndroidManifest.xml | 13 +- .../IFirebaseAuthenticationCallback.java | 13 ++ .../DAL/FirebaseAuthenticate.java | 45 +++++ .../DAL/IFirebaseAuthenticate.java | 22 ++ .../rehabilitation/GUI/ContactActivity.java | 4 + .../rehabilitation/GUI/LoginActivity.java | 191 ++++++++++++++++++ .../GUI/Model/FirebaseClientModel.java | 28 ++- .../rehabilitation/GUI/ProfileActivity.java | 4 + app/src/main/res/drawable-hdpi/logout.png | Bin 0 -> 714 bytes app/src/main/res/drawable-mdpi/logout.png | Bin 0 -> 326 bytes app/src/main/res/drawable-xhdpi/logout.png | Bin 0 -> 989 bytes app/src/main/res/drawable-xxhdpi/logout.png | Bin 0 -> 1661 bytes app/src/main/res/layout/activity_login.xml | 78 +++++++ app/src/main/res/menu/activity_contact.xml | 5 + app/src/main/res/menu/activity_profile.xml | 5 + app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/strings.xml | 15 ++ 20 files changed, 428 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/dk/adamino/rehabilitation/Callbacks/IFirebaseAuthenticationCallback.java create mode 100644 app/src/main/java/dk/adamino/rehabilitation/DAL/FirebaseAuthenticate.java create mode 100644 app/src/main/java/dk/adamino/rehabilitation/DAL/IFirebaseAuthenticate.java create mode 100644 app/src/main/java/dk/adamino/rehabilitation/GUI/LoginActivity.java create mode 100644 app/src/main/res/drawable-hdpi/logout.png create mode 100644 app/src/main/res/drawable-mdpi/logout.png create mode 100644 app/src/main/res/drawable-xhdpi/logout.png create mode 100644 app/src/main/res/drawable-xxhdpi/logout.png create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/values/dimens.xml diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml index 265ca01..9858654 100644 --- a/.idea/assetWizardSettings.xml +++ b/.idea/assetWizardSettings.xml @@ -35,8 +35,9 @@ diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index b5a59a1c8a43bcbd9d5c3a28be3207b84f437740..2f0ceb9ea37ea2fa914446181bc5f049d4c62e7e 100644 GIT binary patch delta 35 rcmbQiGJ|Ep3>N1rj_W7RDHm8DqkJ)~t0P!Fqxq+0V{7(gWyY@n3nULq delta 35 tcmV+;0NnqW1egSnm;@Vn%F&UWcMyDEB5rB*=&}#q+?#+W#U_&@0reP;5KRC8 diff --git a/app/build.gradle b/app/build.gradle index 1d73085..87f9e02 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,10 +22,12 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.1.0' - implementation 'com.google.firebase:firebase-firestore:15.0.0' + implementation 'com.google.firebase:firebase-firestore:16.0.0' + implementation 'com.google.firebase:firebase-auth:15.0.0' + implementation 'com.android.support:design:26.1.0' testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.1' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation 'com.android.support:cardview-v7:26.1.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9db1b8d..ad2ea90 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,11 @@ + + + + + + + + + - - \ No newline at end of file diff --git a/app/src/main/java/dk/adamino/rehabilitation/Callbacks/IFirebaseAuthenticationCallback.java b/app/src/main/java/dk/adamino/rehabilitation/Callbacks/IFirebaseAuthenticationCallback.java new file mode 100644 index 0000000..68384b3 --- /dev/null +++ b/app/src/main/java/dk/adamino/rehabilitation/Callbacks/IFirebaseAuthenticationCallback.java @@ -0,0 +1,13 @@ +package dk.adamino.rehabilitation.Callbacks; + +/** + * Created by Adamino. + */ +public interface IFirebaseAuthenticationCallback { + + /*** + * Handle client uid from login + * @param clientUid + */ + void onClientLoggedIn(String clientUid); +} diff --git a/app/src/main/java/dk/adamino/rehabilitation/DAL/FirebaseAuthenticate.java b/app/src/main/java/dk/adamino/rehabilitation/DAL/FirebaseAuthenticate.java new file mode 100644 index 0000000..15348ae --- /dev/null +++ b/app/src/main/java/dk/adamino/rehabilitation/DAL/FirebaseAuthenticate.java @@ -0,0 +1,45 @@ +package dk.adamino.rehabilitation.DAL; + +import android.support.annotation.NonNull; +import android.util.Log; + +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.auth.AuthResult; +import com.google.firebase.auth.FirebaseAuth; + +import dk.adamino.rehabilitation.Callbacks.IFirebaseAuthenticationCallback; + +/** + * Created by Adamino. + */ +public class FirebaseAuthenticate implements IFirebaseAuthenticate { + + private static final String TAG = "FirebaseAuth"; + + private FirebaseAuth mAuth; + + public FirebaseAuthenticate() { + mAuth = FirebaseAuth.getInstance(); + } + + @Override + public void signInWithEmailAndPassword(String email, String password, final IFirebaseAuthenticationCallback callback) { + mAuth.signInWithEmailAndPassword(email, password) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + callback.onClientLoggedIn(mAuth.getCurrentUser().getUid()); + } else { + Log.e(TAG, "Couldn't login"); + } + } + }); + } + + @Override + public void signout() { + mAuth.signOut(); + } +} diff --git a/app/src/main/java/dk/adamino/rehabilitation/DAL/IFirebaseAuthenticate.java b/app/src/main/java/dk/adamino/rehabilitation/DAL/IFirebaseAuthenticate.java new file mode 100644 index 0000000..db58d38 --- /dev/null +++ b/app/src/main/java/dk/adamino/rehabilitation/DAL/IFirebaseAuthenticate.java @@ -0,0 +1,22 @@ +package dk.adamino.rehabilitation.DAL; + +import dk.adamino.rehabilitation.Callbacks.IFirebaseAuthenticationCallback; + +/** + * Created by Adamino. + */ +public interface IFirebaseAuthenticate { + + /*** + * Sign in with provided email and password + * @param email + * @param password + * @param callback + */ + void signInWithEmailAndPassword(String email, String password, IFirebaseAuthenticationCallback callback); + + /** + * Signout client + */ + void signout(); +} diff --git a/app/src/main/java/dk/adamino/rehabilitation/GUI/ContactActivity.java b/app/src/main/java/dk/adamino/rehabilitation/GUI/ContactActivity.java index 8ed8acd..5fe65de 100644 --- a/app/src/main/java/dk/adamino/rehabilitation/GUI/ContactActivity.java +++ b/app/src/main/java/dk/adamino/rehabilitation/GUI/ContactActivity.java @@ -34,6 +34,10 @@ public boolean onOptionsItemSelected(MenuItem item) { Intent contactIntent = ProfileActivity.newIntent(this); startActivity(contactIntent); return true; + case R.id.signout: + Intent logoutIntent = LoginActivity.newIntent(this); + startActivity(logoutIntent); + return true; default: return super.onOptionsItemSelected(item); } diff --git a/app/src/main/java/dk/adamino/rehabilitation/GUI/LoginActivity.java b/app/src/main/java/dk/adamino/rehabilitation/GUI/LoginActivity.java new file mode 100644 index 0000000..a4fb99a --- /dev/null +++ b/app/src/main/java/dk/adamino/rehabilitation/GUI/LoginActivity.java @@ -0,0 +1,191 @@ +package dk.adamino.rehabilitation.GUI; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.text.TextUtils; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.inputmethod.EditorInfo; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import dk.adamino.rehabilitation.Callbacks.IFirebaseAuthenticationCallback; +import dk.adamino.rehabilitation.GUI.Model.FirebaseClientModel; +import dk.adamino.rehabilitation.R; + +/** + * A login screen that offers login via email/password. + */ +public class LoginActivity extends AppCompatActivity implements IFirebaseAuthenticationCallback { + /** + * Keep track of the login task to ensure we can cancel it if requested. + */ + private FirebaseClientModel mFirebaseClientModel; + + // UI references. + private AutoCompleteTextView mEmailView; + private EditText mPasswordView; + private View mProgressView; + private View mLoginFormView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + setupViews(); + + // Create reference to model + mFirebaseClientModel = FirebaseClientModel.getInstance(); + } + + private void setupViews() { + // Set up the login form. + mEmailView = findViewById(R.id.email); + + mPasswordView = findViewById(R.id.password); + mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { + if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) { + attemptLogin(); + return true; + } + return false; + } + }); + + Button mEmailSignInButton = findViewById(R.id.email_sign_in_button); + mEmailSignInButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + attemptLogin(); + } + }); + + mLoginFormView = findViewById(R.id.login_form); + mProgressView = findViewById(R.id.login_progress); + } + + + /** + * Attempts to sign in or register the account specified by the login form. + * If there are form errors (invalid email, missing fields, etc.), the + * errors are presented and no actual login attempt is made. + */ + private void attemptLogin() { + + // Reset errors. + mEmailView.setError(null); + mPasswordView.setError(null); + + // Store values at the time of the login attempt. + String email = mEmailView.getText().toString(); + String password = mPasswordView.getText().toString(); + + boolean cancel = false; + View focusView = null; + + // Check for a valid password, if the user entered one. + if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) { + mPasswordView.setError(getString(R.string.error_invalid_password)); + focusView = mPasswordView; + cancel = true; + } + + // Check for a valid email address. + if (TextUtils.isEmpty(email)) { + mEmailView.setError(getString(R.string.error_field_required)); + focusView = mEmailView; + cancel = true; + } else if (!isEmailValid(email)) { + mEmailView.setError(getString(R.string.error_invalid_email)); + focusView = mEmailView; + cancel = true; + } + + if (cancel) { + // There was an error; don't attempt login and focus the first + // form field with an error. + focusView.requestFocus(); + } else { + // Show a progress spinner, and kick off a background task to + showProgress(true); + // perform the user login attempt. + mFirebaseClientModel.loginWithEmailAndPassword(email, password,this); + } + } + + @Override + public void onClientLoggedIn(String clientUid) { + showProgress(false); + mFirebaseClientModel.setCurrentClientUid(clientUid); + Intent intent = ProfileActivity.newIntent(this); + startActivity(intent); + } + + private boolean isEmailValid(String email) { + //TODO: Replace this with your own logic + return email.contains("@"); + } + + private boolean isPasswordValid(String password) { + //TODO: Replace this with your own logic + return password.length() > 4; + } + + /** + * Shows the progress UI and hides the login form. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) + private void showProgress(final boolean show) { + // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow + // for very easy animations. If available, use these APIs to fade-in + // the progress spinner. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { + int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime); + + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + mLoginFormView.animate().setDuration(shortAnimTime).alpha( + show ? 0 : 1).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + } + }); + + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + mProgressView.animate().setDuration(shortAnimTime).alpha( + show ? 1 : 0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + } + }); + } else { + // The ViewPropertyAnimator APIs are not available, so simply show + // and hide the relevant UI components. + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + } + } + + /** + * Create Intent to navigate to this activity + * @param context + * @return + */ + public static Intent newIntent(Context context) { + Intent intent = new Intent(context, ProfileActivity.class); + return intent; + } +} + diff --git a/app/src/main/java/dk/adamino/rehabilitation/GUI/Model/FirebaseClientModel.java b/app/src/main/java/dk/adamino/rehabilitation/GUI/Model/FirebaseClientModel.java index 6b8319f..3ce96bd 100644 --- a/app/src/main/java/dk/adamino/rehabilitation/GUI/Model/FirebaseClientModel.java +++ b/app/src/main/java/dk/adamino/rehabilitation/GUI/Model/FirebaseClientModel.java @@ -1,8 +1,11 @@ package dk.adamino.rehabilitation.GUI.Model; +import dk.adamino.rehabilitation.Callbacks.IFirebaseAuthenticationCallback; import dk.adamino.rehabilitation.Callbacks.IFirestoreCallback; +import dk.adamino.rehabilitation.DAL.FirebaseAuthenticate; import dk.adamino.rehabilitation.DAL.FirestoreDAO; import dk.adamino.rehabilitation.DAL.IFirestore; +import dk.adamino.rehabilitation.DAL.IFirebaseAuthenticate; /** * Created by Adamino. @@ -11,11 +14,13 @@ public class FirebaseClientModel { private static FirebaseClientModel instance = null; + private IFirebaseAuthenticate mIFirebaseAuthenticate; private IFirestore mFirestoreDAO; + private String mCurrentClientUid; - protected FirebaseClientModel() { + private FirebaseClientModel() { mFirestoreDAO = new FirestoreDAO(); - + mIFirebaseAuthenticate = new FirebaseAuthenticate(); } public static FirebaseClientModel getInstance() { @@ -25,13 +30,24 @@ public static FirebaseClientModel getInstance() { return instance; } + public void setCurrentClientUid(String currentClientUid) { + mCurrentClientUid = currentClientUid; + } + /** - * Get currently logged in client + * Login user + * @param email + * @param password + */ + public void loginWithEmailAndPassword(String email, String password, IFirebaseAuthenticationCallback callback) { + mIFirebaseAuthenticate.signInWithEmailAndPassword(email, password, callback); + } + + /*** + * Load currently loggedin client * @param response */ public void loadLoggedInClientAsync(IFirestoreCallback response) { - // TODO ALH: Replace! - String adamUID = "7fdjYuWZC1ZQD4npgb1YG3kfNK02"; - mFirestoreDAO.getClientByIdAsync(adamUID, response); + mFirestoreDAO.getClientByIdAsync(mCurrentClientUid, response); } } diff --git a/app/src/main/java/dk/adamino/rehabilitation/GUI/ProfileActivity.java b/app/src/main/java/dk/adamino/rehabilitation/GUI/ProfileActivity.java index fef2896..84b9e61 100644 --- a/app/src/main/java/dk/adamino/rehabilitation/GUI/ProfileActivity.java +++ b/app/src/main/java/dk/adamino/rehabilitation/GUI/ProfileActivity.java @@ -74,6 +74,10 @@ public boolean onOptionsItemSelected(MenuItem item) { Intent contactIntent = ContactActivity.newIntent(this); startActivity(contactIntent); return true; + case R.id.signout: + Intent logoutIntent = LoginActivity.newIntent(this); + startActivity(logoutIntent); + return true; default: return super.onOptionsItemSelected(item); } diff --git a/app/src/main/res/drawable-hdpi/logout.png b/app/src/main/res/drawable-hdpi/logout.png new file mode 100644 index 0000000000000000000000000000000000000000..347998d7ce9d8f74832aa4b9ef0c077e178a979f GIT binary patch literal 714 zcmV;*0yX`KP)?>OIihK8zEwW?KZAX}%?VSs}c_^0p_ zG{G3SMvTzk>x9}L(Tp7jr{I9TS5Twx%KchM0-P3D()F}5uw zV4N3{fcVIJO9Fxu@EVMI249mBu+9sW5G&aMm2>b8+;$9}NeNh!4o#runSkIASSPL9 zf@cN^btN4tA?_$46FcC7C3u~Lv^&x0L<@8E83Dv^vVTjC>isEG*bk3*cTuw`5LCRhPymIUs_1|RE2 wxr3j-0bXhzGX)c|!3RmGYE`RR)%w!@1Bhe%hpQS8-}52nje~Q7tEjlafb%b)t~j`(*SM>MF^NZT+pKm;5JZ&5LH0S zCV(5u4q#HU19B!4Df(iR7Qj@@*2pgdM$m__%m8+M3b9`VEZ`)#3iqrbNgcp;%cJ08 z2vB5wf(b<3{scqe4POPQ6pQen-=o?Dlp#oQ7M%4-=QbNrg7PrB*{lKb)9~&EPzTgq z2!J7A;z|XwjlY%dlRrmjZ?X z4L}$0%YmPO2Y_k79>6$YH(&yfy9`Vizv*)+$E(Kub^>aE1dnxu`Q4u*M*uYW;hX?y ztO4!;PWl|fz}Bn)=N+ev`)sF>S7UPopg~Z8OA(8J7CP52d3D|ihXT9-E?NODJONz5 zG1g07oj++QfTXVfr2rRR0A_KVQ1S^*D?Qd2b!wGC0X9D3g0?Myysdy8)OCeZ0Vz`e zG~NOZ6Uirz0Fc&|GAh8tQzH4AV+xQKRc-~mVh(`95d!&ojskKQRW1eSn{)*NaGpTE zZf%2O)E$K@6Vwh#DBvuCy!t$X(_j+MCvB%30WcksQh<;Ax)Ujm03-z{AcXv(kX&LA z72x~lJAj9PO_W56Qvvk?6hM*v9A5Vbu+^7HaVbDMCZT|HfpflNT>A`ngsfbGMFFN` zQVIxJ@aC-Rl}~$JFtjP(rz`gc(sLncy+D-nDenK6ae!c^MIE*SjiY?6{mo? zqp&F;6;wbwFulF(7~>t_s7;ugOgbXOb-2&oV&1>6U0_K`1BS$xrO z?3nf2OaOUxJP6oGAzvm1yf+#ntw06*lipIvm#r~!4X_2}YkuD(l2?*Hh1ftLU&gZd zTE;Fw`RDDbu$Me{S-c^X2Q-PYcLiYJ8zU|SWX<`?Hb$Dn1+TJ=kv4I)uWVzaN!;q} zH$*Da7-1PSfN3f65{;28$5fa0I^a_uQ-GbkL}SF~*a()aUIzr=j1+l^ z#t79hy;v?20&rT2yhLLp)UgI8I!m4ah>&LqK(uJlqD6}qt+MtT#4R-Q8uZ#c00000 LNkvXXu0mjf(X^;- literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/logout.png b/app/src/main/res/drawable-xxhdpi/logout.png new file mode 100644 index 0000000000000000000000000000000000000000..bb56492c3a7f84b329a903b7685276d9dbedf65a GIT binary patch literal 1661 zcmV-@27>vCP)a6zQY;ivo7P6K2DCMolU^*o_@v-NpA@`-Z9xzp6hx5< z6)(-fR)}ChK}1op)#`sr*T~wi7-sgKJ!|&Nm`-`u`4n>I;ENJvOX zNJvOXNJvOXNJvOXSYmyBy$9%Ec@l6G@Sg*20X_g60!#yD0DG`JjpZ3q-fbv91b6_; z4i=V zNPB=w%de^FKLf>?FLN&+S3NT38zTmviu?r50&2~C2|*BNkab;KlJ5%W|72`F$K@FL)@kg}uo zU|oV(bwo)3ZIuBfs0_aVUIE-2u;ouUNKmboU;J&9ae%z&;Rp#R@CV>EzW?Uu`RA9lEe1im?=ML&oB?w$3fiBItJLAHTmR)Xe&9KIsq{^@2V0UBLVvS zI0p%;-SRn8&8j+LB!ISCepl$2zfX@Nj7&EF) zObP1Yhe|C$FNBaCz*cS8gO@#XRZP6&yIY9KK2-A!XYIn z#DF`cJ@-ePme%TBFgYNAi>Pg47apI?z$&4$Jd@SB=b}clBcR z0AFKPlM)nSoO5TU&lAV^%jq3G%h4Y<2K4EtZiV+nCj58nc>^AlmYu!71e` z{xp?WN%^YdA<#=X7_;d7IJE=~EG>Vbq5716=~ckyRK~1sL&hxcb%15SU1+%8H%a10 z&Y0yRLAoRi8m{+s>;=hywCI?@l-&El%OS$-+ zuN?>Bvshm%jaep~3ACR-RZ-7PzzH#Cl}ixkFlA>+9e<(JW@pUGL4pP$PO?fCV!r_% zb*v+r1krt$*$gCzeT;|)GWxTB2JCYsKZ-5Iu<6yL1gROvw}4;EdO!g3I|d25j$h`> zhx`QI&@))aoq%r~CCJyFvQ4u*?x;)ts2Q`?wB`Om>=9)WA|_xO-MYR0UK z+6q_I`$LHYamX(u=reRjb=k1Oy=FNN;E-R} zm}Mn@)QnlT@LGYQzFl}IVv``>mcMbxPhiZtf>#>c4fw?JJIkJy#De_C0MFt9&OQ$L zqiW1jJ>j28^-8$Wfm6ZHUjah>l18$ugv%JQYHX5@|GWT}s+?eH5N85~9OPq~0 zg9PPEzQvezD1&JzXG_`T#w_Oqr!Z#i&R`|X*-|!15Nq2yCiz`_iSH9jkoSJR)&t(k zntaU_LA7!zaRNApDKr7|+yk2w#;gwBTJ=#v3F4e&4asd;n?szIHNFn83{)x!A0|k^ zBIg(pTm6AhN0ljzS^EC^9NNEgxG`&zySK9uj9Iq37nGs(Q|*5KrZ(fUAU~Bct6#C* z`!L{ri=!a~o^K8L24hyg#-^9L?e=Z-+J{WQiCoB!ITiK3X$pP#=PDe}wGU)+AU~Bc z%lFFQE%>G9pO^_a-Wu{P#w_kPQ+l|iyylsJ=W-!Gi7_kCmm_-kZ}TPJ11Ot@WHe3BqSsxBqSsxBqSsxBqSsxHgwzuDe>mWnZDzb00000NkvXX Hu0mjfhw&5N literal 0 HcmV?d00001 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..20a946b --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + +