├── .gitignore ├── android ├── app │ ├── .gitignore │ ├── src │ │ └── main │ │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── toolbar.xml │ │ │ │ ├── activity_price_history.xml │ │ │ │ ├── activity_multi_tracker.xml │ │ │ │ ├── activity_multi_tracker_recycler_view.xml │ │ │ │ ├── historical_price_list_item.xml │ │ │ │ ├── stock_price_list_item.xml │ │ │ │ └── activity_main.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── hyperaware │ │ │ │ └── android │ │ │ │ └── firebasejetpack │ │ │ │ ├── model │ │ │ │ └── StockPrice.kt │ │ │ │ ├── repo │ │ │ │ ├── Deserializer.kt │ │ │ │ ├── BaseStockRepository.kt │ │ │ │ ├── paging │ │ │ │ │ └── LoadingState.kt │ │ │ │ ├── firestore │ │ │ │ │ ├── Deserializers.kt │ │ │ │ │ ├── DeserializeDocumentSnapshotTransform.kt │ │ │ │ │ ├── DeserializeDocumentSnapshotsTransform.kt │ │ │ │ │ ├── DeserializeDocumentSnapshotAsyncTransform.kt │ │ │ │ │ ├── DocumentSyncCallable.kt │ │ │ │ │ ├── FirestoreStockRepository.kt │ │ │ │ │ └── FirestoreQueryDataSource.kt │ │ │ │ ├── StockRepositoryCommon.kt │ │ │ │ ├── rtdb │ │ │ │ │ ├── DeserializeDataSnapshotTransform.kt │ │ │ │ │ ├── DeserializeQuerySnapshotTransform.kt │ │ │ │ │ ├── Deserializers.kt │ │ │ │ │ ├── DatabaseReferenceSyncCallable.kt │ │ │ │ │ ├── RealtimeDatabaseStockRepository.kt │ │ │ │ │ └── RealtimeDatabaseQueryDataSource.kt │ │ │ │ └── StockRepository.kt │ │ │ │ ├── activity │ │ │ │ ├── livepricehistory │ │ │ │ │ ├── HistoricalPriceViewHolder.kt │ │ │ │ │ ├── StockPriceHistoryListAdapter.kt │ │ │ │ │ ├── StockPriceHistoryRecyclerViewAdapter.kt │ │ │ │ │ └── StockPriceHistoryActivity.kt │ │ │ │ ├── allstockspagedrv │ │ │ │ │ ├── StockViewHolder.kt │ │ │ │ │ ├── StocksPagedListAdapter.kt │ │ │ │ │ └── AllStocksPagedRecyclerViewActivity.kt │ │ │ │ ├── multitrackerrv │ │ │ │ │ ├── StockViewHolder.kt │ │ │ │ │ ├── StockPriceTrackerRecyclerViewActivity.kt │ │ │ │ │ └── StocksRecyclerViewAdapter.kt │ │ │ │ ├── multitracker │ │ │ │ │ └── StockPriceTrackerActivity.kt │ │ │ │ └── main │ │ │ │ │ └── MainActivity.kt │ │ │ │ ├── common │ │ │ │ └── DataOrException.kt │ │ │ │ ├── diffcallback │ │ │ │ ├── QueryItemDiffCallback.kt │ │ │ │ └── QueryItemOrExceptionDiffUtilItemCallback.kt │ │ │ │ ├── livedata │ │ │ │ ├── tasks │ │ │ │ │ └── TaskLiveData.kt │ │ │ │ ├── firestore │ │ │ │ │ ├── FirestoreQueryLiveData.kt │ │ │ │ │ └── FirestoreDocumentLiveData.kt │ │ │ │ ├── rtdb │ │ │ │ │ └── RealtimeDatabaseQueryLiveData.kt │ │ │ │ └── common │ │ │ │ │ └── LingeringLiveData.kt │ │ │ │ ├── viewmodel │ │ │ │ ├── PagedStockPricesViewModel.kt │ │ │ │ ├── StockPriceViewModel.kt │ │ │ │ ├── StockPriceDisplay.kt │ │ │ │ └── StockPriceHistoryViewModel.kt │ │ │ │ ├── koin │ │ │ │ ├── KoinInitContentProvider.kt │ │ │ │ └── Modules.kt │ │ │ │ ├── config │ │ │ │ └── AppExecutors.kt │ │ │ │ ├── fcm │ │ │ │ └── MyFirebaseMessagingService.kt │ │ │ │ └── worker │ │ │ │ └── StockPriceSyncWorker.kt │ │ │ └── AndroidManifest.xml │ ├── proguard-rules.pro │ └── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── build.gradle ├── gradle.properties ├── gradlew.bat └── gradlew ├── backend ├── .gitignore ├── functions │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── src │ │ ├── stocks │ │ │ ├── repo.ts │ │ │ ├── firestore-repo.ts │ │ │ ├── database-repo.ts │ │ │ └── stock-machine.ts │ │ ├── msg.ts │ │ ├── index.ts │ │ └── tick.ts │ └── tslint.json ├── storage.rules ├── firestore.rules ├── database.rules.json ├── firestore.indexes.json ├── firebase.json └── public │ ├── 404.html │ └── privacy.html ├── CONTRIBUTING.md ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | /google-services.json 4 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | /.firebase 2 | /.firebaserc 3 | /firebase-debug.log 4 | -------------------------------------------------------------------------------- /backend/functions/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib 3 | /service-account.json 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingDoug/firebase-jetpack/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /.gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | 10 | /scratch 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingDoug/firebase-jetpack/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingDoug/firebase-jetpack/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingDoug/firebase-jetpack/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingDoug/firebase-jetpack/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingDoug/firebase-jetpack/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /backend/storage.rules: -------------------------------------------------------------------------------- 1 | service firebase.storage { 2 | match /b/{bucket}/o { 3 | match /{allPaths=**} { 4 | allow read, write: if false; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingDoug/firebase-jetpack/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingDoug/firebase-jetpack/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingDoug/firebase-jetpack/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingDoug/firebase-jetpack/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingDoug/firebase-jetpack/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'firebase-jetpack' 2 | 3 | include ':app' 4 | 5 | if (hasProperty('useScratch') && useScratch.toBoolean()) { 6 | include ':scratch' 7 | } 8 | 9 | -------------------------------------------------------------------------------- /backend/firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | match /{document=**} { 4 | allow read: if request.auth != null; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /backend/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": "auth != null", 4 | ".write": false, 5 | 6 | "stocks-history": { 7 | "$ticker": { 8 | ".indexOn": "revTime" 9 | } 10 | } 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Firebase & Jetpack Demo 3 | Track Two Stocks 4 | Track Stocks with RecyclerView 5 | 6 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 19 14:34:30 PDT 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip 7 | -------------------------------------------------------------------------------- /backend/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["es2017"], 5 | "module": "commonjs", 6 | "noImplicitReturns": true, 7 | "outDir": "lib", 8 | "sourceMap": true 9 | }, 10 | "compileOnSave": true, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /backend/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | // Example: 3 | // 4 | // "indexes": [ 5 | // { 6 | // "collectionId": "widgets", 7 | // "fields": [ 8 | // { "fieldPath": "foo", "mode": "ASCENDING" }, 9 | // { "fieldPath": "bar", "mode": "DESCENDING" } 10 | // ] 11 | // } 12 | // ] 13 | "indexes": [] 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | -------------------------------------------------------------------------------- /backend/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "firestore": { 6 | "rules": "firestore.rules", 7 | "indexes": "firestore.indexes.json" 8 | }, 9 | "functions": { 10 | "predeploy": [ 11 | "npm --prefix \"$RESOURCE_DIR\" run lint", 12 | "npm --prefix \"$RESOURCE_DIR\" run build" 13 | ], 14 | "source": "functions" 15 | }, 16 | "storage": { 17 | "rules": "storage.rules" 18 | }, 19 | "hosting": { 20 | "public": "public", 21 | "ignore": [ 22 | "firebase.json", 23 | "**/.*", 24 | "**/node_modules/**" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | android_support_version = '28.0.0' 4 | kotlin_version = '1.3.72' 5 | lifecycle_version = '2.2.0' 6 | } 7 | repositories { 8 | google() 9 | jcenter() 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.6.3' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | classpath 'com.google.gms:google-services:4.3.3' 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_price_history.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_multi_tracker.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_multi_tracker_recycler_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /backend/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase serve --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "main": "lib/index.js", 13 | "dependencies": { 14 | "command-line-args": "^5.1.1", 15 | "firebase-admin": "^8.12.1", 16 | "firebase-functions": "^3.6.1", 17 | "source-map-support": "^0.5.19" 18 | }, 19 | "devDependencies": { 20 | "tslint": "^6.1.2", 21 | "typescript": "^3.9.3" 22 | }, 23 | "engines": { 24 | "node": "8" 25 | }, 26 | "private": true 27 | } 28 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.useAndroidX=true 10 | org.gradle.jvmargs=-Xmx1536m 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/model/StockPrice.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.model 18 | 19 | import java.util.Date 20 | 21 | data class StockPrice( 22 | val ticker: String, 23 | val price: Float, 24 | val time: Date 25 | ) 26 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/Deserializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo 18 | 19 | interface Deserializer { 20 | 21 | @Throws(DeserializerException::class) 22 | fun deserialize(input: I): O 23 | 24 | class DeserializerException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/activity/livepricehistory/HistoricalPriceViewHolder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.activity.livepricehistory 18 | 19 | import androidx.recyclerview.widget.RecyclerView 20 | import com.hyperaware.android.firebasejetpack.databinding.HistoricalPriceListItemBinding 21 | 22 | internal class HistoricalPriceViewHolder(val binding: HistoricalPriceListItemBinding) 23 | : RecyclerView.ViewHolder(binding.root) 24 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/BaseStockRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo 18 | 19 | import java.util.* 20 | 21 | abstract class BaseStockRepository : StockRepository { 22 | 23 | override val allTickers: SortedSet = sortedSetOf( 24 | "HSTK", "FBAS", 25 | "QIX", "GORF", "ZAXN", "PCMN", "GLXN", 26 | "VGTA", "GOKU", "BLMA", "GOHN", "FRZA", "CELL", "BUU", 27 | "MCOY", "SPOK", "KIRK", "UHRA", "CHKV", "SULU" 28 | ) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). -------------------------------------------------------------------------------- /backend/functions/src/stocks/repo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export interface StockPrice { 18 | price: number 19 | time: Date 20 | } 21 | 22 | export interface StockRepository { 23 | getStockLive(ticker: string): Promise 24 | updateStockPrice(ticker: string, stock: StockPrice): Promise 25 | } 26 | 27 | export abstract class StockRepositoryBase implements StockRepository { 28 | protected historyExpires: number = 2 * 60 * 1000 29 | abstract getStockLive(ticker: string): Promise 30 | abstract updateStockPrice(ticker: string, stock: StockPrice): Promise 31 | } 32 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/common/DataOrException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.common 18 | 19 | import java.lang.IllegalArgumentException 20 | 21 | /** 22 | * Generic holder for an object that either holds a result of a desired type, 23 | * or some exception. 24 | */ 25 | 26 | data class DataOrException(val data: T?, val exception: E?) { 27 | init { 28 | if (data == null && exception == null) { 29 | throw IllegalArgumentException("Both data and exception can't be null") 30 | } 31 | else if (data != null && exception != null) { 32 | throw IllegalArgumentException("Both data and exception can't be non-null") 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/activity/allstockspagedrv/StockViewHolder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.activity.allstockspagedrv 18 | 19 | import android.view.View 20 | import androidx.recyclerview.widget.RecyclerView 21 | import com.hyperaware.android.firebasejetpack.databinding.StockPriceListItemBinding 22 | 23 | internal class StockViewHolder(val binding: StockPriceListItemBinding) 24 | : RecyclerView.ViewHolder(binding.root), View.OnClickListener { 25 | 26 | init { 27 | binding.root.setOnClickListener(this) 28 | } 29 | 30 | var ticker: String? = null 31 | var itemClickListener: StocksPagedListAdapter.ItemClickListener? = null 32 | 33 | override fun onClick(v: View) { 34 | itemClickListener?.onItemClick(this) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/paging/LoadingState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo.paging 18 | 19 | /** 20 | * Loading state exposed by FirestoreQueryDataSource and 21 | * RealtimeDatabaseQueryDataSource. 22 | */ 23 | 24 | enum class LoadingState { 25 | /** 26 | * Loading initial data. 27 | */ 28 | LOADING_INITIAL, 29 | 30 | /** 31 | * Loading a page other than the first page. 32 | */ 33 | LOADING_MORE, 34 | 35 | /** 36 | * Not currently loading any pages, at least one page loaded. 37 | */ 38 | LOADED, 39 | 40 | /** 41 | * The last page loaded had zero documents, and therefore no further pages will be loaded. 42 | */ 43 | FINISHED, 44 | 45 | /** 46 | * The most recent load encountered an error. 47 | */ 48 | ERROR 49 | } 50 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/diffcallback/QueryItemDiffCallback.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.diffcallback 18 | 19 | import android.annotation.SuppressLint 20 | import androidx.recyclerview.widget.DiffUtil 21 | import com.hyperaware.android.firebasejetpack.repo.QueryItem 22 | 23 | /** 24 | * Utility for diffing lists of QueryItem elements for use with RecyclerView. 25 | * This code makes the assumption that the generic type T is a data class, 26 | * which has an automatically accurate equals() implementation. 27 | */ 28 | 29 | open class QueryItemDiffCallback : DiffUtil.ItemCallback>() { 30 | override fun areItemsTheSame(oldItem: QueryItem, newItem: QueryItem): Boolean { 31 | return oldItem.id == newItem.id 32 | } 33 | 34 | @SuppressLint("DiffUtilEquals") // equals() is OK for data classes 35 | override fun areContentsTheSame(oldItem: QueryItem, newItem: QueryItem): Boolean { 36 | return oldItem.item == newItem.item 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/firestore/Deserializers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo.firestore 18 | 19 | import com.google.firebase.firestore.DocumentSnapshot 20 | import com.hyperaware.android.firebasejetpack.model.StockPrice 21 | import com.hyperaware.android.firebasejetpack.repo.Deserializer 22 | 23 | internal interface DocumentSnapshotDeserializer : Deserializer 24 | 25 | internal class StockPriceDocumentSnapshotDeserializer : DocumentSnapshotDeserializer { 26 | override fun deserialize(input: DocumentSnapshot): StockPrice { 27 | val ticker = input.id 28 | val price = input.getDouble("price") ?: 29 | throw Deserializer.DeserializerException("price was missing for stock price document $ticker") 30 | val time = input.getDate("time") ?: 31 | throw Deserializer.DeserializerException("time was missing for stock price document $ticker") 32 | 33 | return StockPrice(ticker, price.toFloat(), time) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/functions/src/msg.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // 18 | // Command line program that uses the Firebase Admin SDK to send a single 19 | // topic message to the ticker given in the --ticker flag. 20 | // 21 | 22 | require('source-map-support').install() 23 | 24 | import * as admin from 'firebase-admin' 25 | 26 | interface Options { 27 | ticker: string 28 | } 29 | 30 | const args = require('command-line-args') 31 | const options: Options = args([ 32 | { name: 'ticker', alias: 't', type: String } 33 | ]) 34 | 35 | if (!options.ticker) { 36 | console.error("--ticker is required") 37 | process.exit(1) 38 | } 39 | 40 | const serviceAccount = require('../service-account.json') 41 | admin.initializeApp({ 42 | credential: admin.credential.cert(serviceAccount) 43 | }) 44 | 45 | const payload = { 46 | data: { 47 | ticker: options.ticker 48 | } 49 | } 50 | 51 | admin.messaging().sendToTopic(options.ticker, payload) 52 | .then(response => { 53 | console.log(response) 54 | process.exit(0) 55 | }) 56 | .catch(error => { 57 | console.log(error) 58 | process.exit(1) 59 | }) 60 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/activity/multitrackerrv/StockViewHolder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.activity.multitrackerrv 18 | 19 | import androidx.lifecycle.LiveData 20 | import androidx.lifecycle.Observer 21 | import androidx.recyclerview.widget.RecyclerView 22 | import android.view.View 23 | import com.hyperaware.android.firebasejetpack.databinding.StockPriceListItemBinding 24 | import com.hyperaware.android.firebasejetpack.viewmodel.StockPriceDisplayOrException 25 | 26 | internal class StockViewHolder(val binding: StockPriceListItemBinding) 27 | : RecyclerView.ViewHolder(binding.root), View.OnClickListener { 28 | 29 | init { 30 | binding.root.setOnClickListener(this) 31 | } 32 | 33 | var ticker: String? = null 34 | var stockPriceLiveData: LiveData? = null 35 | var observer: Observer? = null 36 | var itemClickListener: StocksRecyclerViewAdapter.ItemClickListener? = null 37 | 38 | override fun onClick(v: View) { 39 | itemClickListener?.onItemClick(this) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/diffcallback/QueryItemOrExceptionDiffUtilItemCallback.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.diffcallback 18 | 19 | import android.annotation.SuppressLint 20 | import androidx.recyclerview.widget.DiffUtil 21 | import com.hyperaware.android.firebasejetpack.repo.QueryItemOrException 22 | 23 | /** 24 | * T must be a data class for this to work, as it depends on the structural 25 | * equality to compare objects. 26 | */ 27 | 28 | class QueryItemOrExceptionDiffUtilItemCallback : DiffUtil.ItemCallback>() { 29 | 30 | override fun areItemsTheSame(oldItem: QueryItemOrException, newItem: QueryItemOrException): Boolean { 31 | return if (oldItem.data != null && newItem.data != null) { 32 | oldItem.data.id == newItem.data.id 33 | } 34 | else { 35 | oldItem === newItem 36 | } 37 | } 38 | 39 | @SuppressLint("DiffUtilEquals") // equals() is OK for data classes 40 | override fun areContentsTheSame(oldItem: QueryItemOrException, newItem: QueryItemOrException): Boolean { 41 | return oldItem.data == newItem.data 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/StockRepositoryCommon.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo 18 | 19 | import com.hyperaware.android.firebasejetpack.common.DataOrException 20 | import com.hyperaware.android.firebasejetpack.model.StockPrice 21 | 22 | /** 23 | * An item of data type T that resulted from a query. It adds the notion of 24 | * a unique id to that item. 25 | */ 26 | 27 | interface QueryItem { 28 | val item: T 29 | val id: String 30 | } 31 | 32 | typealias QueryItemOrException = DataOrException, Exception> 33 | 34 | 35 | data class StockPriceQueryItem(private val _item: StockPrice, private val _id: String) : QueryItem { 36 | override val item: StockPrice 37 | get() = _item 38 | override val id: String 39 | get() = _id 40 | } 41 | 42 | typealias StockPriceOrException = DataOrException 43 | 44 | /** 45 | * The results of a database query (a List of QueryItems), or an Exception. 46 | */ 47 | 48 | typealias QueryResultsOrException = DataOrException>, E> 49 | 50 | typealias StockPriceHistoryQueryResults = QueryResultsOrException 51 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/livedata/tasks/TaskLiveData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.livedata.tasks 18 | 19 | import androidx.lifecycle.LiveData 20 | import com.google.android.gms.tasks.OnFailureListener 21 | import com.google.android.gms.tasks.OnSuccessListener 22 | import com.google.android.gms.tasks.Task 23 | import com.hyperaware.android.firebasejetpack.common.DataOrException 24 | 25 | /** 26 | * Converts a Play services Task to LiveData. Currently unused. 27 | */ 28 | 29 | class TaskLiveData(private val task: Task) 30 | : LiveData>(), OnSuccessListener, OnFailureListener { 31 | 32 | private var added = false 33 | 34 | override fun onActive() { 35 | if (!added) { 36 | added = true 37 | task.addOnSuccessListener(this) 38 | task.addOnFailureListener(this) 39 | } 40 | } 41 | 42 | override fun onInactive() { 43 | } 44 | 45 | override fun onSuccess(result: T) { 46 | value = DataOrException(result, null) 47 | } 48 | 49 | override fun onFailure(exception: Exception) { 50 | value = DataOrException(null, exception) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /backend/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/rtdb/DeserializeDataSnapshotTransform.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo.rtdb 18 | 19 | import androidx.arch.core.util.Function 20 | import com.hyperaware.android.firebasejetpack.common.DataOrException 21 | import com.hyperaware.android.firebasejetpack.livedata.rtdb.DataSnapshotOrException 22 | import com.hyperaware.android.firebasejetpack.repo.Deserializer 23 | 24 | internal class DeserializeDataSnapshotTransform( 25 | private val deserializer: DataSnapshotDeserializer 26 | ) : Function> { 27 | 28 | override fun apply(input: DataSnapshotOrException): DataOrException { 29 | val (snapshot, exception) = input 30 | return when { 31 | snapshot != null -> try { 32 | val value = deserializer.deserialize(snapshot) 33 | DataOrException(value, null) 34 | } 35 | catch (e: Deserializer.DeserializerException) { 36 | DataOrException(null, e) 37 | } 38 | 39 | exception != null -> DataOrException(null, exception) 40 | 41 | else -> DataOrException(null, Exception()) 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/StockRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo 18 | 19 | import androidx.lifecycle.LiveData 20 | import androidx.paging.PagedList 21 | import com.google.common.util.concurrent.ListenableFuture 22 | import com.hyperaware.android.firebasejetpack.model.StockPrice 23 | import java.util.* 24 | import java.util.concurrent.TimeUnit 25 | 26 | interface StockRepository { 27 | 28 | val allTickers: SortedSet 29 | 30 | /** 31 | * Gets a LiveData object from this repo that reflects the current value of 32 | * a single Stock, given by its ticker. 33 | */ 34 | fun getStockPriceLiveData(ticker: String): LiveData 35 | 36 | fun getStockPriceHistoryLiveData(ticker: String): LiveData 37 | 38 | fun getStockPricePagedListLiveData(pageSize: Int): LiveData>> 39 | 40 | /** 41 | * Synchronizes one stock record so it's available to this repo while offline 42 | */ 43 | fun syncStockPrice(ticker: String, timeout: Long, unit: TimeUnit): ListenableFuture 44 | 45 | 46 | enum class SyncResult { 47 | SUCCESS, UNKNOWN, FAILURE, TIMEOUT 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/viewmodel/PagedStockPricesViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.viewmodel 18 | 19 | import androidx.lifecycle.LiveData 20 | import androidx.lifecycle.ViewModel 21 | import androidx.paging.PagedList 22 | import androidx.annotation.MainThread 23 | import com.hyperaware.android.firebasejetpack.model.StockPrice 24 | import com.hyperaware.android.firebasejetpack.repo.QueryItemOrException 25 | import com.hyperaware.android.firebasejetpack.repo.StockRepository 26 | import org.koin.standalone.KoinComponent 27 | import org.koin.standalone.inject 28 | 29 | class PagedStockPricesViewModel : ViewModel(), KoinComponent { 30 | 31 | private val stockRepo by inject() 32 | 33 | private var stockPricesLiveData: LiveData>>? = null 34 | 35 | @MainThread 36 | fun getAllStockPricesPagedListLiveData(): LiveData>> { 37 | var liveData = stockPricesLiveData 38 | if (liveData == null) { 39 | // 5 is a ridiculously low page size in practice 40 | liveData = stockRepo.getStockPricePagedListLiveData(5) 41 | stockPricesLiveData = liveData 42 | } 43 | return liveData 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/firestore/DeserializeDocumentSnapshotTransform.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo.firestore 18 | 19 | import androidx.arch.core.util.Function 20 | import com.hyperaware.android.firebasejetpack.common.DataOrException 21 | import com.hyperaware.android.firebasejetpack.livedata.firestore.DocumentSnapshotOrException 22 | import com.hyperaware.android.firebasejetpack.repo.Deserializer 23 | 24 | internal class DeserializeDocumentSnapshotTransform( 25 | private val deserializer: DocumentSnapshotDeserializer 26 | ) : Function> { 27 | 28 | override fun apply(input: DocumentSnapshotOrException): DataOrException { 29 | val (snapshot, exception) = input 30 | return when { 31 | snapshot != null -> try { 32 | val data = deserializer.deserialize(snapshot) 33 | DataOrException(data, null) 34 | } 35 | catch (e: Deserializer.DeserializerException) { 36 | DataOrException(null, e) 37 | } 38 | 39 | exception != null -> DataOrException(null, exception) 40 | 41 | else -> DataOrException(null, Exception("Both data and exception were null")) 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/historical_price_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 31 | 32 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/stock_price_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 32 | 33 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 34 | 35 | 38 | 39 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/viewmodel/StockPriceViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.viewmodel 18 | 19 | import androidx.lifecycle.LiveData 20 | import androidx.lifecycle.Transformations 21 | import androidx.lifecycle.ViewModel 22 | import androidx.annotation.MainThread 23 | import com.hyperaware.android.firebasejetpack.common.DataOrException 24 | import com.hyperaware.android.firebasejetpack.repo.StockRepository 25 | import org.koin.standalone.KoinComponent 26 | import org.koin.standalone.inject 27 | 28 | typealias StockPriceDisplayOrException = DataOrException 29 | 30 | class StockPriceViewModel : ViewModel(), KoinComponent { 31 | 32 | private val stockRepo by inject() 33 | 34 | private var stockPricesLiveData = HashMap>() 35 | 36 | @MainThread 37 | fun getStockLiveData(ticker: String): LiveData { 38 | var liveData = stockPricesLiveData[ticker] 39 | if (liveData == null) { 40 | val ld = stockRepo.getStockPriceLiveData(ticker) 41 | liveData = Transformations.map(ld) { 42 | StockPriceDisplayOrException(it.data?.toStockPriceDisplay(), it.exception) 43 | } 44 | stockPricesLiveData[ticker] = liveData 45 | } 46 | return liveData 47 | } 48 | 49 | val allTickers = stockRepo.allTickers 50 | 51 | } 52 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/activity/livepricehistory/StockPriceHistoryListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.hyperaware.android.firebasejetpack.activity.livepricehistory 2 | 3 | import androidx.recyclerview.widget.AsyncDifferConfig 4 | import androidx.recyclerview.widget.ListAdapter 5 | import android.view.LayoutInflater 6 | import android.view.ViewGroup 7 | import com.hyperaware.android.firebasejetpack.config.AppExecutors 8 | import com.hyperaware.android.firebasejetpack.databinding.HistoricalPriceListItemBinding 9 | import com.hyperaware.android.firebasejetpack.repo.QueryItem 10 | import com.hyperaware.android.firebasejetpack.viewmodel.StockPriceDisplay 11 | import com.hyperaware.android.firebasejetpack.viewmodel.stockPriceDisplayDiffCallback 12 | import org.koin.standalone.KoinComponent 13 | import org.koin.standalone.inject 14 | 15 | /** 16 | * This ListAdapter maps a QueryItem data object into a 17 | * HistoricalPriceViewHolder via data binding in historical_price_list_item. 18 | */ 19 | 20 | internal class StockPriceHistoryListAdapter 21 | : ListAdapter, HistoricalPriceViewHolder>(asyncDifferConfig) { 22 | 23 | companion object : KoinComponent { 24 | private val executors by inject() 25 | private val asyncDifferConfig = 26 | AsyncDifferConfig.Builder>(stockPriceDisplayDiffCallback) 27 | .setBackgroundThreadExecutor(executors.cpuExecutorService) 28 | .build() 29 | } 30 | 31 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoricalPriceViewHolder { 32 | // Using data binding on the individual views 33 | val inflater = LayoutInflater.from(parent.context) 34 | val binding = HistoricalPriceListItemBinding.inflate(inflater, parent, false) 35 | return HistoricalPriceViewHolder(binding) 36 | } 37 | 38 | override fun onBindViewHolder(holder: HistoricalPriceViewHolder, position: Int) { 39 | val qItem = getItem(position) 40 | holder.binding.stockPrice = qItem.item 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/livedata/firestore/FirestoreQueryLiveData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.livedata.firestore 18 | 19 | import com.google.firebase.firestore.* 20 | import com.hyperaware.android.firebasejetpack.common.DataOrException 21 | import com.hyperaware.android.firebasejetpack.config.AppExecutors 22 | import com.hyperaware.android.firebasejetpack.livedata.common.LingeringLiveData 23 | import org.koin.standalone.KoinComponent 24 | import org.koin.standalone.inject 25 | 26 | typealias DocumentSnapshotsOrException = DataOrException?, FirebaseFirestoreException?> 27 | 28 | class FirestoreQueryLiveData(private val query: Query) 29 | : LingeringLiveData(), EventListener, KoinComponent { 30 | 31 | private val executors by inject() 32 | 33 | private var listenerRegistration: ListenerRegistration? = null 34 | 35 | override fun beginLingering() { 36 | listenerRegistration = query.addSnapshotListener( 37 | executors.cpuExecutorService, 38 | MetadataChanges.INCLUDE, 39 | this) 40 | } 41 | 42 | override fun endLingering() { 43 | listenerRegistration?.remove() 44 | } 45 | 46 | override fun onEvent(snapshot: QuerySnapshot?, e: FirebaseFirestoreException?) { 47 | val documents = snapshot?.documents 48 | postValue(DocumentSnapshotsOrException(documents, e)) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion 29 8 | defaultConfig { 9 | applicationId "com.hyperaware.android.firebasejetpack" 10 | minSdkVersion 21 11 | targetSdkVersion 29 12 | versionCode 3 13 | versionName "0.1" 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | dataBinding { 23 | enabled true 24 | } 25 | kotlinOptions { 26 | jvmTarget = "1.8" 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 32 | 33 | implementation 'androidx.activity:activity-ktx:1.1.0' 34 | implementation 'androidx.appcompat:appcompat:1.1.0' 35 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 36 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 37 | implementation "androidx.paging:paging-runtime:2.1.2" 38 | implementation 'androidx.work:work-runtime-ktx:2.3.4' 39 | implementation 'androidx.work:work-gcm:2.3.4' 40 | 41 | implementation 'com.google.android.material:material:1.1.0' 42 | 43 | implementation 'com.google.firebase:firebase-core:17.4.2' 44 | implementation 'com.google.firebase:firebase-auth:19.3.1' 45 | implementation 'com.google.firebase:firebase-database:19.3.0' 46 | implementation 'com.google.firebase:firebase-firestore:21.4.3' 47 | // needed to get Firestore/GRPC/Guava to play with WorkManager/ListenableFuture 48 | implementation 'com.google.guava:guava:27.0.1-android' 49 | implementation 'com.google.firebase:firebase-messaging:20.2.0' 50 | implementation 'com.firebaseui:firebase-ui-auth:6.2.1' 51 | 52 | implementation 'org.koin:koin-android:1.0.2' 53 | } 54 | 55 | apply plugin: 'com.google.gms.google-services' 56 | -------------------------------------------------------------------------------- /backend/functions/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | require('source-map-support').install() 18 | 19 | import * as functions from 'firebase-functions' 20 | import { initializeApp } from 'firebase-admin' 21 | import { StockMachine } from './stocks/stock-machine' 22 | 23 | const PERIOD = 1500 // 1.5 seconds between ticks 24 | 25 | let machine: StockMachine // lazy init 26 | 27 | /* 28 | * This function implements a periodic tick system that advances the state 29 | * of the stock market "machine" implemented in StockMachine. It uses 30 | * writes to RTDB to schedule the next run of this function. The function 31 | * will likely execute faster than the period, so it will sleep for as long 32 | * as required to delay execution of the next tick. 33 | */ 34 | 35 | export const onStockMachineWrite = 36 | functions.database.ref('/machine').onWrite(async (change, context) => { 37 | const after = change.after.val() 38 | if (!after) { 39 | console.log("/machine is empty") 40 | return null 41 | } 42 | 43 | if (!after.enabled) { 44 | console.log("machine is disabled") 45 | return null 46 | } 47 | 48 | if (!machine) { 49 | const app = initializeApp() 50 | machine = new StockMachine(app) 51 | } 52 | 53 | await machine.onTick() 54 | 55 | const wait = PERIOD - (Date.now() - after.lastTick) 56 | if (wait > 0) { 57 | await sleep(wait) 58 | } 59 | 60 | await change.after.ref.child('lastTick').set(Date.now()) 61 | }) 62 | 63 | 64 | async function sleep(ms) { 65 | return new Promise(resolve => setTimeout(resolve, ms)); 66 | } 67 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/viewmodel/StockPriceDisplay.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.viewmodel 18 | 19 | import android.annotation.SuppressLint 20 | import com.hyperaware.android.firebasejetpack.diffcallback.QueryItemDiffCallback 21 | import com.hyperaware.android.firebasejetpack.model.StockPrice 22 | import com.hyperaware.android.firebasejetpack.viewmodel.Formatters.priceFormatter 23 | import com.hyperaware.android.firebasejetpack.viewmodel.Formatters.timeFormatter 24 | import java.text.NumberFormat 25 | import java.text.SimpleDateFormat 26 | 27 | /** 28 | * A container class for displaying properly formatted stock price data. 29 | */ 30 | 31 | data class StockPriceDisplay( 32 | val ticker: String, 33 | val price: String, 34 | val time: String 35 | ) 36 | 37 | /** 38 | * Converts a StockPrice object into a StockPriceDisplay object. 39 | */ 40 | 41 | fun StockPrice.toStockPriceDisplay() = StockPriceDisplay( 42 | this.ticker, 43 | priceFormatter.format(this.price), 44 | timeFormatter.format(this.time) 45 | ) 46 | 47 | val stockPriceDisplayDiffCallback = object : QueryItemDiffCallback() {} 48 | 49 | 50 | @SuppressLint("SimpleDateFormat") 51 | private object Formatters { 52 | 53 | val timeFormatter by lazy { 54 | SimpleDateFormat("HH:mm:ss") 55 | } 56 | 57 | val priceFormatter by lazy { 58 | val priceFormatter = NumberFormat.getNumberInstance() 59 | priceFormatter.minimumFractionDigits = 2 60 | priceFormatter.maximumFractionDigits = 2 61 | priceFormatter 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/koin/KoinInitContentProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.koin 18 | 19 | import android.content.ContentProvider 20 | import android.content.ContentValues 21 | import android.database.Cursor 22 | import android.net.Uri 23 | import android.util.Log 24 | import org.koin.android.ext.android.startKoin 25 | 26 | /** 27 | * Initializes Koin automatically at app process start when this 28 | * ContentProvider is created. 29 | */ 30 | 31 | class KoinInitContentProvider : ContentProvider() { 32 | 33 | companion object { 34 | private const val TAG = "KoinInitContentProvider" 35 | } 36 | 37 | override fun onCreate(): Boolean { 38 | startKoin(context!!, allModules) 39 | val config = SingletonRuntimeConfig.instance 40 | Log.i(TAG, "StockRepository: ${config.stockRepository.javaClass.canonicalName}") 41 | return true 42 | } 43 | 44 | override fun insert(p0: Uri, values: ContentValues?): Uri? { 45 | return null 46 | } 47 | 48 | override fun query(p0: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? { 49 | return null 50 | } 51 | 52 | override fun update(p0: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int { 53 | return 0 54 | } 55 | 56 | override fun delete(p0: Uri, selection: String?, selectionArgs: Array?): Int { 57 | return 0 58 | } 59 | 60 | override fun getType(p0: Uri): String? { 61 | return null 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/config/AppExecutors.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.config 18 | 19 | import android.os.Handler 20 | import android.os.Looper 21 | import java.util.concurrent.Executor 22 | import java.util.concurrent.ExecutorService 23 | import java.util.concurrent.Executors 24 | import kotlin.math.max 25 | 26 | /** 27 | * Shared executors for use throughout the app by way of dependency injection. 28 | */ 29 | 30 | class AppExecutors internal constructor() { 31 | 32 | companion object { 33 | private const val NUM_NETWORK_THREADS = 3 34 | private val NUM_CPU_THREADS = max(1, Runtime.getRuntime().availableProcessors() - 1) 35 | val instance: AppExecutors by lazy { AppExecutors() } 36 | } 37 | 38 | /** For work that's bound to local disk activity. */ 39 | val diskExecutorService: ExecutorService by lazy { Executors.newSingleThreadExecutor() } 40 | /** For work that's bound to networking. */ 41 | val networkExecutorService: ExecutorService by lazy { Executors.newFixedThreadPool(NUM_NETWORK_THREADS) } 42 | /** For work that's bound to CPU computation. */ 43 | val cpuExecutorService: ExecutorService by lazy { Executors.newFixedThreadPool(NUM_CPU_THREADS) } 44 | /** For work that must run on the main thread. */ 45 | val mainExecutor by lazy { MainThreadExecutor() } 46 | 47 | class MainThreadExecutor : Executor { 48 | private val mainThreadHandler = Handler(Looper.getMainLooper()) 49 | 50 | override fun execute(command: Runnable) { 51 | mainThreadHandler.post(command) 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /backend/functions/src/tick.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // 18 | // This is a command line program that advances the state of StockMachine 19 | // by calling its onTick method some number of times. It will advance 20 | // forever by default, or with a number of ticks given in the --ticks flag. 21 | // 22 | 23 | require('source-map-support').install() 24 | 25 | import * as admin from 'firebase-admin' 26 | import { StockMachine } from './stocks/stock-machine' 27 | 28 | const PERIOD = 1000 29 | 30 | interface Options { 31 | ticks: number 32 | } 33 | 34 | const args = require('command-line-args') 35 | const options: Options = args([ 36 | { name: 'ticks', alias: 't', type: Number } 37 | ]) 38 | 39 | let ticks = options.ticks 40 | const forever = !(ticks > 0) 41 | 42 | const serviceAccount = require('../service-account.json') 43 | const app = admin.initializeApp({ 44 | credential: admin.credential.cert(serviceAccount), 45 | databaseURL: `https://${serviceAccount.project_id}.firebaseio.com` 46 | }) 47 | 48 | // Needed temporarily to kill the warning about timestamps 49 | app.firestore().settings({ timestampsInSnapshots: true }) 50 | 51 | async function sleep(ms) { 52 | return new Promise(resolve => setTimeout(resolve, ms)); 53 | } 54 | 55 | async function go() { 56 | const machine = new StockMachine(app) 57 | let lastTick 58 | while (ticks-- > 0 || forever) { 59 | lastTick = Date.now() 60 | console.log("tick") 61 | await machine.onTick() 62 | await sleep(PERIOD - (Date.now() - lastTick)) 63 | } 64 | } 65 | 66 | go().then(() => { 67 | process.exit(0) 68 | }) 69 | .catch(err => { 70 | console.error(err) 71 | process.exit(1) 72 | }) 73 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/livedata/firestore/FirestoreDocumentLiveData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.livedata.firestore 18 | 19 | import com.google.firebase.firestore.* 20 | import com.hyperaware.android.firebasejetpack.config.AppExecutors 21 | import com.hyperaware.android.firebasejetpack.livedata.common.LingeringLiveData 22 | import com.hyperaware.android.firebasejetpack.common.DataOrException 23 | import org.koin.standalone.KoinComponent 24 | import org.koin.standalone.inject 25 | 26 | typealias DocumentSnapshotOrException = DataOrException 27 | 28 | class FirestoreDocumentLiveData(private val ref: DocumentReference) 29 | : LingeringLiveData(), EventListener, KoinComponent { 30 | 31 | private val executors by inject() 32 | 33 | private var listenerRegistration: ListenerRegistration? = null 34 | 35 | override fun beginLingering() { 36 | listenerRegistration = ref.addSnapshotListener(executors.cpuExecutorService, this) 37 | } 38 | 39 | override fun endLingering() { 40 | listenerRegistration?.remove() 41 | } 42 | 43 | override fun onEvent(snapshot: DocumentSnapshot?, e: FirebaseFirestoreException?) { 44 | // Using postValue instead of setValue here, since Firestore events can be 45 | // configured to be received on any executor. 46 | // 47 | if (snapshot != null) { 48 | postValue(DocumentSnapshotOrException(snapshot, null)) 49 | } 50 | else if (e != null) { 51 | postValue(DocumentSnapshotOrException(null, e)) 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/livedata/rtdb/RealtimeDatabaseQueryLiveData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.livedata.rtdb 18 | 19 | import com.google.firebase.database.* 20 | import com.hyperaware.android.firebasejetpack.livedata.common.LingeringLiveData 21 | import com.hyperaware.android.firebasejetpack.common.DataOrException 22 | 23 | typealias DataSnapshotOrException = DataOrException 24 | 25 | class RealtimeDatabaseQueryLiveData : LingeringLiveData, ValueEventListener { 26 | 27 | private val query: Query 28 | private val displayPath: String 29 | 30 | constructor(ref: DatabaseReference) { 31 | this.query = ref 32 | this.displayPath = refToPath(ref) 33 | } 34 | 35 | constructor(query: Query) { 36 | this.query = query 37 | this.displayPath = "query@${refToPath(query.ref)}" 38 | } 39 | 40 | private fun refToPath(ref: DatabaseReference): String { 41 | var r = ref 42 | val parts = mutableListOf() 43 | while (r.key != null) { 44 | parts.add(r.key!!) 45 | r = r.parent!! 46 | } 47 | return parts.asReversed().joinToString("/") 48 | } 49 | 50 | override fun beginLingering() { 51 | query.addValueEventListener(this) 52 | } 53 | 54 | override fun endLingering() { 55 | query.removeEventListener(this) 56 | } 57 | 58 | override fun onDataChange(snap: DataSnapshot) { 59 | value = DataSnapshotOrException(snap, null) 60 | } 61 | 62 | override fun onCancelled(e: DatabaseError) { 63 | value = DataSnapshotOrException(null, e.toException()) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firebase & Jetpack 2 | 3 | This repo contains an Android app that demonstrates an (evolving) approach to 4 | the use of Firebase along with some of Android's [Jetpack][2] components, 5 | including ViewModel, LiveData, paging, data binding, and WorkManager. Full 6 | implementations are provided for [Realtime Database][3] and [Firestore][4]. 7 | 8 | ## Live Demo 9 | 10 | You can use a fully functioning version of this app by [installing it from the Play Store][1]. 11 | 12 | ## Setup 13 | 14 | Minimally, to get started: 15 | 16 | 1. Create a Firebase project 17 | 1. Enable Firestore and Realtime Database 18 | 1. Enable Google auth 19 | 1. Add add an Android app with the package name "com.hyperaware.android.firebasejetpack" and your SHA-1 20 | 1. Download google-services.json and place it in `android/app/google-services.json` 21 | 1. Download a service account from the console and place it in `backend/functions/service-account.json` 22 | 1. Initialize the backend code with `cd backend/functions; firebase init` and attach it to your project 23 | 1. Deploy the backend with `firebase deploy` 24 | 1. Run `node ./lib/tick.js --ticks 1` to bootstrap the database. Run with no args to tick infinitely. 25 | 1. Build and run the Android app to watch stock price changes in real time. 26 | 27 | More details instructions and walkthrough coming later. 28 | 29 | ## License 30 | 31 | The code in this project is licensed under the Apache License 2.0. 32 | 33 | ```text 34 | Copyright 2018 Google LLC 35 | 36 | Licensed under the Apache License, Version 2.0 (the "License"); 37 | you may not use this file except in compliance with the License. 38 | You may obtain a copy of the License at 39 | 40 | https://www.apache.org/licenses/LICENSE-2.0 41 | 42 | Unless required by applicable law or agreed to in writing, software 43 | distributed under the License is distributed on an "AS IS" BASIS, 44 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 45 | See the License for the specific language governing permissions and 46 | limitations under the License. 47 | ``` 48 | 49 | ## Disclaimer 50 | 51 | This is not an officially supported Google product. 52 | 53 | 54 | [1]: https://play.google.com/store/apps/details?id=com.hyperaware.android.firebasejetpack 55 | [2]: https://developer.android.com/jetpack/ 56 | [3]: https://firebase.google.com/docs/database/ 57 | [4]: https://firebase.google.com/docs/firestore/ 58 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/rtdb/DeserializeQuerySnapshotTransform.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo.rtdb 18 | 19 | import androidx.arch.core.util.Function 20 | import com.hyperaware.android.firebasejetpack.livedata.rtdb.DataSnapshotOrException 21 | import com.hyperaware.android.firebasejetpack.repo.Deserializer 22 | import com.hyperaware.android.firebasejetpack.repo.QueryItem 23 | import com.hyperaware.android.firebasejetpack.repo.QueryResultsOrException 24 | 25 | internal class DeserializeQuerySnapshotTransform( 26 | private val deserializer: DataSnapshotDeserializer 27 | ) : Function> { 28 | 29 | override fun apply(input: DataSnapshotOrException): QueryResultsOrException { 30 | val (snapshot, exception) = input 31 | return when { 32 | snapshot != null -> return try { 33 | val items = snapshot.children.map { child -> 34 | val item = deserializer.deserialize(child) 35 | object : QueryItem { 36 | override val item: T 37 | get() = item 38 | override val id: String 39 | get() = child.key!! 40 | } 41 | } 42 | QueryResultsOrException(items, null) 43 | } 44 | catch (e: Deserializer.DeserializerException) { 45 | QueryResultsOrException(null, e) 46 | } 47 | 48 | exception != null -> QueryResultsOrException(null, exception) 49 | 50 | else -> QueryResultsOrException(null, Exception("")) 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/rtdb/Deserializers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo.rtdb 18 | 19 | import com.google.firebase.database.DataSnapshot 20 | import com.hyperaware.android.firebasejetpack.model.StockPrice 21 | import com.hyperaware.android.firebasejetpack.repo.Deserializer 22 | import java.util.* 23 | 24 | internal interface DataSnapshotDeserializer : Deserializer 25 | 26 | internal class StockPriceSnapshotDeserializer : DataSnapshotDeserializer { 27 | override fun deserialize(input: DataSnapshot): StockPrice { 28 | val data = input.value 29 | return if (data is Map<*, *>) { 30 | val ticker = input.key!! 31 | 32 | val price = data["price"] ?: 33 | throw Deserializer.DeserializerException("price was missing for stock price snapshot $ticker") 34 | val priceFloat = if (price is Number) { 35 | price.toFloat() 36 | } 37 | else { 38 | throw Deserializer.DeserializerException("price not a float for stock price snapshot $ticker") 39 | } 40 | 41 | val time = data["time"] ?: 42 | throw Deserializer.DeserializerException("time was missing for stock price snapshot $ticker") 43 | val timeDate = if (time is Number) { 44 | Date(time.toLong()) 45 | } 46 | else { 47 | throw Deserializer.DeserializerException("time not a number for stock price snapshot $ticker") 48 | } 49 | 50 | StockPrice(ticker, priceFloat, timeDate) 51 | } 52 | else { 53 | throw Deserializer.DeserializerException("DataSnapshot value wasn't an object Map") 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/fcm/MyFirebaseMessagingService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.fcm 18 | 19 | import android.util.Log 20 | import androidx.work.* 21 | import com.google.firebase.messaging.FirebaseMessaging 22 | import com.google.firebase.messaging.FirebaseMessagingService 23 | import com.google.firebase.messaging.RemoteMessage 24 | import com.hyperaware.android.firebasejetpack.worker.StockPriceSyncWorker 25 | 26 | class MyFirebaseMessagingService : FirebaseMessagingService() { 27 | 28 | companion object { 29 | private const val TAG = "MessagingService" 30 | } 31 | 32 | override fun onNewToken(token: String) { 33 | Log.d(TAG, "FCM token: $token") 34 | // TODO do this proper with auth and stuff 35 | FirebaseMessaging.getInstance().subscribeToTopic("HSTK") 36 | } 37 | 38 | override fun onMessageReceived(message: RemoteMessage) { 39 | Log.d(TAG, "From: ${message.from}, To: ${message.to}") 40 | Log.d(TAG, "Data: ${message.data}") 41 | 42 | val ticker = message.data["ticker"] 43 | if (ticker == null || ticker.isEmpty()) { 44 | Log.w(TAG, "No ticker found in message") 45 | return 46 | } 47 | 48 | val constraints = Constraints.Builder() 49 | .setRequiredNetworkType(NetworkType.CONNECTED) 50 | .build() 51 | 52 | val data = workDataOf("ticker" to ticker) 53 | val workRequest = OneTimeWorkRequestBuilder() 54 | .setConstraints(constraints) 55 | .setInputData(data) 56 | .build() 57 | 58 | WorkManager.getInstance(this) 59 | .beginUniqueWork("sync_$ticker", ExistingWorkPolicy.REPLACE, workRequest) 60 | .enqueue() 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/firestore/DeserializeDocumentSnapshotsTransform.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo.firestore 18 | 19 | import androidx.arch.core.util.Function 20 | import com.hyperaware.android.firebasejetpack.livedata.firestore.DocumentSnapshotsOrException 21 | import com.hyperaware.android.firebasejetpack.repo.Deserializer 22 | import com.hyperaware.android.firebasejetpack.repo.QueryItem 23 | import com.hyperaware.android.firebasejetpack.repo.QueryResultsOrException 24 | 25 | internal class DeserializeDocumentSnapshotsTransform( 26 | private val deserializer: DocumentSnapshotDeserializer 27 | ) : Function> { 28 | 29 | override fun apply(input: DocumentSnapshotsOrException): QueryResultsOrException { 30 | val (snapshots, exception) = input 31 | return when { 32 | snapshots != null -> return try { 33 | val items = snapshots.map { snapshot -> 34 | val data = deserializer.deserialize(snapshot) 35 | object : QueryItem { 36 | override val item: T 37 | get() = data 38 | override val id: String 39 | get() = snapshot.id 40 | } 41 | } 42 | QueryResultsOrException(items, null) 43 | } 44 | catch (e: Deserializer.DeserializerException) { 45 | QueryResultsOrException(null, e) 46 | } 47 | 48 | exception != null -> QueryResultsOrException(null, exception) 49 | 50 | else -> QueryResultsOrException(null, Exception("Both data and exception were null")) 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/livedata/common/LingeringLiveData.kt: -------------------------------------------------------------------------------- 1 | package com.hyperaware.android.firebasejetpack.livedata.common 2 | 3 | import androidx.lifecycle.LiveData 4 | import android.os.Handler 5 | import androidx.annotation.CallSuper 6 | 7 | /** 8 | * A LiveData that allows its resource management to follow its active state, 9 | * but on a slight delay after inactivity, in order to allow expensive 10 | * resources to remain active during an Android configuration change. For 11 | * example, if it's expensive or undesirable for a LiveData to detach and 12 | * reattach a listener to some resource because of a configuration change, 13 | * LingeringLiveData softens that cost by allowing it to keep that listener 14 | * attach during the change. The downside is that the resource doesn't get 15 | * released at the very moment that it might no longer be removed. 16 | */ 17 | 18 | abstract class LingeringLiveData : LiveData() { 19 | 20 | companion object { 21 | private const val STOP_LISTENING_DELAY = 2000L 22 | } 23 | 24 | // To be fully unit-testable, this code should use an abstraction for 25 | // future work scheduling rather than Handler itself. 26 | private val handler = Handler() 27 | 28 | private var stopLingeringPending = false 29 | private val stopLingeringRunnable = StopLingeringRunnable() 30 | 31 | /** 32 | * Called during onActive, but only if it was not previously in a 33 | * "lingering" state. 34 | */ 35 | abstract fun beginLingering() 36 | 37 | /** 38 | * Called two seconds after onInactive, but only if onActive is not 39 | * called during that time. 40 | */ 41 | abstract fun endLingering() 42 | 43 | @CallSuper 44 | override fun onActive() { 45 | if (stopLingeringPending) { 46 | handler.removeCallbacks(stopLingeringRunnable) 47 | } 48 | else { 49 | beginLingering() 50 | } 51 | stopLingeringPending = false 52 | } 53 | 54 | @CallSuper 55 | override fun onInactive() { 56 | handler.postDelayed(stopLingeringRunnable, STOP_LISTENING_DELAY) 57 | stopLingeringPending = true 58 | } 59 | 60 | private inner class StopLingeringRunnable : Runnable { 61 | override fun run() { 62 | if (stopLingeringPending) { 63 | stopLingeringPending = false 64 | endLingering() 65 | } 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/koin/Modules.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.koin 18 | 19 | import com.google.firebase.auth.FirebaseAuth 20 | import com.google.firebase.database.FirebaseDatabase 21 | import com.google.firebase.firestore.FirebaseFirestore 22 | import com.hyperaware.android.firebasejetpack.config.AppExecutors 23 | import com.hyperaware.android.firebasejetpack.repo.StockRepository 24 | import com.hyperaware.android.firebasejetpack.repo.firestore.FirestoreStockRepository 25 | import com.hyperaware.android.firebasejetpack.repo.rtdb.RealtimeDatabaseStockRepository 26 | import org.koin.dsl.module.Module 27 | import org.koin.dsl.module.module 28 | 29 | interface RuntimeConfig { 30 | var stockRepository: StockRepository 31 | } 32 | 33 | class SingletonRuntimeConfig : RuntimeConfig { 34 | companion object { 35 | val instance = SingletonRuntimeConfig() 36 | } 37 | 38 | override var stockRepository: StockRepository = firestoreStockRepository 39 | } 40 | 41 | private val firestoreStockRepository by lazy { FirestoreStockRepository() } 42 | private val realtimeDatabaseStockRepository by lazy { RealtimeDatabaseStockRepository() } 43 | 44 | val appModule: Module = module { 45 | single { firestoreStockRepository } 46 | single { realtimeDatabaseStockRepository } 47 | single { SingletonRuntimeConfig.instance as RuntimeConfig } 48 | factory { get().stockRepository } 49 | single { AppExecutors.instance } 50 | } 51 | 52 | val firebaseModule: Module = module { 53 | single { FirebaseAuth.getInstance() } 54 | single { FirebaseFirestore.getInstance() } 55 | single { 56 | val instance = FirebaseDatabase.getInstance() 57 | instance.setPersistenceEnabled(false) 58 | instance 59 | } 60 | } 61 | 62 | val allModules = listOf(appModule, firebaseModule) 63 | -------------------------------------------------------------------------------- /backend/functions/src/stocks/firestore-repo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { StockRepositoryBase, StockPrice } from './repo' 18 | import { firestore } from 'firebase-admin' 19 | 20 | export class FirestoreStockRepository extends StockRepositoryBase { 21 | 22 | private firestore: firestore.Firestore 23 | private stocksLiveCollection: firestore.CollectionReference 24 | 25 | constructor(fstore: firestore.Firestore) { 26 | super() 27 | this.firestore = fstore 28 | this.stocksLiveCollection = this.firestore.collection('stocks-live') 29 | } 30 | 31 | async getStockLive(ticker: string): Promise { 32 | const snap = await this.stocksLiveCollection.doc(ticker).get() 33 | if (snap.exists) { 34 | const price = snap.data() as StockPrice 35 | return { 36 | price: price.price, 37 | time: price.time 38 | } 39 | } 40 | else { 41 | return null 42 | } 43 | } 44 | 45 | async updateStockPrice(ticker: string, stockPrice: StockPrice): Promise { 46 | const stockDoc = this.stocksLiveCollection.doc(ticker) 47 | const historyColl = stockDoc.collection('recent-history') 48 | await this.deleteOldHistory(historyColl) 49 | 50 | const p1 = stockDoc.set(stockPrice, { merge: true }) 51 | 52 | const historyId = stockPrice.time.getTime().toString() 53 | const historyDoc = historyColl.doc(historyId) 54 | const p2 = historyDoc.set(stockPrice) 55 | 56 | return Promise.all([p1, p2]) 57 | } 58 | 59 | private async deleteOldHistory(historyColl: firestore.CollectionReference): Promise { 60 | const expired = new Date(Date.now() - this.historyExpires) 61 | const snapshot = await historyColl.where('time', '<', expired).get() 62 | return Promise.all(snapshot.docs.map(snap => snap.ref.delete())) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/repo/firestore/DeserializeDocumentSnapshotAsyncTransform.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.repo.firestore 18 | 19 | import androidx.arch.core.util.Function 20 | import androidx.lifecycle.LiveData 21 | import androidx.lifecycle.MutableLiveData 22 | import com.hyperaware.android.firebasejetpack.common.DataOrException 23 | import com.hyperaware.android.firebasejetpack.config.AppExecutors 24 | import com.hyperaware.android.firebasejetpack.livedata.firestore.DocumentSnapshotOrException 25 | import com.hyperaware.android.firebasejetpack.repo.Deserializer 26 | import org.koin.standalone.KoinComponent 27 | import org.koin.standalone.inject 28 | 29 | internal class DeserializeDocumentSnapshotAsyncTransform( 30 | private val deserializer: DocumentSnapshotDeserializer 31 | ) : Function>>, KoinComponent { 32 | 33 | private val executors by inject() 34 | 35 | override fun apply(input: DocumentSnapshotOrException): LiveData> { 36 | val (snapshot, exception) = input 37 | val liveData = MutableLiveData>() 38 | if (snapshot != null) { 39 | // Do this in a thread when DataSnapshot deserialization 40 | // could be costly (e.g. using reflection). 41 | executors.cpuExecutorService.execute { 42 | try { 43 | val value = deserializer.deserialize(snapshot) 44 | liveData.postValue(DataOrException(value, null)) 45 | } 46 | catch (e: Deserializer.DeserializerException) { 47 | liveData.postValue(DataOrException(null, e)) 48 | } 49 | } 50 | } 51 | else if (exception != null) { 52 | liveData.value = DataOrException(null, exception) 53 | } 54 | 55 | return liveData 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/hyperaware/android/firebasejetpack/viewmodel/StockPriceHistoryViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.hyperaware.android.firebasejetpack.viewmodel 18 | 19 | import androidx.lifecycle.LiveData 20 | import androidx.lifecycle.Transformations 21 | import androidx.lifecycle.ViewModel 22 | import com.hyperaware.android.firebasejetpack.model.StockPrice 23 | import com.hyperaware.android.firebasejetpack.repo.QueryItem 24 | import com.hyperaware.android.firebasejetpack.repo.QueryResultsOrException 25 | import com.hyperaware.android.firebasejetpack.repo.StockRepository 26 | import org.koin.standalone.KoinComponent 27 | import org.koin.standalone.inject 28 | 29 | class StockPriceHistoryViewModel : ViewModel(), KoinComponent { 30 | 31 | private val stockRepo by inject() 32 | 33 | private var stockHistoriesLiveData = HashMap>() 34 | 35 | fun getStockPriceHistory(ticker: String): LiveData { 36 | var liveData = stockHistoriesLiveData[ticker] 37 | if (liveData == null) { 38 | // Convert StockPriceHistoryQueryResults to StockPriceDisplayHistoryQueryResults 39 | val historyLiveData = stockRepo.getStockPriceHistoryLiveData(ticker) 40 | liveData = Transformations.map(historyLiveData) { results -> 41 | val convertedResults = results.data?.map { StockPriceDisplayQueryItem(it) } 42 | val exception = results.exception 43 | StockPriceDisplayHistoryQueryResults(convertedResults, exception) 44 | } 45 | } 46 | return liveData 47 | } 48 | 49 | } 50 | 51 | 52 | typealias StockPriceDisplayHistoryQueryResults = QueryResultsOrException 53 | 54 | private data class StockPriceDisplayQueryItem(private val _item: QueryItem) : QueryItem { 55 | private val convertedPrice = _item.item.toStockPriceDisplay() 56 | 57 | override val item: StockPriceDisplay 58 | get() = convertedPrice 59 | override val id: String 60 | get() = _item.id 61 | } 62 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 18 | 19 | 24 | 25 |