018c - Email/Password Login Success + FBRef


Replace delayed jump with real FirebaseAuth login and keep Google button for next lesson

This lesson replaces the fake delayed navigation from 018a with a real Firebase Email/Password login flow.

Result at the end:

  1. LoginActivity stays on screen until the user logs in successfully.
  2. MenuActivity opens only after signInWithEmailAndPassword(...) succeeds.
  3. A minimal FBRef helper exists (ready to expand later, and convenient for 018d Google login).
  4. The Google button still exists but remains a stub for the next lesson.

Prerequisites

  • 018a.LoginActivityFromGui.md completed (login UI shell + launcher setup)
  • 018b.FirebaseProjectRtdbAuthSetup.md completed or equivalent shared-backend setup (Firebase SDKs added, package registered in Firebase, matching google-services.json)
  • At least one Email/Password user exists in Firebase Authentication

This lesson does not add registration UI yet.
If you do not already have a test user, create one in Firebase Console:

  • Authentication -> Users -> Add user

No SHA1 is needed for this lesson (SHA1 becomes relevant in 018d for Google Sign-In).

Shared / teacher backend path (common in class)

If students skip Firebase project creation and reuse a teacher/group Firebase project, 018c can still work well.

Required checklist (before testing login):

  1. The student’s app package (applicationId) is registered as an Android app in that Firebase project.
  2. The student downloads the matching google-services.json for their package (not just any JSON from that project).
  3. Email/Password provider is enabled in Firebase Authentication.
  4. A test user exists in that Firebase project (Authentication -> Users).

If students copy a google-services.json from another app package, Gradle may fail with:

  • No matching client found for package name ...

Fix summary:

  • Add the student’s package as another Android app in the same Firebase project
  • Download a new matching google-services.json

Detailed explanation and screenshots flow are in 018b Step 3.1 / 3.2.

Teacher checklist for 018c on a shared backend

For a teacher-managed Firebase project used by multiple students:

  1. Register each student app package (or each classroom package variant) as an Android app in the same Firebase project.
  2. Give each student the correct google-services.json for their package.
  3. Enable Email/Password sign-in provider once (project-level setting).
  4. Create test users in Authentication -> Users (or let authorized students create them in console).

Optional Step 0 - Manual Gradle Firebase wiring (only if 018b wizard stages were skipped)

Skip this section if the Firebase Assistant flow in 018b already completed successfully for this app.

Use this only when students:

  • bypassed the 018b Android Studio Assistant setup,
  • copied/received google-services.json manually, and/or
  • are missing Firebase Auth / RTDB dependencies in Gradle.

Do not duplicate plugin/dependency lines if the Assistant already added them.

Manual Gradle edits fix missing SDK/plugin wiring, but they do not fix a wrong google-services.json package. If build fails with No matching client found for package name ..., see the shared-backend notes above and 018b Step 3.1 / 3.2.

0.1 Top-level build.gradle (project)

Add the Google Services plugin to the top-level plugins block:

 plugins {
     id 'com.android.application' version '8.9.1' apply false
+    id 'com.google.gms.google-services' version '4.4.1' apply false
 }

0.2 app/build.gradle (module)

Add Firebase dependencies and (optionally) apply the Google Services plugin.

Example fallback diff:

 plugins {
     id 'com.android.application'
 }
 
+// Fallback for students who bypassed 018b wizard setup.
+// This keeps Gradle sync/build calmer before app/google-services.json is placed.
+if (file("google-services.json").exists()) {
+    apply plugin: 'com.google.gms.google-services'
+}
 
 dependencies {
+    implementation platform('com.google.firebase:firebase-bom:34.1.0')
+    implementation 'com.google.firebase:firebase-auth'
+    implementation 'com.google.firebase:firebase-database'
+
     implementation 'androidx.appcompat:appcompat:1.7.0'
     ...
 }

If the student already has a correct app/google-services.json, applying the Google Services plugin directly is also fine. The conditional if (file(...)) form is a classroom-friendly fallback to avoid confusion before the JSON is copied.


Step 1 - Add a minimal FBRef helper (new file)

Create:

  • app/src/main/java/com/example/tictacmenu/services/FBRef.java

Minimal version for this lesson:

package com.example.tictacmenu.services;

import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;

public final class FBRef {

