Jason Muinde · 668504 · APT3060
move O overview N notes F full 1 / 15
APT3060XA · MOBILE APPLICATION DEVELOPMENT · SEMESTER PROJECT

USIU Varsity Sports
Every trial. One app.

A native Android app in Java on Firebase Authentication and Cloud Firestore, built from the class example — completing the basketball cards, and making team cards something you can add without writing code.

4
Working squads
62
Tests passing
12
Base defects fixed
3.46 MB
Release APK

Jason Muinde · Student No. 668504 · Facilitator: Paul Mwaniki

The brief

Three deliverables

“Find the updated repository here — you just have to set up the firestore and the firebase on your end, and then improve the project and build another team card / complete the basketball cards.”

DONE

1 · Set up Firebase & Firestore

Live project, Email/Password auth, Firestore in nam5, hardened rules, 3 indexes, 16 seeded documents.

DONE

2 · Complete the basketball cards

Two of the three cards were dead views. Both now open working screens.

DONE

3 · Build another team card

Solved structurally: four sports today, a fifth needs no code.

What I found

Five cards on screen. Three did nothing.

activity_basketball_dashboard.xml declared three cards…

<!-- all three declared in the layout -->
<CardView android:id="@+id/cardApply"   … />
<CardView android:id="@+id/cardPlayers" … />
<CardView android:id="@+id/cardResults" … />

basketballDashboard.java wired one.

apply = findViewById(R.id.cardApply);
apply.setOnClickListener(v -> startActivity(…));

// cardPlayers — never looked up
// cardResults — never looked up

The same pattern one level up

cardBasketballwired
cardHockeydead

Hockey was visible on the dashboard and unreachable. Tapping it did nothing — the third dead card.

Plus 12 defects

  • No INTERNET permission in the manifest
  • Sign-up showed an error but didn't return — accounts created with empty credentials
  • getException().getMessage() with no null check
  • Writes carried no ownerUid — anyone could edit anything
  • Firestore keys with spaces: "Player Name"
Deliverable 2

The basketball cards, completed

CardBeforeAfter
Apply to TeamworkedWorks — and now for every sport
View Playersdead viewSquadActivity — live roster, RecyclerView, real-time
View Resultsdead viewFixturesActivity — fixtures, scores, season W-D-L

Real-time, not a refresh button

A Firestore snapshot listener pushes changes, so an application submitted on another handset appears in the squad list immediately. ListAdapter + DiffUtil rebind only the rows that changed.

The record can't lie

W-D-L is derived in the client from the same fixture list shown beneath it — so the summary and the rows are computed from one source and can never disagree.

Deliverable 3 · the decision

“Build another team card”
has two very different answers.

THE LITERAL READING

Wire the dead card. Hard-code a third.

Ten minutes of work. Satisfies the words exactly.

And the next sport costs the same ten minutes again — another block of XML, another findViewById, another listener, another Activity.

WHAT I BUILT

Make a sport data.

One item_sport.xml. One SportAdapter. The list is whatever the Firestore sports collection contains.

Four sports ship today. A fifth is a database row — no code, no rebuild, no redeploy.

That is the difference between adding a card and being able to add cards.

The result

Four squads. One adapter. Zero copy-paste.

Basketball

From the class example — completed.

5 positions

Football

Added — no new Java.

7 positions

Hockey

The dead card, brought to life.

5 positions

Rugby Sevens

Added — no new Java.

6 positions

SportAdapter.java — one adapter, N sports

