diff --git a/FireMarkers/.gitignore b/FireMarkers/.gitignore new file mode 100644 index 00000000..3cc7d265 --- /dev/null +++ b/FireMarkers/.gitignore @@ -0,0 +1,34 @@ +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Secrets +secrets.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.aab +*.apk +output-metadata.json + +# IntelliJ +*.iml +.idea/ + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof \ No newline at end of file diff --git a/FireMarkers/ARCHITECTURE.md b/FireMarkers/ARCHITECTURE.md new file mode 100644 index 00000000..68d15bad --- /dev/null +++ b/FireMarkers/ARCHITECTURE.md @@ -0,0 +1,92 @@ +# FireMarkers Architecture + +This document provides a visual overview of the FireMarkers application's architecture. The core of the app is a **controller/agent synchronization pattern** that uses Firebase Realtime Database to keep multiple devices in sync. + +## Architecture Diagram + +```mermaid +flowchart TD + subgraph "Firebase Cloud" + MarkersDB[(Firebase Realtime Database\n/markers)] + AnimationDB[(Firebase Realtime Database\n/animation)] + end + + subgraph "Android App" + subgraph "UI Layer (View)" + A[MainActivity] --> B(MapScreen Composable); + B -- User Clicks --> C["TopAppBar Actions
(toggleAnimation, seedDatabase, clearMarkers, takeControl)"]; + B -- Renders Markers --> D[GoogleMap Composable]; + end + + subgraph "State & Logic Layer (ViewModel)" + VM[MarkersViewModel]; + end + + subgraph "Data Layer" + FC[FirebaseConnection]; + SD[ShapeData]; + M(MarkerData Model); + end + + subgraph "Dependency Injection" + Hilt(Hilt/Dagger); + end + end + + %% --- Interactions --- + C -- Calls function --> VM; + VM -- Uses --> FC; + VM -- Reads shape vectors --> SD; + + %% Controller writes to Firebase + VM -- Writes animation state --> AnimationDB; + VM -- Writes marker data --> MarkersDB; + + %% All clients listen for real-time updates + MarkersDB -.->|Real-time updates| FC; + AnimationDB -.->|Real-time updates| FC; + + FC -.-> VM; + VM -- Updates StateFlow --> B; + VM -- Uses Model --> M; + + %% --- DI Graph --- + Hilt -- Injects --> FC; + Hilt -- Injects --> VM; + + %% --- Styling --- + style A fill:#cde,stroke:#333,stroke-width:2px + style B fill:#cde,stroke:#333,stroke-width:2px + style C fill:#cde,stroke:#333,stroke-width:2px + style D fill:#cde,stroke:#333,stroke-width:2px + style VM fill:#dce,stroke:#333,stroke-width:2px + style FC fill:#edc,stroke:#333,stroke-width:2px + style SD fill:#edc,stroke:#333,stroke-width:2px + style M fill:#edc,stroke:#333,stroke-width:2px + style MarkersDB fill:#f9d,stroke:#333,stroke-width:2px + style AnimationDB fill:#f9d,stroke:#333,stroke-width:2px + style Hilt fill:#eee,stroke:#333,stroke-width:2px +``` + +### How it Works + +The application uses a controller/agent model to synchronize animations across devices. + +1. **Controller and Agents:** At any time, only one device is the **controller**. It is responsible for running the animation loop and writing the current animation state (progress, running status) to the `/animation` node in Firebase. All other devices are **agents** that passively listen for changes to this node. +2. **UI Layer:** The `MainActivity` hosts the `MapScreen` composable. The UI in the `TopAppBar` is dynamic: + * If the device is the **controller**, it shows buttons to `toggleAnimation`, `seedDatabase`, and `clearMarkers`. + * If the device is an **agent**, it shows a single button that allows the user to `takeControl`. +3. **ViewModel:** UI interactions call functions on the `MarkersViewModel`. + * If the controller toggles the animation, the ViewModel starts a local animation loop and writes the progress to the `/animation` node in Firebase. + * If an agent requests control, the ViewModel updates the `controllerId` field in the `/animation` node. +4. **Data Layer:** + * The `MarkersViewModel` uses the `FirebaseConnection` service to interact with Firebase. + * The `ShapeData` object provides the static vector coordinates for the jack-o'-lantern and tree shapes. +5. **Real-time Updates:** The `MarkersViewModel` establishes listeners on two Firebase paths: + * `/markers`: When the marker data changes (e.g., after seeding), Firebase pushes the updates to all clients. + * `/animation`: When the animation state changes (written by the controller), Firebase pushes the new state to all agents. +6. **State Flow & Interpolation:** The `MarkersViewModel` uses a `combine` operator on two `StateFlow`s (one for markers, one for animation state). When new data is received from either listener, it recalculates the interpolated position and color for every marker based on the animation progress (`fraction`). +7. **UI Update:** The `MapScreen` composable collects the final `StateFlow` of interpolated marker data. On each new emission, the `GoogleMap` recomposes and smoothly animates the markers to their new positions and colors. +8. **Dependency Injection:** Hilt provides the `FirebaseConnection` as a singleton to the `MarkersViewModel`. + +``` \ No newline at end of file diff --git a/FireMarkers/LICENSE b/FireMarkers/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/FireMarkers/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/FireMarkers/README.md b/FireMarkers/README.md new file mode 100644 index 00000000..6eaca9fb --- /dev/null +++ b/FireMarkers/README.md @@ -0,0 +1,75 @@ +# Realtime, Synchronized Map Animations with Firebase and Google Maps + +This sample demonstrates how to use the Firebase Realtime Database to drive perfectly synchronized, live animations on a Google Map across multiple Android devices. It showcases a **controller/agent architecture** where one device drives the animation state, and all other connected devices act as passive observers, ensuring all users see the same animation at the same time. + +The app displays a set of markers that can animate between two complex shapes: a jack-o'-lantern and a Christmas tree. One device acts as the "controller," with UI controls to start, stop, and reset the animation. All other devices are "agents," with a button to take control. + +| Controller View | Agent View | +|:--------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------| +| Screenshot of the controller UI with animation controls | Screenshot of the agent UI with the 'Take Control' button | + +## Key Concepts Demonstrated + +* **Firebase Realtime Database:** Connecting an Android app to a Firebase Realtime Database. +* **Controller/Agent Synchronization:** A robust pattern where one device ("controller") writes animation state to Firebase, and other devices ("agents") listen for real-time updates to synchronize their UIs. +* **Shared Real-time State:** Using a separate `/animation` node in Firebase to store and sync shared state like animation progress, running status, and the current controller's ID. +* **Dynamic, State-Driven UI:** Using Jetpack Compose to build a UI that dynamically changes its controls based on whether the device is the current controller or an agent. +* **Procedural Animation:** Implementing a client-side animation loop that smoothly interpolates marker properties (latitude, longitude, and color) between two predefined vector shapes. +* **Hilt for Dependency Injection:** Using Hilt to provide a singleton `FirebaseDatabase` instance to the application's ViewModel. + +For a detailed visual guide to the project's structure and data flow, see the [**ARCHITECTURE.md**](ARCHITECTURE.md) file. + +## Getting Started + +This sample uses the Gradle build system. To build the app, use the `gradlew build` command or use "Import Project" in Android Studio. + +### Prerequisites + +* Android Studio (latest version recommended) +* An Android device or emulator with API level 24 or higher +* A Google account to create a Firebase project + +### Setup + +To run this sample, you will need a Firebase project and a Google Maps API key. + +**1. Set up your Firebase Project:** + +The easiest way to connect your app to Firebase is by using the **Firebase Assistant** in Android Studio. + +* In Android Studio, go to **Tools > Firebase**. +* In the Assistant panel, expand **Realtime Database** and follow the on-screen instructions to: + 1. **Connect to Firebase:** This will open a browser for you to log in and either create a new Firebase project or select an existing one. + 2. **Add Realtime Database to your app:** This will automatically add the necessary dependencies to your `build.gradle.kts` file and download the `google-services.json` configuration file into the `app/` directory. + +* For more detailed instructions, see the official guide: [**Add Firebase to your Android project**](https://firebase.google.com/docs/android/setup). + +**2. Configure Database Rules:** + +For this sample to work, your Realtime Database must be configured with public read/write rules. In the Firebase console, navigate to your **Realtime Database** and select the **Rules** tab. Replace the existing rules with the following: + +```json +{ + "rules": { + ".read": "true", + ".write": "true" + } +} +``` +**Note:** These rules are for demonstration purposes only and are **insecure**. They allow anyone to read or write to your database. For a production app, you must implement more restrictive rules to protect your data. See the official guide on [**Securing Realtime Database Rules**](https://firebase.google.com/docs/database/security) for more information. + +**3. Add your Google Maps API Key:** + +The app requires a Google Maps API key to display the map. + +1. Follow the [**Maps SDK for Android documentation**](https://developers.google.com/maps/documentation/android-sdk/get-api-key) to get an API key. +2. Create a file named `secrets.properties` in the project's root directory (at the same level as `local.properties`). +3. Add your API key to the `secrets.properties` file, like this: + ``` + MAPS_API_KEY="YOUR_API_KEY" + ``` + (Replace `YOUR_API_KEY` with the key you obtained). The project is configured to read this key via the Secrets Gradle Plugin. + +> **⚠️ IMPORTANT SECURITY NOTE:** You must prevent your API key from being checked into source control. The included `.gitignore` file is configured to ignore the `secrets.properties` file. **Do not remove this entry.** Committing your API key to a public repository can lead to unauthorized use and result in unexpected charges to your account. + +Once you have completed these steps, you can run the app on your device or emulator. diff --git a/FireMarkers/app/.gitignore b/FireMarkers/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/FireMarkers/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/FireMarkers/app/build.gradle.kts b/FireMarkers/app/build.gradle.kts new file mode 100644 index 00000000..337aad81 --- /dev/null +++ b/FireMarkers/app/build.gradle.kts @@ -0,0 +1,190 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + // --------------------------------------------------------------------------------------------- + // Core App Plugins + // + // These plugins are essential for building an Android application with Kotlin and Jetpack + // Compose. They handle the core compilation, packaging, and resource processing. + // --------------------------------------------------------------------------------------------- + alias(libs.plugins.android.application) // Applies the core Android application plugin. + alias(libs.plugins.kotlin.android) // Enables Kotlin language support for Android. + alias(libs.plugins.kotlin.compose) // Provides support for the Jetpack Compose compiler. + + // --------------------------------------------------------------------------------------------- + // Dependency Injection + // + // These plugins set up Hilt for dependency injection, which simplifies the management of + // dependencies and improves the app's architecture. KSP (Kotlin Symbol Processing) is used + // by Hilt for efficient code generation. + // --------------------------------------------------------------------------------------------- + alias(libs.plugins.ksp) // Enables the Kotlin Symbol Processing tool. + alias(libs.plugins.hilt.android) // Integrates Hilt for dependency injection. + + // --------------------------------------------------------------------------------------------- + // Utility Plugins + // + // These plugins provide additional functionalities, such as managing API keys with the + // Secrets Gradle Plugin and enabling Kotlin serialization for data conversion. + // --------------------------------------------------------------------------------------------- + alias(libs.plugins.secrets.gradle.plugin) // Manages secrets and API keys. + alias(libs.plugins.kotlin.serialization) // Provides Kotlin serialization capabilities. + id("com.google.gms.google-services") // Integrates Google services, like Firebase. +} + +android { + namespace = "com.example.firemarkers" + compileSdk = 36 + + defaultConfig { + applicationId = "com.example.firemarkers" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + buildConfig = true + } + kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) + } + } +} + +dependencies { + // --------------------------------------------------------------------------------------------- + // AndroidX & Jetpack + // + // These dependencies from the AndroidX library are fundamental for modern Android development. + // They provide core functionalities, lifecycle-aware components, and the Jetpack Compose UI + // toolkit for building the app's user interface. + // --------------------------------------------------------------------------------------------- + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.material.icons.extended) + implementation(libs.androidx.hilt.navigation.compose) + + // --------------------------------------------------------------------------------------------- + // Dependency Injection + // + // Hilt is used for dependency injection, which helps in managing the object graph and + // providing dependencies to different parts of the application. + // --------------------------------------------------------------------------------------------- + implementation(libs.dagger) + ksp(libs.hilt.android.compiler) + implementation(libs.hilt.android) + + // --------------------------------------------------------------------------------------------- + // Firebase + // + // The Firebase Bill of Materials (BOM) ensures that all Firebase libraries use compatible + // versions. The `firebase-database` dependency is included for real-time data synchronization. + // --------------------------------------------------------------------------------------------- + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.database) + + // --------------------------------------------------------------------------------------------- + // Google Maps + // + // These dependencies provide the necessary components for integrating Google Maps into the + // application using Jetpack Compose. This includes the Maps Compose library, extended material + // icons, and navigation components for Compose. + // --------------------------------------------------------------------------------------------- + implementation(libs.maps.compose) + implementation(libs.maps.utils.ktx) + + // --------------------------------------------------------------------------------------------- + // Kotlin Libraries + // + // Provides additional Kotlin functionalities, such as the `kotlinx-datetime` library for + // handling dates and times in a more idiomatic and multiplatform-friendly way. + // --------------------------------------------------------------------------------------------- + implementation(libs.kotlinx.datetime) + + // --------------------------------------------------------------------------------------------- + // Unit Testing + // + // These dependencies are used for running local unit tests on the JVM. They include JUnit for + // the core testing framework, Robolectric for running Android-specific code without an + // emulator, and coroutines-test for testing asynchronous code. + // --------------------------------------------------------------------------------------------- + testImplementation(libs.junit) + testImplementation(libs.robolectric) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.google.truth) + testImplementation(libs.mockito.kotlin) + + // --------------------------------------------------------------------------------------------- + // Flow Testing + // + // Turbine is a small testing library for Kotlin Flows. It makes it easy to test Flows by + // providing a simple and concise API for asserting on the items that a Flow emits. This is + // particularly useful for testing ViewModels that expose data using StateFlow or SharedFlow. + // --------------------------------------------------------------------------------------------- + testImplementation(libs.turbine) + + // --------------------------------------------------------------------------------------------- + // Instrumentation Testing + // + // These dependencies are for running tests on an Android device or emulator. They include + // AndroidX Test libraries for JUnit and Espresso, as well as Compose-specific testing tools + // for UI testing. + // --------------------------------------------------------------------------------------------- + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + + // --------------------------------------------------------------------------------------------- + // Debug Tools + // + // These dependencies are only included in the debug build. They provide tools for inspecting + // and debugging the application's UI and other components. + // --------------------------------------------------------------------------------------------- + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} + +secrets { + propertiesFileName = "secrets.properties" + defaultPropertiesFileName = "local.defaults.properties" +} \ No newline at end of file diff --git a/FireMarkers/app/proguard-rules.pro b/FireMarkers/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/FireMarkers/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/FireMarkers/app/src/main/AndroidManifest.xml b/FireMarkers/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..905a35aa --- /dev/null +++ b/FireMarkers/app/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FireMarkers/app/src/main/java/com/example/firemarkers/FireMarkersApplication.kt b/FireMarkers/app/src/main/java/com/example/firemarkers/FireMarkersApplication.kt new file mode 100644 index 00000000..2e8dd5bb --- /dev/null +++ b/FireMarkers/app/src/main/java/com/example/firemarkers/FireMarkersApplication.kt @@ -0,0 +1,82 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.example.firemarkers + +import android.app.Application +import android.content.pm.PackageManager +import android.util.Log +import android.widget.Toast +import dagger.hilt.android.HiltAndroidApp +import java.util.Objects +import kotlin.text.isBlank + +/** + * The main application class for the FireMarkers app. + * + * This class is the entry point of the application and is responsible for initializing Hilt for + * dependency injection. + */ +@HiltAndroidApp +class FireMarkersApplication : Application() { + /** + * Called when the application is starting, before any other application objects have been created. + * + * This method is used to initialize the application, including setting up Hilt for dependency + * injection. + */ + override fun onCreate() { + super.onCreate() + checkApiKey() + } + + /** + * Checks if the API key for Google Maps is properly configured in the application's metadata. + * + * This method retrieves the API key from the application's metadata, specifically looking for + * a string value associated with the key "com.google.android.geo.maps.API_KEY". + * The key must be present, not blank, and not set to the placeholder value "DEFAULT_API_KEY". + * + * If any of these checks fail, a Toast message is displayed indicating that the API key is missing or + * incorrectly configured, and a RuntimeException is thrown. + */ + private fun checkApiKey() { + try { + val appInfo = + packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA) + val bundle = Objects.requireNonNull(appInfo.metaData) + + val mapsApiKey = + bundle.getString("com.google.android.geo.API_KEY") // Key name is important! + + if (mapsApiKey == null || mapsApiKey.isBlank() || mapsApiKey == "DEFAULT_API_KEY") { + Toast.makeText( + this, + "Maps API Key was not set in secrets.properties", + Toast.LENGTH_LONG + ).show() + throw RuntimeException("Maps API Key was not set in secrets.properties") + } + } catch (e: PackageManager.NameNotFoundException) { + Log.e(TAG, "Package name not found.", e) + throw RuntimeException("Error getting package info.", e) + } catch (e: NullPointerException) { + Log.e(TAG, "Error accessing meta-data.", e) // Handle the case where meta-data is completely missing. + throw RuntimeException("Error accessing meta-data in manifest", e) + } + } + + companion object { + private val TAG = this::class.java.simpleName + } +} \ No newline at end of file diff --git a/FireMarkers/app/src/main/java/com/example/firemarkers/MainActivity.kt b/FireMarkers/app/src/main/java/com/example/firemarkers/MainActivity.kt new file mode 100644 index 00000000..5658ab24 --- /dev/null +++ b/FireMarkers/app/src/main/java/com/example/firemarkers/MainActivity.kt @@ -0,0 +1,160 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.example.firemarkers + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddLocation +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.sp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.example.firemarkers.ui.theme.FireMarkersTheme +import com.example.firemarkers.viewmodel.MarkersViewModel +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MarkerState +import com.google.maps.android.compose.rememberCameraPositionState +import dagger.hilt.android.AndroidEntryPoint + +import androidx.core.view.WindowCompat +import kotlinx.coroutines.flow.collectLatest + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() + setContent { + FireMarkersTheme { + MapScreen(modifier = Modifier.fillMaxSize()) + } + } + } +} + +/** + * The main screen of the application. This composable function displays the Google Map, + * observes the list of markers from the [MarkersViewModel], and renders them. It also + * provides a dynamic TopAppBar that changes its controls based on whether the current + * device is the "controller" of the animation or an "agent" observing it. + * + * @param modifier The modifier to be applied to the MapScreen. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MapScreen(modifier: Modifier = Modifier) { + val viewModel: MarkersViewModel = hiltViewModel() + val markers by viewModel.markers.collectAsState() + val animationRunning by viewModel.animationRunning.collectAsState() + val hasMarkers by viewModel.hasMarkers.collectAsState() + val isController by viewModel.isController.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + + val boulder = LatLng(40.0150, -105.2705) + val cameraPositionState = rememberCameraPositionState { + position = CameraPosition.fromLatLngZoom(boulder, 10f) + } + + LaunchedEffect(Unit) { + viewModel.errorEvents.collectLatest { message -> + snackbarHostState.showSnackbar(message) + } + } + + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + topBar = { + TopAppBar( + title = { + Text( + "FireMarkers", + fontSize = 20.sp + ) + }, + actions = { + if (isController) { + Text("🎄") // Tree emoji for controller + IconButton( + onClick = { viewModel.toggleAnimation() }, + enabled = hasMarkers + ) { + Icon( + imageVector = if (animationRunning) Icons.Default.Stop else Icons.Default.PlayArrow, + contentDescription = "Animate Shape" + ) + } + IconButton(onClick = { viewModel.seedDatabase() }) { + Icon( + imageVector = Icons.Default.AddLocation, + contentDescription = "Seed Database" + ) + } + IconButton(onClick = { viewModel.clearMarkers() }) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Clear Markers" + ) + } + } else { + // Agent UI + IconButton(onClick = { viewModel.takeControl() }) { + Text("🎃") // Pumpkin emoji for agent + } + } + } + ) + }, + modifier = modifier + ) { innerPadding -> + GoogleMap( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + cameraPositionState = cameraPositionState + ) { + markers.forEach { markerData -> + Marker( + state = MarkerState(position = LatLng(markerData.latitude, markerData.longitude)), + title = markerData.label, + icon = BitmapDescriptorFactory.defaultMarker(markerData.color) + ) + } + } + } +} diff --git a/FireMarkers/app/src/main/java/com/example/firemarkers/data/FirebaseConnectionProvider.kt b/FireMarkers/app/src/main/java/com/example/firemarkers/data/FirebaseConnectionProvider.kt new file mode 100644 index 00000000..2d4086cc --- /dev/null +++ b/FireMarkers/app/src/main/java/com/example/firemarkers/data/FirebaseConnectionProvider.kt @@ -0,0 +1,33 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.example.firemarkers.data + +import com.google.firebase.database.FirebaseDatabase +import javax.inject.Inject +import javax.inject.Singleton + +/** + * A singleton provider for the [FirebaseDatabase] instance. + * + * This class is responsible for creating and providing a single instance of the [FirebaseDatabase] + * throughout the application. It is injected into the [com.example.firemarkers.viewmodel.MarkersViewModel] to provide access to the + * database. + */ +@Singleton +class FirebaseConnection @Inject constructor() { + /** + * The singleton instance of the [FirebaseDatabase]. + */ + val database: FirebaseDatabase = FirebaseDatabase.getInstance() +} diff --git a/FireMarkers/app/src/main/java/com/example/firemarkers/data/ShapeData.kt b/FireMarkers/app/src/main/java/com/example/firemarkers/data/ShapeData.kt new file mode 100644 index 00000000..8d5177cc --- /dev/null +++ b/FireMarkers/app/src/main/java/com/example/firemarkers/data/ShapeData.kt @@ -0,0 +1,186 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.example.firemarkers.data + +import com.google.android.gms.maps.model.BitmapDescriptorFactory + +/** + * A data class to represent a single point in a shape definition. + * @param latOffset The latitude offset from the shape's center. + * @param lonOffset The longitude offset from the shape's center. + * @param color The hue for the marker's color. + */ +data class ShapePoint(val latOffset: Double, val lonOffset: Double, val color: Float) + +/** + * A static object that holds the vector definitions for shapes. + */ +object ShapeData { + + // Base color hues for the shapes + private const val ORANGE_HUE = BitmapDescriptorFactory.HUE_ORANGE + private const val BLACK_HUE = 0f // Black doesn't have a hue, we'll handle it + private const val GREEN_HUE = BitmapDescriptorFactory.HUE_GREEN + private const val BROWN_HUE = 30f // Brownish hue + private const val YELLOW_HUE = BitmapDescriptorFactory.HUE_YELLOW + private const val RED_HUE = BitmapDescriptorFactory.HUE_RED + private const val BLUE_HUE = BitmapDescriptorFactory.HUE_AZURE + + /** + * Defines the points for a Jack-O'-Lantern shape. + * Total points: 56 + */ + val jackOLanternShape: List = listOf( + // Pumpkin Outline (scaled up) + ShapePoint(0.2, 0.0, ORANGE_HUE), + ShapePoint(0.196, 0.04, ORANGE_HUE), + ShapePoint(0.184, 0.08, ORANGE_HUE), + ShapePoint(0.16, 0.12, ORANGE_HUE), + ShapePoint(0.12, 0.16, ORANGE_HUE), + ShapePoint(0.08, 0.184, ORANGE_HUE), + ShapePoint(0.04, 0.196, ORANGE_HUE), + ShapePoint(0.0, 0.2, ORANGE_HUE), + ShapePoint(-0.04, 0.196, ORANGE_HUE), + ShapePoint(-0.08, 0.184, ORANGE_HUE), + ShapePoint(-0.12, 0.16, ORANGE_HUE), + ShapePoint(-0.16, 0.12, ORANGE_HUE), + ShapePoint(-0.184, 0.08, ORANGE_HUE), + ShapePoint(-0.196, 0.04, ORANGE_HUE), + ShapePoint(-0.2, 0.0, ORANGE_HUE), + ShapePoint(-0.196, -0.04, ORANGE_HUE), + ShapePoint(-0.184, -0.08, ORANGE_HUE), + ShapePoint(-0.16, -0.12, ORANGE_HUE), + ShapePoint(-0.12, -0.16, ORANGE_HUE), + ShapePoint(-0.08, -0.184, ORANGE_HUE), + ShapePoint(-0.04, -0.196, ORANGE_HUE), + ShapePoint(0.0, -0.2, ORANGE_HUE), + ShapePoint(0.04, -0.196, ORANGE_HUE), + ShapePoint(0.08, -0.184, ORANGE_HUE), + ShapePoint(0.12, -0.16, ORANGE_HUE), + ShapePoint(0.16, -0.12, ORANGE_HUE), + ShapePoint(0.184, -0.08, ORANGE_HUE), + ShapePoint(0.196, -0.04, ORANGE_HUE), + + // Left Eye + ShapePoint(0.1, -0.1, BLACK_HUE), + ShapePoint(0.04, -0.06, BLACK_HUE), + ShapePoint(0.04, -0.14, BLACK_HUE), + + // Right Eye + ShapePoint(0.1, 0.1, BLACK_HUE), + ShapePoint(0.04, 0.06, BLACK_HUE), + ShapePoint(0.04, 0.14, BLACK_HUE), + + // Nose + ShapePoint(-0.02, 0.0, BLACK_HUE), + ShapePoint(-0.06, -0.04, BLACK_HUE), + ShapePoint(-0.06, 0.04, BLACK_HUE), + + // Mouth with teeth (more points) + ShapePoint(-0.1, -0.12, BLACK_HUE), + ShapePoint(-0.12, -0.1, BLACK_HUE), // down + ShapePoint(-0.1, -0.08, BLACK_HUE), // up (tooth) + ShapePoint(-0.12, -0.06, BLACK_HUE), // down + ShapePoint(-0.1, -0.04, BLACK_HUE), // up (tooth) + ShapePoint(-0.12, -0.02, BLACK_HUE), // down + ShapePoint(-0.1, 0.0, BLACK_HUE), // up (center) + ShapePoint(-0.12, 0.02, BLACK_HUE), // down + ShapePoint(-0.1, 0.04, BLACK_HUE), // up (tooth) + ShapePoint(-0.12, 0.06, BLACK_HUE), // down + ShapePoint(-0.1, 0.08, BLACK_HUE), // up (tooth) + ShapePoint(-0.12, 0.1, BLACK_HUE), // down + ShapePoint(-0.1, 0.12, BLACK_HUE), + ShapePoint(-0.14, 0.0, BLACK_HUE), // Center dip + ShapePoint(-0.15, -0.12, BLACK_HUE), + ShapePoint(-0.15, 0.12, BLACK_HUE), + ShapePoint(0.16, 0.0, ORANGE_HUE), // Extra point to match count + ShapePoint(0.0, 0.0, ORANGE_HUE), // Extra point to match count + ShapePoint(-0.16, 0.0, ORANGE_HUE) // Extra point to match count + ) + + /** + * Defines the points for a Christmas Tree shape. + * Must have the same number of points as jackOLanternShape. + * Total points: 56 + */ + val christmasTreeShape: List = listOf( + // Star Topper (5 points) + ShapePoint(0.22, 0.0, YELLOW_HUE), + ShapePoint(0.19, 0.03, YELLOW_HUE), + ShapePoint(0.19, -0.03, YELLOW_HUE), + ShapePoint(0.17, 0.05, YELLOW_HUE), + ShapePoint(0.17, -0.05, YELLOW_HUE), + + // Top Tier (6 points) + ShapePoint(0.15, 0.0, GREEN_HUE), + ShapePoint(0.12, 0.08, GREEN_HUE), + ShapePoint(0.12, -0.08, GREEN_HUE), + ShapePoint(0.13, 0.04, GREEN_HUE), + ShapePoint(0.13, -0.04, GREEN_HUE), + ShapePoint(0.10, 0.0, GREEN_HUE), + + // Middle Tier (10 points) + ShapePoint(0.08, 0.12, GREEN_HUE), + ShapePoint(0.08, -0.12, GREEN_HUE), + ShapePoint(0.06, 0.08, GREEN_HUE), + ShapePoint(0.06, -0.08, GREEN_HUE), + ShapePoint(0.04, 0.16, GREEN_HUE), + ShapePoint(0.04, -0.16, GREEN_HUE), + ShapePoint(0.02, 0.12, GREEN_HUE), + ShapePoint(0.02, -0.12, GREEN_HUE), + ShapePoint(0.0, 0.08, GREEN_HUE), + ShapePoint(0.0, -0.08, GREEN_HUE), + + // Bottom Tier (14 points) + ShapePoint(-0.02, 0.22, GREEN_HUE), + ShapePoint(-0.02, -0.22, GREEN_HUE), + ShapePoint(-0.04, 0.18, GREEN_HUE), + ShapePoint(-0.04, -0.18, GREEN_HUE), + ShapePoint(-0.06, 0.24, GREEN_HUE), + ShapePoint(-0.06, -0.24, GREEN_HUE), + ShapePoint(-0.08, 0.20, GREEN_HUE), + ShapePoint(-0.08, -0.20, GREEN_HUE), + ShapePoint(-0.1, 0.16, GREEN_HUE), + ShapePoint(-0.1, -0.16, GREEN_HUE), + ShapePoint(-0.12, 0.12, GREEN_HUE), + ShapePoint(-0.12, -0.12, GREEN_HUE), + ShapePoint(-0.14, 0.08, GREEN_HUE), + ShapePoint(-0.14, -0.08, GREEN_HUE), + + // Trunk (4 points) + ShapePoint(-0.16, 0.04, BROWN_HUE), + ShapePoint(-0.16, -0.04, BROWN_HUE), + ShapePoint(-0.2, 0.04, BROWN_HUE), + ShapePoint(-0.2, -0.04, BROWN_HUE), + + // Ornaments (17 points) + ShapePoint(0.11, 0.0, RED_HUE), + ShapePoint(0.07, 0.05, BLUE_HUE), + ShapePoint(0.07, -0.05, YELLOW_HUE), + ShapePoint(0.03, 0.1, RED_HUE), + ShapePoint(0.03, -0.1, BLUE_HUE), + ShapePoint(-0.01, 0.15, YELLOW_HUE), + ShapePoint(-0.01, -0.15, RED_HUE), + ShapePoint(-0.05, 0.2, BLUE_HUE), + ShapePoint(-0.05, -0.2, YELLOW_HUE), + ShapePoint(-0.09, 0.15, RED_HUE), + ShapePoint(-0.09, -0.15, BLUE_HUE), + ShapePoint(-0.13, 0.0, YELLOW_HUE), + ShapePoint(0.0, 0.0, RED_HUE), + ShapePoint(-0.04, 0.0, BLUE_HUE), + ShapePoint(-0.08, 0.0, YELLOW_HUE), + ShapePoint(-0.12, 0.05, RED_HUE), + ShapePoint(-0.12, -0.05, BLUE_HUE) + ) +} diff --git a/FireMarkers/app/src/main/java/com/example/firemarkers/di/AppModule.kt b/FireMarkers/app/src/main/java/com/example/firemarkers/di/AppModule.kt new file mode 100644 index 00000000..46054524 --- /dev/null +++ b/FireMarkers/app/src/main/java/com/example/firemarkers/di/AppModule.kt @@ -0,0 +1,43 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.example.firemarkers.di + +import com.example.firemarkers.data.FirebaseConnection +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * The Hilt module for providing dependencies to the application. + * + * This module is responsible for providing the [FirebaseConnection] as a singleton dependency + * to the application. + */ +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + /** + * Provides a singleton instance of the [FirebaseConnection]. + * + * @return A singleton instance of the [FirebaseConnection]. + */ + @Provides + @Singleton + fun provideFirebaseConnection(): FirebaseConnection { + return FirebaseConnection() + } +} diff --git a/FireMarkers/app/src/main/java/com/example/firemarkers/model/MarkerData.kt b/FireMarkers/app/src/main/java/com/example/firemarkers/model/MarkerData.kt new file mode 100644 index 00000000..947463f2 --- /dev/null +++ b/FireMarkers/app/src/main/java/com/example/firemarkers/model/MarkerData.kt @@ -0,0 +1,37 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.example.firemarkers.model + +/** + * A data class representing a single marker on the map. + * + * This class is used to store the data for each marker, including its location, label, and style. + * It is used by the [com.example.firemarkers.viewmodel.MarkersViewModel] to manage the state of the markers and by the UI to + * render the markers on the map. + * + * @property id The unique ID of the marker. + * @property latitude The latitude of the marker's location. + * @property longitude The longitude of the marker's location. + * @property label The label to be displayed for the marker. + * @property style The style of the marker. + * @property color The color of the marker, represented as a hue value. + */ +data class MarkerData( + val id: String = "", + val latitude: Double = 0.0, + val longitude: Double = 0.0, + val label: String = "", + val style: String = "", + val color: Float = 0.0f +) diff --git a/FireMarkers/app/src/main/java/com/example/firemarkers/ui/theme/Color.kt b/FireMarkers/app/src/main/java/com/example/firemarkers/ui/theme/Color.kt new file mode 100644 index 00000000..99ff2dda --- /dev/null +++ b/FireMarkers/app/src/main/java/com/example/firemarkers/ui/theme/Color.kt @@ -0,0 +1,24 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.example.firemarkers.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/FireMarkers/app/src/main/java/com/example/firemarkers/ui/theme/Theme.kt b/FireMarkers/app/src/main/java/com/example/firemarkers/ui/theme/Theme.kt new file mode 100644 index 00000000..33201355 --- /dev/null +++ b/FireMarkers/app/src/main/java/com/example/firemarkers/ui/theme/Theme.kt @@ -0,0 +1,69 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.example.firemarkers.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun FireMarkersTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/FireMarkers/app/src/main/java/com/example/firemarkers/ui/theme/Type.kt b/FireMarkers/app/src/main/java/com/example/firemarkers/ui/theme/Type.kt new file mode 100644 index 00000000..635602fa --- /dev/null +++ b/FireMarkers/app/src/main/java/com/example/firemarkers/ui/theme/Type.kt @@ -0,0 +1,47 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.example.firemarkers.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/FireMarkers/app/src/main/java/com/example/firemarkers/viewmodel/MarkersViewModel.kt b/FireMarkers/app/src/main/java/com/example/firemarkers/viewmodel/MarkersViewModel.kt new file mode 100644 index 00000000..b6d5e0f9 --- /dev/null +++ b/FireMarkers/app/src/main/java/com/example/firemarkers/viewmodel/MarkersViewModel.kt @@ -0,0 +1,404 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.example.firemarkers.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.firemarkers.data.FirebaseConnection +import com.example.firemarkers.data.ShapeData +import com.example.firemarkers.data.ShapePoint +import com.example.firemarkers.model.MarkerData +import com.google.android.gms.maps.model.LatLng +import com.google.firebase.database.DataSnapshot +import com.google.firebase.database.DatabaseError +import com.google.firebase.database.ServerValue +import com.google.firebase.database.ValueEventListener +import com.google.maps.android.ktx.utils.withSphericalLinearInterpolation +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.util.UUID +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds + +/** + * ViewModel for the FireMarkers application, demonstrating integration of Google Maps Platform + * with Firebase Realtime Database for synchronized marker animations. + * + * This class serves as a reference implementation for Android developers looking to integrate + * Google Maps with Firebase. It illustrates basic concepts of real-time data synchronization + * and animated map markers. + * + * **Note:** While this ViewModel demonstrates Firebase integration, the Firebase-specific + * implementation aspects are for illustrative purposes and may not represent the most + * optimized or production-ready patterns for all Firebase use cases. The primary focus + * is on showcasing Google Maps Platform capabilities with a real-time backend. + * + * It manages the state for the map screen, including the markers and their synchronized animation. + * + * This ViewModel is the core of the application's logic, orchestrating data flow between the + * Firebase Realtime Database and the UI. It employs a controller/agent architecture to ensure + * that animations are perfectly synchronized across all connected devices. + * + * ### Architecture and Synchronization + * + * A key concept is the **controller/agent** model. At any given time, only one device acts as the + * "controller." This device is responsible for: + * 1. Running the animation loop (`startAnimationDriver`). + * 2. Calculating the animation's progress (`fraction`). + * 3. Writing the updated animation state to the `/animation` node in Firebase. + * 4. Seeding the initial marker data or clearing all markers. + * + * All other connected devices act as "agents." They passively listen for changes to the `/animation` + * node and update their UI accordingly. A device can become the controller by calling `takeControl()`. + * + * The shared state, including whether the animation is running, its progress, and the current + * controller's ID, is stored in the `AnimationState` data class and persisted in Firebase. + * + * ### State Management and Data Flow + * + * This ViewModel uses `StateFlow` to expose data to the Compose UI in a reactive way. + * - **`_markers`**: A private `MutableStateFlow` holding the raw list of `MarkerData` fetched + * from the `/markers` node in Firebase. + * - **`_animationStateDB`**: A private `MutableStateFlow` that mirrors the state of the `/animation` + * node in Firebase. + * - **`markers`**: A public `StateFlow` that is the main output for the UI. It's created by using + * the `combine` operator on `_markers` and `_animationStateDB`. For each emission, it calculates + * the new interpolated geographic coordinates and colors for every marker based on the current + * animation `fraction`, ensuring a smooth visual transition. + * - **`isController`**, **`animationRunning`**, **`hasMarkers`**: These are simple `StateFlow`s + * derived from the primary state flows to control UI elements like buttons and icons. + * + * @param firebaseConnection The Hilt-injected provider for the Firebase Realtime Database instance. + */ +@HiltViewModel +class MarkersViewModel @Inject constructor( + private val firebaseConnection: FirebaseConnection +) : ViewModel() { + + internal val viewModelId = UUID.randomUUID().toString().substring(0, 4) + private val _markers = MutableStateFlow>(emptyList()) + private val _animationStateDB = MutableStateFlow(AnimationState()) + private var animationJob: Job? = null + + private val _errorEvents = MutableSharedFlow() + val errorEvents = _errorEvents.asSharedFlow() + + val markers: StateFlow> = combine( + _markers, + _animationStateDB + ) { markers, animState -> + if (markers.isEmpty()) { + markers + } else { + updateMarkers(markers, animState.fraction) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) + + val animationRunning: StateFlow = _animationStateDB.map { it.running }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = false + ) + + val hasMarkers: StateFlow = _markers.map { it.isNotEmpty() }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = false + ) + + val isController: StateFlow = _animationStateDB.map { it.controllerId == viewModelId }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = false + ) + + init { + Log.d(TAG, "[$viewModelId] ViewModel initialized.") + listenForMarkerUpdates() + listenForAnimationState() + } + + private fun listenForMarkerUpdates() { + firebaseConnection.database.getReference("markers") + .addValueEventListener(object : ValueEventListener { + override fun onDataChange(snapshot: DataSnapshot) { + val markerList = snapshot.children.mapNotNull { it.getValue(MarkerData::class.java) } + _markers.value = markerList + } + + override fun onCancelled(error: DatabaseError) { + Log.e(TAG, "[$viewModelId] Database error on markers: ${error.message}") + viewModelScope.launch { + _errorEvents.emit("Database error on markers: ${error.message}") + } + } + }) + } + + private fun listenForAnimationState() { + firebaseConnection.database.getReference("animation") + .addValueEventListener(object : ValueEventListener { + override fun onDataChange(snapshot: DataSnapshot) { + val animState = snapshot.getValue(AnimationState::class.java) ?: AnimationState() + _animationStateDB.value = animState + Log.d(TAG, "[$viewModelId] DB anim state received: $animState") + + + val shouldDrive = animState.controllerId == viewModelId && animState.running + val isDriving = animationJob?.isActive == true + + Log.d(TAG, "[$viewModelId] Evaluating driver state: shouldDrive=$shouldDrive, isDriving=$isDriving") + + if (shouldDrive && !isDriving) { + Log.d(TAG, "[$viewModelId] Condition met: Starting animation driver.") + startAnimationDriver() + } else if (!shouldDrive && isDriving) { + Log.d(TAG, "[$viewModelId] Condition met: Stopping animation driver.") + animationJob?.cancel() + animationJob = null + } + } + + override fun onCancelled(error: DatabaseError) { + Log.e(TAG, "[$viewModelId] DB error on animation: ${error.message}") + viewModelScope.launch { + _errorEvents.emit("DB error on animation: ${error.message}") + } + } + }) + } + + /** + * Toggles the animation state (running/paused) in Firebase. + * + * This action is only permitted if the current ViewModel instance is the designated controller. + * If a non-controller attempts to toggle the animation, the action is ignored. + * The updated animation state is written to the `/animation` node in Firebase, which + * then propagates to all connected clients. + */ + fun toggleAnimation() { + if (_animationStateDB.value.controllerId != viewModelId) { + Log.w(TAG, "[$viewModelId] Agent tried to toggle animation. Ignoring.") + return + } + + val animRef = firebaseConnection.database.getReference("animation") + val currentState = _animationStateDB.value + val newState = currentState.copy(running = !currentState.running) + Log.d(TAG, "[$viewModelId] toggleAnimation: Writing new state to DB: $newState") + animRef.setValue(newState.toMap()) + } + + /** + * Attempts to designate the current ViewModel instance as the animation controller. + * + * This updates the `controllerId` in the `/animation` node in Firebase to this ViewModel's + * unique ID. When successful, this instance will become responsible for driving the animation. + * The animation is automatically paused when control is taken. + */ + fun takeControl() { + Log.d(TAG, "[$viewModelId] takeControl: Attempting to become controller.") + val animRef = firebaseConnection.database.getReference("animation") + val currentState = _animationStateDB.value + val newState = currentState.copy( + controllerId = viewModelId, + running = false + ) + Log.d(TAG, "[$viewModelId] takeControl: Writing new state to DB: $newState") + animRef.setValue(newState.toMap()) + } + + private fun startAnimationDriver() { + animationJob?.cancel() + animationJob = viewModelScope.launch { + var loopState = _animationStateDB.value + Log.d(TAG, "[$viewModelId] Animation driver started with initial state: $loopState") + + while (isActive) { + val animRef = firebaseConnection.database.getReference("animation") + + var nextFraction = loopState.fraction + loopState.direction * ANIMATION_STEP_SIZE + var nextDirection = loopState.direction + + if (nextFraction >= 1.0 || nextFraction <= 0.0) { + nextFraction = nextFraction.coerceIn(0.0, 1.0) + nextDirection *= -1 + } + + loopState = loopState.copy( + fraction = nextFraction, + direction = nextDirection + ) + Log.d(TAG, "[$viewModelId] Driver loop tick: Writing new state to DB: $loopState") + animRef.setValue(loopState.toMap()) + + if (nextFraction >= 1.0 || nextFraction <= 0.0) { + delay(PAUSE_DURATION) + } else { + delay(ANIMATION_DELAY) + } + } + } + } + + private fun updateMarkers(markers: List, fraction: Double): List { + val newLocations = Shape.locationPairs.map { (startPoint, endPoint) -> + startPoint.withSphericalLinearInterpolation(endPoint, fraction) + } + val colors = Shape.colorPairs.map { (a, b) -> a + (b - a) * fraction } + + return markers.mapIndexed { index, marker -> + marker.copy( + latitude = newLocations[index].latitude, + longitude = newLocations[index].longitude, + label = if (fraction > 0.5) Shape.Tree.label else Shape.JackOLantern.label, + color = colors[index].toFloat() + ) + } + } + + /** + * Seeds the Firebase Realtime Database with an initial set of marker data. + * + * This operation is only allowed if the current ViewModel instance is the controller, + * or if no controller is currently assigned (allowing the first user to initialize the data). + * It populates the `/markers` node with a predefined set of `MarkerData` objects + * representing the "Jack-o'-lantern" shape. + */ + fun seedDatabase() { + // Only the controller can seed the database. + // The `isNotEmpty` check allows the very first user of the app (when no controller + // is assigned yet) to claim control and seed the initial data. + if (_animationStateDB.value.controllerId != viewModelId && _animationStateDB.value.controllerId.isNotEmpty()) { + Log.w(TAG, "[$viewModelId] Agent cannot seed. Take control first.") + return + } + Log.d(TAG, "[$viewModelId] Seeding database as controller.") + val animState = AnimationState(controllerId = viewModelId) + firebaseConnection.database.getReference("animation").setValue(animState.toMap()) + viewModelScope.launch { + val databaseReference = firebaseConnection.database.getReference("markers") + val markersToSeed = Shape.JackOLantern.shapeData.map { shapePoint -> + val markerId = databaseReference.push().key ?: "" + MarkerData( + id = markerId, + latitude = BOULDER_LAT_LNG.latitude + shapePoint.latOffset, + longitude = BOULDER_LAT_LNG.longitude + shapePoint.lonOffset, + label = "Pumpkin", + style = "jack-o-lantern", + color = shapePoint.color + ) + } + databaseReference.setValue(markersToSeed) + } + } + + /** + * Clears all marker data from the Firebase Realtime Database. + * + * This operation is only allowed if the current ViewModel instance is the controller, + * or if no controller is currently assigned (allowing the first user to clear data). + * It removes all data from the `/markers` node in Firebase and resets the animation state. + */ + fun clearMarkers() { + if (_animationStateDB.value.controllerId != viewModelId && _animationStateDB.value.controllerId.isNotEmpty()) { + Log.w(TAG, "[$viewModelId] Agent cannot clear. Take control first.") + return + } + Log.d(TAG, "[$viewModelId] Clearing markers as controller.") + firebaseConnection.database.getReference("animation").setValue(AnimationState(controllerId = viewModelId)) + firebaseConnection.database.getReference("markers").removeValue() + } + + /** + * Represents the complete, synchronized state of the animation. + * This state is stored in the Firebase Realtime Database under the `/animation` node + * and serves as the single source of truth for all connected clients. + * + * @property running True if the animation should be playing, false if paused. + * Note: This property is named `running` instead of `isRunning` to avoid + * a known issue with Firebase's automatic deserialization of Kotlin + * data classes where properties prefixed with "is" may not be correctly + * mapped. + * @property fraction The current progress of the animation, from 0.0 (start shape) to 1.0 (end shape). + * @property direction The current direction of the animation (-1.0 or 1.0). + * @property controllerId The unique ID of the ViewModel instance currently driving the animation. + * @property timestamp The server-side timestamp of the last state update. + */ + internal data class AnimationState( + val running: Boolean = false, + val fraction: Double = 0.0, + val direction: Double = 1.0, + val controllerId: String = "", + val timestamp: Long = 0 + ) { + /** + * Converts this object to a Map for writing to Firebase. + * This is necessary to include the `ServerValue.TIMESTAMP`, which allows Firebase + * to write a consistent, server-side timestamp for when the update occurred. + */ + fun toMap(): Map { + return mapOf( + "running" to running, + "fraction" to fraction, + "direction" to direction, + "controllerId" to controllerId, + "timestamp" to ServerValue.TIMESTAMP + ) + } + } + + private sealed class Shape(val shapeData: List, val label: String) { + val locations = shapeData.map { + BOULDER_LAT_LNG + it + } + val colors = shapeData.map { it.color } + + data object Tree : Shape(ShapeData.christmasTreeShape, "Tree") + data object JackOLantern : Shape(ShapeData.jackOLanternShape, "Pumpkin") + + companion object { + val locationPairs by lazy { JackOLantern.locations.zip(Tree.locations) } + val colorPairs by lazy { JackOLantern.colors.zip(Tree.colors) } + } + } + + companion object { + private const val TAG = "MarkersViewModel" + private val BOULDER_LAT_LNG = LatLng(40.0150, -105.2705) + private val ANIMATION_DELAY = 100.milliseconds + private val PAUSE_DURATION = 1000.milliseconds + private const val ANIMATION_STEP_SIZE = 0.05 + } +} + +private operator fun LatLng.plus(other: ShapePoint): LatLng { + return LatLng(other.latOffset + this.latitude, other.lonOffset + this.longitude) +} \ No newline at end of file diff --git a/FireMarkers/app/src/main/res/drawable/ic_launcher_background.xml b/FireMarkers/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..bdf5ba85 --- /dev/null +++ b/FireMarkers/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FireMarkers/app/src/main/res/drawable/ic_launcher_foreground.xml b/FireMarkers/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..27a3ba98 --- /dev/null +++ b/FireMarkers/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/FireMarkers/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/FireMarkers/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..4293905e --- /dev/null +++ b/FireMarkers/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/FireMarkers/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/FireMarkers/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..4293905e --- /dev/null +++ b/FireMarkers/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/FireMarkers/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/FireMarkers/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..c209e78e Binary files /dev/null and b/FireMarkers/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/FireMarkers/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/FireMarkers/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000..b2dfe3d1 Binary files /dev/null and b/FireMarkers/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/FireMarkers/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/FireMarkers/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..4f0f1d64 Binary files /dev/null and b/FireMarkers/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/FireMarkers/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/FireMarkers/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..62b611da Binary files /dev/null and b/FireMarkers/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/FireMarkers/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/FireMarkers/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..948a3070 Binary files /dev/null and b/FireMarkers/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/FireMarkers/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/FireMarkers/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..1b9a6956 Binary files /dev/null and b/FireMarkers/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/FireMarkers/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/FireMarkers/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..28d4b77f Binary files /dev/null and b/FireMarkers/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/FireMarkers/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/FireMarkers/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9287f508 Binary files /dev/null and b/FireMarkers/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/FireMarkers/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/FireMarkers/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..aa7d6427 Binary files /dev/null and b/FireMarkers/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/FireMarkers/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/FireMarkers/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9126ae37 Binary files /dev/null and b/FireMarkers/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/FireMarkers/app/src/main/res/values/colors.xml b/FireMarkers/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..ae25b229 --- /dev/null +++ b/FireMarkers/app/src/main/res/values/colors.xml @@ -0,0 +1,26 @@ + + + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/FireMarkers/app/src/main/res/values/strings.xml b/FireMarkers/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..b57f8b52 --- /dev/null +++ b/FireMarkers/app/src/main/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + Fire Markers + \ No newline at end of file diff --git a/FireMarkers/app/src/main/res/values/themes.xml b/FireMarkers/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..66717c09 --- /dev/null +++ b/FireMarkers/app/src/main/res/values/themes.xml @@ -0,0 +1,21 @@ + + + + + +