    public static final FirebaseAuth refAuth = FirebaseAuth.getInstance();
    public static final FirebaseDatabase FBDB = FirebaseDatabase.getInstance();
    public static final DatabaseReference refUsers = FBDB.getReference("Users");

    public static String uid;
    public static DatabaseReference refUser;

    private FBRef() { }

    public static void getUser(FirebaseUser fbuser) {
        if (fbuser == null) {
            uid = null;
            refUser = null;
            return;
        }

        uid = fbuser.getUid();
        refUser = refUsers.child(uid);
    }
}

This is intentionally small. We will expand FBRef later with more app-specific RTDB references.


Step 2 - Replace delayed shell logic in LoginActivity

Edit:

  • app/src/main/java/com/example/tictacmenu/activities/LoginActivity.java

Key changes:

  1. Remove the Handler / delayed auto-jump.
  2. Read email + password from the screen.
  3. Call FBRef.refAuth.signInWithEmailAndPassword(...).
  4. Navigate to MenuActivity only on success.
  5. Keep onGoogleLoginClick(...) as a stub for 018d.

Below is a diff against the current 018a shell file (recommended for teaching the transition):

 package com.example.tictacmenu.activities;
 
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
+import android.text.TextUtils;
 import android.view.View;
+import android.widget.Button;
 import android.widget.CheckBox;
+import android.widget.EditText;
 import android.widget.Toast;
 
 import androidx.activity.EdgeToEdge;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.core.graphics.Insets;
 import androidx.core.view.ViewCompat;
 import androidx.core.view.WindowInsetsCompat;
 
+import com.example.tictacmenu.services.FBRef;
 import com.example.tictacmenu.R;
 import com.example.tictacmenu.shell.MenuActivity;
+import com.google.firebase.auth.FirebaseUser;
 
 public class LoginActivity extends AppCompatActivity {
 
-    private static final long LOGIN_SHELL_DELAY_MS = 4000;
     private static final String PREFS_NAME = "PREFS_NAME";
     private static final String KEY_STAY_CONNECT = "stayConnect";
 
     private SharedPreferences settings;
     private CheckBox cBstayconnect;
-    private final Handler handler = new Handler(Looper.getMainLooper());
+    private EditText eTemail;
+    private EditText eTpass;
+    private Button btnLogin;
+    private boolean loginInProgress;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         EdgeToEdge.enable(this);
         setContentView(R.layout.activity_login);
         ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
             Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
             v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
             return insets;
         });
 
         settings = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
-        cBstayconnect = findViewById(R.id.cBstayconnect);
+        initViews();
 
         cBstayconnect.setChecked(settings.getBoolean(KEY_STAY_CONNECT, false));
         cBstayconnect.setOnCheckedChangeListener((buttonView, isChecked) ->
                 settings.edit().putBoolean(KEY_STAY_CONNECT, isChecked).apply()
         );
 