public class SportAdapter extends
    ListAdapter<Sport, SportAdapter.SportViewHolder> {

TrialApplicationActivity.java — the form adapts too

// basketball offers "Point Guard",
// football offers "Goalkeeper"
positions.addAll(sport.getPositions());

A fifth sport needs no code — it ships with a generic icon until artwork is added, which resolveIcon() handles deliberately.

Architecture

One repository. Two sources. No duplication.

UI LAYER SportsHubActivity SportDashboardActivity SquadActivity FixturesActivity 3 × RecyclerView adapters SportsRepository the only class that talks to Firestore typed models out CLOUD FIRESTORE sports · fixtures trialApplications rules enforced server-side ROOM (SQLite) offline mirror 3 DAOs · versioned schema arbitrary SQL queries both start at once — first to answer paints

The race, handled

Firestore's own cache often beats Room. A cache result arriving after a network result is discarded — otherwise it overwrites fresher data and flips the offline banner on while you're online.

Threading

Room throws if queried on the main thread. All access goes through a single-threaded executor, which also serialises reads against writes.

Lifecycle

Every observation returns a Subscription. Closing it detaches the listener and suppresses in-flight database results, so a destroyed Activity isn't retained.

The most important thing that happened

Everything was green.
The app didn't start.

PASS

assembleDebug

PASS

assembleRelease (R8)

33 / 33

Unit tests

CLEAN

Lint — “No issues found”

The launcher activity ran on a theme whose ancestry ends at a framework theme — not AppCompat:

Theme.VarsitySports.Splash
  → Theme.SplashScreen
    → … → android:Theme.DeviceDefault  ← not AppCompat

AppCompatDelegate checks for windowActionBar inside setContentView and throws:

java.lang.IllegalStateException: You need to use a
Theme.AppCompat theme (or descendant) with this activity.

The fix was one line

SplashScreen.installSplashScreen(this);
super.onCreate(savedInstanceState);

It's what reads postSplashScreenTheme and swaps the Activity onto the real theme.

The real fix was a test

Six Robolectric tests now launch the real Activity on the JVM — real AppCompatDelegate, real theme resolution, real inflation.

Delete that line again and 5 of the 6 fail. I verified that by actually deleting it, not by assuming.

Build-green is not run-green. None of those four signals ever executed an Activity.

Review before merge

13 findings. All resolved.

BLOCKERS
  • App crashed on launch — every device, every time
  • BadTokenException — write callback with no lifecycle guard; rotating the device on a weak network crashed the app
  • Student ID numbers were world-readable
  • Two false claims in my own documentation — a “4/4 rules assertions” row for tests that did not exist
SHOULD-FIX
  • createdAt unvalidated — queue position was forgeable
  • “One application per student” claimed, nothing enforcing it
  • Six Room callbacks retained destroyed Activities
  • Cache/network race flipped the offline banner at random
  • Trials-closed gate nested inside an unrelated condition
  • Staggered list animation declared in XML — never fired
  • Two Espresso assertions could pass for the wrong reason
  • Blanket ProGuard keeps defeating R8
  • Auto-backup uploading email + student number to Google

Removing the blanket ProGuard keeps alone took the release APK from 4.21 MB → 3.46 MB — proof they really were suppressing R8.

Security & privacy

A real privacy failure — found, and fixed

WHAT I SHIPPED FIRST
match /trialApplications/{id} {
  allow read: if true;   // so the website could show squads
}

Those documents carry studentId — a real six-digit USIU student number — beside full name, gender and school.

Anyone could take the project id out of the APK and dump the whole roster with one unauthenticated GET.

WHAT IT IS NOW
match /trialApplications/{id} {
  allow read: if isSignedIn();
}

A roster of names is a design choice. Publishing identity numbers beside them is not — on a course whose outline names “user privacy” explicitly.

The website reads only sports and fixtures. No personal data.

23 rules tests

Against the real rules engine in the Firestore emulator. A student cannot apply in another's name, self-select, forge createdAt, or delete someone else's application.

The API key is not a secret

It's compiled into every APK and extractable in 30 seconds. Treating it as secret makes it no harder to get and breaks a fresh clone. The rules are the boundary.

Backup was leaking

Both backup rule files shipped as empty Android Studio templates — one still had a TODO — so email and student number were being uploaded to Google. Now closed.

Testing

62 tests executed. All passing.

SuiteTestsRuns onStatus
JVM unit — Validators, Converters, Fixture, TrialApplication33JVMpassing
Robolectric — LoginActivityLaunchTest6JVMpassing
Firestore security rules23Firebase emulatorpassing
Room instrumented — AppDatabaseTest10devicenot run
Espresso UI — LoginActivityTest5devicenot run

The gap, stated plainly

This machine has virtualisation disabled in firmware, so the Android emulator cannot start and connectedDebugAndroidTest has never run. There are no device screenshots.

I'd rather say that than imply a device run I didn't do — that gap is exactly what let the launch crash survive.

Tests that assert something real

  • The Room list delimiter is ASCII 0x1F, not a comma — because a position may contain a comma. There's a test for exactly that.
  • A 20-digit jersey number that would throw NumberFormatException if the parse were unguarded.
  • A fixture marked complete with only one score — must not count as played.
Course outline coverage

Every topic, mapped

WeekTopicWhere
3UI, buttons, click events, messages7 screens; Snackbar via BaseActivity
4–5Views, ViewGroups, layout managersConstraint / Linear / Frame / Scroll / Coordinator
4–5Resource folder, menusvalues-night/, 14 vectors, anim/, plurals, string array
5–6Activity lifecycleAll 8 callbacks — incl. onRestart — logged under VarsityLifecycle
5–6Intents & Bundles, navigationEXTRA_SPORT_ID; parentActivityName on every screen below the hub
5–6Configuration changesSubscriptions closed in onStop, reattached in onStart
8–9SharedPreferences and RoomSessionManager + AppDatabase, 3 DAOs
8–9Reading/writing, local databases@Transaction atomic replace, exported schema
10RecyclerView3 adapters, all ListAdapter + DiffUtil
Firebase & librariesAuth, Firestore, Material 3, ViewBinding
Animations & transitionsActivity push/pop + staggered list entry
Async / background processingAppExecutors — serial disk executor
Security & user privacy23 rules tests, R8, SECURITY.md
13PublishingValid reverse-DNS id; 3.46 MB minified release
Beyond the brief

A companion site, live on the same data

usiu-varsity-sports.pages.dev

React 19 · TypeScript · Tailwind v4 · Vite · Cloudflare Pages

Live, not a mockup

Reads the same public Firestore collections over REST — no Firebase JS SDK, which would have added ~200 KB gzipped for data that changes a few times a season.

The phone can't go stale

The hero device is rendered from the same live documents. Add a sport and both the app and the page grow a card.

Two bugs caught in verification

The sport cards rendered at opacity:0 — they mount after the fetch, so the observer had already run. And the hidden state was the CSS default, so any script failure blanked the page.

Bundle: 68 KB gzipped JS, 7 KB CSS.

Where it landed

Final state

62
Tests passing
0
Lint issues
12
Base defects fixed
13
Review findings resolved
Activities7
RecyclerView adapters3
Room entities / DAOs3 / 3
Layouts / vector drawables10 / 13
Sports supported4, extensible without code
Debug APK12.59 MB
Release APK3.46 MB

What I'd do next — honestly

  • Run it on a device. The single biggest gap; everything else follows from it.
  • Move studentId into a private subcollection so signed-in students can't read each other's ID numbers.
  • Add a staff role via a Firebase custom claim, so shortlisting happens in the app rather than the console.
  • Build the “My applications” screen — the DAO query exists and is tested; the screen isn't built.
  • Push-notify on status change — the feature students would actually ask for.
THANK YOU

Questions?

The first commit in the repository is the class example as provided, so every change is a reviewable diff.

Full report

REPORT.md · SECURITY.md · PROJECT.md in the repository root

Jason Muinde · 668504 · APT3060XA Mobile Application Development · USIU-Africa

Slide overview — click to jump, or press O / Esc

Speaker notes