diff --git a/.DS_Store b/.DS_Store
index 5008ddf..c2b0b83 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index c842498..c65a0c2 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -20,11 +20,38 @@ jobs:
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
+ run: ./gradlew build --stacktrace
- name: Run Tests
- run: ./gradlew test
\ No newline at end of file
+ run: ./gradlew test
diff --git a/.gitignore b/.gitignore
index ab485f9..ec8a5ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
*.iml
.gradle
+.vscode
/local.properties
/.idea
.DS_Store
@@ -20,7 +21,10 @@ out/
# Gradle files
.gradle/
-build/
+.gradle/**/*
+
+# IDE files
+.vscode/
# Local configuration file (sdk path, etc)
local.properties
@@ -59,4 +63,20 @@ vcs.xml
lint/intermediates/
lint/generated/
lint/outputs/
-lint/tmp/
\ No newline at end of file
+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/.gradle/8.10.2/checksums/checksums.lock b/.gradle/8.10.2/checksums/checksums.lock
deleted file mode 100644
index 526040c..0000000
Binary files a/.gradle/8.10.2/checksums/checksums.lock and /dev/null differ
diff --git a/.gradle/8.10.2/checksums/md5-checksums.bin b/.gradle/8.10.2/checksums/md5-checksums.bin
deleted file mode 100644
index 27911fe..0000000
Binary files a/.gradle/8.10.2/checksums/md5-checksums.bin and /dev/null differ
diff --git a/.gradle/8.10.2/checksums/sha1-checksums.bin b/.gradle/8.10.2/checksums/sha1-checksums.bin
deleted file mode 100644
index 017772f..0000000
Binary files a/.gradle/8.10.2/checksums/sha1-checksums.bin and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidPluginAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidPluginAccessors.class
deleted file mode 100644
index 6715f29..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidPluginAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxEspressoLibraryAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxEspressoLibraryAccessors.class
deleted file mode 100644
index 5219695..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxEspressoLibraryAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxLibraryAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxLibraryAccessors.class
deleted file mode 100644
index 122b429..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$AndroidxLibraryAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$BundleAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$BundleAccessors.class
deleted file mode 100644
index c4caf0b..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$BundleAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$PluginAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$PluginAccessors.class
deleted file mode 100644
index 17b0402..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$PluginAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$VersionAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$VersionAccessors.class
deleted file mode 100644
index f351234..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs$VersionAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs.class
deleted file mode 100644
index a46d0ba..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibs.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidPluginAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidPluginAccessors.class
deleted file mode 100644
index b62b535..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidPluginAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxEspressoLibraryAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxEspressoLibraryAccessors.class
deleted file mode 100644
index 8c8bd88..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxEspressoLibraryAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxLibraryAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxLibraryAccessors.class
deleted file mode 100644
index d42a79f..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$AndroidxLibraryAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$BundleAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$BundleAccessors.class
deleted file mode 100644
index efc10f1..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$BundleAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$PluginAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$PluginAccessors.class
deleted file mode 100644
index cc6d433..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$PluginAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$VersionAccessors.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$VersionAccessors.class
deleted file mode 100644
index 40ca415..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock$VersionAccessors.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.class b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.class
deleted file mode 100644
index 11a86ad..0000000
Binary files a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/classes/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.class and /dev/null differ
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/metadata.bin b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/metadata.bin
deleted file mode 100644
index a2fbd1d..0000000
--- a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/metadata.bin
+++ /dev/null
@@ -1 +0,0 @@
-›obb6ye2mprgjphhpsvy2ytelmq67Oø³Ï·¦ç[š'TŸˆclassesŒşJ˜Ú h*¥={•+ˆsources Pÿlı
ím¹%Ò¶º;å
\ No newline at end of file
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/sources/org/gradle/accessors/dm/LibrariesForLibs.java b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/sources/org/gradle/accessors/dm/LibrariesForLibs.java
deleted file mode 100644
index 711b517..0000000
--- a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/sources/org/gradle/accessors/dm/LibrariesForLibs.java
+++ /dev/null
@@ -1,273 +0,0 @@
-package org.gradle.accessors.dm;
-
-import org.gradle.api.NonNullApi;
-import org.gradle.api.artifacts.MinimalExternalModuleDependency;
-import org.gradle.plugin.use.PluginDependency;
-import org.gradle.api.artifacts.ExternalModuleDependencyBundle;
-import org.gradle.api.artifacts.MutableVersionConstraint;
-import org.gradle.api.provider.Provider;
-import org.gradle.api.model.ObjectFactory;
-import org.gradle.api.provider.ProviderFactory;
-import org.gradle.api.internal.catalog.AbstractExternalDependencyFactory;
-import org.gradle.api.internal.catalog.DefaultVersionCatalog;
-import java.util.Map;
-import org.gradle.api.internal.attributes.ImmutableAttributesFactory;
-import org.gradle.api.internal.artifacts.dsl.CapabilityNotationParser;
-import javax.inject.Inject;
-
-/**
- * A catalog of dependencies accessible via the {@code libs} extension.
- */
-@NonNullApi
-public class LibrariesForLibs extends AbstractExternalDependencyFactory {
-
- private final AbstractExternalDependencyFactory owner = this;
- private final AndroidxLibraryAccessors laccForAndroidxLibraryAccessors = new AndroidxLibraryAccessors(owner);
- private final VersionAccessors vaccForVersionAccessors = new VersionAccessors(providers, config);
- private final BundleAccessors baccForBundleAccessors = new BundleAccessors(objects, providers, config, attributesFactory, capabilityNotationParser);
- private final PluginAccessors paccForPluginAccessors = new PluginAccessors(providers, config);
-
- @Inject
- public LibrariesForLibs(DefaultVersionCatalog config, ProviderFactory providers, ObjectFactory objects, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) {
- super(config, providers, objects, attributesFactory, capabilityNotationParser);
- }
-
- /**
- * Dependency provider for junit with junit:junit coordinates and
- * with version reference junit
- *
- * This dependency was declared in catalog libs.versions.toml
- */
- public Provider getJunit() {
- return create("junit");
- }
-
- /**
- * Dependency provider for material with com.google.android.material:material coordinates and
- * with version reference material
- *
- * This dependency was declared in catalog libs.versions.toml
- */
- public Provider getMaterial() {
- return create("material");
- }
-
- /**
- * Group of libraries at androidx
- */
- public AndroidxLibraryAccessors getAndroidx() {
- return laccForAndroidxLibraryAccessors;
- }
-
- /**
- * Group of versions at versions
- */
- public VersionAccessors getVersions() {
- return vaccForVersionAccessors;
- }
-
- /**
- * Group of bundles at bundles
- */
- public BundleAccessors getBundles() {
- return baccForBundleAccessors;
- }
-
- /**
- * Group of plugins at plugins
- */
- public PluginAccessors getPlugins() {
- return paccForPluginAccessors;
- }
-
- public static class AndroidxLibraryAccessors extends SubDependencyFactory {
- private final AndroidxEspressoLibraryAccessors laccForAndroidxEspressoLibraryAccessors = new AndroidxEspressoLibraryAccessors(owner);
-
- public AndroidxLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
-
- /**
- * Dependency provider for appcompat with androidx.appcompat:appcompat coordinates and
- * with version reference appcompat
- *
- * This dependency was declared in catalog libs.versions.toml
- */
- public Provider getAppcompat() {
- return create("androidx.appcompat");
- }
-
- /**
- * Dependency provider for constraintlayout with androidx.constraintlayout:constraintlayout coordinates and
- * with version reference constraintlayout
- *
- * This dependency was declared in catalog libs.versions.toml
- */
- public Provider getConstraintlayout() {
- return create("androidx.constraintlayout");
- }
-
- /**
- * Dependency provider for core with androidx.core:core coordinates and
- * with version reference core
- *
- * This dependency was declared in catalog libs.versions.toml
- */
- public Provider getCore() {
- return create("androidx.core");
- }
-
- /**
- * Dependency provider for junit with androidx.test.ext:junit coordinates and
- * with version reference junitVersion
- *
- * This dependency was declared in catalog libs.versions.toml
- */
- public Provider getJunit() {
- return create("androidx.junit");
- }
-
- /**
- * Group of libraries at androidx.espresso
- */
- public AndroidxEspressoLibraryAccessors getEspresso() {
- return laccForAndroidxEspressoLibraryAccessors;
- }
-
- }
-
- public static class AndroidxEspressoLibraryAccessors extends SubDependencyFactory {
-
- public AndroidxEspressoLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
-
- /**
- * Dependency provider for core with androidx.test.espresso:espresso-core coordinates and
- * with version reference espressoCore
- *
- * This dependency was declared in catalog libs.versions.toml
- */
- public Provider getCore() {
- return create("androidx.espresso.core");
- }
-
- }
-
- public static class VersionAccessors extends VersionFactory {
-
- public VersionAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
-
- /**
- * Version alias agp with value 8.8.2
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getAgp() { return getVersion("agp"); }
-
- /**
- * Version alias appcompat with value 1.6.1
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getAppcompat() { return getVersion("appcompat"); }
-
- /**
- * Version alias constraintlayout with value 2.1.4
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getConstraintlayout() { return getVersion("constraintlayout"); }
-
- /**
- * Version alias core with value 1.12.0
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getCore() { return getVersion("core"); }
-
- /**
- * Version alias espressoCore with value 3.6.1
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getEspressoCore() { return getVersion("espressoCore"); }
-
- /**
- * Version alias junit with value 4.13.2
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getJunit() { return getVersion("junit"); }
-
- /**
- * Version alias junitVersion with value 1.2.1
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getJunitVersion() { return getVersion("junitVersion"); }
-
- /**
- * Version alias material with value 1.11.0
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getMaterial() { return getVersion("material"); }
-
- }
-
- public static class BundleAccessors extends BundleFactory {
-
- public BundleAccessors(ObjectFactory objects, ProviderFactory providers, DefaultVersionCatalog config, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) { super(objects, providers, config, attributesFactory, capabilityNotationParser); }
-
- }
-
- public static class PluginAccessors extends PluginFactory {
- private final AndroidPluginAccessors paccForAndroidPluginAccessors = new AndroidPluginAccessors(providers, config);
-
- public PluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
-
- /**
- * Group of plugins at plugins.android
- */
- public AndroidPluginAccessors getAndroid() {
- return paccForAndroidPluginAccessors;
- }
-
- }
-
- public static class AndroidPluginAccessors extends PluginFactory {
-
- public AndroidPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
-
- /**
- * Plugin provider for android.application with plugin id com.android.application and
- * with version reference agp
- *
- * This plugin was declared in catalog libs.versions.toml
- */
- public Provider getApplication() { return createPlugin("android.application"); }
-
- }
-
-}
diff --git a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/sources/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.java b/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/sources/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.java
deleted file mode 100644
index 32ef50d..0000000
--- a/.gradle/8.10.2/dependencies-accessors/0fe5d5c85eb824cf9669a3c726c6414603295ffa/sources/org/gradle/accessors/dm/LibrariesForLibsInPluginsBlock.java
+++ /dev/null
@@ -1,325 +0,0 @@
-package org.gradle.accessors.dm;
-
-import org.gradle.api.NonNullApi;
-import org.gradle.api.artifacts.MinimalExternalModuleDependency;
-import org.gradle.plugin.use.PluginDependency;
-import org.gradle.api.artifacts.ExternalModuleDependencyBundle;
-import org.gradle.api.artifacts.MutableVersionConstraint;
-import org.gradle.api.provider.Provider;
-import org.gradle.api.model.ObjectFactory;
-import org.gradle.api.provider.ProviderFactory;
-import org.gradle.api.internal.catalog.AbstractExternalDependencyFactory;
-import org.gradle.api.internal.catalog.DefaultVersionCatalog;
-import java.util.Map;
-import org.gradle.api.internal.attributes.ImmutableAttributesFactory;
-import org.gradle.api.internal.artifacts.dsl.CapabilityNotationParser;
-import javax.inject.Inject;
-
-/**
- * A catalog of dependencies accessible via the {@code libs} extension.
- */
-@NonNullApi
-public class LibrariesForLibsInPluginsBlock extends AbstractExternalDependencyFactory {
-
- private final AbstractExternalDependencyFactory owner = this;
- private final AndroidxLibraryAccessors laccForAndroidxLibraryAccessors = new AndroidxLibraryAccessors(owner);
- private final VersionAccessors vaccForVersionAccessors = new VersionAccessors(providers, config);
- private final BundleAccessors baccForBundleAccessors = new BundleAccessors(objects, providers, config, attributesFactory, capabilityNotationParser);
- private final PluginAccessors paccForPluginAccessors = new PluginAccessors(providers, config);
-
- @Inject
- public LibrariesForLibsInPluginsBlock(DefaultVersionCatalog config, ProviderFactory providers, ObjectFactory objects, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) {
- super(config, providers, objects, attributesFactory, capabilityNotationParser);
- }
-
- /**
- * Dependency provider for junit with junit:junit coordinates and
- * with version reference junit
- *
- * This dependency was declared in catalog libs.versions.toml
- *
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public Provider getJunit() {
- org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
- return create("junit");
- }
-
- /**
- * Dependency provider for material with com.google.android.material:material coordinates and
- * with version reference material
- *
- * This dependency was declared in catalog libs.versions.toml
- *
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public Provider getMaterial() {
- org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
- return create("material");
- }
-
- /**
- * Group of libraries at androidx
- *
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public AndroidxLibraryAccessors getAndroidx() {
- org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
- return laccForAndroidxLibraryAccessors;
- }
-
- /**
- * Group of versions at versions
- */
- public VersionAccessors getVersions() {
- return vaccForVersionAccessors;
- }
-
- /**
- * Group of bundles at bundles
- *
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public BundleAccessors getBundles() {
- org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
- return baccForBundleAccessors;
- }
-
- /**
- * Group of plugins at plugins
- */
- public PluginAccessors getPlugins() {
- return paccForPluginAccessors;
- }
-
- /**
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public static class AndroidxLibraryAccessors extends SubDependencyFactory {
- private final AndroidxEspressoLibraryAccessors laccForAndroidxEspressoLibraryAccessors = new AndroidxEspressoLibraryAccessors(owner);
-
- public AndroidxLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
-
- /**
- * Dependency provider for appcompat with androidx.appcompat:appcompat coordinates and
- * with version reference appcompat
- *
- * This dependency was declared in catalog libs.versions.toml
- *
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public Provider getAppcompat() {
- org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
- return create("androidx.appcompat");
- }
-
- /**
- * Dependency provider for constraintlayout with androidx.constraintlayout:constraintlayout coordinates and
- * with version reference constraintlayout
- *
- * This dependency was declared in catalog libs.versions.toml
- *
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public Provider getConstraintlayout() {
- org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
- return create("androidx.constraintlayout");
- }
-
- /**
- * Dependency provider for core with androidx.core:core coordinates and
- * with version reference core
- *
- * This dependency was declared in catalog libs.versions.toml
- *
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public Provider getCore() {
- org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
- return create("androidx.core");
- }
-
- /**
- * Dependency provider for junit with androidx.test.ext:junit coordinates and
- * with version reference junitVersion
- *
- * This dependency was declared in catalog libs.versions.toml
- *
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public Provider getJunit() {
- org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
- return create("androidx.junit");
- }
-
- /**
- * Group of libraries at androidx.espresso
- *
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public AndroidxEspressoLibraryAccessors getEspresso() {
- org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
- return laccForAndroidxEspressoLibraryAccessors;
- }
-
- }
-
- /**
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public static class AndroidxEspressoLibraryAccessors extends SubDependencyFactory {
-
- public AndroidxEspressoLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
-
- /**
- * Dependency provider for core with androidx.test.espresso:espresso-core coordinates and
- * with version reference espressoCore
- *
- * This dependency was declared in catalog libs.versions.toml
- *
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public Provider getCore() {
- org.gradle.internal.deprecation.DeprecationLogger.deprecateBehaviour("Accessing libraries or bundles from version catalogs in the plugins block.").withAdvice("Only use versions or plugins from catalogs in the plugins block.").willBeRemovedInGradle9().withUpgradeGuideSection(8, "kotlin_dsl_deprecated_catalogs_plugins_block").nagUser();
- return create("androidx.espresso.core");
- }
-
- }
-
- public static class VersionAccessors extends VersionFactory {
-
- public VersionAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
-
- /**
- * Version alias agp with value 8.8.2
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getAgp() { return getVersion("agp"); }
-
- /**
- * Version alias appcompat with value 1.6.1
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getAppcompat() { return getVersion("appcompat"); }
-
- /**
- * Version alias constraintlayout with value 2.1.4
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getConstraintlayout() { return getVersion("constraintlayout"); }
-
- /**
- * Version alias core with value 1.12.0
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getCore() { return getVersion("core"); }
-
- /**
- * Version alias espressoCore with value 3.6.1
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getEspressoCore() { return getVersion("espressoCore"); }
-
- /**
- * Version alias junit with value 4.13.2
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getJunit() { return getVersion("junit"); }
-
- /**
- * Version alias junitVersion with value 1.2.1
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getJunitVersion() { return getVersion("junitVersion"); }
-
- /**
- * Version alias material with value 1.11.0
- *
- * If the version is a rich version and cannot be represented as a
- * single version string, an empty string is returned.
- *
- * This version was declared in catalog libs.versions.toml
- */
- public Provider getMaterial() { return getVersion("material"); }
-
- }
-
- /**
- * @deprecated Will be removed in Gradle 9.0.
- */
- @Deprecated
- public static class BundleAccessors extends BundleFactory {
-
- public BundleAccessors(ObjectFactory objects, ProviderFactory providers, DefaultVersionCatalog config, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) { super(objects, providers, config, attributesFactory, capabilityNotationParser); }
-
- }
-
- public static class PluginAccessors extends PluginFactory {
- private final AndroidPluginAccessors paccForAndroidPluginAccessors = new AndroidPluginAccessors(providers, config);
-
- public PluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
-
- /**
- * Group of plugins at plugins.android
- */
- public AndroidPluginAccessors getAndroid() {
- return paccForAndroidPluginAccessors;
- }
-
- }
-
- public static class AndroidPluginAccessors extends PluginFactory {
-
- public AndroidPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
-
- /**
- * Plugin provider for android.application with plugin id com.android.application and
- * with version reference agp
- *
- * This plugin was declared in catalog libs.versions.toml
- */
- public Provider getApplication() { return createPlugin("android.application"); }
-
- }
-
-}
diff --git a/.gradle/8.10.2/dependencies-accessors/gc.properties b/.gradle/8.10.2/dependencies-accessors/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/.gradle/8.10.2/executionHistory/executionHistory.lock b/.gradle/8.10.2/executionHistory/executionHistory.lock
deleted file mode 100644
index 33951f4..0000000
Binary files a/.gradle/8.10.2/executionHistory/executionHistory.lock and /dev/null differ
diff --git a/.gradle/8.10.2/fileChanges/last-build.bin b/.gradle/8.10.2/fileChanges/last-build.bin
deleted file mode 100644
index f76dd23..0000000
Binary files a/.gradle/8.10.2/fileChanges/last-build.bin and /dev/null differ
diff --git a/.gradle/8.10.2/fileHashes/fileHashes.bin b/.gradle/8.10.2/fileHashes/fileHashes.bin
deleted file mode 100644
index 6d960d2..0000000
Binary files a/.gradle/8.10.2/fileHashes/fileHashes.bin and /dev/null differ
diff --git a/.gradle/8.10.2/fileHashes/fileHashes.lock b/.gradle/8.10.2/fileHashes/fileHashes.lock
deleted file mode 100644
index 266846e..0000000
Binary files a/.gradle/8.10.2/fileHashes/fileHashes.lock and /dev/null differ
diff --git a/.gradle/8.10.2/gc.properties b/.gradle/8.10.2/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock
deleted file mode 100644
index 1024ecc..0000000
Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ
diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties
deleted file mode 100644
index e71494c..0000000
--- a/.gradle/buildOutputCleanup/cache.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-#Mon Mar 31 11:26:32 CEST 2025
-gradle.version=8.10.2
diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe
deleted file mode 100644
index 4096145..0000000
Binary files a/.gradle/file-system.probe and /dev/null differ
diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/README.md b/README.md
index 1c77935..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
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 ce5eb55..7026fa2 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -3,6 +3,14 @@ plugins {
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 {
namespace 'com.pineapple.capture'
compileSdk 35
@@ -15,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 {
@@ -28,6 +41,7 @@ android {
targetCompatibility JavaVersion.VERSION_17
}
buildFeatures {
+ buildConfig true
viewBinding true
}
}
@@ -37,15 +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 platform('com.google.firebase:firebase-bom:32.7.4')
- implementation 'com.google.firebase:firebase-auth'
- implementation 'com.google.firebase:firebase-firestore'
- implementation 'com.google.firebase:firebase-storage'
+ 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..0d53ad3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,27 +2,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
index c0972e8..f58ae3c 100644
--- a/app/src/main/java/com/pineapple/capture/auth/AuthActivity.java
+++ b/app/src/main/java/com/pineapple/capture/auth/AuthActivity.java
@@ -4,12 +4,16 @@
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;
-import com.pineapple.capture.feed.MainFeedActivity;
+
public class AuthActivity extends AppCompatActivity {
private AuthViewModel authViewModel;
@@ -18,6 +22,8 @@ public class AuthActivity extends AppCompatActivity {
private MaterialButton loginButton;
private MaterialButton signupButton;
private TextView errorText;
+ private TextView resetPasswordLink;
+ private FirebaseAuth mAuth;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -26,26 +32,24 @@ protected void onCreate(Bundle savedInstanceState) {
authViewModel = new ViewModelProvider(this).get(AuthViewModel.class);
- // Initialize views
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);
-
- // Set up click listeners
+ resetPasswordLink = findViewById(R.id.reset_password_link);
+
loginButton.setOnClickListener(v -> handleLogin());
signupButton.setOnClickListener(v -> handleSignup());
+ resetPasswordLink.setOnClickListener(v -> handleResetPassword());
- // Observe authentication state changes
authViewModel.getAuthState().observe(this, isAuthenticated -> {
if (isAuthenticated) {
- startActivity(new Intent(this, MainFeedActivity.class));
+ startActivity(new Intent(this, MainActivity.class));
finish();
}
});
- // Observe error messages
authViewModel.getErrorMessage().observe(this, error -> {
if (error != null && !error.isEmpty()) {
errorText.setText(error);
@@ -54,6 +58,18 @@ protected void onCreate(Bundle savedInstanceState) {
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() {
@@ -73,6 +89,22 @@ private void handleSignup() {
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()) {
@@ -96,4 +128,5 @@ private boolean validateInput(String username, String password) {
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
index b4029a7..3938587 100644
--- a/app/src/main/java/com/pineapple/capture/auth/AuthViewModel.java
+++ b/app/src/main/java/com/pineapple/capture/auth/AuthViewModel.java
@@ -38,12 +38,11 @@ public void signUp(String email, String password) {
if (document.exists()) {
errorMessage.setValue("Username already taken");
} else {
- // Create new user
auth.createUserWithEmailAndPassword(email, password)
.addOnSuccessListener(authResult -> {
FirebaseUser user = authResult.getUser();
if (user != null) {
- // Create user profile
+ // user profile
UserProfile profile = new UserProfile(username, "");
db.collection("users")
.document(user.getUid())
@@ -65,9 +64,10 @@ public void signUp(String email, String password) {
.addOnFailureListener(e -> errorMessage.setValue("Failed to check username availability"));
}
- public void signOut() {
- auth.signOut();
- authState.setValue(false);
+ 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() {
@@ -93,5 +93,6 @@ public UsernameReservation(String 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
index 19414fa..bd444fb 100644
--- a/app/src/main/java/com/pineapple/capture/feed/FeedItem.java
+++ b/app/src/main/java/com/pineapple/capture/feed/FeedItem.java
@@ -1,30 +1,108 @@
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