-        handler.postDelayed(() -> {
-            settings.edit().putBoolean(KEY_STAY_CONNECT, cBstayconnect.isChecked()).apply();
-            startActivity(new Intent(LoginActivity.this, MenuActivity.class));
-            finish();
-        }, LOGIN_SHELL_DELAY_MS);
+        setLoginInProgress(false);
+    }
+
+    private void initViews() {
+        cBstayconnect = findViewById(R.id.cBstayconnect);
+        eTemail = findViewById(R.id.eTemail);
+        eTpass = findViewById(R.id.eTpass);
+        btnLogin = findViewById(R.id.btn);
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        boolean stayConnected = settings.getBoolean(KEY_STAY_CONNECT, false);
+        FirebaseUser currentUser = FBRef.refAuth.getCurrentUser();
+        if (currentUser != null && stayConnected) {
+            FBRef.getUser(currentUser);
+            openMenu();
+        }
     }
 
     public void onLoginClick(View view) {
-        Toast.makeText(this, "Email/password auth is added in a later lesson", Toast.LENGTH_SHORT).show();
+        if (loginInProgress) return;
+
+        String email = eTemail.getText().toString().trim();
+        String password = eTpass.getText().toString();
+
+        if (TextUtils.isEmpty(email)) {
+            eTemail.setError("Enter e-mail");
+            eTemail.requestFocus();
+            return;
+        }
+
+        if (TextUtils.isEmpty(password)) {
+            eTpass.setError("Enter password");
+            eTpass.requestFocus();
+            return;
+        }
+
+        setLoginInProgress(true);
+
+        FBRef.refAuth.signInWithEmailAndPassword(email, password)
+                .addOnCompleteListener(this, task -> {
+                    setLoginInProgress(false);
+                    if (task.isSuccessful()) {
+                        FirebaseUser user = FBRef.refAuth.getCurrentUser();
+                        if (user == null) {
+                            Toast.makeText(this, "Login succeeded but no user was returned", Toast.LENGTH_SHORT).show();
+                            return;
+                        }
+                        FBRef.getUser(user);
+                        settings.edit().putBoolean(KEY_STAY_CONNECT, cBstayconnect.isChecked()).apply();
+                        Toast.makeText(this, "Login success", Toast.LENGTH_SHORT).show();
+                        openMenu();
+                        return;
+                    }
+                    String errorMessage = (task.getException() != null)
+                            ? task.getException().getLocalizedMessage()
+                            : "Login failed";
+                    Toast.makeText(this, "Login failed: " + errorMessage, Toast.LENGTH_LONG).show();
+                });
+    }
+
+    private void setLoginInProgress(boolean inProgress) {
+        loginInProgress = inProgress;
+        btnLogin.setEnabled(!inProgress);
+        btnLogin.setText(inProgress ? "Logging in..." : "Login");
+    }
+
+    private void openMenu() {
+        startActivity(new Intent(this, MenuActivity.class));
+        finish();
     }
 
     public void onGoogleLoginClick(View view) {
-        Toast.makeText(this, "Google sign-in is an advanced later phase", Toast.LENGTH_SHORT).show();
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        handler.removeCallbacksAndMessages(null);
+        Toast.makeText(this, "Google sign-in is added in 018d (requires SHA1)", Toast.LENGTH_SHORT).show();
     }
 }

If you prefer copy/paste over patching line-by-line, use the full replacement below:

package com.example.tictacmenu.activities;

import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

import com.example.tictacmenu.services.FBRef;
import com.example.tictacmenu.R;
import com.example.tictacmenu.shell.MenuActivity;
import com.google.firebase.auth.FirebaseUser;

public class LoginActivity extends AppCompatActivity {

    private static final String PREFS_NAME = "PREFS_NAME";
    private static final String KEY_STAY_CONNECT = "stayConnect";

    private SharedPreferences settings;
    private CheckBox cBstayconnect;
    private EditText eTemail;
    private EditText eTpass;
    private Button btnLogin;
    private boolean loginInProgress;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_login);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        settings = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
        initViews();

        cBstayconnect.setChecked(settings.getBoolean(KEY_STAY_CONNECT, false));
        cBstayconnect.setOnCheckedChangeListener((buttonView, isChecked) ->
                settings.edit().putBoolean(KEY_STAY_CONNECT, isChecked).apply()
        );

        setLoginInProgress(false);
    }

    private void initViews() {
        cBstayconnect = findViewById(R.id.cBstayconnect);
        eTemail = findViewById(R.id.eTemail);
        eTpass = findViewById(R.id.eTpass);
        btnLogin = findViewById(R.id.btn);
    }

    @Override
    protected void onStart() {
        super.onStart();

        boolean stayConnected = settings.getBoolean(KEY_STAY_CONNECT, false);
        FirebaseUser currentUser = FBRef.refAuth.getCurrentUser();

        if (currentUser != null && stayConnected) {
            FBRef.getUser(currentUser);
            openMenu();
        }
    }

    public void onLoginClick(View view) {
        if (loginInProgress) return;

        String email = eTemail.getText().toString().trim();
        String password = eTpass.getText().toString();

        if (TextUtils.isEmpty(email)) {
            eTemail.setError("Enter e-mail");
            eTemail.requestFocus();
            return;
        }

        if (TextUtils.isEmpty(password)) {
            eTpass.setError("Enter password");
            eTpass.requestFocus();
            return;
        }

        setLoginInProgress(true);

        FBRef.refAuth.signInWithEmailAndPassword(email, password)
                .addOnCompleteListener(this, task -> {
                    setLoginInProgress(false);

                    if (task.isSuccessful()) {
                        FirebaseUser user = FBRef.refAuth.getCurrentUser();
                        if (user == null) {
                            Toast.makeText(this, "Login succeeded but no user was returned", Toast.LENGTH_SHORT).show();
                            return;
                        }

                        FBRef.getUser(user);
                        settings.edit().putBoolean(KEY_STAY_CONNECT, cBstayconnect.isChecked()).apply();
                        Toast.makeText(this, "Login success", Toast.LENGTH_SHORT).show();
                        openMenu();
                        return;
                    }

                    String errorMessage = (task.getException() != null)
                            ? task.getException().getLocalizedMessage()
                            : "Login failed";

                    Toast.makeText(this, "Login failed: " + errorMessage, Toast.LENGTH_LONG).show();
                });
    }

    private void setLoginInProgress(boolean inProgress) {
        loginInProgress = inProgress;
        btnLogin.setEnabled(!inProgress);
        btnLogin.setText(inProgress ? "Logging in..." : "Login");
    }

    private void openMenu() {
        startActivity(new Intent(this, MenuActivity.class));
        finish();
    }

    public void onGoogleLoginClick(View view) {
        Toast.makeText(this, "Google sign-in is added in 018d (requires SHA1)", Toast.LENGTH_SHORT).show();
    }
}

