feat: Introduce FireMarkers Sample (#2325)
* feat: Adds a new sample demonstrating how to use Google Maps with Firebase Realtime Database on Android. * feat(viewmodel): Add tests and error handling for controller logic This commit introduces unit tests for the `MarkersViewModel` and enhances the app's robustness by adding error handling and improving documentation. - **ViewModel Unit Tests:** Adds unit tests for `MarkersViewModel` using Mockito, Turbine, and Robolectric. The tests verify the controller/agent logic, ensuring that state is correctly managed based on the `controllerId` from Firebase. It also tests the new error reporting mechanism. - **Error Handling:** Implements a `SharedFlow` in the `MarkersViewModel` to propagate database errors to the UI. The `MainActivity` now observes this flow and displays errors to the user in a `Snackbar`. - **Architecture Documentation:** Replaces the static SVG architecture diagram with a more detailed Mermaid diagram in `ARCHITECTURE.md`. The new documentation explains the controller/agent synchronization pattern used for animations. - **Dependency Updates:** Upgrades Gradle to version 9.1.0 and adds `mockito-kotlin` and `turbine` as test dependencies. The `libs.versions.toml` file is reorganized for better clarity. * chore: Configure Gradle JVM args and expose ViewModel property This commit includes two maintenance changes: enabling custom JVM arguments for the Gradle daemon and updating the visibility of a property in the `MarkersViewModel`. * chore: Annotate the version catalog and build.gradle.kts file - **Gradle Build Documentation:** Introduces extensive documentation and organization to the `libs.versions.toml` and `app/build.gradle.kts` files. Dependencies, plugins, and versions are now grouped logically with comments explaining their purpose, improving maintainability and clarity. - **README Update:** The main `README.md` is updated to include a description of the new `FireMarkers` sample. - **Manifest Cleanup:** Removes the redundant `android:label` from the `MainActivity` in the manifest. * chore: adds copyright to the new source files * chore: headers --------- Co-authored-by: Enrique López Mañas <eenriquelopez@gmail.com>
34
FireMarkers/.gitignore
vendored
Normal file
@ -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
|
||||
92
FireMarkers/ARCHITECTURE.md
Normal file
@ -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<br>(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`.
|
||||
|
||||
```
|
||||
201
FireMarkers/LICENSE
Normal file
@ -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.
|
||||
75
FireMarkers/README.md
Normal file
@ -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 |
|
||||
|:--------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------|
|
||||
| <img src="screenshots/jack-o-lantern.png" alt="Screenshot of the controller UI with animation controls" width="400"/> | <img src="screenshots/tree.png" alt="Screenshot of the agent UI with the 'Take Control' button" width="400"/> |
|
||||
|
||||
## 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.
|
||||
1
FireMarkers/app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
||||
190
FireMarkers/app/build.gradle.kts
Normal file
@ -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"
|
||||
}
|
||||
21
FireMarkers/app/proguard-rules.pro
vendored
Normal file
@ -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
|
||||
50
FireMarkers/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".FireMarkersApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.FireMarkers">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="${MAPS_API_KEY}"/>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.FireMarkers">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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<ShapePoint> = 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<ShapePoint> = 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)
|
||||
)
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
@ -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)
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
*/
|
||||
)
|
||||
@ -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<List<MarkerData>>(emptyList())
|
||||
private val _animationStateDB = MutableStateFlow(AnimationState())
|
||||
private var animationJob: Job? = null
|
||||
|
||||
private val _errorEvents = MutableSharedFlow<String>()
|
||||
val errorEvents = _errorEvents.asSharedFlow()
|
||||
|
||||
val markers: StateFlow<List<MarkerData>> = 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<Boolean> = _animationStateDB.map { it.running }.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
initialValue = false
|
||||
)
|
||||
|
||||
val hasMarkers: StateFlow<Boolean> = _markers.map { it.isNotEmpty() }.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
initialValue = false
|
||||
)
|
||||
|
||||
val isController: StateFlow<Boolean> = _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<MarkerData>, fraction: Double): List<MarkerData> {
|
||||
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<String, Any> {
|
||||
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<ShapePoint>, 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)
|
||||
}
|
||||
186
FireMarkers/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,186 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@ -0,0 +1,46 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
FireMarkers/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
FireMarkers/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
FireMarkers/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
FireMarkers/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
FireMarkers/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
FireMarkers/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
FireMarkers/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
BIN
FireMarkers/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
26
FireMarkers/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
19
FireMarkers/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="app_name">Fire Markers</string>
|
||||
</resources>
|
||||
21
FireMarkers/app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="Theme.FireMarkers" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
29
FireMarkers/app/src/main/res/xml/backup_rules.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older than API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
35
FireMarkers/app/src/main/res/xml/data_extraction_rules.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@ -0,0 +1,18 @@
|
||||
// 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 org.robolectric.RobolectricTestRunner
|
||||
|
||||
class CustomTestRunner(klass: Class<*>) : RobolectricTestRunner(klass)
|
||||
@ -0,0 +1,168 @@
|
||||
// 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 com.example.firemarkers.CustomTestRunner
|
||||
import com.example.firemarkers.data.FirebaseConnection
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.firebase.database.DataSnapshot
|
||||
import com.google.firebase.database.DatabaseError
|
||||
import com.google.firebase.database.DatabaseReference
|
||||
import com.google.firebase.database.FirebaseDatabase
|
||||
import com.google.firebase.database.ValueEventListener
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.argumentCaptor
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.never
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.robolectric.annotation.Config
|
||||
import app.cash.turbine.test
|
||||
|
||||
@RunWith(CustomTestRunner::class)
|
||||
@Config(manifest=Config.NONE)
|
||||
@ExperimentalCoroutinesApi
|
||||
class MarkersViewModelTest {
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
|
||||
private lateinit var viewModel: MarkersViewModel
|
||||
private val mockFirebaseConnection: FirebaseConnection = mock()
|
||||
private val mockFirebaseDatabase: FirebaseDatabase = mock()
|
||||
private val mockMarkersRef: DatabaseReference = mock()
|
||||
private val mockAnimationRef: DatabaseReference = mock()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
whenever(mockFirebaseConnection.database).thenReturn(mockFirebaseDatabase)
|
||||
whenever(mockFirebaseDatabase.getReference("markers")).thenReturn(mockMarkersRef)
|
||||
whenever(mockFirebaseDatabase.getReference("animation")).thenReturn(mockAnimationRef)
|
||||
whenever(mockMarkersRef.addValueEventListener(any())).thenReturn(mock())
|
||||
whenever(mockAnimationRef.addValueEventListener(any())).thenReturn(mock())
|
||||
|
||||
viewModel = MarkersViewModel(mockFirebaseConnection)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isController is true when controllerId matches viewModelId`() = runTest {
|
||||
val listenerCaptor = argumentCaptor<ValueEventListener>()
|
||||
verify(mockAnimationRef).addValueEventListener(listenerCaptor.capture())
|
||||
|
||||
// Use Turbine to test the flow
|
||||
viewModel.isController.test {
|
||||
// The initial value is always false
|
||||
assertThat(awaitItem()).isFalse()
|
||||
|
||||
// Simulate the Firebase update making this VM the controller
|
||||
val mockSnapshot = mockAnimationStateSnapshot(
|
||||
MarkersViewModel.AnimationState(controllerId = viewModel.viewModelId)
|
||||
)
|
||||
listenerCaptor.firstValue.onDataChange(mockSnapshot)
|
||||
|
||||
// Assert that the flow now emits true
|
||||
assertThat(awaitItem()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isController is false when controllerId does not match`() = runTest {
|
||||
val listenerCaptor = argumentCaptor<ValueEventListener>()
|
||||
verify(mockAnimationRef).addValueEventListener(listenerCaptor.capture())
|
||||
|
||||
val mockSnapshot = mockAnimationStateSnapshot(
|
||||
MarkersViewModel.AnimationState(controllerId = "some-other-id")
|
||||
)
|
||||
listenerCaptor.firstValue.onDataChange(mockSnapshot)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(viewModel.isController.value).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toggleAnimation does nothing if not controller`() = runTest {
|
||||
val listenerCaptor = argumentCaptor<ValueEventListener>()
|
||||
verify(mockAnimationRef).addValueEventListener(listenerCaptor.capture())
|
||||
val mockSnapshot = mockAnimationStateSnapshot(
|
||||
MarkersViewModel.AnimationState(controllerId = "not-this-vm")
|
||||
)
|
||||
listenerCaptor.firstValue.onDataChange(mockSnapshot)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.toggleAnimation()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
verify(mockAnimationRef, never()).setValue(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `takeControl updates controllerId in Firebase`() = runTest {
|
||||
viewModel.takeControl()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
val stateMapCaptor = argumentCaptor<Map<String, Any>>()
|
||||
verify(mockAnimationRef).setValue(stateMapCaptor.capture())
|
||||
|
||||
assertThat(stateMapCaptor.firstValue["controllerId"]).isEqualTo(viewModel.viewModelId)
|
||||
assertThat(stateMapCaptor.firstValue["running"]).isEqualTo(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `errorEvents emits message on database error`() = runTest {
|
||||
val listenerCaptor = argumentCaptor<ValueEventListener>()
|
||||
verify(mockAnimationRef).addValueEventListener(listenerCaptor.capture())
|
||||
|
||||
val error = mock<DatabaseError> {
|
||||
on { message }.thenReturn("Test Error")
|
||||
}
|
||||
|
||||
var errorMessage: String? = null
|
||||
val job = launch(testDispatcher) {
|
||||
viewModel.errorEvents.first {
|
||||
errorMessage = it
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
listenerCaptor.firstValue.onCancelled(error)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(errorMessage).isEqualTo("DB error on animation: Test Error")
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mockAnimationStateSnapshot(state: MarkersViewModel.AnimationState): DataSnapshot {
|
||||
val mockSnapshot = mock<DataSnapshot>()
|
||||
whenever(mockSnapshot.getValue(eq(MarkersViewModel.AnimationState::class.java))).thenReturn(state)
|
||||
return mockSnapshot
|
||||
}
|
||||
70
FireMarkers/architecture.dot
Normal file
@ -0,0 +1,70 @@
|
||||
digraph Architecture {
|
||||
rankdir=TB;
|
||||
node [shape=box, style=rounded];
|
||||
|
||||
subgraph "cluster_firebase" {
|
||||
label="Firebase Cloud";
|
||||
style="filled";
|
||||
color="lightgrey";
|
||||
MarkersDB [label="Firebase Realtime Database
|
||||
/markers"];
|
||||
AnimationDB [label="Firebase Realtime Database
|
||||
/animation"];
|
||||
}
|
||||
|
||||
subgraph "cluster_android" {
|
||||
label="Android App";
|
||||
style="filled";
|
||||
color="lightgrey";
|
||||
|
||||
subgraph "cluster_ui" {
|
||||
label="UI Layer (View)";
|
||||
A [label="MainActivity"];
|
||||
B [label="MapScreen Composable"];
|
||||
C [label="TopAppBar Actions
|
||||
(toggleAnimation, seedDatabase, clearMarkers, takeControl)"];
|
||||
D [label="GoogleMap Composable"];
|
||||
}
|
||||
|
||||
subgraph "cluster_vm" {
|
||||
label="State & Logic Layer (ViewModel)";
|
||||
VM [label="MarkersViewModel"];
|
||||
}
|
||||
|
||||
subgraph "cluster_data" {
|
||||
label="Data Layer";
|
||||
FC [label="FirebaseConnection"];
|
||||
SD [label="ShapeData"];
|
||||
M [label="MarkerData Model"];
|
||||
}
|
||||
|
||||
subgraph "cluster_di" {
|
||||
label="Dependency Injection";
|
||||
Hilt [label="Hilt/Dagger"];
|
||||
}
|
||||
}
|
||||
|
||||
// --- Interactions ---
|
||||
A -> B;
|
||||
B -> C;
|
||||
B -> D [label="Renders Markers"];
|
||||
C -> VM [label="Calls function"];
|
||||
VM -> FC [label="Uses"];
|
||||
VM -> SD [label="Reads shape vectors"];
|
||||
|
||||
// Controller writes to Firebase
|
||||
VM -> AnimationDB [label="Writes animation state"];
|
||||
VM -> MarkersDB [label="Writes marker data"];
|
||||
|
||||
// All clients listen for real-time updates
|
||||
MarkersDB -> FC [label="Real-time updates", style=dashed];
|
||||
AnimationDB -> FC [label="Real-time updates", style=dashed];
|
||||
|
||||
FC -> VM;
|
||||
VM -> B [label="Updates StateFlow"];
|
||||
VM -> M [label="Uses Model"];
|
||||
|
||||
// --- DI Graph ---
|
||||
Hilt -> FC [label="Injects"];
|
||||
Hilt -> VM [label="Injects"];
|
||||
}
|
||||
29
FireMarkers/build.gradle.kts
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
|
||||
alias(libs.plugins.hilt.android) apply false
|
||||
alias(libs.plugins.secrets.gradle.plugin) apply false
|
||||
|
||||
alias(libs.plugins.ksp)
|
||||
|
||||
id("com.google.gms.google-services") version "4.4.4" apply false
|
||||
}
|
||||
20
FireMarkers/gradle.properties
Normal file
@ -0,0 +1,20 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
189
FireMarkers/gradle/libs.versions.toml
Normal file
@ -0,0 +1,189 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
# #################################################################################################
|
||||
# GRADLE VERSION CATALOG
|
||||
#
|
||||
# This file defines the dependencies used in the project, organized into four main sections:
|
||||
# - `[versions]`: Centralizes dependency versions for easy management and updates.
|
||||
# - `[libraries]`: Declares the actual dependencies, referencing versions from the `[versions]` block.
|
||||
# - `[bundles]`: (Not used here) Groups libraries that are commonly used together.
|
||||
# - `[plugins]`: Configures Gradle plugins used for building and packaging the application.
|
||||
#
|
||||
# For more information, see https://docs.gradle.org/current/userguide/platforms.html
|
||||
# #################################################################################################
|
||||
|
||||
|
||||
[versions]
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# Android SDK
|
||||
#
|
||||
# Specifies the core Android SDK versions for compilation, minimum supported API level, and target
|
||||
# API level. These versions are fundamental to how the application is built and how it interacts
|
||||
# with the Android operating system.
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
android-compile-sdk = "36"
|
||||
android-min-sdk = "24"
|
||||
android-target-sdk = "36"
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# Kotlin
|
||||
#
|
||||
# Defines the versions for the Kotlin programming language and related libraries, such as
|
||||
# coroutines for asynchronous programming and datetime for handling dates and times.
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
kotlin = "2.2.21"
|
||||
kotlinx-coroutines-test = "1.10.2"
|
||||
kotlinx-datetime = "0.7.1"
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# AndroidX
|
||||
#
|
||||
# Centralizes versions for AndroidX libraries, which are part of the Android Jetpack suite.
|
||||
# This includes components for UI (Compose), core functionality (KTX), lifecycle management,
|
||||
# and testing.
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
androidx-activity-compose = "1.11.0"
|
||||
androidx-compose-bom = "2025.10.01"
|
||||
androidx-core-ktx = "1.17.0"
|
||||
androidx-espresso-core = "3.7.0"
|
||||
androidx-hilt-navigation-compose = "1.3.0"
|
||||
androidx-junit = "1.3.0"
|
||||
androidx-lifecycle-runtime-ktx = "2.9.4"
|
||||
androidx-material-icons-extended = "1.7.8"
|
||||
androidx-material3 = "1.4.0"
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# Google & Firebase
|
||||
#
|
||||
# Manages versions for Google-provided libraries, including Firebase for backend services,
|
||||
# Hilt for dependency injection, and Google Maps Platform SDKs for mapping functionalities.
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
dagger = "2.57.2"
|
||||
firebase-bom = "34.5.0"
|
||||
hilt-android = "2.57.2"
|
||||
maps-compose = "6.12.1"
|
||||
maps-utils-ktx = "5.2.1"
|
||||
truth = "1.4.5"
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# Testing
|
||||
#
|
||||
# Specifies the versions for testing frameworks and libraries used for unit and instrumentation
|
||||
# tests. This includes JUnit, Mockito for mocking objects, Robolectric for running Android tests
|
||||
# on the JVM, and Turbine for testing Kotlin Flows.
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
junit = "4.13.2"
|
||||
mockito = "6.1.0"
|
||||
robolectric = "4.16"
|
||||
turbine = "1.2.1"
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# Gradle Plugins
|
||||
#
|
||||
# Defines the versions for Gradle plugins that provide core build functionalities, such as the
|
||||
# Android Gradle Plugin (AGP), Kotlin plugins, and utility plugins like the Secrets Gradle Plugin
|
||||
# for managing API keys.
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
android-gradle-plugin = "8.13.0"
|
||||
ksp = "2.2.20-2.0.4"
|
||||
secrets-gradle-plugin = "2.0.1"
|
||||
|
||||
|
||||
[libraries]
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# AndroidX
|
||||
#
|
||||
# This section declares the AndroidX libraries used in the project. These libraries provide
|
||||
# backward-compatible versions of Android framework APIs, as well as new features and utilities
|
||||
# for modern Android development.
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
|
||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" }
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" }
|
||||
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt-navigation-compose" }
|
||||
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime-ktx" }
|
||||
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "androidx-material-icons-extended" }
|
||||
androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "androidx-material3" }
|
||||
androidx-ui = { module = "androidx.compose.ui:ui" }
|
||||
androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
|
||||
androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# Google & Firebase
|
||||
#
|
||||
# Declares dependencies for Google and Firebase services. This includes the Maps SDK for Compose,
|
||||
# Firebase Realtime Database, and Hilt for dependency injection.
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
|
||||
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
|
||||
firebase-database = { module = "com.google.firebase:firebase-database" }
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" }
|
||||
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt-android" }
|
||||
maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "maps-compose" }
|
||||
maps-utils-ktx = { module = "com.google.maps.android:maps-utils-ktx", version.ref = "maps-utils-ktx" }
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# Kotlin
|
||||
#
|
||||
# Defines Kotlin-specific libraries, such as the datetime library for handling dates and times
|
||||
# in a multiplatform-friendly way.
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# Testing
|
||||
#
|
||||
# This section lists all dependencies required for testing. It includes libraries for both local
|
||||
# unit tests (JUnit, Mockito, Robolectric) and instrumented tests that run on an Android device
|
||||
# or emulator (Espresso, AndroidX Test).
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso-core" }
|
||||
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
|
||||
androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
|
||||
androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
||||
google-truth = { module = "com.google.truth:truth", version.ref = "truth" }
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test" }
|
||||
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito" }
|
||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
|
||||
|
||||
|
||||
[plugins]
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# Gradle Plugins
|
||||
#
|
||||
# This section defines the Gradle plugins used in the project. Plugins extend Gradle's capabilities,
|
||||
# enabling tasks like compiling Android applications, processing Kotlin code, managing secrets,
|
||||
# and handling dependency injection with Hilt and KSP.
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
||||
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt-android" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
secrets-gradle-plugin = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets-gradle-plugin" }
|
||||
BIN
FireMarkers/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
8
FireMarkers/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
248
FireMarkers/gradlew
vendored
Executable file
@ -0,0 +1,248 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
93
FireMarkers/gradlew.bat
vendored
Normal file
@ -0,0 +1,93 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
1
FireMarkers/local.defaults.properties
Normal file
@ -0,0 +1 @@
|
||||
MAPS_API_KEY=DEFAULT_API_KEY
|
||||
BIN
FireMarkers/screenshots/jack-o-lantern.png
Normal file
|
After Width: | Height: | Size: 694 KiB |
BIN
FireMarkers/screenshots/tree.png
Normal file
|
After Width: | Height: | Size: 699 KiB |
39
FireMarkers/settings.gradle.kts
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "Fire Markers"
|
||||
include(":app")
|
||||
@ -19,6 +19,7 @@ Samples demonstrating how to use
|
||||
This repo contains the following samples:
|
||||
|
||||
1. [ApiDemos](ApiDemos): A collection of small demos showing most features of the Maps SDK for Android.
|
||||
1. [FireMarkers](FireMarkers): Demonstrates how to use Firebase Realtime Database to drive synchronized, live animations on a Google Map across multiple devices using a controller/agent architecture.
|
||||
1. [WearOS](WearOS):
|
||||
Displays a map on a Wear OS device. This sample demonstrates the basic
|
||||
setup required for a gradle-based Android Studio project.
|
||||
|
||||