diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..c2b0b83
Binary files /dev/null and b/.DS_Store differ
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..27cf69a
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,27 @@
+# Pull Request
+
+## Description
+
+
+## Related Issues
+
+
+## Type of Change
+- [ ] Bug fix (non-breaking change that fixes an issue)
+- [ ] New feature (non-breaking change that adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] This change requires a documentation update
+
+## How Has This Been Tested?
+
+- [ ] Unit tests
+- [ ] Integration tests
+- [ ] Manual testing
+
+## Checklist:
+- [ ] My code follows the project's coding style
+- [ ] I have commented my code, particularly in hard-to-understand areas
+- [ ] I have made corresponding changes to the documentation
+- [ ] My changes generate no new warnings
+- [ ] I have added tests that prove my fix is effective or that my feature works
+- [ ] New and existing unit tests pass locally with my changes
\ No newline at end of file
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 0000000..c65a0c2
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,57 @@
+name: Android CI
+
+on:
+ push:
+ branches: [ main, develop, production ]
+ pull_request:
+ branches: [ main, develop, production ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: gradle
+
+ - name: Create dummy google-services.json
+ run: |
+ echo '{
+ "project_info": {
+ "project_number": "123456789",
+ "project_id": "dummy-project",
+ "storage_bucket": "dummy-project.appspot.com"
+ },
+ "client": [{
+ "client_info": {
+ "mobilesdk_app_id": "1:123456789:android:abcdef123456",
+ "android_client_info": {
+ "package_name": "com.pineapple.capture"
+ }
+ },
+ "api_key": [{
+ "current_key": "dummy_api_key"
+ }],
+ "oauth_client": [],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ }]
+ }' > app/google-services.json
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Build with Gradle
+ run: ./gradlew build --stacktrace
+
+ - name: Run Tests
+ run: ./gradlew test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ec8a5ec
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,82 @@
+*.iml
+.gradle
+.vscode
+/local.properties
+/.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+
+# Firebase configuration file
+google-services.json
+app/google-services.json
+
+# Generated files
+bin/
+gen/
+out/
+
+# Gradle files
+.gradle/
+.gradle/**/*
+
+# IDE files
+.vscode/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# 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
+.idea/caches
+
+# Keystore files
+*.jks
+*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+.cxx/
+
+# Version control
+vcs.xml
+
+# lint
+lint/intermediates/
+lint/generated/
+lint/outputs/
+lint/tmp/
+
+# Gradle cache files
+.gradle/
+build/
+.idea/
+local.properties
+
+# Android Studio generated files
+*.iml
+.idea/
+captures/
+.externalNativeBuild/
+.cxx/
+
+# Android Profiling
+*.hprof
diff --git a/README.md b/README.md
index 4401384..ffb8c8f 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,32 @@
# Capture - Android App
-A Java-based Android application.
+A Java-based Android application. Capture is a real-time social diary that lets you stay truly connected with your closest friends. Share spontaneous photos and thoughts directly to your friends’ home screens – as it happens. No filters, no algorithms, just raw, real-life moments.
+
+Whether it’s a quick smile or a funny thought – Capture makes every little moment meaningful. It’s not about likes or followers. It’s about living and sharing life together, in real time.
+
+## Project overview
+
+### Features
+
+- Take, post real-time pictures
+- Like, share, comments on posts
+- Lightweight, fast, and privacy-focused
+
+### Team Members
+
+- Phuong Khanh Pham
+- Natthanicha Vongjarit
+- Debojyoti Mishra
+- Bui Tien Quoc
+
+### Tech Stack
+
+- **Language**: Java
+- **Platform**: Android SDK (API level 26+)
+- **IDE**: Android Studio
+- **Version Control**: Git + GitHub
+- **CI/CD**: GitHub Actions
## Branching Strategy
@@ -76,7 +101,125 @@ When the develop branch is stable and ready for release:
## Development Setup
-[Add your development setup instructions here]
+### Requirements
+
+- Android Studio Iguana (2023.2.1) or newer
+- JDK 17 or higher
+- Android SDK with minimum API level 26 (Android 8.0 Oreo)
+- Git
+
+### Getting Started
+
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/arcreane/android-project-pineapple.git
+ cd android-project-pineapple
+ ```
+
+2. Set up the git hooks:
+ ```bash
+ ./scripts/setup-hooks.sh
+ ```
+
+3. Open the project in Android Studio:
+ - Launch Android Studio
+ - Select "Open an existing project"
+ - Navigate to the cloned repository and click "Open"
+
+4. Sync the project with Gradle files:
+ - Android Studio should automatically sync
+ - If not, select "File > Sync Project with Gradle Files"
+
+5. Run the app:
+ - Connect an Android device or use an emulator
+ - Click the "Run" button (green triangle) in Android Studio
+
+### Troubleshooting
+
+- If you encounter Gradle sync issues, ensure you have the correct JDK version
+- For build errors, check the "Build" tab in Android Studio for specific error messages
+- If GitHub Actions CI builds fail but local builds succeed, check Java version compatibility
+
+## Contributing
+
+### First-Time Setup
+
+1. Fork the repository on GitHub:
+ - Visit the repository page on GitHub
+ - Click the "Fork" button in the top-right corner
+ - Select your account as the destination
+
+2. Clone your forked repository:
+ ```bash
+ git clone https://github.com/YOUR_USERNAME/android-project-pineapple.git
+ cd android-project-pineapple
+ ```
+
+3. Add the original repository as upstream:
+ ```bash
+ git remote add upstream https://github.com/arcreane/android-project-pineapple.git
+ ```
+
+### Making Contributions
+
+1. Sync your local develop branch:
+ ```bash
+ git checkout develop
+ git pull upstream develop
+ git push origin develop
+ ```
+
+2. Create a new feature branch:
+ ```bash
+ git checkout -b feature/your-feature-name develop
+ ```
+ Branch naming convention:
+ - `feature/*` for new features
+ - `bugfix/*` for bug fixes
+ - `docs/*` for documentation changes
+ - `hotfix/*` for urgent production fixes
+
+3. Make your changes:
+ - Write clear, concise commit messages following our convention:
+ ```bash
+ git commit -m "type(scope): description"
+ ```
+ - Types: feat, fix, docs, style, refactor, test, chore
+ - Example: `git commit -m "feat(login): add biometric authentication"`
+
+4. Push your changes:
+ ```bash
+ git push -u origin feature/your-feature-name
+ ```
+
+5. Create a Pull Request:
+ - Go to the original repository on GitHub
+ - Click "Pull Requests" > "New Pull Request"
+ - Select `develop` as the base branch
+ - Write a clear PR description explaining your changes
+ - Link any related issues
+
+6. Address review feedback:
+ - Make requested changes in your feature branch
+ - Commit and push updates
+ - The PR will update automatically
+
+7. After PR is merged:
+ ```bash
+ git checkout develop
+ git pull upstream develop
+ git push origin develop
+ git branch -d feature/your-feature-name
+ ```
+
+### Best Practices
+
+- Keep PRs focused and reasonably sized
+- Update your feature branch regularly with develop
+- Write meaningful commit messages
+- Add tests for new features
+- Update documentation as needed
+- Ensure CI checks pass before requesting review
## UML Diagram

\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
index 42afabf..056f29c 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1 +1,3 @@
-/build
\ No newline at end of file
+/build
+.gradle/file-system.probe
+.DS_Store
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index b04e2fe..7026fa2 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,5 +1,14 @@
plugins {
id 'com.android.application'
+ id 'com.google.gms.google-services'
+}
+
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
}
android {
@@ -14,6 +23,11 @@ android {
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ // Read Cloudinary credentials from local.properties
+ buildConfigField "String", "CLOUDINARY_CLOUD_NAME", "\"${localProperties.getProperty('CLOUDINARY_CLOUD_NAME', '')}\""
+ buildConfigField "String", "CLOUDINARY_API_KEY", "\"${localProperties.getProperty('CLOUDINARY_API_KEY', '')}\""
+ buildConfigField "String", "CLOUDINARY_API_SECRET", "\"${localProperties.getProperty('CLOUDINARY_API_SECRET', '')}\""
}
buildTypes {
@@ -27,6 +41,7 @@ android {
targetCompatibility JavaVersion.VERSION_17
}
buildFeatures {
+ buildConfig true
viewBinding true
}
}
@@ -36,6 +51,27 @@ dependencies {
implementation 'androidx.core:core:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.11.0'
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
+// implementation platform('com.google.firebase:firebase-bom:32.7.4')
+// implementation 'com.google.firebase:firebase-common'
+ implementation("com.google.firebase:firebase-firestore:25.0.0")
+ implementation 'com.google.guava:guava:32.1.2-android'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel:2.7.0'
+ implementation 'androidx.lifecycle:lifecycle-livedata:2.7.0'
+ implementation 'androidx.lifecycle:lifecycle-runtime:2.7.0'
+ implementation 'androidx.navigation:navigation-fragment:2.7.7'
+ implementation 'androidx.navigation:navigation-ui:2.7.7'
+ implementation "androidx.camera:camera-core:1.3.0"
+ implementation "androidx.camera:camera-camera2:1.3.0"
+ implementation "androidx.camera:camera-lifecycle:1.3.0"
+ implementation "androidx.camera:camera-view:1.3.0"
+ implementation 'de.hdodenhof:circleimageview:3.1.0'
+ implementation libs.firebase.auth
+ implementation libs.firebase.storage
+// implementation libs.firebase.inappmessaging
+ implementation 'com.github.bumptech.glide:glide:4.16.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
+ implementation("com.cloudinary:cloudinary-android:3.0.2")
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
diff --git a/app/git_test.txt b/app/git_test.txt
new file mode 100644
index 0000000..865c2d0
--- /dev/null
+++ b/app/git_test.txt
@@ -0,0 +1 @@
+test ev
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 091386d..e48c0b0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,27 +2,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:theme="@style/Theme.Capture.NoActionBar">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..ece3315
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/com/pineapple/capture/MainActivity.java b/app/src/main/java/com/pineapple/capture/MainActivity.java
index b2225ed..3dbf4dd 100644
--- a/app/src/main/java/com/pineapple/capture/MainActivity.java
+++ b/app/src/main/java/com/pineapple/capture/MainActivity.java
@@ -1,11 +1,27 @@
package com.pineapple.capture;
+import android.content.Intent;
import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.view.ViewCompat;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+import com.google.firebase.auth.FirebaseAuth;
+import com.pineapple.capture.activities.LoginActivity;
import com.pineapple.capture.databinding.ActivityMainBinding;
+import com.pineapple.capture.fragment.CameraFragment;
+import com.pineapple.capture.fragment.HomeFragment;
+import com.pineapple.capture.fragment.ProfileFragment;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
+ private FirebaseAuth mAuth;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -14,6 +30,90 @@ protected void onCreate(Bundle savedInstanceState) {
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
- binding.textViewGreeting.setText("Hello Android!");
+ MenuItem cameraItem = binding.bottomNavigation.getMenu().findItem(R.id.navigation_camera);
+ cameraItem.setIconTintList(null);
+
+ mAuth = FirebaseAuth.getInstance();
+
+ /* Set up toolbar
+ Toolbar toolbar = binding.toolbar;
+ setSupportActionBar(toolbar);
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().setDisplayShowTitleEnabled(false);
+ }
+ */
+
+ // Remove bottom navigation padding
+ ViewCompat.setOnApplyWindowInsetsListener(binding.bottomNavigation, (v, insets) -> {
+ v.setPadding(0, 0, 0, 0);
+ return insets;
+ });
+
+ // Set up bottom navigation
+ binding.bottomNavigation.setOnItemSelectedListener(item -> {
+ if (item.getItemId() == R.id.navigation_home) {
+ loadFragment(new HomeFragment());
+ return true;
+ } else if (item.getItemId() == R.id.navigation_camera) {
+ loadFragment(new CameraFragment());
+ return true;
+ } else if (item.getItemId() == R.id.navigation_profile) {
+ loadFragment(new ProfileFragment());
+ return true;
+ }
+ return false;
+ });
+
+ // Start with home fragment if this is a fresh start
+ if (savedInstanceState == null) {
+ loadFragment(new HomeFragment());
+ binding.bottomNavigation.setSelectedItemId(R.id.navigation_home);
+ }
+ }
+
+ // Helper method to load fragments
+ private void loadFragment(Fragment fragment) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.fragment_container, fragment)
+ .commit();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // Always refresh the HomeFragment when returning to the app
+ if (binding.bottomNavigation.getSelectedItemId() == R.id.navigation_home) {
+ HomeFragment homeFragment = (HomeFragment) getSupportFragmentManager()
+ .findFragmentById(R.id.fragment_container);
+
+ if (homeFragment != null) {
+ homeFragment.loadFeedPosts();
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == R.id.action_logout) {
+ logout();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void logout() {
+ mAuth.signOut();
+ Intent intent = new Intent(this, LoginActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ finish();
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/pineapple/capture/activities/InterestsActivity.java b/app/src/main/java/com/pineapple/capture/activities/InterestsActivity.java
new file mode 100644
index 0000000..c997cb2
--- /dev/null
+++ b/app/src/main/java/com/pineapple/capture/activities/InterestsActivity.java
@@ -0,0 +1,374 @@
+package com.pineapple.capture.activities;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.android.material.chip.Chip;
+import com.google.android.material.chip.ChipGroup;
+import com.google.firebase.auth.FirebaseAuth;
+import com.google.firebase.auth.FirebaseUser;
+import com.google.firebase.firestore.FirebaseFirestore;
+import com.pineapple.capture.R;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class InterestsActivity extends AppCompatActivity {
+
+ private FirebaseFirestore db;
+ private FirebaseUser currentUser;
+ private TextView selectedCountText;
+ private Button saveButton;
+ private List selectedInterests = new ArrayList<>();
+ private final int MAX_SELECTIONS = 6;
+
+ // interest categories and their options
+ private final Map> interestCategories = new HashMap>() {{
+ put("Creativity", Arrays.asList("Art", "Dancing", "Make-up", "Video", "Cosplay", "Design", "Photography", "Crafts", "Fashion", "Singing"));
+ put("Sports", Arrays.asList("Badminton", "Bouldering", "Crew", "Baseball", "Bowling", "Cricket", "Basketball", "Boxing", "Cycling"));
+ put("Pets", Arrays.asList("Amphibians", "Cats", "Horses", "Arthropods", "Dogs", "Rabbits", "Birds", "Fish", "Reptiles", "Turtles"));
+ }};
+
+ // Map to store interest colors
+ private Map interestColors = new HashMap<>();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_interests);
+
+ db = FirebaseFirestore.getInstance();
+ currentUser = FirebaseAuth.getInstance().getCurrentUser();
+
+ selectedCountText = findViewById(R.id.interests_count);
+ saveButton = findViewById(R.id.save_button);
+ ImageButton backButton = findViewById(R.id.back_button);
+ Button clearAllButton = findViewById(R.id.clear_all_button);
+
+ for (List interestList : interestCategories.values()) {
+ for (String interest : interestList) {
+ interestColors.put(interest, getInterestCategoryColor(interest));
+ }
+ }
+
+ setupInterestCategories();
+ updateSelectedCount();
+
+ backButton.setOnClickListener(v -> finish());
+ saveButton.setOnClickListener(v -> saveInterests());
+
+ findViewById(R.id.help_button).setOnClickListener(v -> {
+ showHelpDialog();
+ });
+
+ clearAllButton.setOnClickListener(v -> {
+ if (!selectedInterests.isEmpty()) {
+ new AlertDialog.Builder(this)
+ .setTitle("Clear All Interests")
+ .setMessage("Are you sure you want to remove all selected interests?")
+ .setPositiveButton("Clear All", (dialog, which) -> {
+ clearAllInterests();
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ } else {
+ Toast.makeText(this, "No interests selected", Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ loadExistingInterests();
+ }
+
+ private void setupInterestCategories() {
+ LinearLayout interestsContainer = findViewById(R.id.interests_container);
+
+ for (Map.Entry> category : interestCategories.entrySet()) {
+ TextView categoryTitle = new TextView(this);
+ categoryTitle.setText(category.getKey());
+ categoryTitle.setTextSize(24);
+ categoryTitle.setTextColor(getResources().getColor(R.color.white));
+ categoryTitle.setPadding(0, 40, 0, 20);
+ interestsContainer.addView(categoryTitle);
+
+ ChipGroup chipGroup = new ChipGroup(this);
+ chipGroup.setChipSpacingHorizontal(16);
+ chipGroup.setChipSpacingVertical(16);
+
+ for (String interest : category.getValue()) {
+ Chip chip = new Chip(this);
+ chip.setText(interest);
+ chip.setCheckable(true);
+ chip.setClickable(true);
+
+ chip.setChipBackgroundColorResource(R.color.background_dark);
+ chip.setTextColor(getResources().getColor(R.color.white));
+ chip.setChipStrokeWidth(1);
+ chip.setChipStrokeColorResource(R.color.white);
+
+ setChipIcon(chip, interest);
+
+ chip.setTag(interestColors.get(interest));
+
+ chip.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (isChecked) {
+ if (selectedInterests.size() >= MAX_SELECTIONS) {
+ chip.setChecked(false);
+ Toast.makeText(InterestsActivity.this,
+ "You can select up to " + MAX_SELECTIONS + " interests",
+ Toast.LENGTH_SHORT).show();
+ } else {
+ selectedInterests.add(interest);
+ chip.setChipBackgroundColorResource(interestColors.get(interest));
+ chip.setTextColor(getResources().getColor(R.color.black));
+ chip.setElevation(2f);
+ chip.setChipCornerRadius(16f);
+ }
+ } else {
+ selectedInterests.remove(interest);
+ chip.setChipBackgroundColorResource(R.color.background_dark);
+ chip.setTextColor(getResources().getColor(R.color.white));
+ chip.setElevation(0f);
+ chip.setChipCornerRadius(8f);
+ }
+ updateSelectedCount();
+ });
+
+ chipGroup.addView(chip);
+ }
+
+ interestsContainer.addView(chipGroup);
+ }
+
+ Toast.makeText(this, "Tip: Tap an interest to select/deselect it", Toast.LENGTH_LONG).show();
+ }
+
+ private void setChipIcon(Chip chip, String interest) {
+ // Set appropriate icon for each interest
+ // This would ideally use a mapping or switch statement to set the correct drawable
+ // For simplicity, using placeholder approach
+ int iconResId = getInterestIconResource(interest);
+ if (iconResId != 0) {
+ chip.setChipIconResource(iconResId);
+ chip.setChipIconVisible(true);
+ }
+ }
+
+ private int getInterestIconResource(String interest) {
+ String lowerInterest = interest.toLowerCase();
+
+ if (lowerInterest.equals("art")) return R.drawable.ic_art;
+ if (lowerInterest.equals("dancing")) return R.drawable.ic_dancing;
+ if (lowerInterest.equals("photography")) return R.drawable.ic_photography;
+ if (lowerInterest.equals("singing")) return R.drawable.ic_music;
+
+ if (Arrays.asList("make-up", "design", "crafts", "fashion", "video", "cosplay").contains(lowerInterest)) {
+ return R.drawable.ic_creativity;
+ }
+
+ if (Arrays.asList("badminton", "bouldering", "crew", "baseball", "bowling", "cricket", "basketball", "boxing", "cycling").contains(lowerInterest)) {
+ return R.drawable.ic_sports;
+ }
+
+ if (Arrays.asList("amphibians", "cats", "horses", "arthropods", "dogs", "rabbits", "birds", "fish", "reptiles", "turtles").contains(lowerInterest)) {
+ return R.drawable.ic_pets;
+ }
+
+ return R.drawable.ic_favorite; // Default icon
+ }
+
+ private void updateSelectedCount() {
+ int count = selectedInterests.size();
+ selectedCountText.setText(String.format("%d/%d selected", count, MAX_SELECTIONS));
+
+ Button clearAllButton = findViewById(R.id.clear_all_button);
+ clearAllButton.setVisibility(count > 0 ? View.VISIBLE : View.GONE);
+
+ if (count > 0) {
+ clearAllButton.setBackgroundResource(R.drawable.rounded_button_secondary);
+ }
+
+ saveButton.setEnabled(count > 0);
+ }
+
+ private void loadExistingInterests() {
+ if (currentUser != null) {
+ db.collection("users").document(currentUser.getUid())
+ .get()
+ .addOnSuccessListener(documentSnapshot -> {
+ if (documentSnapshot.exists() && documentSnapshot.contains("interests")) {
+ List userInterests = (List) documentSnapshot.get("interests");
+ if (userInterests != null && !userInterests.isEmpty()) {
+ selectedInterests = new ArrayList<>(userInterests);
+ updateChipSelections();
+ updateSelectedCount(); // update the Clear All button visibility
+ }
+ }
+ })
+ .addOnFailureListener(e ->
+ Toast.makeText(this, "Failed to load interests: " + e.getMessage(),
+ Toast.LENGTH_SHORT).show());
+ }
+ }
+
+ private void updateChipSelections() {
+ for (String category : interestCategories.keySet()) {
+ ChipGroup chipGroup = findChipGroupForCategory(category);
+ if (chipGroup != null) {
+ for (int i = 0; i < chipGroup.getChildCount(); i++) {
+ View child = chipGroup.getChildAt(i);
+ if (child instanceof Chip) {
+ Chip chip = (Chip) child;
+ String interestName = chip.getText().toString();
+ boolean isSelected = selectedInterests.contains(interestName);
+ chip.setChecked(isSelected);
+
+ if (isSelected) {
+ chip.setChipBackgroundColorResource(interestColors.get(interestName));
+ chip.setTextColor(getResources().getColor(R.color.black));
+ chip.setElevation(2f);
+ chip.setChipCornerRadius(16f);
+ } else {
+ chip.setChipBackgroundColorResource(R.color.background_dark);
+ chip.setTextColor(getResources().getColor(R.color.white));
+ chip.setElevation(0f);
+ chip.setChipCornerRadius(8f);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private ChipGroup findChipGroupForCategory(String category) {
+ LinearLayout container = findViewById(R.id.interests_container);
+ boolean foundCategory = false;
+
+ for (int i = 0; i < container.getChildCount(); i++) {
+ View child = container.getChildAt(i);
+
+ if (child instanceof TextView && ((TextView) child).getText().equals(category)) {
+ foundCategory = true;
+ } else if (foundCategory && child instanceof ChipGroup) {
+ return (ChipGroup) child;
+ }
+ }
+
+ return null;
+ }
+
+ private void saveInterests() {
+ if (currentUser != null) {
+ db.collection("users").document(currentUser.getUid())
+ .update("interests", selectedInterests)
+ .addOnSuccessListener(aVoid -> {
+ Toast.makeText(InterestsActivity.this,
+ "Interests saved successfully",
+ Toast.LENGTH_SHORT).show();
+
+ setResult(RESULT_OK);
+ finish();
+ })
+ .addOnFailureListener(e ->
+ Toast.makeText(InterestsActivity.this,
+ "Failed to save interests: " + e.getMessage(),
+ Toast.LENGTH_SHORT).show());
+ }
+ }
+
+ /**
+ * Assigns consistent colors to interests based on their category
+ */
+ private int getInterestCategoryColor(String interest) {
+ String lowerInterest = interest.toLowerCase();
+
+ // Creativity (orange/yellow)
+ if (Arrays.asList("art", "design", "photography", "crafts", "fashion", "singing", "dancing", "video", "cosplay", "make-up").contains(lowerInterest)) {
+ if (lowerInterest.equals("art")) return R.color.pastel_orange;
+ if (lowerInterest.equals("dancing")) return R.color.pastel_yellow;
+ if (lowerInterest.equals("photography")) return R.color.pastel_orange;
+ if (lowerInterest.equals("singing")) return R.color.pastel_yellow;
+ return R.color.pastel_orange;
+ }
+
+ // Sports (blue/green)
+ if (Arrays.asList("badminton", "bouldering", "crew", "baseball", "bowling", "cricket", "basketball", "boxing", "cycling").contains(lowerInterest)) {
+ if (lowerInterest.equals("basketball")) return R.color.pastel_blue;
+ if (lowerInterest.equals("cycling")) return R.color.pastel_green;
+ if (lowerInterest.equals("baseball")) return R.color.pastel_blue;
+ return R.color.pastel_green;
+ }
+
+ // Pets (purple/pink)
+ if (Arrays.asList("amphibians", "cats", "horses", "arthropods", "dogs", "rabbits", "birds", "fish", "reptiles", "turtles").contains(lowerInterest)) {
+ if (lowerInterest.equals("cats")) return R.color.pastel_purple;
+ if (lowerInterest.equals("dogs")) return R.color.pastel_pink;
+ if (lowerInterest.equals("birds")) return R.color.pastel_purple;
+ return R.color.pastel_pink;
+ }
+
+ // Default colors based on first letter for any other interest
+ char firstChar = lowerInterest.charAt(0);
+ switch (firstChar % 8) {
+ case 0: return R.color.pastel_blue;
+ case 1: return R.color.pastel_green;
+ case 2: return R.color.pastel_purple;
+ case 3: return R.color.pastel_pink;
+ case 4: return R.color.pastel_orange;
+ case 5: return R.color.pastel_yellow;
+ case 6: return R.color.pastel_teal;
+ case 7: return R.color.pastel_cyan;
+ default: return R.color.pastel_blue;
+ }
+ }
+
+ private void showHelpDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("How to Use")
+ .setMessage("• Tap an interest to select it\n• Tap again to remove it\n• Use 'Clear All' to remove all selections\n• You can select up to " + MAX_SELECTIONS + " interests\n• Selected interests will appear on your profile")
+ .setPositiveButton("Got it", null)
+ .show();
+ }
+
+ /**
+ * Clears all selected interests
+ */
+ private void clearAllInterests() {
+ selectedInterests.clear();
+ clearAllChips();
+ updateSelectedCount();
+ Toast.makeText(this, "All interests cleared", Toast.LENGTH_SHORT).show();
+ }
+
+ /**
+ * Resets all chips to unselected state
+ */
+ private void clearAllChips() {
+ for (String category : interestCategories.keySet()) {
+ ChipGroup chipGroup = findChipGroupForCategory(category);
+ if (chipGroup != null) {
+ for (int i = 0; i < chipGroup.getChildCount(); i++) {
+ View child = chipGroup.getChildAt(i);
+ if (child instanceof Chip) {
+ Chip chip = (Chip) child;
+ chip.setChecked(false);
+ chip.setChipBackgroundColorResource(R.color.background_dark);
+ chip.setTextColor(getResources().getColor(R.color.white));
+ chip.setElevation(0f);
+ chip.setChipCornerRadius(8f);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pineapple/capture/activities/LoginActivity.java b/app/src/main/java/com/pineapple/capture/activities/LoginActivity.java
new file mode 100644
index 0000000..eb6764e
--- /dev/null
+++ b/app/src/main/java/com/pineapple/capture/activities/LoginActivity.java
@@ -0,0 +1,113 @@
+package com.pineapple.capture.activities;
+
+import android.content.Intent; //changing screen
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Toast;
+import androidx.appcompat.app.AppCompatActivity;
+import com.pineapple.capture.MainActivity;
+import com.pineapple.capture.R;
+import com.pineapple.capture.auth.AuthManager;
+import com.pineapple.capture.databinding.ActivityLoginBinding;
+import com.pineapple.capture.utils.NetworkUtils;
+
+public class LoginActivity extends AppCompatActivity {
+ private ActivityLoginBinding binding;
+ private AuthManager authManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(R.style.Theme_Capture_NoActionBar);
+ super.onCreate(savedInstanceState);
+ binding = ActivityLoginBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ authManager = AuthManager.getInstance();
+
+ if (authManager.getCurrentUser() != null) {
+ startMainActivity();
+ finish();
+ return;
+ }
+
+ setupClickListeners();
+ }
+
+ private void setupClickListeners() {
+ binding.loginButton.setOnClickListener(v -> handleLogin());
+ binding.signupButton.setOnClickListener(v -> startSignupActivity());
+ binding.resetPasswordLink.setOnClickListener(v -> handleResetPassword());
+ }
+
+ private void handleLogin() {
+ if (!NetworkUtils.isConnected(LoginActivity.this)) {
+ Toast.makeText(this, "No internet connection", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ String email = binding.emailEditText.getText().toString().trim();
+ String password = binding.passwordEditText.getText().toString().trim();
+
+ if (email.isEmpty() || password.isEmpty()) {
+ Toast.makeText(this, "Please fill in all fields", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ showLoading(true);
+ authManager.login(email, password)
+ .addOnCompleteListener(task -> {
+ showLoading(false);
+ if (task.isSuccessful()) {
+ startMainActivity();
+ finish();
+ } else {
+ String error = task.getException() != null ?
+ task.getException().getMessage() :
+ "Authentication failed";
+ Toast.makeText(LoginActivity.this, error, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ private void startSignupActivity() {
+ Intent intent = new Intent(this, SignupActivity.class);
+ startActivity(intent);
+ }
+
+ private void handleResetPassword() {
+ String email = binding.emailEditText.getText().toString().trim();
+ if (email.isEmpty()) {
+ Toast.makeText(this, "Please enter your email", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ showLoading(true);
+ authManager.sendPasswordResetEmail(email)
+ .addOnCompleteListener(task -> {
+ showLoading(false);
+ if (task.isSuccessful()) {
+ Toast.makeText(this, "Password reset email sent", Toast.LENGTH_SHORT).show();
+ } else {
+ String error = task.getException() != null ?
+ task.getException().getMessage() :
+ "Failed to send reset password email";
+ Toast.makeText(LoginActivity.this, error, Toast.LENGTH_LONG).show();
+
+ }
+ });
+ }
+
+ private void startMainActivity() {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ }
+
+ private void showLoading(boolean show) {
+ binding.progressBar.setVisibility(show ? View.VISIBLE : View.GONE);
+ binding.loginButton.setEnabled(!show);
+ binding.signupButton.setEnabled(!show);
+ binding.emailEditText.setEnabled(!show);
+ binding.passwordEditText.setEnabled(!show);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pineapple/capture/activities/SignupActivity.java b/app/src/main/java/com/pineapple/capture/activities/SignupActivity.java
new file mode 100644
index 0000000..8b10ebc
--- /dev/null
+++ b/app/src/main/java/com/pineapple/capture/activities/SignupActivity.java
@@ -0,0 +1,104 @@
+package com.pineapple.capture.activities;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Toast;
+import androidx.appcompat.app.AppCompatActivity;
+import com.pineapple.capture.MainActivity;
+import com.pineapple.capture.auth.AuthManager;
+import com.pineapple.capture.databinding.ActivitySignupBinding;
+import com.pineapple.capture.utils.NetworkUtils;
+
+public class SignupActivity extends AppCompatActivity {
+
+ private ActivitySignupBinding binding;
+ private AuthManager authManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivitySignupBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ authManager = AuthManager.getInstance();
+
+ setupClickListeners();
+ }
+
+ private void setupClickListeners() {
+ binding.signupButton.setOnClickListener(v -> handleSignup());
+ binding.loginButton.setOnClickListener(v -> finish());
+ }
+
+ private void handleSignup() {
+ if (!NetworkUtils.isConnected(this)) {
+ Toast.makeText(this, "No internet connection", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ String displayName = binding.displayNameEditText.getText().toString().trim();
+ String username = binding.usernameEditText.getText().toString().trim();
+ String email = binding.emailEditText.getText().toString().trim();
+ String password = binding.passwordEditText.getText().toString().trim();
+
+ if (displayName.isEmpty() || username.isEmpty() || email.isEmpty() || password.isEmpty()) {
+ Toast.makeText(this, "Please fill in all fields", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // Username validation
+ if (username.length() < 1 || username.length() > 30) {
+ Toast.makeText(this, "Username must be 1-30 characters", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ if (username.contains(" ")) {
+ Toast.makeText(this, "Username cannot contain spaces", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ if (!username.matches("^[a-zA-Z0-9._]+$")) {
+ Toast.makeText(this, "Username can only contain letters, numbers, periods, and underscores", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ if (username.startsWith(".") || username.endsWith(".")) {
+ Toast.makeText(this, "Username cannot start or end with a period", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ showLoading(true);
+ authManager.isUsernameUnique(username, isUnique -> {
+ if (!isUnique) {
+ showLoading(false);
+ Toast.makeText(this, "Username is already taken", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ authManager.signup(displayName, username, email, password)
+ .addOnCompleteListener(task -> {
+ showLoading(false);
+ if (task.isSuccessful()) {
+ startMainActivity();
+ } else {
+ String error = task.getException() != null ?
+ task.getException().getMessage() :
+ "Signup failed";
+ Toast.makeText(this, error, Toast.LENGTH_LONG).show();
+ }
+ });
+ });
+ }
+
+ private void startMainActivity() {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ }
+
+ private void showLoading(boolean show) {
+ binding.progressBar.setVisibility(show ? View.VISIBLE : View.GONE);
+ binding.signupButton.setEnabled(!show);
+ binding.loginButton.setEnabled(!show);
+ binding.displayNameEditText.setEnabled(!show);
+ binding.emailEditText.setEnabled(!show);
+ binding.passwordEditText.setEnabled(!show);
+ }
+}
diff --git a/app/src/main/java/com/pineapple/capture/auth/AuthActivity.java b/app/src/main/java/com/pineapple/capture/auth/AuthActivity.java
new file mode 100644
index 0000000..f58ae3c
--- /dev/null
+++ b/app/src/main/java/com/pineapple/capture/auth/AuthActivity.java
@@ -0,0 +1,132 @@
+package com.pineapple.capture.auth;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TextView;
+import android.util.Patterns;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProvider;
+import com.google.android.material.button.MaterialButton;
+import com.google.android.material.textfield.TextInputEditText;
+import com.google.firebase.auth.FirebaseAuth;
+import com.google.firebase.auth.FirebaseUser;
+import com.pineapple.capture.MainActivity;
+import com.pineapple.capture.R;
+
+
+public class AuthActivity extends AppCompatActivity {
+ private AuthViewModel authViewModel;
+ private TextInputEditText usernameInput;
+ private TextInputEditText passwordInput;
+ private MaterialButton loginButton;
+ private MaterialButton signupButton;
+ private TextView errorText;
+ private TextView resetPasswordLink;
+ private FirebaseAuth mAuth;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_auth);
+
+ authViewModel = new ViewModelProvider(this).get(AuthViewModel.class);
+
+ usernameInput = findViewById(R.id.username_input);
+ passwordInput = findViewById(R.id.password_input);
+ loginButton = findViewById(R.id.login_button);
+ signupButton = findViewById(R.id.signup_button);
+ errorText = findViewById(R.id.error_text);
+ resetPasswordLink = findViewById(R.id.reset_password_link);
+
+ loginButton.setOnClickListener(v -> handleLogin());
+ signupButton.setOnClickListener(v -> handleSignup());
+ resetPasswordLink.setOnClickListener(v -> handleResetPassword());
+
+ authViewModel.getAuthState().observe(this, isAuthenticated -> {
+ if (isAuthenticated) {
+ startActivity(new Intent(this, MainActivity.class));
+ finish();
+ }
+ });
+
+ authViewModel.getErrorMessage().observe(this, error -> {
+ if (error != null && !error.isEmpty()) {
+ errorText.setText(error);
+ errorText.setVisibility(View.VISIBLE);
+ } else {
+ errorText.setVisibility(View.GONE);
+ }
+ });
+
+ mAuth = FirebaseAuth.getInstance();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ FirebaseUser currentUser = mAuth.getCurrentUser();
+ if (currentUser != null) {
+ startActivity(new Intent(this, MainActivity.class));
+ finish();
+ }
+ }
+
+ private void handleLogin() {
+ String username = usernameInput.getText().toString().trim();
+ String password = passwordInput.getText().toString().trim();
+
+ if (validateInput(username, password)) {
+ authViewModel.signIn(username + "@pineapple.com", password);
+ }
+ }
+
+ private void handleSignup() {
+ String username = usernameInput.getText().toString().trim();
+ String password = passwordInput.getText().toString().trim();
+
+ if (validateInput(username, password)) {
+ authViewModel.signUp(username + "@pineapple.com", password);
+ }
+ }
+
+ private void handleResetPassword() {
+ String email = usernameInput.getText().toString().trim();
+
+ if (email.isEmpty()) {
+ errorText.setText("Please enter your email");
+ errorText.setVisibility(View.VISIBLE);
+ } else if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
+ errorText.setText("Invalid email format");
+ errorText.setVisibility(View.VISIBLE);
+ } else {
+ authViewModel.resetPassword(email);
+ }
+
+ }
+
+
+ private boolean validateInput(String username, String password) {
+ if (username.isEmpty()) {
+ errorText.setText("Username cannot be empty");
+ errorText.setVisibility(View.VISIBLE);
+ return false;
+ }
+
+ if (password.isEmpty()) {
+ errorText.setText("Password cannot be empty");
+ errorText.setVisibility(View.VISIBLE);
+ return false;
+ }
+
+ if (password.length() < 6) {
+ errorText.setText("Password must be at least 6 characters");
+ errorText.setVisibility(View.VISIBLE);
+ return false;
+ }
+
+ errorText.setVisibility(View.GONE);
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pineapple/capture/auth/AuthManager.java b/app/src/main/java/com/pineapple/capture/auth/AuthManager.java
new file mode 100644
index 0000000..15414e9
--- /dev/null
+++ b/app/src/main/java/com/pineapple/capture/auth/AuthManager.java
@@ -0,0 +1,87 @@
+package com.pineapple.capture.auth;
+
+import com.google.android.gms.tasks.Task;
+import com.google.firebase.auth.AuthResult;
+import com.google.firebase.auth.FirebaseAuth;
+import com.google.firebase.auth.FirebaseUser;
+import com.google.firebase.auth.UserProfileChangeRequest;
+import com.google.firebase.firestore.FirebaseFirestore;
+import com.pineapple.capture.models.User;
+
+public class AuthManager {
+ private static AuthManager instance;
+ private final FirebaseAuth auth;
+ private final FirebaseFirestore db;
+ private static final String USERS_COLLECTION = "users";
+
+ private AuthManager() {
+ auth = FirebaseAuth.getInstance();
+ db = FirebaseFirestore.getInstance();
+ }
+
+ public static synchronized AuthManager getInstance() {
+ if (instance == null) {
+ instance = new AuthManager();
+ }
+ return instance;
+ }
+
+ public interface UsernameUniqueCallback {
+ void onResult(boolean isUnique);
+ }
+
+ public void isUsernameUnique(String username, UsernameUniqueCallback callback) {
+ db.collection(USERS_COLLECTION)
+ .whereEqualTo("username", username)
+ .get()
+ .addOnSuccessListener(queryDocumentSnapshots -> {
+ callback.onResult(queryDocumentSnapshots.isEmpty());
+ })
+ .addOnFailureListener(e -> {
+ // On failure, treat as not unique to be safe
+ callback.onResult(false);
+ });
+ }
+
+ // Updated signup to accept username
+ public Task signup(String displayName, String username, String email, String password) {
+ return auth.createUserWithEmailAndPassword(email, password)
+ .addOnSuccessListener(authResult -> {
+ FirebaseUser firebaseUser = authResult.getUser();
+ if (firebaseUser != null) {
+ UserProfileChangeRequest profileUpdates = new UserProfileChangeRequest.Builder()
+ .setDisplayName(displayName)
+ .build();
+ firebaseUser.updateProfile(profileUpdates);
+
+ User user = new User(
+ firebaseUser.getUid(),
+ username,
+ email,
+ null,
+ displayName,
+ "",
+ ""
+ );
+ db.collection(USERS_COLLECTION)
+ .document(firebaseUser.getUid())
+ .set(user);
+ }
+ });
+ }
+
+ public Task login(String email, String password) {
+ return auth.signInWithEmailAndPassword(email, password);
+ }
+
+
+ public FirebaseUser getCurrentUser() {
+ return auth.getCurrentUser();
+ }
+
+
+ public Task sendPasswordResetEmail(String email) {
+ return auth.sendPasswordResetEmail(email);
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pineapple/capture/auth/AuthViewModel.java b/app/src/main/java/com/pineapple/capture/auth/AuthViewModel.java
new file mode 100644
index 0000000..3938587
--- /dev/null
+++ b/app/src/main/java/com/pineapple/capture/auth/AuthViewModel.java
@@ -0,0 +1,98 @@
+package com.pineapple.capture.auth;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import com.google.firebase.auth.FirebaseAuth;
+import com.google.firebase.auth.FirebaseUser;
+import com.google.firebase.firestore.FirebaseFirestore;
+import com.pineapple.capture.profile.UserProfile;
+
+public class AuthViewModel extends ViewModel {
+ private FirebaseAuth auth;
+ private FirebaseFirestore db;
+ private MutableLiveData authState;
+ private MutableLiveData errorMessage;
+
+ public AuthViewModel() {
+ auth = FirebaseAuth.getInstance();
+ db = FirebaseFirestore.getInstance();
+ authState = new MutableLiveData<>();
+ errorMessage = new MutableLiveData<>();
+ }
+
+ public void signIn(String email, String password) {
+ auth.signInWithEmailAndPassword(email, password)
+ .addOnSuccessListener(authResult -> authState.setValue(true))
+ .addOnFailureListener(e -> errorMessage.setValue("Invalid username or password"));
+ }
+
+ public void signUp(String email, String password) {
+ String username = email.replace("@pineapple.com", "");
+
+ // Check if username already exists
+ db.collection("usernames")
+ .document(username)
+ .get()
+ .addOnSuccessListener(document -> {
+ if (document.exists()) {
+ errorMessage.setValue("Username already taken");
+ } else {
+ auth.createUserWithEmailAndPassword(email, password)
+ .addOnSuccessListener(authResult -> {
+ FirebaseUser user = authResult.getUser();
+ if (user != null) {
+ // user profile
+ UserProfile profile = new UserProfile(username, "");
+ db.collection("users")
+ .document(user.getUid())
+ .set(profile)
+ .addOnSuccessListener(aVoid -> {
+ // Reserve username
+ db.collection("usernames")
+ .document(username)
+ .set(new UsernameReservation(user.getUid()))
+ .addOnSuccessListener(aVoid2 -> authState.setValue(true))
+ .addOnFailureListener(e -> errorMessage.setValue("Failed to create user profile"));
+ })
+ .addOnFailureListener(e -> errorMessage.setValue("Failed to create user profile"));
+ }
+ })
+ .addOnFailureListener(e -> errorMessage.setValue("Failed to create account"));
+ }
+ })
+ .addOnFailureListener(e -> errorMessage.setValue("Failed to check username availability"));
+ }
+
+ public void resetPassword(String email) {
+ auth.sendPasswordResetEmail(email)
+ .addOnSuccessListener(aVoid -> authState.setValue(true))
+ .addOnFailureListener(e -> errorMessage.setValue("Failed to send reset email"));
+ }
+
+ public LiveData getAuthState() {
+ return authState;
+ }
+
+ public LiveData getErrorMessage() {
+ return errorMessage;
+ }
+
+ public FirebaseUser getCurrentUser() {
+ return auth.getCurrentUser();
+ }
+
+ private static class UsernameReservation {
+ private String userId;
+
+ public UsernameReservation() {}
+
+ public UsernameReservation(String userId) {
+ this.userId = userId;
+ }
+
+ public String getUserId() { return userId; }
+ public void setUserId(String userId) { this.userId = userId; }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pineapple/capture/feed/FeedItem.java b/app/src/main/java/com/pineapple/capture/feed/FeedItem.java
new file mode 100644
index 0000000..bd444fb
--- /dev/null
+++ b/app/src/main/java/com/pineapple/capture/feed/FeedItem.java
@@ -0,0 +1,175 @@
+package com.pineapple.capture.feed;
+
+import com.google.firebase.Timestamp;
+import com.google.firebase.firestore.Exclude;
+import com.google.firebase.firestore.ServerTimestamp;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class FeedItem {
+ private String id;
+ private String userId;
+ private String content;
+ private String imageUrl;
+
+ @ServerTimestamp
+ private Timestamp timestamp;
+
+ private int likes;
+ private List likedBy;
+ private List