If you already moved UI texts to strings.xml in 018a, replace the inline strings in this example ("Login success", "Logging in...", etc.) with your @string/... resources.


Step 3 - What changed conceptually (important)

In 018a, the app always navigated after ~4 seconds:

  • good for UI shell demo
  • bad for real login testing

In 018c, navigation happens only after:

  • Firebase returns success
  • FBRef.getUser(...) stores the authenticated UID context
  • Or the app already has a Firebase session and Remember me (stayConnect) is true (checked in onStart())

This is the bridge we need before adding Google login in 018d.

The quick jump to MenuActivity on later app launches is expected behavior in this step when:

  • user already logged in successfully before, and
  • Remember me is checked, and
  • no logout flow exists yet

This is not the old fake 018a delay. It is the real session bypass implemented in onStart().


Step 4 - Validation checklist

Run the app and verify:

  1. App opens to LoginActivity and does not auto-jump after 4 seconds.
  2. Empty email/password shows validation errors.
  3. Wrong credentials show a failure toast and stay on login screen.
  4. Correct credentials show success toast and open MenuActivity.
  5. If Remember me is checked, reopening the app can quickly skip login and jump to MenuActivity (expected until logout is added).
  6. If Remember me is not checked, the login screen remains available on next launch even if a Firebase session still exists.

At this point the login screen is doing real Firebase auth work, but the Google button is still intentionally deferred.

Recommended Git checkpoint (after your first successful login is working):

  • Commit now before adding the optional disconnect menu.
  • Example commit message:
    • 018c: replace delayed login shell with Firebase email/password login

Testing login again before logout exists (temporary classroom tips)

Until a logout button is implemented, students can still retest login by using one of these:

  1. Leave Remember me unchecked while testing Email/Password flow.
  2. Clear app data / uninstall-reinstall if they already got stuck in auto-jump mode with Remember me checked.
  3. Add a temporary drawer-menu logout in MenuActivity (recommended before heavy login testing and before 018d).

Goal:

  • add a Disconnect / Logout item to the drawer
  • sign out from FirebaseAuth
  • reset stayConnect=false
  • return to LoginActivity

This gives a clean login-testing loop before 018d.

A. Add a drawer item in menu_drawer.xml

Edit:

  • app/src/main/res/menu/menu_drawer.xml
 <item
     android:id="@+id/nav_profile"
     android:title="@string/menu_profile" />
 
+<item
+    android:id="@+id/nav_logout"
+    android:title="@string/menu_logout" />
 
 </menu>

B. Add a string for the new menu item

Edit:

  • app/src/main/res/values/strings.xml
 <string name="menu_profile">Profile</string>
+<string name="menu_logout">Disconnect</string>

C. Handle logout in MenuActivity

Edit:

  • app/src/main/java/com/example/tictacmenu/shell/MenuActivity.java

Add imports:

 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.view.MenuItem;
 
 ...
 
+import com.example.tictacmenu.services.FBRef;
 import com.example.tictacmenu.R;
+import com.example.tictacmenu.activities.LoginActivity;
 import com.example.tictacmenu.activities.Main2Activity;
 import com.example.tictacmenu.activities.MainActivity;

Add the drawer action:

         navigationView.setNavigationItemSelectedListener(item -> {
             if (item.getItemId() == R.id.nav_home) {
                 showFragment(new HomeFragment());
             } else if (item.getItemId() == R.id.nav_local_game) {
                 startActivity(new Intent(this, MainActivity.class));
             } else if (item.getItemId() == R.id.nav_rtdb_prep) {
                 startActivity(new Intent(this, Main2Activity.class));
             } else if (item.getItemId() == R.id.nav_profile) {
                 showFragment(new ProfileFragment());
+            } else if (item.getItemId() == R.id.nav_logout) {
+                logoutAndOpenLogin();
             }
             drawerLayout.closeDrawer(GravityCompat.START);
             return true;
         });

Add this helper method to MenuActivity:

private void logoutAndOpenLogin() {
    // Turn off the automatic "stay connected" bypass.
    SharedPreferences settings = getSharedPreferences("PREFS_NAME", MODE_PRIVATE);
    settings.edit().putBoolean("stayConnect", false).apply();

    // Firebase Email/Password sign-out.
    FBRef.refAuth.signOut();

    Intent intent = new Intent(this, LoginActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    startActivity(intent);
    finish();
}

In 018d (Google Sign-In), extend this logout flow to also sign out from the Google client. For now, FirebaseAuth sign-out + clearing stayConnect is enough for Email/Password testing.

Recommended Git checkpoint (after adding the drawer disconnect menu):

  • Commit the logout menu changes.
  • Push this commit so you can safely experiment with Google login in 018d.
  • Example commit message:
    • 018c: add drawer disconnect action (sign out + clear stayConnect)

Optional emulator lab: inspect and reset SharedPreferences (stayConnect)

This is a great mini-debugging exercise now that Remember me has real meaning.

Goal:

  • open the app’s SharedPreferences file on the emulator
  • find stayConnect
  • reset the file by deleting PREFS_NAME.xml
  • relaunch the app and observe that auto-jump is disabled

In Android Studio, open the tool window menu location shown in the screenshot and then open Device Explorer.

Then browse to the app data path:

  • data/data/<your-package>/shared_prefs/PREFS_NAME.xml

Package examples:

  • com.example.tictacmenu (main tutorial project)
  • com.example.tictactoe (sandbox / twin project)

Android Studio menu location for opening Device Explorer

Typical XML entry you will see:

<boolean name="stayConnect" value="true" />

Recommended action (easier and more reliable than editing):

  1. Stop the app first (close it / stop the running app).
  2. In Device Explorer, delete:
    • data/data/<your-package>/shared_prefs/PREFS_NAME.xml
  3. Relaunch the app.
  4. Confirm the app stays on LoginActivity instead of jumping to MenuActivity.

Android Studio Device Explorer often opens a read-only temp copy of the XML. Clear read-only status only affects the editor copy and may not push the change back to the emulator file. For this lesson, deleting PREFS_NAME.xml is usually the simplest reset.

Why deleting works here:

  • LoginActivity reads stayConnect with default false when the file/key is missing
  • the file is recreated automatically later when the app saves preferences again

Troubleshooting

FirebaseAuth / FirebaseDatabase cannot be resolved

Your 018b assistant-generated setup did not land (or was reverted). Re-check:

  • Auth SDK dependency was added
  • RTDB SDK dependency was added
  • Firebase project connection completed
  • Gradle sync succeeded

Login always fails immediately

Check Firebase Console:

  • Authentication -> Sign-in method -> Email/Password is enabled
  • The test user exists
  • Email/password are entered exactly

App crashes on Firebase init

Usually indicates Firebase setup/config is incomplete (for example missing or wrong google-services.json for this app package).

processDebugGoogleServices fails with No matching client found for package name ...

This usually happens in 018c when students skipped project creation and copied google-services.json from another app/package.

Quick fix:

  1. Open the same Firebase project you want to reuse (teacher/group project).
  2. Add this app package as an Android app in that Firebase project.
  3. Download the matching google-services.json.
  4. Replace app/google-services.json and rebuild.

See 018b Step 3.1 / 3.2 for the detailed explanation.

Login screen seems to “disappear” after the first success

Usually not a bug. In this step, onStart() intentionally auto-opens MenuActivity when:

  • a Firebase user session exists, and
  • Remember me is stored as checked

This continues until logout is implemented or app data is cleared.


Next lesson