Showing preview only (265K chars total). Download the full file or copy to clipboard to get everything.
Repository: BracketCove/SpaceNotes
Branch: master
Commit: d799560fbb94
Files: 115
Total size: 230.6 KB
Directory structure:
gitextract_qf805k6a/
├── .gitignore
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── wiseassblog/
│ │ │ └── spacenotes/
│ │ │ ├── SpaceNotes.kt
│ │ │ ├── common/
│ │ │ │ ├── AndroidExt.kt
│ │ │ │ ├── BaseLogic.kt
│ │ │ │ ├── Constants.kt
│ │ │ │ └── Navigation.kt
│ │ │ ├── login/
│ │ │ │ ├── ILoginContract.kt
│ │ │ │ ├── LoginActivity.kt
│ │ │ │ ├── LoginLogic.kt
│ │ │ │ ├── LoginResult.kt
│ │ │ │ └── buildlogic/
│ │ │ │ └── LoginInjector.kt
│ │ │ ├── notedetail/
│ │ │ │ ├── INoteDetailContract.kt
│ │ │ │ ├── NoteDetailActivity.kt
│ │ │ │ ├── NoteDetailLogic.kt
│ │ │ │ ├── NoteDetailView.kt
│ │ │ │ ├── NoteDetailViewModel.kt
│ │ │ │ └── buildlogic/
│ │ │ │ └── NoteDetailInjector.kt
│ │ │ └── notelist/
│ │ │ ├── INoteListContract.kt
│ │ │ ├── NoteDiffUtilCallback.kt
│ │ │ ├── NoteListActivity.kt
│ │ │ ├── NoteListAdapter.kt
│ │ │ ├── NoteListLogic.kt
│ │ │ ├── NoteListView.kt
│ │ │ ├── NoteListViewModel.kt
│ │ │ └── buildlogic/
│ │ │ └── NoteListInjector.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── antenna_loop.xml
│ │ │ ├── antenna_loop_fast.xml
│ │ │ ├── ic_access_time_black_24dp.xml
│ │ │ ├── ic_arrow_back_black_24dp.xml
│ │ │ ├── ic_baseline_add_24px.xml
│ │ │ ├── ic_baseline_event_24px.xml
│ │ │ ├── ic_delete_forever_black_24dp.xml
│ │ │ ├── ic_done_black_24dp.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── ic_visibility_off_black_24dp.xml
│ │ │ ├── ic_vpn_key_black_24dp.xml
│ │ │ ├── satellite_beam.xml
│ │ │ └── space_loop.xml
│ │ ├── drawable-v24/
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── layout/
│ │ │ ├── activity_login.xml
│ │ │ ├── activity_note_detail.xml
│ │ │ ├── activity_note_list.xml
│ │ │ ├── activity_user_auth.xml
│ │ │ ├── fragment_note_detail.xml
│ │ │ ├── fragment_note_list.xml
│ │ │ └── item_note.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ └── values/
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ ├── styles.xml
│ │ └── view_styles.xml
│ └── test/
│ └── java/
│ └── com/
│ └── wiseassblog/
│ └── spacenotes/
│ ├── LoginLogicTest.kt
│ ├── NoteDetailLogicTest.kt
│ └── NoteListLogicTest.kt
├── build.gradle
├── data/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── wiseassblog/
│ │ │ └── data/
│ │ │ ├── DataExt.kt
│ │ │ ├── auth/
│ │ │ │ └── FirebaseAuthRepositoryImpl.kt
│ │ │ ├── datamodels/
│ │ │ │ ├── AnonymousRoomNote.kt
│ │ │ │ ├── FirebaseNote.kt
│ │ │ │ ├── RegisteredRoomNote.kt
│ │ │ │ └── RegisteredRoomTransaction.kt
│ │ │ ├── note/
│ │ │ │ ├── anonymous/
│ │ │ │ │ ├── AnonymousNoteDao.kt
│ │ │ │ │ ├── RoomAnonymousNoteDatabase.kt
│ │ │ │ │ └── RoomLocalAnonymousRepositoryImpl.kt
│ │ │ │ ├── public/
│ │ │ │ │ └── FirestorePublicNoteRepositoryImpl.kt
│ │ │ │ └── registered/
│ │ │ │ ├── FirestorePrivateRemoteNoteImpl.kt
│ │ │ │ ├── RegisteredNoteDao.kt
│ │ │ │ ├── RegisteredNoteRepositoryImpl.kt
│ │ │ │ ├── RegisteredTransactionDao.kt
│ │ │ │ ├── RoomLocalCacheImpl.kt
│ │ │ │ └── RoomRegisteredNoteDatabase.kt
│ │ │ └── transaction/
│ │ │ ├── RoomRegisteredTransactionDatabase.kt
│ │ │ └── RoomTransactionRepositoryImpl.kt
│ │ └── res/
│ │ └── values/
│ │ └── strings.xml
│ └── test/
│ └── java/
│ └── com/
│ └── wiseassblog/
│ └── data/
│ ├── ExtTest.kt
│ └── RegisteredNoteRepositoryTest.kt
├── domain/
│ ├── .gitignore
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── wiseassblog/
│ │ └── domain/
│ │ ├── DispatcherProvider.kt
│ │ ├── domainmodel/
│ │ │ ├── Note.kt
│ │ │ ├── NoteTransaction.kt
│ │ │ ├── Result.kt
│ │ │ └── User.kt
│ │ ├── error/
│ │ │ └── SpaceNotesError.kt
│ │ ├── interactor/
│ │ │ ├── AnonymousNoteSource.kt
│ │ │ ├── AuthSource.kt
│ │ │ ├── PublicNoteSource.kt
│ │ │ └── RegisteredNoteSource.kt
│ │ ├── repository/
│ │ │ ├── IAuthRepository.kt
│ │ │ ├── ILocalNoteRepository.kt
│ │ │ ├── IPublicNoteRepository.kt
│ │ │ ├── IRemoteNoteRepository.kt
│ │ │ └── ITransactionRepository.kt
│ │ └── servicelocator/
│ │ ├── NoteServiceLocator.kt
│ │ └── UserServiceLocator.kt
│ └── test/
│ └── java/
│ └── com/
│ └── wiseassblog/
│ └── domain/
│ ├── AnonymousNoteSourceTest.kt
│ ├── PublicNoteSourceTest.kt
│ └── RegisteredNoteSourceTest.kt
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── versions.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.DS_Store
/build
/captures
.externalNativeBuild
================================================
FILE: README.md
================================================
### Please Note:
- Since I see people are still looking at this repo, I want to be clear that I no longer recommend multi-module (i.e. multiple gradle subprojects) architecture unless you actually have a good reason for it. In this case, it really just adds extra complexity for no real benefit. However, multi-platform projects would be a great example of a situation where you'd want to use multi-module.
# SpaceNotes
New to Kotlin? Whether you are a seasoned Java veteran, or you're just starting
out with Kotlin for Android, consider checking out [Application Programming
Fundamentals w/ Kotlin](https://www.udemy.com/application-programming-fundamentals-with-kotlin/learn/v4/overview). If you like my videos and repositories, I think
you'll really enjoy a more polished course experience from wiseAss!
## What is SpaceNotes?
SpaceNotes is a Kotlin based Android Application, which was built with
best practices an innovation in mind. The app uses Coroutines for
concurrency and cross-module/boundary communication, a Clean Domain
Layer to allow the application to work properly across multiple platforms,
and a few of my favourite APIs from Android Architecture Component
and Firebase.
## Software Architecture
### Feature Specific (Front End Android):
#### NoteList Feature
<img src="notelistfeature.gif" alt="Note List" width="270" height="480"/>
This feature displays whatever Notes are currently available based on the user's status, such as: Anonymous, Registered Private, Registered Public
* INoteListContract specifies the different interactions between classes and the events which may occur in this particular feature
* NoteListActivity is a feature level “container”, within which these different things are deployed in to (it is also the entry point of the feature)
* NoteListLogic is the “decision maker” of the feature, which handles the events and interactions specified in the contract (this kind of class is the most important to test)
* NoteListView contains logic and bindings to the user interface
* NoteListAdapter contains a decoupled RecyclerView.ListAdapter w/ DiffUtil
* NoteListViewModel contains the most recent data which has been returned from the “backend” of the application (or data which is passed into the feature via navigation), and persists this data so that the logic class or view does not need to (if they did, it would break the separation of concerns)
* NoteListInjector: Build logic (Dpependency Injection Implementation) for this feature.
#### NoteDetail Feature
<img src="notedetail.png" alt="Note Detail" width="270" height="480"/>
This feature allows the User to view, update, create, and delete a Note. Data is stored in various local/remote datasources based on whether the user is or isn't logged in, and if they are in public or private mode.
* INoteDetailContract specifies the different interactions between classes and the events which may occur in this particular feature
* NoteDetailActivity is a feature level “container”, within which these different things are deployed in to (it is also the entry point of the feature)
* NoteDetailLogic is the “decision maker” of the feature, which handles the events and interactions specified in the contract (this kind of class is the most important to test)
* NoteDetailView contains logic and bindings to the user interface
* NoteDetailViewModel contains the most recent data which has been returned from the “backend” of the application (or data which is passed into the feature via navigation), and persists this data so that the logic class or view does not need to (if they did, it would break the separation of concerns)
* NoteDetailInjector: Build logic (Dpependency Injection Implementation) for this feature.
#### Login Feature
<img src="login.png" alt="Login" width="270" height="480"/>
This feature allows the User to authenticate with GoogleSignIn; which
is currently the only supported sign in function. No passwords or in-app
Sign up is required.
**Note:** I normally advocate against using Activities as Views, but I
ran in to a tight-coupling problem with GoogleSignIn API (which requires you
to override Activity.onActivityResult(...). Given this tight coupling,
and the simplicity of this feature (it only has two buttons including the toolbar), I decided to just use the Activity as a pragmatic decision.
* ILoginContract specifies the different interactions between classes and the events which may occur in this particular feature
* LoginActivity acts as the View and Container in this feature (for reasons mentioned above)
* LoginLogic is the “decision maker” of the feature, which handles the events and interactions specified in the contract (this kind of class is the most important to test)
* LoginResult Wrapper for when GoogleSignInProviders does it's thing (logging a User In)
* LoginInjector: Build logic (Dpependency Injection Implementation) for this feature.
#### Common:
* Navigation.kt: Contains Top-level functions for starting each feature with the appropriate arguments.
* Constants.kt: Contains messages and keys for front end Android
* BaseLogic.kt: Abstract class for Logic classes. Could be optimized, currently just contains a DispatcherProvider (for Coroutines) as a property, and a Job object for keeping track and disposing in-flight coroutines.
* AndroidExt.kt: Some handy Extensions functions for front end Android
### Domain:
The Domain Layer of this application has three primary purposes:
* Abstraction of the Data Layer of the application
* Providing a common, 3rd party library free set of Models (such as Note.kt) for different platforms of the App
* Providing a High-Level description of the applications primary functions based on problem domain analysis (such as User Stories)
#### Packages:
* domainmodel: POKOs (Plain Old Kotlin Objects) to be shared as a common model between different modules
* error: Sealed Class which contains application specific errors
* interactor: Interactors exist to coordinate the back end data sources. This is generally only necessary when there is more than one back data source, otherwise it ends up being
another unnecessary layer of abstraction over the repository.
* repository: Repository Interfaces which dictate the contractual obligations of each part of the back end. This allows the domain layer to coordinate the different back end components without needing to know their real implementations/libraries/dependencies.
* servicelocator: Service Locator promotes Functional Purity of the Interactor's functions, and acts as a method of Dependency Injection (by providing the dependencies as arguments to functions)
* DispatcherProvider: Used for Coroutines Implementation
### Data (Android Back End):
The Data Layer of this application contains implementations of the data sources which are described in the repository package of the domain layer.
#### Auth:
Currently implemented with FirebaseAuth; manages user authentication.
#### Data Models:
API specific data models, which are mapped from/to domain models. Each data model is created for a particular API, such as Firestore and Room.
#### Note:
Implementations for the anonymous, registered private, and registered public data sources which persist Note objects.
#### Transaction:
Transaction is only used for registered private users. It's purpose is to store offline transactions that the user makes to their registered repository, and attempts to push those transactions when the user reconnects to the remote firestore database.
#### DataExt.kt:
Contains all of the obnoxious but necessary Data Model Mapping functions, and some Coroutine wrappers over Firebase/GMS Tasks API
## Can I use code from this Repo?
Absolutely, pursuant to the project's [LICENSE](LICENSE.md). That being said, the logo and name are my intellectual creations, so don't use them unless you are linking/reffering to this Repo.
Follow the rules in the license, and you're good.
## Architecture Style:
This project uses Model-View-Whatever. It is the software architecture that has no particular style, yet accommodates all situations. In a less Zen way of speaking, I don't follow MVP, MVC, or MVVM strictly. I use parts of all styles of architectures based on whatever feature I'm creating, and that is what dictates the ultimate architecture of a given feature.
If you want me to explain in slightly more familiar terms, I basically apply MVP + VM as a front-end session datastore (such as arguments passed in from Activity Intents, current user states, current note to be displayed). I don't use ViewModels as Decision Maker Classes, hence the Logic class.
## Contact/Support me:
Follow the wiseAss Community:
https://www.instagram.com/wiseassbrand/
https://www.facebook.com/wiseassblog/
https://twitter.com/wiseass301
http://wiseassblog.com/
https://www.linkedin.com/in/ryan-kay-808388114
Support wiseAss here:
https://www.paypal.me/ryanmkay
## License
* Copyright 2016, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion build_versions.target_sdk
defaultConfig {
applicationId "com.wiseassblog.spacenotes"
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes {
debug {
testCoverageEnabled = true
minifyEnabled false
}
release {
testCoverageEnabled = false
minifyEnabled true
}
}
testOptions.unitTests.all {
useJUnitPlatform()
testLogging {
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
}
}
}
dependencies {
implementation project(":domain")
implementation project(":data")
//Dependencies
implementation deps.android.constraint_layout
implementation deps.android.lifecycle_extensions
implementation deps.android.ktx_fragment
implementation deps.android.fragment
implementation deps.android.appcompat
implementation deps.android.recyclerview
implementation deps.android.design
implementation deps.room.runtime
implementation deps.firebase.auth
implementation deps.firebase.firestore
implementation deps.play_services.auth
implementation deps.kotlin.kotlin_jre
implementation deps.kotlin.coroutines_core
implementation deps.kotlin.coroutines_android
kapt deps.room.compiler
testImplementation deps.test.junit
testRuntimeOnly deps.test.jupiter_engine
testRuntimeOnly deps.test.vintage_engine
testImplementation deps.test.mockk
testImplementation deps.test.kotlin_junit
}
apply plugin: 'com.google.gms.google-services'
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wiseassblog.spacenotes">
<application
android:name=".SpaceNotes"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".notedetail.NoteDetailActivity"></activity>
<activity android:name=".notelist.NoteListActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".login.LoginActivity"></activity>
</application>
</manifest>
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/SpaceNotes.kt
================================================
package com.wiseassblog.spacenotes
import android.app.Application
class SpaceNotes: Application() {
override fun onCreate() {
super.onCreate()
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/common/AndroidExt.kt
================================================
package com.wiseassblog.spacenotes.common
import android.app.Activity
import android.content.Intent
import android.text.Editable
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.wiseassblog.spacenotes.notedetail.NoteDetailActivity
import com.wiseassblog.spacenotes.notedetail.NoteDetailView
import com.wiseassblog.spacenotes.notelist.NoteListActivity
import java.text.SimpleDateFormat
import java.util.*
internal fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
internal fun Activity.attachFragment(manager: FragmentManager, containerId: Int, view: Fragment, tag: String) {
manager.beginTransaction()
.replace(containerId, view, tag)
.commitNowAllowingStateLoss()
}
internal fun Fragment.getCalendarTime(): String {
val cal = Calendar.getInstance(TimeZone.getDefault())
val format = SimpleDateFormat("d MMM yyyy HH:mm:ss Z")
format.timeZone = cal.timeZone
return format.format(cal.time)
}
internal fun Fragment.makeToast(value: String) {
Toast.makeText(activity, value, Toast.LENGTH_SHORT).show()
}
internal fun Fragment.restartCurrentFeature() {
val i: Intent
when (this) {
is NoteDetailView -> {
i = Intent(this.activity, NoteDetailActivity::class.java)
}
//To Be Added
else -> {
i = Intent(this.activity, NoteListActivity::class.java)
}
}
this.activity?.finish()
startActivity(i)
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/common/BaseLogic.kt
================================================
package com.wiseassblog.spacenotes.common
import com.wiseassblog.domain.DispatcherProvider
import kotlinx.coroutines.Job
/**
* Why use a base class? To both share implementation (properties and functions), and enforce a contract (interface) for all listener classes
*/
abstract class BaseLogic(val dispatcher: DispatcherProvider) {
protected lateinit var jobTracker: Job
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/common/Constants.kt
================================================
package com.wiseassblog.spacenotes.common
internal const val MESSAGE_DELETE_SUCCESSFUL = "Note successfully deleted."
internal const val MESSAGE_DELETE = "DELETE"
internal const val MESSAGE_DELETE_CONFIRMATION = "Delete note permanently?"
internal const val MESSAGE_GENERIC_ERROR = "An error has occured."
internal const val MESSAGE_LOGIN = "Log in to use public mode."
internal const val BOOLEAN_EXTRA_IS_PRIVATE = "BOOLEAN_EXTRA_IS_PRIVATE"
internal const val STRING_EXTRA_NOTE_ID = "STRING_EXTRA_NOTE_ID"
internal const val MODE_PRIVATE = "Private Notes"
internal const val MODE_PUBLIC = "Public Notes"
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/common/Navigation.kt
================================================
package com.wiseassblog.spacenotes.common
import android.app.Activity
import android.content.Intent
import com.wiseassblog.spacenotes.login.LoginActivity
import com.wiseassblog.spacenotes.notedetail.NoteDetailActivity
import com.wiseassblog.spacenotes.notelist.NoteListActivity
internal fun startListFeature(activity: Activity?) {
activity?.startActivity(
Intent(
activity,
NoteListActivity::class.java
)
).also { activity?.finish() }
}
internal fun startNoteDetailFeatureWithExtras(activity: Activity?, noteId: String, isPrivate: Boolean) {
val i = Intent(activity, NoteDetailActivity::class.java)
i.putExtra(STRING_EXTRA_NOTE_ID, noteId)
i.putExtra(BOOLEAN_EXTRA_IS_PRIVATE, isPrivate)
activity?.startActivity(i)
}
internal fun startLoginFeature(activity: Activity?) {
val i = Intent(activity, LoginActivity::class.java)
activity?.startActivity(i)
.also { activity?.finish() }
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/login/ILoginContract.kt
================================================
package com.wiseassblog.spacenotes.login
import androidx.lifecycle.Observer
interface ILoginContract {
interface View {
fun setLoginStatus(text: String)
fun setAuthButton(text: String)
fun showLoopAnimation()
fun setStatusDrawable(imageURL: String)
fun startSignInFlow()
fun setObserver(observer: Observer<LoginEvent<LoginResult>>)
fun startListFeature()
}
interface Logic {
fun event(event: LoginEvent<LoginResult>)
}
}
internal const val SIGN_OUT = "SIGN OUT"
internal const val SIGN_IN = "SIGN IN"
internal const val SIGNED_IN = "Signed In"
internal const val SIGNED_OUT = "Signed Out"
internal const val ERROR_NETWORK_UNAVAILABLE = "Network Unavailable"
internal const val ERROR_AUTH = "An Error Has Occured"
internal const val RETRY = "RETRY"
internal const val ANTENNA_EMPTY = "antenna_empty"
internal const val ANTENNA_FULL = "antenna_full"
/**
* This value is just a constant to denote our sign in request; It can be any int.
* Would have been great if that was explained in the docs, I assumed at first that it had to
* be a specific value.
*/
internal const val RC_SIGN_IN = 1337
sealed class LoginEvent<out T> {
object OnAuthButtonClick : LoginEvent<Nothing>()
object OnBackClick : LoginEvent<Nothing>()
object OnStart : LoginEvent<Nothing>()
data class OnGoogleSignInResult<out LoginResult>(val result: LoginResult) : LoginEvent<LoginResult>()
object OnDestroy : LoginEvent<Nothing>()
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/login/LoginActivity.kt
================================================
package com.wiseassblog.spacenotes.login
import android.content.Intent
import android.graphics.drawable.AnimationDrawable
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.ApiException
import com.wiseassblog.spacenotes.R
import com.wiseassblog.spacenotes.login.buildlogic.LoginInjector
import kotlinx.android.synthetic.main.activity_login.*
/**
* Q: Why did I decide to use an Activity as the View in this feature?
* A: Since I want to be able to use the GoogleSignIn API, there is necessary tight coupling
* with Activity in this feature. Further, since this feature is quite simple to begin with,
* I didn't mind breaking SoC a little bit in exchange for the GoogleSignIn functionality.
*
*/
class LoginActivity : AppCompatActivity(), ILoginContract.View {
override fun setObserver(observer: Observer<LoginEvent<LoginResult>>) = event.observeForever(observer)
override fun startListFeature() = com.wiseassblog.spacenotes.common.startListFeature(this)
val event = MutableLiveData<LoginEvent<LoginResult>>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
//Note: I cal setObserver within the LoginInjector function
ViewModelProviders.of(this)
.get(LoginInjector::class.java)
.buildLoginLogic(this)
btn_auth_attempt.setOnClickListener { event.value = LoginEvent.OnAuthButtonClick }
imb_toolbar_back.setOnClickListener { event.value = LoginEvent.OnBackClick }
}
override fun onResume() {
super.onResume()
event.value = LoginEvent.OnStart
}
override fun setLoginStatus(text: String) {
lbl_login_status_display.text = text
}
override fun setAuthButton(text: String) {
btn_auth_attempt.text = text
}
override fun showLoopAnimation() {
imv_antenna_animation.setImageResource(
resources.getIdentifier("antenna_loop_fast", "drawable", this.packageName)
)
val satelliteLoop = imv_antenna_animation.drawable as AnimationDrawable
satelliteLoop.start()
}
override fun setStatusDrawable(imageURL: String) {
imv_antenna_animation.setImageResource(
resources.getIdentifier(imageURL, "drawable", this.packageName)
)
}
override fun startSignInFlow() {
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.build()
val googleSignInClient = GoogleSignIn.getClient(this, gso)
val signInIntent = googleSignInClient.signInIntent
startActivityForResult(signInIntent, RC_SIGN_IN)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == RC_SIGN_IN) {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
try {
val account: GoogleSignInAccount? = task.getResult(ApiException::class.java)
event.value = LoginEvent.OnGoogleSignInResult(
LoginResult(
requestCode,
account
)
)
} catch (exception: Exception) {
event.value = LoginEvent.OnGoogleSignInResult(
LoginResult(
0,
null
)
)
}
}
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/login/LoginLogic.kt
================================================
package com.wiseassblog.spacenotes.login
import androidx.lifecycle.Observer
import com.wiseassblog.domain.DispatcherProvider
import com.wiseassblog.domain.servicelocator.UserServiceLocator
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.error.SpaceNotesError
import com.wiseassblog.domain.interactor.AuthSource
import com.wiseassblog.spacenotes.common.BaseLogic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
class LoginLogic(dispatcher: DispatcherProvider,
val userLocator: UserServiceLocator,
val view: ILoginContract.View,
val authSource: AuthSource) : BaseLogic(dispatcher), CoroutineScope, Observer<LoginEvent<LoginResult>> {
init {
jobTracker = Job()
}
override val coroutineContext: CoroutineContext
get() = dispatcher.provideUIContext() + jobTracker
override fun onChanged(event: LoginEvent<LoginResult>) {
when (event) {
is LoginEvent.OnStart -> onStart()
is LoginEvent.OnDestroy -> jobTracker.cancel()
is LoginEvent.OnBackClick -> onBackClick()
is LoginEvent.OnAuthButtonClick -> onAuthButtonClick()
is LoginEvent.OnGoogleSignInResult -> onSignInResult(event.result)
}
}
private fun onSignInResult(result: LoginResult) = launch {
if (result.requestCode == RC_SIGN_IN && result.account != null) {
view.showLoopAnimation()
val createGoogleUserResult = authSource.createGoogleUser(
result.account.idToken!!,
userLocator
)
when (createGoogleUserResult) {
is Result.Value -> onStart()
is Result.Error -> handleError(createGoogleUserResult.error)
}
} else {
renderErrorState(ERROR_AUTH)
}
}
private fun onAuthButtonClick() = launch {
view.showLoopAnimation()
val authResult = authSource.getCurrentUser(userLocator)
when (authResult) {
is Result.Value -> {
if (authResult.value == null) view.startSignInFlow()
else signUserOut()
}
is Result.Error -> handleError(authResult.error)
}
}
private fun handleError(error: Exception) {
when (error) {
is SpaceNotesError.NetworkUnavailableException -> renderErrorState(
ERROR_NETWORK_UNAVAILABLE
)
else -> renderErrorState(ERROR_AUTH)
}
}
private suspend fun signUserOut() {
val signOutResult = authSource.signOutCurrentUser(userLocator)
when (signOutResult) {
is Result.Value -> renderNullUser()
is Result.Error -> renderErrorState(ERROR_AUTH)
}
}
private fun onBackClick() {
view.startListFeature()
}
private fun onStart() = launch {
view.showLoopAnimation()
val authResult = authSource.getCurrentUser(userLocator)
when (authResult) {
is Result.Value -> {
if (authResult.value == null) renderNullUser()
else renderActiveUser()
}
is Result.Error -> handleError(authResult.error)
}
}
private fun renderActiveUser() {
view.setStatusDrawable(ANTENNA_FULL)
view.setAuthButton(SIGN_OUT)
view.setLoginStatus(SIGNED_IN)
}
private fun renderNullUser() {
view.setStatusDrawable(ANTENNA_EMPTY)
view.setAuthButton(SIGN_IN)
view.setLoginStatus(SIGNED_OUT)
}
private fun renderErrorState(message: String) {
view.setStatusDrawable(ANTENNA_EMPTY)
view.setAuthButton(RETRY)
view.setLoginStatus(message)
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/login/LoginResult.kt
================================================
package com.wiseassblog.spacenotes.login
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.tasks.Task
/**
* Wrapper class for data recieved in LoginActivity's onActivityResult()
* function
*/
data class LoginResult(val requestCode: Int, val account: GoogleSignInAccount?)
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/login/buildlogic/LoginInjector.kt
================================================
package com.wiseassblog.spacenotes.login.buildlogic
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.google.firebase.FirebaseApp
import com.wiseassblog.data.auth.FirebaseAuthRepositoryImpl
import com.wiseassblog.domain.DispatcherProvider
import com.wiseassblog.domain.servicelocator.UserServiceLocator
import com.wiseassblog.domain.interactor.AuthSource
import com.wiseassblog.domain.repository.IAuthRepository
import com.wiseassblog.spacenotes.login.LoginActivity
import com.wiseassblog.spacenotes.login.LoginLogic
class LoginInjector(application: Application) : AndroidViewModel(application) {
init {
FirebaseApp.initializeApp(application)
}
//For user management
private val auth: IAuthRepository by lazy {
//by using lazy, I don't load this resource until I need it
FirebaseAuthRepositoryImpl()
}
fun buildLoginLogic(view: LoginActivity): LoginLogic = LoginLogic(
DispatcherProvider,
UserServiceLocator(auth),
view,
AuthSource()
).also { view.setObserver(it) }
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/INoteDetailContract.kt
================================================
package com.wiseassblog.spacenotes.notedetail
import androidx.lifecycle.Observer
import com.wiseassblog.domain.domainmodel.Note
/**
* Created by R_KAY on 10/8/2017.
*/
interface INoteDetailContract {
interface View {
fun setBackgroundImage(imageUrl: String)
fun setDateLabel(date: String)
fun setNoteBody(content: String)
fun setObserver(observer: Observer<NoteDetailEvent>)
fun hideBackButton()
fun getNoteBody(): String
fun getTime(): String
fun restartFeature()
fun showMessage(message: String)
fun showConfirmDeleteSnackbar()
fun startListFeature()
}
interface ViewModel {
fun setIsPrivateMode(isPrivateMode: Boolean)
fun getIsPrivateMode(): Boolean
fun setNoteState(note: Note)
fun getNoteState(): Note?
fun setId(id: String)
fun getId(): String?
}
}
sealed class NoteDetailEvent {
object OnDoneClick : NoteDetailEvent()
object OnDeleteClick : NoteDetailEvent()
object OnDeleteConfirmed : NoteDetailEvent()
object OnBackClick : NoteDetailEvent()
object OnStart : NoteDetailEvent()
object OnBind : NoteDetailEvent()
object OnDestroy : NoteDetailEvent()
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailActivity.kt
================================================
package com.wiseassblog.spacenotes.notedetail
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.wiseassblog.spacenotes.R
import com.wiseassblog.spacenotes.common.BOOLEAN_EXTRA_IS_PRIVATE
import com.wiseassblog.spacenotes.common.STRING_EXTRA_NOTE_ID
import com.wiseassblog.spacenotes.common.attachFragment
import com.wiseassblog.spacenotes.notedetail.buildlogic.NoteDetailInjector
import com.wiseassblog.spacenotes.notelist.NoteListActivity
private const val VIEW = "NOTE_DETAIL"
class NoteDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_note_detail)
//Elvis Operator val i:Intent = if intent is null,
// assign i to Intent(this, NoteListActivity::class.java)
val i: Intent = intent ?: Intent(this, NoteListActivity::class.java)
//if intent is null, then it's time to gtfo
if (intent == null) {
Toast.makeText(this, "Application Restarted.", Toast.LENGTH_SHORT).show()
startActivity(i)
}
val noteId = i.getStringExtra(STRING_EXTRA_NOTE_ID)
val isPrivate = i.getBooleanExtra(BOOLEAN_EXTRA_IS_PRIVATE, true)
val view = this.supportFragmentManager.findFragmentByTag(VIEW) as NoteDetailView?
?: NoteDetailView.newInstance()
attachFragment(supportFragmentManager, R.id.root_activity_detail, view, VIEW)
NoteDetailInjector(application)
.buildNoteDetailLogic(view as NoteDetailView, noteId, isPrivate)
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailLogic.kt
================================================
package com.wiseassblog.spacenotes.notedetail
import androidx.lifecycle.Observer
import com.wiseassblog.domain.DispatcherProvider
import com.wiseassblog.domain.servicelocator.NoteServiceLocator
import com.wiseassblog.domain.servicelocator.UserServiceLocator
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.domainmodel.User
import com.wiseassblog.domain.interactor.AnonymousNoteSource
import com.wiseassblog.domain.interactor.AuthSource
import com.wiseassblog.domain.interactor.PublicNoteSource
import com.wiseassblog.domain.interactor.RegisteredNoteSource
import com.wiseassblog.spacenotes.common.BaseLogic
import com.wiseassblog.spacenotes.common.MESSAGE_DELETE_SUCCESSFUL
import com.wiseassblog.spacenotes.common.MESSAGE_GENERIC_ERROR
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
class NoteDetailLogic(dispatcher: DispatcherProvider,
val noteLocator: NoteServiceLocator,
val userLocator: UserServiceLocator,
val vModel: INoteDetailContract.ViewModel,
val view: INoteDetailContract.View,
val anonymousNoteSource: AnonymousNoteSource,
val registeredNoteSource: RegisteredNoteSource,
val publicNoteSource: PublicNoteSource,
val authSource: AuthSource,
id: String,
isPrivate: Boolean)
: BaseLogic(dispatcher), CoroutineScope, Observer<NoteDetailEvent> {
init {
vModel.setId(id)
vModel.setIsPrivateMode(isPrivate)
jobTracker = Job()
}
fun clear() {
jobTracker.cancel()
}
override val coroutineContext: CoroutineContext
get() = dispatcher.provideUIContext() + jobTracker
override fun onChanged(event: NoteDetailEvent) {
when (event) {
is NoteDetailEvent.OnDoneClick -> onDoneClick()
is NoteDetailEvent.OnDeleteClick -> onDeleteClick()
is NoteDetailEvent.OnBackClick -> onBackClick()
is NoteDetailEvent.OnDeleteConfirmed -> onDeleteConfirmed()
is NoteDetailEvent.OnStart -> onStart()
is NoteDetailEvent.OnBind -> bind()
is NoteDetailEvent.OnDestroy -> clear()
}
}
fun onDoneClick() = launch {
val userResult = authSource.getCurrentUser(userLocator)
when (userResult) {
is Result.Value -> {
//if null, user is anonymous
if (userResult.value == null) prepareAnonymousRepoUpdate()
else if (vModel.getIsPrivateMode()) prepareRegisteredRepoUpdate()
else preparePublicRepoUpdate()
}
}
}
private suspend fun prepareAnonymousRepoUpdate() {
val updatedNote = vModel.getNoteState()!!.copy(contents = view.getNoteBody())
val result = anonymousNoteSource.updateNote(updatedNote, noteLocator)
when (result) {
is Result.Value -> view.startListFeature()
is Result.Error -> view.showMessage(result.error.toString())
}
}
suspend fun prepareRegisteredRepoUpdate() {
val updatedNote = vModel.getNoteState()!!.copy(contents = view.getNoteBody())
val result = registeredNoteSource.updateNote(updatedNote, noteLocator)
when (result) {
is Result.Value -> view.startListFeature()
is Result.Error -> view.showMessage(result.error.toString())
}
}
suspend fun preparePublicRepoUpdate() {
val updatedNote = vModel.getNoteState()!!
.copy(contents = view.getNoteBody())
val result = publicNoteSource.updateNote(updatedNote, noteLocator)
when (result) {
is Result.Value -> view.startListFeature()
is Result.Error -> view.showMessage(result.error.toString())
}
}
fun bind() = launch {
val userResult = authSource.getCurrentUser(userLocator)
when (userResult) {
is Result.Value -> {
val id = vModel.getId()
if (id == "" || id == null) createNewNote(userResult.value)
else getNoteFromSource(id, userResult.value)
}
is Result.Error -> view.showMessage(userResult.error.toString())
}
}
fun createNewNote(user: User?) {
vModel.setNoteState(
Note(
view.getTime(),
"",
0,
"satellite_beam",
user
)
)
//only save or delete with new note
view.hideBackButton()
onStart()
}
fun getNoteFromSource(id: String, user: User?) = launch {
val noteResult: Result<Exception, Note?>
//private anonymous
if (user == null) noteResult = anonymousNoteSource.getNoteById(id, noteLocator)
//private registered
else if (vModel.getIsPrivateMode()) noteResult = registeredNoteSource.getNoteById(id, noteLocator)
//public registered
else noteResult = publicNoteSource.getNoteById(id, noteLocator)
when (noteResult) {
is Result.Value -> {
vModel.setNoteState(noteResult.value!!)
onStart()
}
is Result.Error -> {
val message = noteResult.error.message ?: "An error has occured."
view.showMessage(message)
}
}
}
fun onStart() {
val state = vModel.getNoteState()
//LiveData requires null checks due to nullable return types
if (state != null) {
renderView(state)
} else {
view.showMessage(MESSAGE_GENERIC_ERROR)
view.startListFeature()
}
}
private fun renderView(state: Note) {
view.setBackgroundImage(state.imageUrl)
view.setDateLabel(state.creationDate)
view.setNoteBody(state.contents)
}
fun onBackClick() {
view.startListFeature()
}
fun onDeleteClick() {
view.showConfirmDeleteSnackbar()
}
fun onDeleteConfirmed() = launch {
val currentNote = vModel.getNoteState()
//if VM data is null, we're in a bad spot
if (currentNote == null) {
view.showMessage(MESSAGE_GENERIC_ERROR)
view.restartFeature()
} else {
val userResult = authSource.getCurrentUser(userLocator)
when (userResult) {
is Result.Value -> {
if (userResult.value == null) prepareAnonymousRepoDelete(currentNote)
else if (vModel.getIsPrivateMode()) prepareRegisteredRepoDelete(currentNote)
else preparePublicRepoDelete(currentNote)
}
is Result.Error -> view.showMessage(userResult.error.toString())
}
}
}
private fun preparePublicRepoDelete(note: Note) = launch {
val result = publicNoteSource.deleteNote(note, noteLocator)
when (result) {
is Result.Value -> {
view.showMessage(MESSAGE_DELETE_SUCCESSFUL)
view.startListFeature()
}
is Result.Error -> view.showMessage(result.error.toString())
}
}
private fun prepareRegisteredRepoDelete(note: Note) = launch {
val result = registeredNoteSource.deleteNote(note, noteLocator)
when (result) {
is Result.Value -> {
view.showMessage(MESSAGE_DELETE_SUCCESSFUL)
view.startListFeature()
}
is Result.Error -> view.showMessage(result.error.toString())
}
}
private fun prepareAnonymousRepoDelete(note: Note) = launch {
val result = anonymousNoteSource.deleteNote(note, noteLocator)
when (result) {
is Result.Value -> {
view.showMessage(MESSAGE_DELETE_SUCCESSFUL)
view.startListFeature()
}
is Result.Error -> view.showMessage(result.error.toString())
}
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailView.kt
================================================
package com.wiseassblog.spacenotes.notedetail
import android.graphics.drawable.AnimationDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.google.android.material.snackbar.Snackbar
import com.wiseassblog.spacenotes.R
import com.wiseassblog.spacenotes.R.id.*
import com.wiseassblog.spacenotes.common.*
import com.wiseassblog.spacenotes.notedetail.buildlogic.NoteDetailInjector
import com.wiseassblog.spacenotes.notelist.NoteListEvent
import kotlinx.android.synthetic.main.fragment_note_detail.*
class NoteDetailView : Fragment(), INoteDetailContract.View {
val event = MutableLiveData<NoteDetailEvent>()
override fun setObserver(observer: Observer<NoteDetailEvent>) = event.observeForever(observer)
override fun startListFeature() = com.wiseassblog.spacenotes.common.startListFeature(this.activity)
override fun hideBackButton() {
imb_toolbar_back.visibility = View.INVISIBLE
imb_toolbar_back.isEnabled = false
}
override fun getTime(): String = getCalendarTime()
override fun showConfirmDeleteSnackbar() {
if (activity != null) {
Snackbar.make(frag_note_detail, MESSAGE_DELETE_CONFIRMATION, Snackbar.LENGTH_LONG)
.setAction(MESSAGE_DELETE) { event.value = NoteDetailEvent.OnDeleteConfirmed }
.show()
}
}
override fun showMessage(message: String) = makeToast(message)
override fun restartFeature() = restartCurrentFeature()
override fun getNoteBody(): String {
return edt_note_detail_text.text.toString()
}
override fun setBackgroundImage(imageUrl: String) {
imv_note_detail_satellite.setImageResource(
resources.getIdentifier(imageUrl, "drawable", context?.packageName)
)
val satelliteLoop = imv_note_detail_satellite.drawable as AnimationDrawable
satelliteLoop.start()
}
override fun setDateLabel(date: String) {
lbl_note_detail_date.text = date
}
override fun setNoteBody(content: String) {
edt_note_detail_text.text = content.toEditable()
}
override fun onStart() {
super.onStart()
event.value = NoteDetailEvent.OnBind
}
override fun onDestroy() {
event.value = NoteDetailEvent.OnDestroy
super.onDestroy()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_note_detail, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
imb_toolbar_done.setOnClickListener { event.value = NoteDetailEvent.OnDoneClick }
imb_toolbar_back.setOnClickListener { event.value = NoteDetailEvent.OnBackClick }
imb_toolbar_delete.setOnClickListener { event.value = NoteDetailEvent.OnDeleteClick }
val spaceLoop = frag_note_detail.background as AnimationDrawable
spaceLoop.setEnterFadeDuration(1000)
spaceLoop.setExitFadeDuration(1000)
spaceLoop.start()
super.onViewCreated(view, savedInstanceState)
}
companion object {
@JvmStatic
fun newInstance() =
NoteDetailView()
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailViewModel.kt
================================================
package com.wiseassblog.spacenotes.notedetail
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.spacenotes.notedetail.INoteDetailContract
class NoteDetailViewModel(private var displayState: MutableLiveData<Note> = MutableLiveData(),
private var id: MutableLiveData<String> = MutableLiveData(),
private var isPrivateMode: MutableLiveData<Boolean> = MutableLiveData()) : ViewModel(),
INoteDetailContract.ViewModel {
override fun getIsPrivateMode(): Boolean {
return isPrivateMode.value!!
}
override fun setIsPrivateMode(isPrivateMode: Boolean) {
this.isPrivateMode.value = isPrivateMode
}
override fun setId(id: String) {
this.id.value = id
}
override fun getId(): String? {
return this.id.value
}
override fun getNoteState(): Note? {
return displayState.value
}
override fun setNoteState(note: Note) {
displayState.value = note
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notedetail/buildlogic/NoteDetailInjector.kt
================================================
package com.wiseassblog.spacenotes.notedetail.buildlogic
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModelProviders
import com.google.firebase.FirebaseApp
import com.wiseassblog.data.auth.FirebaseAuthRepositoryImpl
import com.wiseassblog.data.note.anonymous.AnonymousNoteDao
import com.wiseassblog.data.note.anonymous.AnonymousNoteDatabase
import com.wiseassblog.data.note.anonymous.RoomLocalAnonymousRepositoryImpl
import com.wiseassblog.data.note.public.FirestoreRemoteNoteImpl
import com.wiseassblog.data.note.registered.*
import com.wiseassblog.data.transaction.RoomRegisteredTransactionDatabase
import com.wiseassblog.data.transaction.RoomTransactionRepositoryImpl
import com.wiseassblog.domain.DispatcherProvider
import com.wiseassblog.domain.servicelocator.NoteServiceLocator
import com.wiseassblog.domain.servicelocator.UserServiceLocator
import com.wiseassblog.domain.interactor.AnonymousNoteSource
import com.wiseassblog.domain.interactor.AuthSource
import com.wiseassblog.domain.interactor.PublicNoteSource
import com.wiseassblog.domain.interactor.RegisteredNoteSource
import com.wiseassblog.domain.repository.*
import com.wiseassblog.spacenotes.notedetail.*
/**
*
*/
class NoteDetailInjector(application: Application) : AndroidViewModel(application) {
init {
FirebaseApp.initializeApp(application)
}
private val anonNoteDao: AnonymousNoteDao by lazy {
AnonymousNoteDatabase.getInstance(getApplication()).roomNoteDao()
}
private val regNoteDao: RegisteredNoteDao by lazy {
RegisteredNoteDatabase.getInstance(getApplication()).roomNoteDao()
}
private val transactionDao: RegisteredTransactionDao by lazy {
RoomRegisteredTransactionDatabase.getInstance(getApplication()).roomTransactionDao()
}
//For non-registered user persistence
private val localAnon: ILocalNoteRepository by lazy {
RoomLocalAnonymousRepositoryImpl(anonNoteDao)
}
//For registered user remote persistence (Source of Truth)
private val remotePrivate: IRemoteNoteRepository by lazy {
FirestorePrivateRemoteNoteImpl()
}
//For registered user remote persistence (Source of Truth)
private val remotePublic: IPublicNoteRepository by lazy {
FirestoreRemoteNoteImpl
}
//For registered user local persistience (cache)
private val cacheReg: ILocalNoteRepository by lazy {
RoomLocalCacheImpl(regNoteDao)
}
//For registered user remote persistence (Source of Truth)
private val remoteRepo: IRemoteNoteRepository by lazy {
RegisteredNoteRepositoryImpl(remotePrivate, cacheReg)
}
//For registered user local persistience (cache)
private val transactionReg: ITransactionRepository by lazy {
RoomTransactionRepositoryImpl(transactionDao)
}
//For user management
private val auth: IAuthRepository by lazy {
FirebaseAuthRepositoryImpl()
}
private lateinit var logic: NoteDetailLogic
fun buildNoteDetailLogic(view: NoteDetailView,
id: String,
isPrivate: Boolean): NoteDetailLogic {
logic = NoteDetailLogic(
DispatcherProvider,
NoteServiceLocator(localAnon, remoteRepo, transactionReg, remotePublic),
UserServiceLocator(auth),
ViewModelProviders.of(view)
.get(NoteDetailViewModel::class.java),
view,
AnonymousNoteSource(),
RegisteredNoteSource(),
PublicNoteSource(),
AuthSource(),
id,
isPrivate
)
view.setObserver(logic)
return logic
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/INoteListContract.kt
================================================
package com.wiseassblog.spacenotes.notelist
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ListAdapter
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.User
/**
* Created by R_KAY on 10/8/2017.
*/
interface INoteListContract {
interface View {
fun setAdapter(adapter: ListAdapter<Note, NoteListAdapter.NoteViewHolder>)
fun setPrivateIcon(isPrivate: Boolean)
fun showList()
fun showEmptyState()
fun showErrorState(message:String)
fun showLoadingView()
fun setToolbarTitle(title: String)
fun startLoginFeature()
fun setObserver(observer: Observer<NoteListEvent<Int>>)
fun startNoteDetailFeatureWithExtras(noteId: String, isPrivate: Boolean)
}
interface ViewModel {
fun setAdapterState(result: List<Note>)
fun getAdapterState(): List<Note>
fun getUserState(): User?
fun setUserState(userResult: User?)
fun getIsPrivateMode(): Boolean
fun setIsPrivateMode(isPrivateMode: Boolean)
}
}
sealed class NoteListEvent<out T> {
data class OnNoteItemClick<out Int>(val position: Int) : NoteListEvent<Int>()
object OnNewNoteClick : NoteListEvent<Nothing>()
object OnLoginClick : NoteListEvent<Nothing>()
object OnTogglePublicMode : NoteListEvent<Nothing>()
object OnStart : NoteListEvent<Nothing>()
object OnBind : NoteListEvent<Nothing>()
object OnDestroy : NoteListEvent<Nothing>()
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteDiffUtilCallback.kt
================================================
package com.wiseassblog.spacenotes.notelist
import androidx.recyclerview.widget.DiffUtil
import com.wiseassblog.domain.domainmodel.Note
class NoteDiffUtilCallback : DiffUtil.ItemCallback<Note>(){
override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean {
return oldItem.creationDate == newItem.creationDate
}
override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean {
return oldItem.creationDate == newItem.creationDate
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListActivity.kt
================================================
package com.wiseassblog.spacenotes.notelist
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import com.wiseassblog.spacenotes.R
import com.wiseassblog.spacenotes.common.attachFragment
import com.wiseassblog.spacenotes.notelist.buildlogic.NoteListInjector
private const val VIEW = "NOTE_LIST"
class NoteListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_note_list)
//A container basically just builds things and sets the feature in motion
val view = this.supportFragmentManager.findFragmentByTag(VIEW)
?: NoteListView.newInstance()
attachFragment(supportFragmentManager, R.id.root_activity_list, view, VIEW)
NoteListInjector(application)
.buildNoteListLogic(view as NoteListView)
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListAdapter.kt
================================================
package com.wiseassblog.spacenotes.notelist
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.spacenotes.R
import kotlinx.android.synthetic.main.item_note.view.*
class NoteListAdapter(var event: MutableLiveData<NoteListEvent<Int>> = MutableLiveData() ) : ListAdapter<Note, NoteListAdapter.NoteViewHolder>(NoteDiffUtilCallback()) {
internal fun setObserver(observer: Observer<NoteListEvent<Int>>) = event.observeForever(observer)
override fun onBindViewHolder(holder: NoteListAdapter.NoteViewHolder, position: Int) {
getItem(position).let { note ->
with(holder) {
holder.content.text = note.contents
holder.date.text = note.creationDate
holder.square.setImageResource(R.drawable.gps_icon)
holder.content.text = note.contents
holder.itemView.setOnClickListener {
event.value = NoteListEvent.OnNoteItemClick(position)
}
}
}
holder.apply {
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
val inflater = LayoutInflater.from(parent.context)
return NoteViewHolder(
inflater.inflate(R.layout.item_note, parent, false)
)
}
class NoteViewHolder(private val root: View) : RecyclerView.ViewHolder(root) {
var square: ImageView = root.imv_list_item_icon
var dateIcon: ImageView = root.imv_date_and_time
var content: TextView = root.lbl_message
var date: TextView = root.lbl_date_and_time
var loading: ProgressBar = root.pro_item_data
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListLogic.kt
================================================
package com.wiseassblog.spacenotes.notelist
import androidx.lifecycle.Observer
import com.wiseassblog.domain.DispatcherProvider
import com.wiseassblog.domain.servicelocator.NoteServiceLocator
import com.wiseassblog.domain.servicelocator.UserServiceLocator
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.error.SpaceNotesError
import com.wiseassblog.domain.interactor.AnonymousNoteSource
import com.wiseassblog.domain.interactor.AuthSource
import com.wiseassblog.domain.interactor.PublicNoteSource
import com.wiseassblog.domain.interactor.RegisteredNoteSource
import com.wiseassblog.spacenotes.common.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
class NoteListLogic(dispatcher: DispatcherProvider,
val noteLocator: NoteServiceLocator,
val userLocator: UserServiceLocator,
val vModel: INoteListContract.ViewModel,
var adapter: NoteListAdapter,
val view: INoteListContract.View,
val anonymousNoteSource: AnonymousNoteSource,
val registeredNoteSource: RegisteredNoteSource,
val publicNoteSource: PublicNoteSource,
val authSource: AuthSource)
: BaseLogic(dispatcher), CoroutineScope, Observer<NoteListEvent<Int>> {
override fun onChanged(event: NoteListEvent<Int>?) {
when (event) {
is NoteListEvent.OnNoteItemClick -> onNoteItemClick(event.position)
is NoteListEvent.OnNewNoteClick -> onNewNoteClick()
is NoteListEvent.OnLoginClick -> onLoginClick()
is NoteListEvent.OnTogglePublicMode -> onTogglePublicMode()
is NoteListEvent.OnStart -> onStart()
is NoteListEvent.OnBind -> bind()
is NoteListEvent.OnDestroy -> clear()
}
}
init {
//This is directly analogous to CompositeDisposable
jobTracker = Job()
}
//dispatcher.provideUIContext is very analogous to observeOn(Dispatchers.UI)
override val coroutineContext: CoroutineContext
get() = dispatcher.provideUIContext() + jobTracker
private fun onNewNoteClick() = view.startNoteDetailFeatureWithExtras(
"",
vModel.getIsPrivateMode()
)
private fun onStart() {
getListData(vModel.getIsPrivateMode())
}
fun getListData(isPrivateMode: Boolean) = launch {
val dataResult: Result<Exception, List<Note>>
when (isPrivateMode) {
true -> dataResult = getPrivateListData()
false -> dataResult = getPublicListData()
}
when (dataResult) {
is Result.Value -> {
vModel.setAdapterState(dataResult.value)
renderView(dataResult.value)
}
is Result.Error -> {
view.showEmptyState()
view.showErrorState(MESSAGE_GENERIC_ERROR)
}
}
}
suspend fun getPublicListData(): Result<Exception, List<Note>> {
return if (vModel.getUserState() != null) publicNoteSource.getNotes(noteLocator)
else Result.build { throw SpaceNotesError.LocalIOException }
}
suspend fun getPrivateListData(): Result<Exception, List<Note>> {
return if (vModel.getUserState() == null) anonymousNoteSource.getNotes(noteLocator)
else registeredNoteSource.getNotes(noteLocator)
}
fun renderView(list: List<Note>) {
view.setPrivateIcon(vModel.getIsPrivateMode())
if (vModel.getIsPrivateMode()) view.setToolbarTitle(MODE_PRIVATE)
else view.setToolbarTitle(MODE_PUBLIC)
if (list.isEmpty()) view.showEmptyState()
else view.showList()
adapter.submitList(list)
}
private fun onTogglePublicMode() {
if (vModel.getUserState() != null) {
if (vModel.getIsPrivateMode()) {
vModel.setIsPrivateMode(false)
getListData(false)
} else {
vModel.setIsPrivateMode(true)
getListData(true)
}
} else {
view.showErrorState(MESSAGE_LOGIN)
}
}
private fun onLoginClick() {
view.startLoginFeature()
}
private fun onNoteItemClick(position: Int) {
val listData = vModel.getAdapterState()
view.startNoteDetailFeatureWithExtras(
listData[position].creationDate, vModel.getIsPrivateMode())
}
fun bind() {
view.setToolbarTitle(MODE_PRIVATE)
view.showLoadingView()
adapter.setObserver(this)
view.setAdapter(adapter)
view.setObserver(this)
launch {
val result = authSource.getCurrentUser(userLocator)
if (result is Result.Value) vModel.setUserState(result.value)
//otherwise defaults to null
}
}
//Single Expression Syntax
fun clear() = jobTracker.cancel()
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListView.kt
================================================
package com.wiseassblog.spacenotes.notelist
import android.graphics.drawable.AnimationDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ListAdapter
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.spacenotes.R
import com.wiseassblog.spacenotes.R.id.*
import com.wiseassblog.spacenotes.common.makeToast
import com.wiseassblog.spacenotes.notelist.buildlogic.NoteListInjector
import kotlinx.android.synthetic.main.fragment_note_list.*
class NoteListView : Fragment(), INoteListContract.View {
override fun setPrivateIcon(isPrivate: Boolean) {
//private mode
if (isPrivate) imv_toolbar_private_toggle.setImageResource(R.drawable.design_ic_visibility_off)
//public mode
else imv_toolbar_private_toggle.setImageResource(R.drawable.design_ic_visibility)
}
val event = MutableLiveData<NoteListEvent<Int>>()
//Event listener
override fun setObserver(observer: Observer<NoteListEvent<Int>>) = event.observeForever(observer)
override fun showErrorState(message: String) = this.makeToast(message)
override fun startLoginFeature() = com.wiseassblog.spacenotes.common.startLoginFeature(this.activity)
override fun startNoteDetailFeatureWithExtras(noteId: String, isPrivate: Boolean) = com.wiseassblog.spacenotes.common.startNoteDetailFeatureWithExtras(this.activity, noteId, isPrivate)
override fun setToolbarTitle(title: String) {
lbl_toolbar_title.text = title
}
override fun setAdapter(adapter: ListAdapter<Note, NoteListAdapter.NoteViewHolder>) {
rec_list_activity.adapter = adapter
}
override fun showLoadingView() {
rec_list_activity.visibility = View.INVISIBLE
fab_create_new_item.hide()
imv_satellite_animation.visibility = View.VISIBLE
//set loading animation
val satelliteLoop = imv_satellite_animation.drawable as AnimationDrawable
satelliteLoop.start()
}
override fun showEmptyState() {
rec_list_activity.visibility = View.INVISIBLE
fab_create_new_item.show()
imv_satellite_animation.visibility = View.VISIBLE
val satelliteLoop = imv_satellite_animation.drawable as AnimationDrawable
satelliteLoop.start()
}
override fun showList() {
rec_list_activity.visibility = View.VISIBLE
fab_create_new_item.show()
imv_satellite_animation.visibility = View.INVISIBLE
val satelliteLoop = imv_satellite_animation.drawable as AnimationDrawable
satelliteLoop.stop()
}
override fun onStart() {
super.onStart()
event.value = NoteListEvent.OnBind
}
override fun onResume() {
super.onResume()
event.value = NoteListEvent.OnStart
}
override fun onDestroy() {
event.value = NoteListEvent.OnDestroy
super.onDestroy()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_note_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
event.value = NoteListEvent.OnBind
imv_toolbar_auth.setOnClickListener { event.value = NoteListEvent.OnLoginClick }
imv_toolbar_private_toggle.setOnClickListener { event.value = NoteListEvent.OnTogglePublicMode }
fab_create_new_item.setOnClickListener { event.value = NoteListEvent.OnNewNoteClick }
val spaceLoop = imv_space_background.drawable as AnimationDrawable
spaceLoop.setEnterFadeDuration(1000)
spaceLoop.setExitFadeDuration(1000)
spaceLoop.start()
super.onViewCreated(view, savedInstanceState)
}
companion object {
@JvmStatic
fun newInstance(): Fragment = NoteListView()
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListViewModel.kt
================================================
package com.wiseassblog.spacenotes.notelist
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.User
/**
* isPrivateMode refers to whether the User wants to post to and read from a shared repo, or they
* would like to store their note in private storage.
*/
class NoteListViewModel(private var adapterData: MutableLiveData<List<Note>> = MutableLiveData(),
private var user: MutableLiveData<User?> = MutableLiveData(),
private var isPrivateMode: MutableLiveData<Boolean> = MutableLiveData()) : ViewModel(),
INoteListContract.ViewModel {
init {
isPrivateMode.value = true
}
override fun getIsPrivateMode(): Boolean {
return isPrivateMode.value!!
}
override fun setIsPrivateMode(isPrivateMode: Boolean) {
this.isPrivateMode.value = isPrivateMode
}
override fun setAdapterState(result: List<Note>) {
adapterData.value = result
}
override fun setUserState(userResult: User?) {
user.value = userResult
}
override fun getUserState(): User? {
return user.value
}
override fun getAdapterState(): List<Note> {
//return current display state or empty string if value is null
//see "Elvis Operator"
return adapterData.value ?: emptyList()
}
}
================================================
FILE: app/src/main/java/com/wiseassblog/spacenotes/notelist/buildlogic/NoteListInjector.kt
================================================
package com.wiseassblog.spacenotes.notelist.buildlogic
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModelProviders
import com.google.firebase.FirebaseApp
import com.wiseassblog.data.auth.FirebaseAuthRepositoryImpl
import com.wiseassblog.data.note.anonymous.AnonymousNoteDao
import com.wiseassblog.data.note.anonymous.AnonymousNoteDatabase
import com.wiseassblog.data.note.anonymous.RoomLocalAnonymousRepositoryImpl
import com.wiseassblog.data.note.public.FirestoreRemoteNoteImpl
import com.wiseassblog.data.note.registered.*
import com.wiseassblog.data.transaction.RoomRegisteredTransactionDatabase
import com.wiseassblog.data.transaction.RoomTransactionRepositoryImpl
import com.wiseassblog.domain.DispatcherProvider
import com.wiseassblog.domain.servicelocator.NoteServiceLocator
import com.wiseassblog.domain.servicelocator.UserServiceLocator
import com.wiseassblog.domain.interactor.AnonymousNoteSource
import com.wiseassblog.domain.interactor.AuthSource
import com.wiseassblog.domain.interactor.PublicNoteSource
import com.wiseassblog.domain.interactor.RegisteredNoteSource
import com.wiseassblog.domain.repository.*
import com.wiseassblog.spacenotes.notelist.NoteListAdapter
import com.wiseassblog.spacenotes.notelist.NoteListLogic
import com.wiseassblog.spacenotes.notelist.NoteListView
import com.wiseassblog.spacenotes.notelist.NoteListViewModel
class NoteListInjector(application: Application) : AndroidViewModel(application) {
init {
FirebaseApp.initializeApp(application)
}
private val anonNoteDao: AnonymousNoteDao by lazy {
AnonymousNoteDatabase.getInstance(application).roomNoteDao()
}
private val regNoteDao: RegisteredNoteDao by lazy {
RegisteredNoteDatabase.getInstance(application).roomNoteDao()
}
private val transactionDao: RegisteredTransactionDao by lazy {
RoomRegisteredTransactionDatabase.getInstance(application).roomTransactionDao()
}
//For non-registered user persistence
private val localAnon: ILocalNoteRepository by lazy {
RoomLocalAnonymousRepositoryImpl(anonNoteDao)
}
//For registered user remote persistence (Source of Truth)
private val remotePrivate: IRemoteNoteRepository by lazy {
FirestorePrivateRemoteNoteImpl()
}
//For registered user remote persistence (Source of Truth)
private val remotePublicRepo: IPublicNoteRepository by lazy {
FirestoreRemoteNoteImpl
}
//For registered user local persistience (cache)
private val cacheReg: ILocalNoteRepository by lazy {
RoomLocalCacheImpl(regNoteDao)
}
//For registered user remote persistence (Source of Truth)
private val remotePrivateRepo: IRemoteNoteRepository by lazy {
RegisteredNoteRepositoryImpl(remotePrivate, cacheReg)
}
//For registered user local persistience (cache)
private val transactionReg: ITransactionRepository by lazy {
RoomTransactionRepositoryImpl(transactionDao)
}
//For user management
private val auth: IAuthRepository by lazy {
FirebaseAuthRepositoryImpl()
}
private lateinit var logic: NoteListLogic
fun buildNoteListLogic(view: NoteListView): NoteListLogic {
logic = NoteListLogic(
DispatcherProvider,
NoteServiceLocator(localAnon, remotePrivateRepo, transactionReg, remotePublicRepo),
UserServiceLocator(auth),
ViewModelProviders.of(view)
.get(NoteListViewModel::class.java),
NoteListAdapter(),
view,
AnonymousNoteSource(),
RegisteredNoteSource(),
PublicNoteSource(),
AuthSource()
)
view.setObserver(logic)
return logic
}
}
================================================
FILE: app/src/main/res/drawable/antenna_loop.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false"
>
<item android:drawable="@drawable/antenna_empty" android:duration="1500" />
<item android:drawable="@drawable/antenna_half" android:duration="1500" />
<item android:drawable="@drawable/antenna_full" android:duration="1500" />
</animation-list>
================================================
FILE: app/src/main/res/drawable/antenna_loop_fast.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false"
>
<item android:drawable="@drawable/antenna_empty" android:duration="100" />
<item android:drawable="@drawable/antenna_half" android:duration="100" />
<item android:drawable="@drawable/antenna_full" android:duration="100" />
</animation-list>
================================================
FILE: app/src/main/res/drawable/ic_access_time_black_24dp.xml
================================================
<vector android:alpha="0.86" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_arrow_back_black_24dp.xml
================================================
<vector android:alpha="0.86" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_baseline_add_24px.xml
================================================
<vector android:alpha="0.86" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_baseline_event_24px.xml
================================================
<vector android:alpha="0.86" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M17,12h-5v5h5v-5zM16,1v2L8,3L8,1L6,1v2L5,3c-1.11,0 -1.99,0.9 -1.99,2L3,19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2h-1L18,1h-2zM19,19L5,19L5,8h14v11z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_delete_forever_black_24dp.xml
================================================
<vector android:alpha="0.86" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_done_black_24dp.xml
================================================
<vector android:alpha="0.86" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
================================================
FILE: app/src/main/res/drawable/ic_visibility_off_black_24dp.xml
================================================
<vector android:alpha="0.86" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_vpn_key_black_24dp.xml
================================================
<vector android:alpha="0.86" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/satellite_beam.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false"
>
<item android:drawable="@drawable/gps_icon" android:duration="500" />
<item android:drawable="@drawable/gps_icon_2" android:duration="500" />
<item android:drawable="@drawable/gps_icon_3" android:duration="500" />
</animation-list>
================================================
FILE: app/src/main/res/drawable/space_loop.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false"
>
<item android:drawable="@drawable/space_bg_one" android:duration="2000" />
<item android:drawable="@drawable/space_bg_two" android:duration="2000" />
<item android:drawable="@drawable/space_bg_three" android:duration="2000" />
</animation-list>
================================================
FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
================================================
FILE: app/src/main/res/layout/activity_login.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_activity_login"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/space_loop"
tools:context="com.wiseassblog.spacenotes.login.LoginActivity"
tools:layout_editor_absoluteY="25dp">
<ImageButton
android:id="@+id/imb_toolbar_back"
style="@style/Widget.AppCompat.ActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:contentDescription="Back Button"
android:src="@drawable/ic_arrow_back_black_24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/lbl_login_status_header"
style="@style/Text.Primary.LoginHeader"
android:layout_width="352dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="Login Status:"
app:layout_constraintBottom_toTopOf="@+id/imv_antenna_animation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
<ImageView
android:id="@+id/imv_antenna_animation"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_marginBottom="8dp"
android:alpha=".86"
android:src="@drawable/antenna_loop"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@+id/lbl_login_status_display"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/lbl_login_status_display"
style="@style/Text.Primary.LoginHeader.Sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Signed In" />
<Button
android:id="@+id/btn_auth_attempt"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="71dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lbl_login_status_display"
app:layout_constraintVertical_bias="0.0"
tools:text="Sign Out" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/activity_note_detail.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/root_activity_detail"
tools:context="com.wiseassblog.spacenotes.notedetail.NoteDetailActivity"/>
================================================
FILE: app/src/main/res/layout/activity_note_list.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/root_activity_list"
tools:context="com.wiseassblog.spacenotes.notelist.NoteListActivity"/>
================================================
FILE: app/src/main/res/layout/activity_user_auth.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.wiseassblog.spacenotes.userauth.UserAuthActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/fragment_note_detail.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frag_note_detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/space_loop"
tools:context="com.wiseassblog.spacenotes.notedetail.NoteDetailActivity">
<FrameLayout
android:id="@+id/tlb_detail_activity"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_marginLeft="0dp"
android:layout_marginTop="0dp"
android:layout_marginRight="0dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:elevation="4dp"
android:background="@color/colorTransparentBlack"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<ImageButton
android:contentDescription="Back Button"
android:id="@+id/imb_toolbar_back"
style="@style/Widget.AppCompat.ActionButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:src="@drawable/ic_arrow_back_black_24dp"
android:layout_gravity="start"
/>
<ImageButton
android:contentDescription="Delete Button"
android:id="@+id/imb_toolbar_delete"
style="@style/Widget.AppCompat.ActionButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:src="@drawable/ic_delete_forever_black_24dp"
android:layout_marginEnd="74dp"
android:layout_gravity="end"
/>
<ImageButton
android:contentDescription="Finish Editing Note Button"
android:id="@+id/imb_toolbar_done"
style="@style/Widget.AppCompat.ActionButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:src="@drawable/ic_done_black_24dp"
android:layout_gravity="end"
/>
</FrameLayout>
<ImageView
android:id="@+id/imv_note_detail_satellite"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/satellite_beam"
android:tint="@android:color/white"
android:alpha=".86"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/gdl_detail_middle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tlb_detail_activity"
/>
<TextView
android:id="@+id/lbl_note_detail_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/edt_note_detail_text"
android:background="@color/colorTransparentBlack"
android:padding="16dp"
android:gravity="center"
android:textSize="16sp"
android:fontFamily="sans-serif-medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/gdl_detail_middle"
tools:text="2:43AM 09/7/2018" />
<EditText
android:inputType="textMultiLine"
android:id="@+id/edt_note_detail_text"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="16dp"
android:background="@color/colorTransparentBlack"
android:gravity="top|start"
android:maxLines="10"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lbl_note_detail_date"
tools:text="" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/gdl_detail_middle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/fragment_note_list.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frag_note_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.wiseassblog.spacenotes.notelist.NoteListActivity">
<TextView
android:id="@+id/lbl_toolbar_title"
style="@style/ToolbarTitle"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_marginLeft="16dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Private Mode" />
<ImageButton
android:id="@+id/imv_toolbar_private_toggle"
style="@style/Widget.AppCompat.ActionButton"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_marginRight="8dp"
android:src="@drawable/ic_visibility_off_black_24dp"
app:layout_constraintRight_toLeftOf="@+id/imv_toolbar_auth"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/imv_toolbar_auth"
style="@style/Widget.AppCompat.ActionButton"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_gravity="right"
android:layout_marginRight="8dp"
android:src="@drawable/ic_vpn_key_black_24dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/imv_space_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginLeft="0dp"
android:layout_marginTop="0dp"
android:layout_marginRight="0dp"
android:layout_marginBottom="0dp"
android:elevation="0dp"
android:scaleType="centerCrop"
android:src="@drawable/space_loop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lbl_toolbar_title">
</ImageView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_create_new_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="64dp"
android:src="@drawable/ic_baseline_add_24px"
android:visibility="invisible"
app:elevation="6dp"
app:fabSize="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:rippleColor="@color/colorTransparentBlack" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rec_list_activity"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="0dp"
android:background="@color/colorTransparentBlack"
android:visibility="invisible"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lbl_toolbar_title" />
<ImageView
android:id="@+id/imv_satellite_animation"
android:layout_width="128dp"
android:layout_height="128dp"
android:alpha=".86"
android:src="@drawable/satellite_beam"
android:tint="@android:color/white"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/item_note.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="88dp"
android:id="@+id/root_list_item"
android:background="?android:attr/selectableItemBackground"
>
<ImageView
android:id="@+id/imv_list_item_icon"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="16dp"
android:src="@drawable/gps_icon"
android:tint="@android:color/white"
android:alpha=".86"
android:scaleType="fitCenter"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp" />
<ImageView
android:id="@+id/imv_date_and_time"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:alpha=".86"
android:padding="4dp"
android:src="@drawable/ic_access_time_black_24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/imv_list_item_icon"
app:layout_constraintTop_toBottomOf="@+id/lbl_message"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/lbl_message"
style="@style/Text.Primary"
android:layout_width="0dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:singleLine="true"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imv_list_item_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="If at first something doesn't make any sense, find another explanation." />
<TextView
android:id="@+id/lbl_date_and_time"
style="@style/Text"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:gravity="center_vertical"
android:textColor="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/imv_date_and_time"
app:layout_constraintTop_toBottomOf="@+id/lbl_message"
app:layout_constraintVertical_bias="0.0"
tools:text="6:30AM 06/01/2017" />
<ProgressBar
android:id="@+id/pro_item_data"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="invisible"
android:background="@color/colorPrimaryDark"
android:layout_marginRight="0dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="0dp"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="0dp" />
</androidx.constraintlayout.widget.ConstraintLayout >
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#121212</color>
<color name="colorPrimaryDark">#000000</color>
<color name="colorAccent">#80D8FF</color>
<color name="colorWindowBackground">#121212</color>
<!-- 52 = 32%, hex to alpha -->
<color name="colorTransparentBlack">#52000000</color>
<color name="colorTransparentWhite">#52FFFFFF</color>
<color name="RED">#FF0000</color>
<color name="BLUE">#0000FF</color>
<color name="GREEN">#00FF00</color>
<color name="YELLOW">#FFEB3B</color>
</resources>
================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
<string name="app_name">SpaceNotes</string>
</resources>
================================================
FILE: app/src/main/res/values/styles.xml
================================================
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">@color/colorWindowBackground</item>
</style>
</resources>
================================================
FILE: app/src/main/res/values/view_styles.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Text">
<item name="android:fontFamily">sans-serif</item>
<item name="android:textSize">14sp</item>
</style>
<style name="Text.Primary">
<item name="android:fontFamily">sans-serif-medium</item>
<item name="android:textSize">16sp</item>
</style>
<style name="Text.Primary.LoginHeader">
<item name="android:fontFamily">sans-serif-light</item>
<item name="android:textSize">24sp</item>
<item name="android:gravity">center</item>
</style>
<style name="Text.Primary.LoginHeader.Sub">
<item name="android:fontFamily">sans-serif-medium</item>
<item name="android:textSize">18sp</item>
</style>
<style name="ToolbarTitle" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
<item name="android:gravity">center_vertical</item>
<item name="android:fontFamily">sans-serif-light</item>
<item name="android:textSize">18dp</item>
</style>
</resources>
================================================
FILE: app/src/test/java/com/wiseassblog/spacenotes/LoginLogicTest.kt
================================================
package com.wiseassblog.spacenotes
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.wiseassblog.domain.DispatcherProvider
import com.wiseassblog.domain.servicelocator.UserServiceLocator
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.domainmodel.User
import com.wiseassblog.domain.error.SpaceNotesError
import com.wiseassblog.domain.interactor.AuthSource
import com.wiseassblog.spacenotes.login.*
import io.mockk.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
class LoginLogicTest {
private val dispatcher: DispatcherProvider = mockk()
private val userLocator: UserServiceLocator = mockk()
private val view: ILoginContract.View = mockk(relaxed = true)
private val auth: AuthSource = mockk()
private val testAccount: GoogleSignInAccount = mockk()
private lateinit var logic: LoginLogic
val testIdToken: String = "8675309"
fun getUser(uid: String = "8675309",
name: String = "Ajahn Chah",
profilePicUrl: String = ""
) = User(uid,
name,
profilePicUrl)
@Before
fun clear() {
clearAllMocks()
logic = LoginLogic(dispatcher, userLocator, view, auth)
}
/**
* In onstart, we give a channel to the firebaseauth backend which it can use to push the latest
* user state to listener.
* the ui appropriately.
* a. User is retrieved successfully
* b. User is null
* c. Exception: no network connectivity
*
* a:
* 1. Check Network status: available
* 2. Ask auth source for current user: User
* 3. Start antenna loop
* 4. set login status: "Signed In"
* 5. set login button: "SIGN OUT"
*
*/
@Test
fun `On Start retrieve User`() = runBlocking {
every {
dispatcher.provideUIContext()
} returns Dispatchers.Unconfined
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { getUser() }
logic.onChanged(LoginEvent.OnStart)
coVerify { auth.getCurrentUser(userLocator) }
verify { view.setLoginStatus(SIGNED_IN) }
verify { view.showLoopAnimation() }
verify { view.setStatusDrawable(ANTENNA_FULL) }
verify { view.setAuthButton(SIGN_OUT) }
}
/**
*b:
* 1. Check Network status: available
* 2. Ask auth source for current user: null
* 3. Set animation to antenna_full
* 4. set login status: "Signed Out"
* 5. set login button: "SIGN IN"
*/
@Test
fun `On Start retrieve null`() = runBlocking {
every {
dispatcher.provideUIContext()
} returns Dispatchers.Unconfined
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { null }
logic.onChanged(LoginEvent.OnStart)
coVerify { auth.getCurrentUser(userLocator) }
verify { view.setLoginStatus(SIGNED_OUT) }
verify { view.showLoopAnimation() }
verify { view.setStatusDrawable(ANTENNA_EMPTY) }
verify { view.setAuthButton(SIGN_IN) }
}
/**
*c:
* 1. Check network status: unavailable
* 2. set animatin to drawable antenna_empty:
* 3. set login status: "Network Unavailable"
* 4. set login button: "RETRY"
*/
@Test
fun `On Start retrieve network error`() = runBlocking {
every {
dispatcher.provideUIContext()
} returns Dispatchers.Unconfined
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { throw SpaceNotesError.NetworkUnavailableException }
logic.onChanged(LoginEvent.OnStart)
coVerify { auth.getCurrentUser(userLocator) }
verify { view.setLoginStatus(ERROR_NETWORK_UNAVAILABLE) }
verify { view.showLoopAnimation() }
verify { view.setStatusDrawable(ANTENNA_EMPTY) }
verify { view.setAuthButton(RETRY) }
}
/**
*In OnAuthButtonClick, the user wishes to sign in to the application. Instruct View to
* create and launch GoogleSignInClient for result, and fire the intent
* a. User is currently signed out
* b. User is currently signed in
* c. network is unavailable
*
* a.
* 1. Check network status: available
* 2. User result: null
* 3. start sign in flow
*/
@Test
fun `On Auth Button Click signed out`() = runBlocking {
every {
dispatcher.provideUIContext()
} returns Dispatchers.Unconfined
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { null }
logic.onChanged(LoginEvent.OnAuthButtonClick)
coVerify { auth.getCurrentUser(userLocator) }
verify { view.showLoopAnimation() }
verify { view.startSignInFlow() }
}
/**
* b.
* 1. Check network status: available
* 2. User result: User
* 3. tell auth to sign user out
* 4. render user signed out view
*/
@Test
fun `On Auth Button Click signed in`() = runBlocking {
every {
dispatcher.provideUIContext()
} returns Dispatchers.Unconfined
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { getUser() }
coEvery {
auth.signOutCurrentUser(userLocator)
} returns Result.build { Unit }
logic.onChanged(LoginEvent.OnAuthButtonClick)
verify { view.showLoopAnimation() }
coVerify { auth.getCurrentUser(userLocator) }
coVerify { auth.signOutCurrentUser(userLocator) }
verify { view.setLoginStatus(SIGNED_OUT) }
verify { view.setStatusDrawable(ANTENNA_EMPTY) }
verify { view.setAuthButton(SIGN_IN) }
}
/**
* c.
* 1. Check network status: unavailable
* 2. render error view
*/
@Test
fun `On Auth Button Click network unavailable`() = runBlocking {
every {
dispatcher.provideUIContext()
} returns Dispatchers.Unconfined
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { throw SpaceNotesError.NetworkUnavailableException }
logic.onChanged(LoginEvent.OnAuthButtonClick)
verify { view.showLoopAnimation() }
coVerify { auth.getCurrentUser(userLocator) }
verify { view.setLoginStatus(ERROR_NETWORK_UNAVAILABLE) }
verify { view.setStatusDrawable(ANTENNA_EMPTY) }
verify { view.setAuthButton(RETRY) }
}
@Test
fun `On Back Click`() = runBlocking {
logic.onChanged(LoginEvent.OnBackClick)
verify { view.startListFeature() }
}
/**
* When the user wishes to create Sign In or create a new account, the result of this
* action will pop up in onActivityResult(), which is called prior to onResume(). Since
* we're already preferring pragmatism over separation of concerns in this feature due to tight
* coupling with Activities, I've chosen to attempt to retrieve the user account from in the
* activity. After that, it's up to the Logic class and backend to figure things out.
*
* a. GoogleSignInAccount succesfully retrieved
* b. GoogleSignInAcccount was null, or the task threw an exception
*
* 1. Pass LoginResult to Logic:
* 2. Check request code. If RC_SIGN_IN, we know that the result has to do with
* Signing In.
* 3. Pass token to backend
* 4. Attempt to await response for auth sign in result. This may timeout.
* 5. Either way ask firebase for the current user
*
*/
@Test
fun `On Sign In Result RC_SIGN_IN account idToken acquired`() = runBlocking {
val loginResult = LoginResult(RC_SIGN_IN, testAccount)
every {
testAccount.idToken
} returns testIdToken
every {
dispatcher.provideUIContext()
} returns Dispatchers.Unconfined
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { getUser() }
coEvery {
auth.createGoogleUser(testIdToken, userLocator)
} returns Result.build { Unit }
logic.onChanged(LoginEvent.OnGoogleSignInResult(loginResult))
coVerify { auth.createGoogleUser(testIdToken, userLocator) }
coVerify { auth.getCurrentUser(userLocator) }
verify { view.setLoginStatus(SIGNED_IN) }
verify { view.showLoopAnimation() }
verify { view.setStatusDrawable(ANTENNA_FULL) }
verify { view.setAuthButton(SIGN_OUT) }
}
/**
* b.
*/
@Test
fun `On Sign In Result RC_SIGN_IN account null`() = runBlocking {
val loginResult = LoginResult(RC_SIGN_IN, null)
every {
dispatcher.provideUIContext()
} returns Dispatchers.Unconfined
logic.onChanged(LoginEvent.OnGoogleSignInResult(loginResult))
verify { view.setLoginStatus(ERROR_AUTH) }
verify { view.setStatusDrawable(ANTENNA_EMPTY) }
verify { view.setAuthButton(RETRY) }
}
@After
fun confirm() {
excludeRecords {
dispatcher.provideUIContext()
testAccount.idToken
}
confirmVerified(dispatcher, userLocator, view, auth, testAccount)
}
}
================================================
FILE: app/src/test/java/com/wiseassblog/spacenotes/NoteDetailLogicTest.kt
================================================
package com.wiseassblog.spacenotes
import com.wiseassblog.domain.DispatcherProvider
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.domainmodel.User
import com.wiseassblog.domain.interactor.AnonymousNoteSource
import com.wiseassblog.domain.interactor.AuthSource
import com.wiseassblog.domain.interactor.PublicNoteSource
import com.wiseassblog.domain.interactor.RegisteredNoteSource
import com.wiseassblog.domain.servicelocator.NoteServiceLocator
import com.wiseassblog.domain.servicelocator.UserServiceLocator
import com.wiseassblog.spacenotes.common.MESSAGE_DELETE_SUCCESSFUL
import com.wiseassblog.spacenotes.notedetail.INoteDetailContract
import com.wiseassblog.spacenotes.notedetail.NoteDetailEvent
import com.wiseassblog.spacenotes.notedetail.NoteDetailLogic
import io.mockk.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* Philipp Hauer
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class NoteDetailLogicTest {
private val dispatcher: DispatcherProvider = mockk()
private val noteLocator: NoteServiceLocator = mockk()
private val userLocator: UserServiceLocator = mockk()
private val vModel: INoteDetailContract.ViewModel = mockk(relaxed = true)
private val view: INoteDetailContract.View = mockk(relaxed = true)
private val anonymous: AnonymousNoteSource = mockk()
private val registered: RegisteredNoteSource = mockk()
private val public: PublicNoteSource = mockk()
private val auth: AuthSource = mockk()
private lateinit var logic: NoteDetailLogic
//Shout out to Philipp Hauer @philipp_hauer for the snippet below (creating test data) with
//a default argument wrapper function:
fun getNote(creationDate: String = "12:30:30, November 3rd, 2018",
contents: String = "When I understand that this glass is already broken, every moment with it becomes precious.",
upVotes: Int = 0,
imageUrl: String = "satellite_beam",
creator: User? = User(
"8675309",
"Ajahn Chah",
"satellite_beam"
)
) = Note(
creationDate = creationDate,
contents = contents,
upVotes = upVotes,
imageUrl = imageUrl,
creator = creator
)
fun getUser(uid: String = "8675309",
name: String = "Ajahn Chah",
profilePicUrl: String = "satellite_beam"
) = User(uid,
name,
profilePicUrl)
fun getLogic(id: String = getNote().creationDate,
isPrivate: Boolean = true) = NoteDetailLogic(
dispatcher,
noteLocator,
userLocator,
vModel,
view,
anonymous,
registered,
public,
auth,
id,
isPrivate
)
@BeforeEach
fun clear() {
clearAllMocks()
every {
dispatcher.provideUIContext()
} returns Dispatchers.Unconfined
}
/**
* When auth presses done, they are finished editing their note. They will be returned to a list
* view of all notes. Depending on if the note isPrivate, and whether or not the user is
* anonymous, will dictate where the note is written to.
*
* a. isPrivate: true, user: null
* b. isPrivate: true, user: not null
* c. isPrivate: false, user: not null
*
* 1. Check current user status: null (anonymous), isPrivate is beside the point if null user
* 2. Create a copy of the note in vM, with updated "content" value
* 3. exit to list activity upon completion
*/
@Test
fun `On Done Click private, not logged in`() = runBlocking {
logic = getLogic()
every {
view.getNoteBody()
} returns getNote().contents
every {
vModel.getNoteState()
} returns getNote()
coEvery {
anonymous.updateNote(getNote(), noteLocator)
} returns Result.build { Unit }
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { null }
//call the unit to be tested
logic.onChanged(NoteDetailEvent.OnDoneClick)
//verify interactions and state if necessary
verify { view.getNoteBody() }
verify { vModel.getNoteState() }
coVerify { auth.getCurrentUser(userLocator) }
coVerify { anonymous.updateNote(getNote(), noteLocator) }
verify { view.startListFeature() }
}
/**
*b:
* 1. get current value of noteBody
* 2. write updated note to repositories
* 3. exit to list activity
*/
@Test
fun `On Done Click private, logged in`() = runBlocking {
logic = getLogic()
every {
view.getNoteBody()
} returns getNote().contents
every {
vModel.getNoteState()
} returns getNote()
every {
vModel.getIsPrivateMode()
} returns true
coEvery {
registered.updateNote(getNote(), noteLocator)
} returns Result.build { Unit }
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { getUser() }
//call the unit to be tested
logic.onChanged(NoteDetailEvent.OnDoneClick)
//verify interactions and state if necessary
verify { view.getNoteBody() }
verify { vModel.getNoteState() }
coVerify { auth.getCurrentUser(userLocator) }
coVerify { registered.updateNote(getNote(), noteLocator) }
verify { view.startListFeature() }
}
/**
*c:
* 1. get current value of noteBody
* 2. write updated note to public
* 3. exit to list activity
*/
@Test
fun `On Done Click public, logged in`() = runBlocking {
logic = getLogic()
every {
view.getNoteBody()
} returns getNote().contents
every {
vModel.getNoteState()
} returns getNote()
every {
vModel.getIsPrivateMode()
} returns false
coEvery {
public.updateNote(getNote(), noteLocator)
} returns Result.build { Unit }
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { getUser() }
//call the unit to be tested
logic.onChanged(NoteDetailEvent.OnDoneClick)
//verify interactions and state if necessary
verify { view.getNoteBody() }
verify { vModel.getNoteState() }
coVerify { auth.getCurrentUser(userLocator) }
coVerify { public.updateNote(getNote(), noteLocator) }
verify { view.startListFeature() }
}
/**
* When auth presses delete, they may wish to delete a note. Show confirmation.
*/
@Test
fun `On Delete Click`() = runBlocking {
logic = getLogic()
every {
view.showConfirmDeleteSnackbar()
} returns Unit
logic.onChanged(NoteDetailEvent.OnDeleteClick)
verify { view.showConfirmDeleteSnackbar() }
}
/**
* When user confirms that they wish to delete a note, delete the note. There are three possible
* places to delete from:
* a. Private Anonymous Repo
* b. Private Registered Repo
* c. Public Repo
*
* a:
* 1. Check status of current user: null
* 2. delete Note from anonymous repo
* 3. show message to indicate if operation was successful
* 3. startListFeature
*/
@Test
fun `On Delete Confirmation successful anonymous`() {
logic = getLogic()
every {
vModel.getNoteState()
} returns getNote()
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { null }
coEvery {
anonymous.deleteNote(getNote(), noteLocator)
} returns Result.build { Unit }
logic.onChanged(NoteDetailEvent.OnDeleteConfirmed)
verify { vModel.getNoteState() }
verify { view.showMessage(MESSAGE_DELETE_SUCCESSFUL) }
verify { view.startListFeature() }
coVerify { anonymous.deleteNote(getNote(), noteLocator) }
coVerify { auth.getCurrentUser(userLocator) }
}
/**
* When user confirms that they wish to delete a note, delete the note. There are three possible
* places to delete from:
* a. Private Anonymous Repo
* b. Private Registered Repo
* c. Public Repo
*
* b:
* 1. Check status of current user: not null
* 2. check isPrivate: true
* 2. delete Note from registered repo
* 3. show message to indicate if operation was successful
* 3. startListFeature
*/
@Test
fun `On Delete Confirmation successful registered`() {
logic = getLogic()
every {
vModel.getNoteState()
} returns getNote()
every {
vModel.getIsPrivateMode()
} returns true
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { getUser() }
coEvery {
registered.deleteNote(getNote(), noteLocator)
} returns Result.build { Unit }
logic.onChanged(NoteDetailEvent.OnDeleteConfirmed)
verify { vModel.getNoteState() }
verify { view.showMessage(MESSAGE_DELETE_SUCCESSFUL) }
verify { view.startListFeature() }
coVerify { registered.deleteNote(getNote(), noteLocator) }
coVerify { auth.getCurrentUser(userLocator) }
}
/**
*
* c:
* 1. Check status of current user: not null
* 2. check isPrivate: false
* 2. delete Note from public repo
* 3. show message to indicate if operation was successful
* 3. startListFeature
*/
@Test
fun `On Delete Confirmation successful public`() {
logic = getLogic()
every {
vModel.getNoteState()
} returns getNote()
every {
vModel.getIsPrivateMode()
} returns false
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { getUser() }
coEvery {
public.deleteNote(getNote(), noteLocator)
} returns Result.build { Unit }
logic.onChanged(NoteDetailEvent.OnDeleteConfirmed)
verify { vModel.getNoteState() }
verify { view.showMessage(MESSAGE_DELETE_SUCCESSFUL) }
verify { view.startListFeature() }
coVerify { public.deleteNote(getNote(), noteLocator) }
coVerify { auth.getCurrentUser(userLocator) }
}
@Test
fun `On Back Click`() {
logic = getLogic()
logic.onChanged(NoteDetailEvent.OnBackClick)
verify { view.startListFeature() }
}
/**
* In on bind, we need to check the status of arguments sent in to the feature via intent,
* check the user status, and call onStart() to render the view.
*
* a. get id from vModel: "" or null (new note)
* b. get id from vModel: not null (not new note)
* c. get user from auth: null
* d. get user from auth: not null
* e. get isPrivate from vModel: true
* f. get isPrivate from vModel: false
*
* a/c:
* 1. Check User state: null
* 2. Check arguments from activity: note id = "", isPrivate = true
* 3. Create new note with date and null user, store in vModel
* 4. render view
* - back set to invisible (only delete or save allowed for new notes
* - start satellite animation
* - set creation date
*/
@Test
fun `On bind a and c`() {
logic = getLogic("", true)
every {
view.getTime()
} returns getNote().creationDate
every {
vModel.getId()
} returns ""
every {
vModel.getIsPrivateMode()
} returns true
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { null }
logic.onChanged(NoteDetailEvent.OnBind)
//creatorId should be null for new note. It will be added if the user saves the note while
//logged in
verify { vModel.setNoteState(getNote(creator = null, contents = "", imageUrl = "satellite_beam")) }
verify { vModel.setIsPrivateMode(true) }
coVerify { auth.getCurrentUser(userLocator) }
verify { vModel.setId("") }
verify { view.getTime() }
verify { view.hideBackButton() }
excludeRecords {
view.setBackgroundImage(any())
view.setDateLabel(any())
view.setNoteBody(any())
}
}
/**
*a: New Note
*d: Not null user
*/
@Test
fun `On bind a and d`() {
logic = getLogic("", true)
every {
view.getTime()
} returns getNote().creationDate
every {
vModel.getId()
} returns ""
every {
vModel.getIsPrivateMode()
} returns true
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { getUser() }
logic.onChanged(NoteDetailEvent.OnBind)
//creatorId should be null for new note. It will be added if the user saves the note while
//logged in
verify { vModel.setNoteState(getNote(creator = getUser(), contents = "", imageUrl = "satellite_beam")) }
verify { vModel.setIsPrivateMode(true) }
coVerify { auth.getCurrentUser(userLocator) }
verify { vModel.setId("") }
verify { view.getTime() }
verify { view.hideBackButton() }
excludeRecords {
view.setBackgroundImage(any())
view.setDateLabel(any())
view.setNoteBody(any())
}
}
/**
*b: Not new Note
*c: User is null
*
* 1. Get current user: null
* 2. Check id: not null
* 3. Query anonymous datasource based on id
*/
@Test
fun `On bind b and c`() {
logic = getLogic(getNote().creationDate, true)
every {
vModel.getId()
} returns getNote().creationDate
every {
vModel.getIsPrivateMode()
} returns true
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { null }
coEvery {
anonymous.getNoteById(getNote().creationDate, noteLocator)
} returns Result.build { getNote() }
logic.onChanged(NoteDetailEvent.OnBind)
//creatorId should be null for new note. It will be added if the user saves the note while
//logged in
verify { vModel.setNoteState(getNote()) }
verify { vModel.setIsPrivateMode(true) }
coVerify { auth.getCurrentUser(userLocator) }
verify { vModel.setId(getNote().creationDate) }
coExcludeRecords {
anonymous.getNoteById(any(), any())
view.setBackgroundImage(any())
view.setDateLabel(any())
view.setNoteBody(any())
}
}
/**
*b: Not new Note
*f: public mode
*/
@Test
fun `On bind b and f`() {
logic = getLogic(getNote().creationDate, false)
every {
vModel.getId()
} returns getNote().creationDate
every {
vModel.getIsPrivateMode()
} returns false
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { getUser() }
coEvery {
public.getNoteById(getNote().creationDate, noteLocator)
} returns Result.build { getNote() }
logic.onChanged(NoteDetailEvent.OnBind)
//creatorId should be null for new note. It will be added if the user saves the note while
//logged in
verify { vModel.setNoteState(getNote()) }
verify { vModel.setIsPrivateMode(false) }
coVerify { auth.getCurrentUser(userLocator) }
coVerify { public.getNoteById(getNote().creationDate, noteLocator) }
verify { vModel.setId(getNote().creationDate) }
coExcludeRecords {
anonymous.getNoteById(any(), any())
view.setBackgroundImage(any())
view.setDateLabel(any())
view.setNoteBody(any())
}
}
/**
*a: New Note
*f: public mode
*/
@Test
fun `On bind a and f`() {
logic = getLogic("", false)
every {
vModel.getId()
} returns ""
every {
view.getTime()
} returns getNote().creationDate
every {
vModel.getIsPrivateMode()
} returns false
coEvery {
auth.getCurrentUser(userLocator)
} returns Result.build { getUser() }
logic.onChanged(NoteDetailEvent.OnBind)
coVerify { auth.getCurrentUser(userLocator) }
verify { vModel.getId() }
verify { view.getTime() }
verify { view.hideBackButton() }
verify {
vModel.setNoteState(
getNote(
contents = ""
)
)
}
coExcludeRecords {
anonymous.getNoteById(any(), any())
view.setBackgroundImage(any())
view.setDateLabel(any())
view.setNoteBody(any())
}
}
/**On start can be considered as a generic event to represent the view telling the listener
* that it's time to rock'n'roll.
*
* 1. Get value of the Note from VM
* 2. Render View
*/
@Test
fun `On start`() = runBlocking {
logic = getLogic(id = getNote().creationDate)
every {
vModel.getNoteState()
} returns getNote()
logic.onChanged(NoteDetailEvent.OnStart)
verify { vModel.getNoteState() }
verify { view.setBackgroundImage(getNote().imageUrl) }
verify { view.setDateLabel(getNote().creationDate) }
verify { view.setNoteBody(getNote().contents) }
}
@AfterEach
fun confirm() {
excludeRecords {
dispatcher.provideUIContext()
vModel.getNoteState()
vModel.setId(any())
vModel.getId()
vModel.setIsPrivateMode(any())
vModel.getIsPrivateMode()
}
confirmVerified(
dispatcher,
noteLocator,
userLocator,
vModel,
view,
anonymous,
registered,
public,
auth
)
}
}
================================================
FILE: app/src/test/java/com/wiseassblog/spacenotes/NoteListLogicTest.kt
================================================
package com.wiseassblog.spacenotes
import com.wiseassblog.domain.DispatcherProvider
import com.wiseassblog.domain.servicelocator.NoteServiceLocator
import com.wiseassblog.domain.servicelocator.UserServiceLocator
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.domainmodel.User
import com.wiseassblog.domain.error.SpaceNotesError
import com.wiseassblog.domain.interactor.AnonymousNoteSource
import com.wiseassblog.domain.interactor.AuthSource
import com.wiseassblog.domain.interactor.PublicNoteSource
import com.wiseassblog.domain.interactor.RegisteredNoteSource
import com.wiseassblog.spacenotes.common.*
import com.wiseassblog.spacenotes.notelist.INoteListContract
import com.wiseassblog.spacenotes.notelist.NoteListAdapter
import com.wiseassblog.spacenotes.notelist.NoteListEvent
import com.wiseassblog.spacenotes.notelist.NoteListLogic
import io.mockk.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class NoteListLogicTest {
//This mocking framework is called Mockk
private val dispatcher: DispatcherProvider = mockk()
private val noteLocator: NoteServiceLocator = mockk()
private val userLocator: UserServiceLocator = mockk()
private val vModel: INoteListContract.ViewModel = mockk(relaxed = true)
private val adapter: NoteListAdapter = mockk(relaxed = true)
private val view: INoteListContract.View = mockk(relaxed = true)
private val anonymous: AnonymousNoteSource = mockk()
private val registered: RegisteredNoteSource = mockk()
private val public: PublicNoteSource = mockk()
private val auth: AuthSource = mockk()
private val logic = NoteListLogic(
dispatcher,
noteLocator,
userLocator,
vModel,
adapter,
view,
anonymous,
registered,
public,
auth
)
//Shout out to Philipp Hauer @philipp_hauer for the snippet below (creating test data) with
//a default argument wrapper function:
fun getNote(creationDate: String = "28/10/2018",
contents: String = "When I understand that this glass is already broken, every moment with it becomes precious.",
upVotes: Int = 0,
imageUrl: String = "",
creator: User? = User(
"8675309",
"Ajahn Chah",
""
)
) = Note(
creationDate = creationDate,
contents = contents,
upVotes = upVotes,
imageUrl = imageUrl,
creator = creator
)
fun getUser(uid: String = "8675309",
name: String = "Ajahn Chah",
profilePicUrl: String = ""
) = User(uid,
name,
profilePicUrl)
val getNoteList = listOf<Note>(
getNote(),
getNote(),
getNote()
)
@BeforeEach
fun build() {
clearAllMocks()
every { dispatcher.provideUIContext() } returns Dispatchers.Unconfined
}
/**
* New Note events will have two possible event streams, based on whether or not the user is
* in private or public mode
* a: User is in private mode (could be logged in or anonymous, either case is fine)
* c: User is in public mode (must be logged in, but we'll check in the other activity to avoid
* shared mutable state issues)
*
* a:
* 1. check isPrivate status from ViewModel: true
* 2. startNoteDetailFeatureWithExtras with empty string as extra
*/
@Test
fun `On New Note Click Private`() {
//prepare mock interactions
every { vModel.getIsPrivateMode() } returns true
//call the unit to be tested
logic.onChanged(NoteListEvent.OnNewNoteClick)
//verify interactions and state if necessary
verify { view.startNoteDetailFeatureWithExtras("", true) }
}
/**
* b:
* 1.
* 2. startNoteDetailFeatureWithExtras with empty string as extra
*/
@Test
fun `On New Note Click Public`() {
//prepare mock interactions
every { vModel.getIsPrivateMode() } returns false
//call the unit to be tested
logic.onChanged(NoteListEvent.OnNewNoteClick)
//verify interactions and state if necessary
verify { view.startNoteDetailFeatureWithExtras("", false) }
}
/**
* On bind process, called by view in onCreate. Check current user state, write that result to
* vModel, show loading graphic, perform some initialization
*
* a. User is Anonymous
* b. User is Registered
*
* a:
* 1. Display Loading View
* 2. Check for a logged in user from auth: null
* 3. write null to vModel user state
* 4. call On start process
*/
@Test
fun `On bind User anonymous`() = runBlocking {
coEvery { auth.getCurrentUser(userLocator) } returns Result.build { null }
logic.onChanged(NoteListEvent.OnBind)
coVerify { auth.getCurrentUser(userLocator) }
verify { vModel.setUserState(null) }
verify { view.showLoadingView() }
verify { view.setToolbarTitle(MODE_PRIVATE) }
verify { view.setAdapter(adapter) }
verify { adapter.setObserver(logic) }
}
@Test
fun `On bind user registered`() = runBlocking {
coEvery { auth.getCurrentUser(userLocator) } returns Result.build { getUser() }
logic.onChanged(NoteListEvent.OnBind)
coVerify { auth.getCurrentUser(userLocator) }
verify { vModel.setUserState(getUser()) }
verify { view.showLoadingView() }
verify { view.setAdapter(adapter) }
verify { view.setToolbarTitle(MODE_PRIVATE) }
verify { adapter.setObserver(logic) }
}
@Test
fun `On bind error retrieving user`() = runBlocking {
coEvery { auth.getCurrentUser(userLocator) } returns Result.build { throw SpaceNotesError.AuthError }
logic.onChanged(NoteListEvent.OnBind)
coVerify { auth.getCurrentUser(userLocator) }
verify { view.showLoadingView() }
verify { view.setToolbarTitle(MODE_PRIVATE) }
verify { view.setAdapter(adapter) }
verify { adapter.setObserver(logic) }
}
/**
*
* On start basically means that we want to render the UI. This depends on whether the user is
* anonymous, or registered (logged out or in), and if they are in public or private mode
* a. User is anonymous (always private in that case)
* b. User is registered, private mode
* c. User is registered, public mode
*
* a:
*1. Check isPrivate status: true
*2. Check login status in backend if necessary
*3. parse datasources accordingly
*4. draw view accordingly
*/
@Test
fun `On Start anonymous`() = runBlocking {
every { vModel.getIsPrivateMode() } returns true
every { vModel.getUserState() } returns null
coEvery { anonymous.getNotes(noteLocator) } returns Result.build { getNoteList }
logic.onChanged(NoteListEvent.OnStart)
verify { vModel.getIsPrivateMode() }
verify { vModel.getUserState() }
verify { view.showList() }
verify { adapter.submitList(getNoteList) }
coVerify { anonymous.getNotes(noteLocator) }
}
/**
* b:
*1. Check isPrivate status: false
*2. Check login status in backend if necessary
*3. parse datasources accordingly
*4. draw view accordingly
*
*/
@Test
fun `On Start Registered Private`() = runBlocking {
every { vModel.getIsPrivateMode() } returns true
every { vModel.getUserState() } returns getUser()
coEvery { registered.getNotes(noteLocator) } returns Result.build { getNoteList }
logic.onChanged(NoteListEvent.OnStart)
verify { vModel.getIsPrivateMode() }
verify { vModel.getUserState() }
verify { view.showList() }
verify { adapter.submitList(getNoteList) }
coVerify { registered.getNotes(noteLocator) }
}
/**
*c:
*1. Check isPrivate status: false
*2. Check login status in backend if necessary
*3. parse datasources accordingly
*4. draw view accordingly
*
*/
@Test
fun `On Start Registered Public`() = runBlocking {
every { vModel.getIsPrivateMode() } returns false
every { vModel.getUserState() } returns getUser()
coEvery { public.getNotes(noteLocator) } returns Result.build { getNoteList }
logic.onChanged(NoteListEvent.OnStart)
verify { vModel.getIsPrivateMode() }
verify { vModel.getUserState() }
verify { view.showList() }
verify { adapter.submitList(getNoteList) }
coVerify { public.getNotes(noteLocator) }
}
/**
* error:
*1. Check isPrivate status: false
*2. Check login status in backend if necessary
*3. parse datasources accordingly
*4. draw view accordingly
*
*/
@Test
fun `On Start Error`() = runBlocking {
every { vModel.getIsPrivateMode() } returns true
every { vModel.getUserState() } returns getUser()
coEvery { registered.getNotes(noteLocator) } returns Result.build { throw SpaceNotesError.RemoteIOException }
logic.onChanged(NoteListEvent.OnStart)
verify { vModel.getIsPrivateMode() }
verify { vModel.getUserState() }
verify { view.showEmptyState() }
verify { view.showErrorState(MESSAGE_GENERIC_ERROR) }
coVerify { registered.getNotes(noteLocator) }
}
/**
* For empty list, leave the loading animation active.
*/
@Test
fun `On Start a with empty list`() = runBlocking {
every { vModel.getIsPrivateMode() } returns true
every { vModel.getUserState() } returns getUser()
coEvery { registered.getNotes(noteLocator) } returns Result.build { emptyList<Note>() }
logic.onChanged(NoteListEvent.OnStart)
verify { vModel.getIsPrivateMode() }
verify { vModel.getUserState() }
verify { view.showEmptyState() }
verify { adapter.submitList(emptyList<Note>()) }
coVerify { registered.getNotes(noteLocator) }
}
/**
* c. auth is logged in and in public mode
*1. Check auth status
*2. Check isPrivate status
*3. parse datasources accordingly
*/
@Test
fun `On Start Public Mode`() = runBlocking {
every { vModel.getIsPrivateMode() } returns false
coEvery { public.getNotes(noteLocator) } returns Result.build { getNoteList }
logic.onChanged(NoteListEvent.OnStart)
verify { vModel.getIsPrivateMode() }
verify { view.showList() }
verify { adapter.submitList(getNoteList) }
coVerify { public.getNotes(noteLocator) }
}
/**
* On login click, send auth to Auth Activity in order to manage their login status
*
*1. start login activity
*/
@Test
fun `On Login Click `() {
logic.onChanged(NoteListEvent.OnLoginClick)
verify { view.startLoginFeature() }
}
/**
* On Note Item Click, auth wishes to navigate to a detailed view of the selected item
*a: isPrivate = true
*1. Get appropriate Note from vModel
*2. Get isPrivate from vModel
*2. Start detail Activity with note id passed as extra, and isPrivate result
*/
@Test
fun `On Note Item Click a`() = runBlocking {
every { vModel.getIsPrivateMode() } returns true
every { vModel.getAdapterState() } returns getNoteList
//auth selects first item in adapter
val clickEvent = NoteListEvent.OnNoteItemClick(0)
logic.onChanged(clickEvent)
verify { view.startNoteDetailFeatureWithExtras(getNote().creationDate, true) }
verify { vModel.getAdapterState() }
verify { vModel.getIsPrivateMode() }
}
/**
*b: isPrivate = false
*1. Get appropriate Note from vModel
*2. Get isPrivate from vModel
*2. Start detail Activity with note id passed as extra, and isPrivate result
*/
@Test
fun `On Note Item Click b`() = runBlocking {
every { vModel.getIsPrivateMode() } returns false
every { vModel.getAdapterState() } returns getNoteList
//auth selects first item in adapter
val clickEvent = NoteListEvent.OnNoteItemClick(0)
logic.onChanged(clickEvent)
verify { view.startNoteDetailFeatureWithExtras(getNote().creationDate, false) }
verify { vModel.getAdapterState() }
verify { vModel.getIsPrivateMode() }
}
/**
* When the user wants to switch between private and public mode
* a: User is logged in, currently in private mode
* b: User is logged in, currently in public mode
* c: User is logged out, private only
*
*a:
*1. Check current user status: User
*2. Get isPrivate from vModel: true
*3. Request public notes from repo: Notes
*4. Update view/adapter appropriately
* */
@Test
fun `On Toggle Public Mode`() = runBlocking {
every { vModel.getIsPrivateMode() } returns true andThen false
coEvery { public.getNotes(noteLocator) } returns Result.build { getNoteList }
logic.onChanged(NoteListEvent.OnTogglePublicMode)
verify { vModel.setAdapterState(getNoteList) }
verify { vModel.getIsPrivateMode() }
verify { adapter.submitList(getNoteList) }
coVerify { public.getNotes(noteLocator) }
//ought to be false and MODE_PUBLIC, but
verify { view.setPrivateIcon(false) }
verify { view.setToolbarTitle(MODE_PUBLIC) }
}
/**
* b:
*1. Check current user status: User
*2. Get isPrivate from vModel: false
*3. Request private notes from repo: Notes
*4. Update view/adapter appropriately
* */
@Test
fun `On Toggle Private Mode`() = runBlocking {
every { vModel.getIsPrivateMode() } returns false andThen true
coEvery { registered.getNotes(noteLocator) } returns Result.build { getNoteList }
logic.onChanged(NoteListEvent.OnTogglePublicMode)
verify { vModel.setAdapterState(getNoteList) }
verify { vModel.getIsPrivateMode() }
verify { adapter.submitList(getNoteList) }
coVerify { registered.getNotes(noteLocator) }
verify { view.setPrivateIcon(true) }
verify { view.setToolbarTitle(MODE_PRIVATE) }
}
/**
* C:
*1. Check current user status: no user
*2. Tell user to log in if they want to use the public feature
* */
@Test
fun `On Toggle Private Mode logged out`() = runBlocking {
every { vModel.getUserState() } returns null
logic.onChanged(NoteListEvent.OnTogglePublicMode)
verify { view.showErrorState(MESSAGE_LOGIN) }
}
@After
fun confirm() {
excludeRecords { dispatcher.provideUIContext() }
confirmVerified(
dispatcher,
noteLocator,
userLocator,
vModel,
adapter,
view,
anonymous,
registered,
public,
auth
)
}
}
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
apply from: 'versions.gradle'
addRepos(repositories)
dependencies {
classpath deps.android_gradle_plugin
classpath deps.kotlin.kotlin_gradle_plugin
classpath 'com.google.gms:google-services:4.2.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
addRepos(repositories)
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: data/.gitignore
================================================
/build
================================================
FILE: data/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion build_versions.target_sdk
lintOptions {
abortOnError false
}
defaultConfig {
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes {
debug {
testCoverageEnabled = true
minifyEnabled false
}
release {
testCoverageEnabled = false
minifyEnabled true
}
}
testOptions.unitTests.all {
useJUnitPlatform()
testLogging {
events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
}
}
}
dependencies {
//to talk to another module within the project:
implementation project(":domain")
implementation deps.room.runtime
implementation deps.firebase.auth
implementation deps.firebase.firestore
implementation deps.play_services.auth
implementation deps.kotlin.kotlin_jre
implementation deps.kotlin.coroutines_core
implementation deps.kotlin.coroutines_android
kapt deps.room.compiler
testImplementation deps.test.junit
testRuntimeOnly deps.test.jupiter_engine
testRuntimeOnly deps.test.vintage_engine
testImplementation deps.test.mockk
testImplementation deps.test.kotlin_junit
}
================================================
FILE: data/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: data/src/main/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wiseassblog.data" />
================================================
FILE: data/src/main/java/com/wiseassblog/data/DataExt.kt
================================================
package com.wiseassblog.data
import android.net.Uri
import com.google.android.gms.tasks.Task
import com.wiseassblog.data.datamodels.AnonymousRoomNote
import com.wiseassblog.data.datamodels.RegisteredRoomNote
import com.wiseassblog.data.datamodels.RegisteredRoomTransaction
import com.wiseassblog.data.datamodels.FirebaseNote
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.NoteTransaction
import com.wiseassblog.domain.domainmodel.TransactionType
import com.wiseassblog.domain.domainmodel.User
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
suspend fun <T> awaitTaskResult(task: Task<T>): T = suspendCoroutine { continuation ->
task.addOnCompleteListener { task ->
if (task.isSuccessful) {
continuation.resume(task.result!!)
} else {
continuation.resumeWithException(task.exception!!)
}
}
}
//Wraps Firebase/GMS calls
suspend fun <T> awaitTaskCompletable(task: Task<T>): Unit = suspendCoroutine { continuation ->
task.addOnCompleteListener { task ->
if (task.isSuccessful) {
continuation.resume(Unit)
} else {
continuation.resumeWithException(task.exception!!)
}
}
}
//Since this.creator is of type Note?, we must give it a default value in such cases.
internal val Note.safeGetUid: String
get() = this.creator?.uid ?: ""
internal val NoteTransaction.safeGetUid: String
get() = this.creator?.uid ?: ""
internal val Uri?.defaultIfEmpty: String
get() = if (this.toString() == "" || this == null) "satellite_beam"
else this.toString()
//"this" refers to the object upon which this extension property is called
internal val Note.toAnonymousRoomNote: AnonymousRoomNote
get() = AnonymousRoomNote(
this.creationDate,
this.contents,
this.upVotes,
this.imageUrl,
this.safeGetUid
)
internal val AnonymousRoomNote.toNote: Note
get() = Note(
this.creationDate,
this.contents,
this.upVotes,
this.imageUrl,
User(this.creatorId)
)
internal val RegisteredRoomTransaction.toTransaction: NoteTransaction
get() = NoteTransaction(
this.creationDate,
this.contents,
this.upVotes,
this.imageUrl,
User(this.creatorId),
this.transactionType.toTransactionType()
)
internal val NoteTransaction.toRegisteredRoomTransaction: RegisteredRoomTransaction
get() = RegisteredRoomTransaction(
this.creationDate,
this.contents,
this.upVotes,
this.imageUrl,
this.safeGetUid,
this.transactionType.value
)
internal val NoteTransaction.toNote: Note
get() = Note(
this.creationDate,
this.contents,
this.upVotes,
this.imageUrl,
User(this.safeGetUid)
)
internal fun String.toTransactionType(): TransactionType {
return if (this.equals(TransactionType.DELETE)) TransactionType.DELETE
else TransactionType.UPDATE
}
internal val Note.toRegisteredRoomNote: RegisteredRoomNote
get() = RegisteredRoomNote(
this.creationDate,
this.contents,
this.upVotes,
this.imageUrl,
this.safeGetUid
)
internal val RegisteredRoomNote.toNote: Note
get() = Note(
this.creationDate,
this.contents,
this.upVotes,
this.imageUrl,
User(this.creatorId)
)
internal val Note.toFirebaseNote: FirebaseNote
get() = FirebaseNote(
this.creationDate,
this.contents,
this.upVotes,
this.imageUrl,
this.safeGetUid
)
internal val FirebaseNote.toNote: Note
get() = Note(
this.creationDate ?: "",
this.contents ?: "",
this.upVotes ?: 0,
this.imageurl ?: "",
User(this.creator ?: "")
)
//Maps from lists of different Data Model types
internal fun List<AnonymousRoomNote>.toNoteListFromAnonymous(): List<Note> = this.flatMap {
listOf(it.toNote)
}
internal fun List<RegisteredRoomNote>.toNoteListFromRegistered(): List<Note> = this.flatMap {
listOf(it.toNote)
}
internal fun List<RegisteredRoomTransaction>.toNoteTransactionListFromRegistered(): List<NoteTransaction> = this.flatMap {
listOf(it.toTransaction)
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/auth/FirebaseAuthRepositoryImpl.kt
================================================
package com.wiseassblog.data.auth
import com.google.android.gms.tasks.Tasks
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.GoogleAuthProvider
import com.wiseassblog.data.awaitTaskCompletable
import com.wiseassblog.data.defaultIfEmpty
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.domainmodel.User
import com.wiseassblog.domain.error.SpaceNotesError
import com.wiseassblog.domain.repository.IAuthRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class FirebaseAuthRepositoryImpl(val auth: FirebaseAuth = FirebaseAuth.getInstance()) : IAuthRepository {
override suspend fun createGoogleUser(idToken: String):
Result<Exception, Unit> = withContext(Dispatchers.IO) {
try {
val credential = GoogleAuthProvider.getCredential(idToken, null)
awaitTaskCompletable(auth.signInWithCredential(credential))
Tasks.await(auth.signInWithCredential(credential))
Result.build { Unit }
} catch (e: Exception) {
Result.build { throw e }
}
}
override suspend fun signOutCurrentUser(): Result<Exception, Unit> {
return Result.build {
auth.signOut()
}
}
override suspend fun deleteCurrentUser(): Result<Exception, Boolean> {
return try {
val user = auth.currentUser ?: throw SpaceNotesError.AuthError
awaitTaskCompletable(user.delete())
Result.build { true }
} catch (exception: Exception) {
Result.build { throw exception }
}
}
override suspend fun getCurrentUser(): Result<Exception, User?> {
val firebaseUser = auth.currentUser
if (firebaseUser == null) return Result.build { null }
else return Result.build {
User(
firebaseUser.uid,
firebaseUser.displayName ?: "",
firebaseUser.photoUrl.defaultIfEmpty
)
}
}
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/datamodels/AnonymousRoomNote.kt
================================================
package com.wiseassblog.data.datamodels
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
//If you're Data Models for a given API require API specific code, then create a separate Data
//Model instead of polluting your domain with platform specific APIs.
@Entity(
tableName = "anonymous_notes",
indices = [Index("creation_date")]
)
data class AnonymousRoomNote(
@PrimaryKey
@ColumnInfo(name = "creation_date")
val creationDate: String,
@ColumnInfo(name = "contents")
val contents: String,
@ColumnInfo(name = "up_votes")
val upVotes: Int,
@ColumnInfo(name = "image_url")
val imageUrl: String,
@ColumnInfo(name = "creator_id")
val creatorId: String
)
================================================
FILE: data/src/main/java/com/wiseassblog/data/datamodels/FirebaseNote.kt
================================================
package com.wiseassblog.data.datamodels
//var and default arguments used due to firestore requiring a no argument constructor to
//deserialize
data class FirebaseNote(
var creationDate: String? = "",
var contents: String? = "",
var upVotes: Int? = 0,
var imageurl: String? = "",
var creator: String? = ""
)
================================================
FILE: data/src/main/java/com/wiseassblog/data/datamodels/RegisteredRoomNote.kt
================================================
package com.wiseassblog.data.datamodels
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
//If you're Data Models for a given API require API specific code, then create a separate Data
//Model instead of polluting your domain with platform specific APIs.
@Entity(
tableName = "registered_notes",
indices = [Index("creation_date")]
)
data class RegisteredRoomNote(
@PrimaryKey
@ColumnInfo(name = "creation_date")
val creationDate: String,
@ColumnInfo(name = "contents")
val contents: String,
@ColumnInfo(name = "up_votes")
val upVotes: Int,
@ColumnInfo(name = "image_url")
val imageUrl: String,
@ColumnInfo(name = "creator_id")
val creatorId: String
)
================================================
FILE: data/src/main/java/com/wiseassblog/data/datamodels/RegisteredRoomTransaction.kt
================================================
package com.wiseassblog.data.datamodels
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
//If you're Data Models for a given API require API specific code, then create a separate Data
//Model instead of polluting your domain with platform specific APIs.
@Entity(
tableName = "transactions",
indices = [Index("creation_date")]
)
data class RegisteredRoomTransaction(
@PrimaryKey
@ColumnInfo(name = "creation_date")
val creationDate: String,
@ColumnInfo(name = "contents")
val contents: String,
@ColumnInfo(name = "up_votes")
val upVotes: Int,
@ColumnInfo(name = "image_url")
val imageUrl: String,
@ColumnInfo(name = "creator_id")
val creatorId: String,
@ColumnInfo(name = "transaction_type")
val transactionType: String
)
================================================
FILE: data/src/main/java/com/wiseassblog/data/note/anonymous/AnonymousNoteDao.kt
================================================
package com.wiseassblog.data.note.anonymous
import androidx.room.*
import com.wiseassblog.data.datamodels.AnonymousRoomNote
@Dao
interface AnonymousNoteDao {
@Query("SELECT * FROM anonymous_notes ORDER BY creation_date")
fun getNotes(): List<AnonymousRoomNote>
@Query("SELECT * FROM anonymous_notes WHERE creation_date = :creationDate ORDER BY creation_date")
fun getNoteById(creationDate: String): AnonymousRoomNote
@Delete
fun deleteNote(noteAnonymous: AnonymousRoomNote)
@Query("DELETE FROM anonymous_notes")
fun deleteAll()
//if update successful, will return number of rows effected, which should be 1
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrUpdateNote(noteAnonymous: AnonymousRoomNote): Long
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/note/anonymous/RoomAnonymousNoteDatabase.kt
================================================
package com.wiseassblog.data.note.anonymous
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.wiseassblog.data.datamodels.AnonymousRoomNote
private const val DATABASE_ANON = "anonymous"
@Database(entities = [AnonymousRoomNote::class],
version = 1,
exportSchema = false)
abstract class AnonymousNoteDatabase : RoomDatabase(){
abstract fun roomNoteDao(): AnonymousNoteDao
//code below courtesy of https://github.com/googlesamples/android-sunflower; it is open
//source just like this application.
companion object {
// For Singleton instantiation
@Volatile private var instance: AnonymousNoteDatabase? = null
fun getInstance(context: Context): AnonymousNoteDatabase {
return instance ?: synchronized(this) {
instance
?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): AnonymousNoteDatabase {
return Room.databaseBuilder(context, AnonymousNoteDatabase::class.java, DATABASE_ANON)
.build()
}
}
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/note/anonymous/RoomLocalAnonymousRepositoryImpl.kt
================================================
package com.wiseassblog.data.note.anonymous
import com.wiseassblog.data.toAnonymousRoomNote
import com.wiseassblog.data.toNote
import com.wiseassblog.data.toNoteListFromAnonymous
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.error.SpaceNotesError
import com.wiseassblog.domain.repository.ILocalNoteRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class RoomLocalAnonymousRepositoryImpl(private val noteDao: AnonymousNoteDao) : ILocalNoteRepository {
//Not to be used
override suspend fun deleteAll(): Result<Exception, Unit> = Result.build { throw SpaceNotesError.LocalIOException }
//Not to be used
override suspend fun updateAll(list: List<Note>): Result<Exception, Unit> = Result.build { throw SpaceNotesError.LocalIOException }
override suspend fun updateNote(note: Note): Result<Exception, Unit> = withContext(Dispatchers.IO) {
val updated = noteDao.insertOrUpdateNote(note.toAnonymousRoomNote)
when {
//TODO verify that if nothing is updated, the resulting value will be 0
updated == 0L -> Result.build { throw SpaceNotesError.LocalIOException }
else -> Result.build { Unit }
}
}
override suspend fun getNote(id: String): Result<Exception, Note?> = withContext(Dispatchers.IO) { Result.build { noteDao.getNoteById(id).toNote } }
override suspend fun getNotes(): Result<Exception, List<Note>> = withContext(Dispatchers.IO) { Result.build { noteDao.getNotes().toNoteListFromAnonymous() } }
override suspend fun deleteNote(note: Note): Result<Exception, Unit> = withContext(Dispatchers.IO) {
noteDao.deleteNote(note.toAnonymousRoomNote)
Result.build { Unit }
}
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/note/public/FirestorePublicNoteRepositoryImpl.kt
================================================
package com.wiseassblog.data.note.public
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.QuerySnapshot
import com.wiseassblog.data.awaitTaskCompletable
import com.wiseassblog.data.awaitTaskResult
import com.wiseassblog.data.datamodels.FirebaseNote
import com.wiseassblog.data.toFirebaseNote
import com.wiseassblog.data.toNote
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.repository.IPublicNoteRepository
const val COLLECTION_PUBLIC = "public_notes"
object FirestoreRemoteNoteImpl : IPublicNoteRepository {
override suspend fun getNotes(): Result<Exception, List<Note>> {
val firestore = FirebaseFirestore.getInstance()
var reference = firestore.collection(COLLECTION_PUBLIC)
return try {
val task = awaitTaskResult(reference.get())
return resultToNoteList(task)
} catch (exception: Exception) {
Result.build { throw exception }
}
}
override suspend fun getNote(id: String): Result<Exception, Note?> {
val firestore = FirebaseFirestore.getInstance()
var reference = firestore.collection(COLLECTION_PUBLIC)
.document(id)
return try {
val task = awaitTaskResult(reference.get())
Result.build {
task.toObject(FirebaseNote::class.java)?.toNote
}
} catch (exception: Exception) {
Result.build { throw exception }
}
}
override suspend fun deleteNote(note: Note): Result<Exception, Unit> {
val firestore = FirebaseFirestore.getInstance()
return try {
awaitTaskCompletable(firestore.collection(COLLECTION_PUBLIC)
.document(note.creationDate)
.delete()
)
Result.build { Unit }
} catch (exception: Exception) {
Result.build { throw exception }
}
}
override suspend fun updateNote(note: Note): Result<Exception, Unit> {
val firestore = FirebaseFirestore.getInstance()
return try {
awaitTaskCompletable(firestore.collection(COLLECTION_PUBLIC)
.document(note.creationDate)
.set(note.toFirebaseNote)
)
Result.build { Unit }
} catch (exception: Exception) {
Result.build { throw exception }
}
}
private fun resultToNoteList(result: QuerySnapshot?): Result<Exception, List<Note>> {
val noteList = mutableListOf<Note>()
result?.forEach { documentSnapshop ->
noteList.add(documentSnapshop.toObject(FirebaseNote::class.java).toNote)
}
return Result.build {
noteList
}
}
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/note/registered/FirestorePrivateRemoteNoteImpl.kt
================================================
package com.wiseassblog.data.note.registered
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.QuerySnapshot
import com.wiseassblog.data.awaitTaskCompletable
import com.wiseassblog.data.awaitTaskResult
import com.wiseassblog.data.datamodels.FirebaseNote
import com.wiseassblog.data.toFirebaseNote
import com.wiseassblog.data.toNote
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.NoteTransaction
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.repository.IRemoteNoteRepository
private const val COLLECTION_NAME = "notes"
class FirestorePrivateRemoteNoteImpl(
val firestore: FirebaseFirestore = FirebaseFirestore.getInstance()
) : IRemoteNoteRepository {
//Currently handled in RegisteredNoteRepositoryImpl
override suspend fun synchronizeTransactions(transactions: List<NoteTransaction>): Result<Exception, Unit> = Result.build { Unit }
override suspend fun getNotes(): Result<Exception, List<Note>> {
var reference = firestore.collection(COLLECTION_NAME)
return try {
val task = awaitTaskResult(reference.get())
return resultToNoteList(task)
} catch (exception: Exception) {
Result.build { throw exception }
}
}
private fun resultToNoteList(result: QuerySnapshot?): Result<Exception, List<Note>> {
val noteList = mutableListOf<Note>()
result?.forEach { documentSnapshop ->
noteList.add(documentSnapshop.toObject(FirebaseNote::class.java).toNote)
}
return Result.build {
noteList
}
}
override suspend fun getNote(id: String): Result<Exception, Note?> {
var reference = firestore.collection(COLLECTION_NAME)
.document(id)
return try {
val task = awaitTaskResult(reference.get())
Result.build {
task.toObject(FirebaseNote::class.java)?.toNote
}
} catch (exception: Exception) {
Result.build { throw exception }
}
}
override suspend fun deleteNote(note: Note): Result<Exception, Unit> {
return try {
awaitTaskCompletable(firestore.collection(COLLECTION_NAME)
.document(note.creationDate)
.delete()
)
Result.build { Unit }
} catch (exception: Exception) {
Result.build { throw exception }
}
}
override suspend fun updateNote(note: Note): Result<Exception, Unit> {
return try {
awaitTaskCompletable(firestore.collection(COLLECTION_NAME)
.document(note.creationDate)
.set(note.toFirebaseNote)
)
Result.build { Unit }
} catch (exception: Exception) {
Result.build { throw exception }
}
}
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/note/registered/RegisteredNoteDao.kt
================================================
package com.wiseassblog.data.note.registered
import androidx.room.*
import com.wiseassblog.data.datamodels.RegisteredRoomNote
@Dao
interface RegisteredNoteDao {
@Query("SELECT * FROM registered_notes ORDER BY creation_date")
fun getNotes(): List<RegisteredRoomNote>
@Query("SELECT * FROM registered_notes WHERE creation_date = :creationDate ORDER BY creation_date")
fun getNoteById(creationDate: String): RegisteredRoomNote
@Delete
fun deleteNote(note: RegisteredRoomNote)
@Query("DELETE FROM registered_notes")
fun deleteAll()
//if update successful, will return number of rows effected, which should be 1
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrUpdateNote(note: RegisteredRoomNote): Long
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/note/registered/RegisteredNoteRepositoryImpl.kt
================================================
package com.wiseassblog.data.note.registered
import com.wiseassblog.data.toNote
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.NoteTransaction
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.domainmodel.TransactionType
import com.wiseassblog.domain.error.SpaceNotesError
import com.wiseassblog.domain.repository.ILocalNoteRepository
import com.wiseassblog.domain.repository.IRemoteNoteRepository
class RegisteredNoteRepositoryImpl(val remote: IRemoteNoteRepository,
val cache: ILocalNoteRepository) : IRemoteNoteRepository {
/**
* Since n number of transactions may need to be pushed to the remote, and may not all
* be successful, it's rather tricky to return a specific result. I figured that the next
* best thing would be to return an error if any of them fail, to at least inform the
* user that something didn't go as planned.
*/
override suspend fun synchronizeTransactions(transactions: List<NoteTransaction>): Result<Exception, Unit> {
//track results
val resultList = mutableListOf<Result<Exception, Unit>>()
transactions.forEach {
if (it.transactionType == TransactionType.UPDATE) remote.updateNote(it.toNote)
.also { updateResult ->
resultList.add(updateResult)
}
else remote.deleteNote(it.toNote).also { deleteResult ->
resultList.add(deleteResult)
}
}
var successful = true
//if any result was an error, throw a generic error
resultList.forEach {
if (it is Result.Error) successful = false
}
if (successful) return Result.build { Unit }
else return Result.build { throw SpaceNotesError.RemoteIOException }
}
override suspend fun getNotes(): Result<Exception, List<Note>> {
val remoteResult = remote.getNotes()
when (remoteResult) {
is Result.Value -> {
cache.deleteAll()
cache.updateAll(remoteResult.value)
}
is Result.Error -> {
return cache.getNotes()
}
}
return remoteResult
}
override suspend fun getNote(id: String): Result<Exception, Note?> {
val remoteResult = remote.getNote(id)
return if (remoteResult is Result.Error) cache.getNote(id)
else remoteResult
}
override suspend fun deleteNote(note: Note): Result<Exception, Unit> = remote.deleteNote(
note
)
override suspend fun updateNote(note: Note): Result<Exception, Unit> = remote.updateNote(
note
)
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/note/registered/RegisteredTransactionDao.kt
================================================
package com.wiseassblog.data.note.registered
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.wiseassblog.data.datamodels.RegisteredRoomTransaction
@Dao
interface RegisteredTransactionDao {
@Query("SELECT * FROM transactions ORDER BY creation_date")
fun getTransactions(): List<RegisteredRoomTransaction>
@Query("DELETE FROM transactions")
fun deleteAll()
//if update successful, will return number of rows effected, which should be 1
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrUpdateTransaction(transaction: RegisteredRoomTransaction): Long
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/note/registered/RoomLocalCacheImpl.kt
================================================
package com.wiseassblog.data.note.registered
import com.wiseassblog.data.toNote
import com.wiseassblog.data.toNoteListFromRegistered
import com.wiseassblog.data.toRegisteredRoomNote
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.repository.ILocalNoteRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* This datasource is used by the RegisteredNoteRepository
*/
class RoomLocalCacheImpl(private val noteDao: RegisteredNoteDao) : ILocalNoteRepository {
override suspend fun deleteAll(): Result<Exception, Unit> = withContext(Dispatchers.IO) {
noteDao.deleteAll()
Result.build { Unit }
}
override suspend fun updateAll(list: List<Note>): Result<Exception, Unit> = withContext(Dispatchers.IO) {
list.forEach {
noteDao.insertOrUpdateNote(it.toRegisteredRoomNote)
}
Result.build { Unit }
}
override suspend fun updateNote(note: Note): Result<Exception, Unit> = withContext(Dispatchers.IO) {
noteDao.insertOrUpdateNote(note.toRegisteredRoomNote)
Result.build { Unit }
}
override suspend fun getNote(id: String): Result<Exception, Note?> = withContext(Dispatchers.IO) {
Result.build { noteDao.getNoteById(id).toNote }
}
override suspend fun getNotes(): Result<Exception, List<Note>> = withContext(Dispatchers.IO) {
Result.build { noteDao.getNotes().toNoteListFromRegistered() }
}
override suspend fun deleteNote(note: Note): Result<Exception, Unit> = withContext(Dispatchers.IO) {
noteDao.deleteNote(note.toRegisteredRoomNote)
Result.build { Unit }
}
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/note/registered/RoomRegisteredNoteDatabase.kt
================================================
package com.wiseassblog.data.note.registered
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.wiseassblog.data.datamodels.RegisteredRoomNote
private const val DATABASE_REG = "registered"
/**
* This database is used as a "cache" for registered users.
*/
@Database(entities = [RegisteredRoomNote::class],
version = 1,
exportSchema = false)
abstract class RegisteredNoteDatabase : RoomDatabase(){
abstract fun roomNoteDao(): RegisteredNoteDao
//code below courtesy of https://github.com/googlesamples/android-sunflower; it is open
//source just like this application.
companion object {
// For Singleton instantiation
@Volatile private var instance: RegisteredNoteDatabase? = null
fun getInstance(context: Context): RegisteredNoteDatabase {
return instance ?: synchronized(this) {
instance
?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): RegisteredNoteDatabase {
return Room.databaseBuilder(context, RegisteredNoteDatabase::class.java, DATABASE_REG)
.build()
}
}
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/transaction/RoomRegisteredTransactionDatabase.kt
================================================
package com.wiseassblog.data.transaction
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.wiseassblog.data.datamodels.RegisteredRoomTransaction
import com.wiseassblog.data.note.registered.RegisteredTransactionDao
private const val DATABASE_TRANSACTION = "transactions"
/**
* This database is used as a "cache" for registered users.
*/
@Database(entities = [RegisteredRoomTransaction::class],
version = 1,
exportSchema = false)
abstract class RoomRegisteredTransactionDatabase : RoomDatabase() {
abstract fun roomTransactionDao(): RegisteredTransactionDao
//code below courtesy of https://github.com/googlesamples/android-sunflower; it is open
//source just like this application.
companion object {
// For Singleton instantiation
@Volatile
private var instance: RoomRegisteredTransactionDatabase? = null
fun getInstance(context: Context): RoomRegisteredTransactionDatabase {
return instance
?: synchronized(this) {
instance
?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): RoomRegisteredTransactionDatabase {
return Room.databaseBuilder(context, RoomRegisteredTransactionDatabase::class.java, DATABASE_TRANSACTION)
.build()
}
}
}
================================================
FILE: data/src/main/java/com/wiseassblog/data/transaction/RoomTransactionRepositoryImpl.kt
================================================
package com.wiseassblog.data.transaction
import com.wiseassblog.data.note.registered.RegisteredTransactionDao
import com.wiseassblog.data.toNoteTransactionListFromRegistered
import com.wiseassblog.data.toRegisteredRoomTransaction
import com.wiseassblog.domain.domainmodel.NoteTransaction
import com.wiseassblog.domain.domainmodel.Result
import com.wiseassblog.domain.repository.ITransactionRepository
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.runBlocking
class RoomTransactionRepositoryImpl(val transactionDao: RegisteredTransactionDao) : ITransactionRepository {
override suspend fun getTransactions():
Result<Exception, List<NoteTransaction>> = runBlocking(IO) {
Result.build {
transactionDao.getTransactions().toNoteTransactionListFromRegistered()
}
}
override suspend fun deleteTransactions(): Result<Exception, Unit> = runBlocking(IO) {
Result.build {
transactionDao.deleteAll()
}
}
override suspend fun updateTransactions(transaction: NoteTransaction):
Result<Exception, Unit> = runBlocking(IO) {
Result.build {
transactionDao.insertOrUpdateTransaction(
transaction.toRegisteredRoomTransaction
).toUnit()
}
}
private fun Long.toUnit(): Unit = Unit
}
================================================
FILE: data/src/main/res/values/strings.xml
================================================
<resources>
<string name="app_name">data</string>
</resources>
================================================
FILE: data/src/test/java/com/wiseassblog/data/ExtTest.kt
================================================
package com.wiseassblog.data
import com.wiseassblog.data.datamodels.AnonymousRoomNote
import com.wiseassblog.data.datamodels.RegisteredRoomNote
import com.wiseassblog.domain.domainmodel.Note
import com.wiseassblog.domain.domainmodel.User
import org.junit.Test
import kotlin.test.assertTrue
class ExtTest{
fun getNote(creationDate: String = "28/10/2018",
contents: String = "When I understand that this glass is already broken, every moment with it becomes precious.",
upVotes: Int = 0,
imageUrl: String = "",
creator: User? = User(
"8675309",
"Ajahn Chah",
""
)
) = Note(
creationDate = creationDate,
contents = contents,
upVotes = upVotes,
imageUrl = imageUrl,
creator = creator
)
fun getAnonymousRoomNote(creationDate: String = "28/10/2018",
contents: String = "When I understand that this glass is already broken, every moment with it becomes precious.",
upVotes: Int = 0,
imageUrl: String = "",
creator: String = "8675309"
) = AnonymousRoomNote(
creationDate = creationDate,
contents = contents,
upVotes = upVotes,
imageUrl = imageUrl,
creatorId = creator
)
fun getRegisteredRoomNote(creationDate: String = "28/10/2018",
contents: String = "When I understand that this glass is already broken, every moment with it becomes precious.",
upVotes: Int = 0,
imageUrl: String = "",
creator: String = "8675309"
) = RegisteredRoomNote(
creationDate = creationDate,
contents = contents,
upVotes = upVotes,
imageUrl = imageUrl,
creatorId = creator
)
@Test
fun testExtensionFlatMap(){
val roomNoteList = listOf<AnonymousRoomNote>(getAnonymousRoomNote(), getAnonymousRoomNote(), getAnonymousRoomNote(contents = "third"))
val result = roomNoteList.toNoteListFromAnonymous()
assertTrue { result.contains(getAnonymousRoomNote().toNote) }
}
}
================================================
FILE: data/src/test/java/com/wiseassblog/data/RegisteredNoteRepositoryTest.kt
================================================
package com.wiseassblog.data
import com.wiseassblog.data.note.registered.RegisteredNoteRepositoryImpl
import com.wiseassblog.domain.DispatcherProvider
import com.wiseassblog.domain.domainmodel.*
import com.wiseassblog.domain.error.SpaceNotesError
import com.wiseassblog.domain.repository.ILocalNoteRepository
import com.wiseassblog.domain.repository.IRemoteNoteRepository
import io.mockk.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class RegisteredNoteRepositoryTest {
val dispatcher: DispatcherProvider = mockk()
val cache: ILocalNoteRepository = mockk()
val remote: IRemoteNoteRepository = mockk()
val repo = RegisteredNoteRepositoryImpl(remote, cache)
fun getNote(creationDate: String = "28/10/2018",
contents: String = "When I understand that this glass is already broken, every moment with it becomes precious.",
upVotes: Int = 0,
imageUrl: String = "",
creator: User? = User(
"8675309",
"Ajahn Chah",
""
)
) = Note(
creationDate = creationDate,
contents = contents,
upVotes = upVotes,
imageUrl = imageUrl,
creator = creator
)
fun getTransaction(
creationDate: String = "28/10/2018",
contents: String = "When I understand that this glass is already broken, every moment with it becomes precious.",
upVotes: Int = 0,
imageUrl: String = "",
creator: User? = User(
"8675309",
"Ajahn Chah",
""
),
transactionType: TransactionType = TransactionType.UPDATE
) = NoteTransaction(
creationDate = creationDate,
contents = contents,
upVotes = upVotes,
imageUrl = imageUrl,
creator = creator,
transactionType = transactionType
)
@BeforeEach
fun setUpRedundantMocks() {
clearAllMocks()
every { dispatcher.provideIOContext() } returns Dispatchers.Unconfined
}
/**
* On get notes, we first request Notes from the Remote. Data is returned either from the remote
* or local based on that result.
* a: Success
* b: Error
*
* a:
* 1. Request Data from Remote: Success
* 2. Update the Local repository
* 3. Return data from Remote
*/
@Test
fun `Get Notes Success`() = runBlocking {
val testList = listOf(getNote(), getNote(), getNote())
coEvery { remote.getNotes() } returns Result.build { testList }
coEvery { cache.updateAll(testList) } returns Result.build { Unit }
coEvery { cache.deleteAll() } returns Result.build { Unit }
val result = repo.getNotes()
coVerify { remote.getNotes() }
coVerify { cache.deleteAll() }
coVerify { cache.updateAll(testList) }
if (result is Result.Value) assertEquals(result.value, testList)
else assertTrue { false }
}
/**
* b:
* 1. Request Data from Remote: Error
* 2. Return Data from Local
*/
@Test
fun `Get Notes Fail`() = runBlocking {
val testNote = getNote()
val testList = listOf(getNote(), getNote(), getNote())
coEvery { remote.getNotes() } returns Result.build { throw SpaceNotesError.RemoteIOException }
coEvery { cache.getNotes() } returns Result.build { testList }
val result = repo.getNotes()
coVerify { remote.getNotes() }
coVerify { cache.getNotes() }
if (result is Result.Value) assertEquals(result.value, testList)
else assertTrue { false }
}
/**
* On get note, we first request Notes from the Remote. Data is returned either from the remote
* or local based on that result.
* a: Success
* b: Fail
*
* a:
* 1. Request Data from Remote: Success
* 2. Return data from Remote
*/
@Test
fun `Get Note Success`() = runBlocking {
val testNote = getNote()
coEvery { remote.getNote(testNote.creationDate) } returns Result.build { testNote }
val result = repo.getNote(testNote.creationDate)
coVerify { remote.getNote(testNote.creationDate) }
if (result is Result.Value) assertEquals(result.value, testNote)
else assertTrue { false }
}
/**
* b:
* 1. Request Data from Remote: Fail
* 2. Return Data from Local
*/
@Test
fun `Get Note Fail`() = runBlocking {
val testNote = getNote()
coEvery { remote.getNote(testNote.creationDate) } returns Result.build {
throw SpaceNotesError.RemoteIOException
}
coEvery { cache.getNote(testNote.creationDate) } returns Result.build { testNote }
val result = repo.getNote(testNote.creationDate)
coVerify { remote.getNote(testNote.creationDate) }
coVerify { cache.getNote(testNote.creationDate) }
if (result is Result.Value) assertEquals(result.value, testNote)
else assertTrue { false }
}
/**
* On delete note:
* a: Success
* b: Fail
*
* a:
* 1. Delete Data from Remote: Success
* 2. Return: Success
*/
@Test
fun `Delete Note Success`() = runBlocking {
val testNote = getNote()
coEvery { remote.deleteNote(testNote) } returns Result.build {
Unit
}
val result = repo.deleteNote(testNote)
coVerify { remote.deleteNote(testNote) }
assertTrue { result is Result.Value }
}
/**
* b:
* 1. Delete Data from Remote: Fail
* 2. Return: Error
*/
@Test
fun `Delete Note Fail`() = runBlocking {
val testNote = getNote()
coEvery { remote.deleteNote(testNote) } returns Result.build {
throw SpaceNotesError.RemoteIOException
}
val result = repo.deleteNote(testNote)
coVerify { remote.deleteNote(testNote) }
assertTrue { result is Result.Error }
}
/**
* On delete note:
* a: Success
* b: Fail
*
* a:
* 1. Update Data from Remote: Success
* 2. Return: Success
*/
@Test
fun `Update Note Success`() = runBlocking {
val testNote = getNote()
coEvery { remote.updateNote(testNote) } returns Result.build {
Unit
}
val result = repo.updateNote(testNote)
coVerify { remote.updateNote(testNote) }
assertTrue { result is Result.Value }
}
/**
* b:
* 1. Update Data from Remote: Fail
* 2. Return: Error
*/
@Test
fun `Update Note Fail`() = runBlocking {
val testNote = getNote()
coEvery { remote.updateNote(testNote) } returns Result.build {
throw SpaceNotesError.RemoteIOException
}
val result = repo.updateNote(testNote)
coVerify { remote.updateNote(testNote) }
assertTrue { result is Result.Error }
}
/**
* On Synchronize Transactions, we want to map transactions to Note objects, and push them
* all to the Remote Repo:
* a: Success
* b: Fail
*
* a:
*
gitextract_qf805k6a/ ├── .gitignore ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── wiseassblog/ │ │ │ └── spacenotes/ │ │ │ ├── SpaceNotes.kt │ │ │ ├── common/ │ │ │ │ ├── AndroidExt.kt │ │ │ │ ├── BaseLogic.kt │ │ │ │ ├── Constants.kt │ │ │ │ └── Navigation.kt │ │ │ ├── login/ │ │ │ │ ├── ILoginContract.kt │ │ │ │ ├── LoginActivity.kt │ │ │ │ ├── LoginLogic.kt │ │ │ │ ├── LoginResult.kt │ │ │ │ └── buildlogic/ │ │ │ │ └── LoginInjector.kt │ │ │ ├── notedetail/ │ │ │ │ ├── INoteDetailContract.kt │ │ │ │ ├── NoteDetailActivity.kt │ │ │ │ ├── NoteDetailLogic.kt │ │ │ │ ├── NoteDetailView.kt │ │ │ │ ├── NoteDetailViewModel.kt │ │ │ │ └── buildlogic/ │ │ │ │ └── NoteDetailInjector.kt │ │ │ └── notelist/ │ │ │ ├── INoteListContract.kt │ │ │ ├── NoteDiffUtilCallback.kt │ │ │ ├── NoteListActivity.kt │ │ │ ├── NoteListAdapter.kt │ │ │ ├── NoteListLogic.kt │ │ │ ├── NoteListView.kt │ │ │ ├── NoteListViewModel.kt │ │ │ └── buildlogic/ │ │ │ └── NoteListInjector.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── antenna_loop.xml │ │ │ ├── antenna_loop_fast.xml │ │ │ ├── ic_access_time_black_24dp.xml │ │ │ ├── ic_arrow_back_black_24dp.xml │ │ │ ├── ic_baseline_add_24px.xml │ │ │ ├── ic_baseline_event_24px.xml │ │ │ ├── ic_delete_forever_black_24dp.xml │ │ │ ├── ic_done_black_24dp.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_visibility_off_black_24dp.xml │ │ │ ├── ic_vpn_key_black_24dp.xml │ │ │ ├── satellite_beam.xml │ │ │ └── space_loop.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ ├── activity_login.xml │ │ │ ├── activity_note_detail.xml │ │ │ ├── activity_note_list.xml │ │ │ ├── activity_user_auth.xml │ │ │ ├── fragment_note_detail.xml │ │ │ ├── fragment_note_list.xml │ │ │ └── item_note.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── values/ │ │ ├── colors.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── view_styles.xml │ └── test/ │ └── java/ │ └── com/ │ └── wiseassblog/ │ └── spacenotes/ │ ├── LoginLogicTest.kt │ ├── NoteDetailLogicTest.kt │ └── NoteListLogicTest.kt ├── build.gradle ├── data/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── wiseassblog/ │ │ │ └── data/ │ │ │ ├── DataExt.kt │ │ │ ├── auth/ │ │ │ │ └── FirebaseAuthRepositoryImpl.kt │ │ │ ├── datamodels/ │ │ │ │ ├── AnonymousRoomNote.kt │ │ │ │ ├── FirebaseNote.kt │ │ │ │ ├── RegisteredRoomNote.kt │ │ │ │ └── RegisteredRoomTransaction.kt │ │ │ ├── note/ │ │ │ │ ├── anonymous/ │ │ │ │ │ ├── AnonymousNoteDao.kt │ │ │ │ │ ├── RoomAnonymousNoteDatabase.kt │ │ │ │ │ └── RoomLocalAnonymousRepositoryImpl.kt │ │ │ │ ├── public/ │ │ │ │ │ └── FirestorePublicNoteRepositoryImpl.kt │ │ │ │ └── registered/ │ │ │ │ ├── FirestorePrivateRemoteNoteImpl.kt │ │ │ │ ├── RegisteredNoteDao.kt │ │ │ │ ├── RegisteredNoteRepositoryImpl.kt │ │ │ │ ├── RegisteredTransactionDao.kt │ │ │ │ ├── RoomLocalCacheImpl.kt │ │ │ │ └── RoomRegisteredNoteDatabase.kt │ │ │ └── transaction/ │ │ │ ├── RoomRegisteredTransactionDatabase.kt │ │ │ └── RoomTransactionRepositoryImpl.kt │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ └── test/ │ └── java/ │ └── com/ │ └── wiseassblog/ │ └── data/ │ ├── ExtTest.kt │ └── RegisteredNoteRepositoryTest.kt ├── domain/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── wiseassblog/ │ │ └── domain/ │ │ ├── DispatcherProvider.kt │ │ ├── domainmodel/ │ │ │ ├── Note.kt │ │ │ ├── NoteTransaction.kt │ │ │ ├── Result.kt │ │ │ └── User.kt │ │ ├── error/ │ │ │ └── SpaceNotesError.kt │ │ ├── interactor/ │ │ │ ├── AnonymousNoteSource.kt │ │ │ ├── AuthSource.kt │ │ │ ├── PublicNoteSource.kt │ │ │ └── RegisteredNoteSource.kt │ │ ├── repository/ │ │ │ ├── IAuthRepository.kt │ │ │ ├── ILocalNoteRepository.kt │ │ │ ├── IPublicNoteRepository.kt │ │ │ ├── IRemoteNoteRepository.kt │ │ │ └── ITransactionRepository.kt │ │ └── servicelocator/ │ │ ├── NoteServiceLocator.kt │ │ └── UserServiceLocator.kt │ └── test/ │ └── java/ │ └── com/ │ └── wiseassblog/ │ └── domain/ │ ├── AnonymousNoteSourceTest.kt │ ├── PublicNoteSourceTest.kt │ └── RegisteredNoteSourceTest.kt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── versions.gradle
Condensed preview — 115 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (255K chars).
[
{
"path": ".gitignore",
"chars": 137,
"preview": "*.iml\n.gradle\n/local.properties\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n.DS_Store\n/build\n/captures\n.ext"
},
{
"path": "README.md",
"chars": 9547,
"preview": "### Please Note:\n- Since I see people are still looking at this repo, I want to be clear that I no longer recommend mult"
},
{
"path": "app/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "app/build.gradle",
"chars": 2067,
"preview": "apply plugin: 'com.android.application'\n\napply plugin: 'kotlin-android'\n\napply plugin: 'kotlin-android-extensions'\n\nappl"
},
{
"path": "app/proguard-rules.pro",
"chars": 751,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 910,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n package="
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/SpaceNotes.kt",
"chars": 167,
"preview": "package com.wiseassblog.spacenotes\n\nimport android.app.Application\n\n\nclass SpaceNotes: Application() {\n\n override fun"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/common/AndroidExt.kt",
"chars": 1536,
"preview": "package com.wiseassblog.spacenotes.common\n\nimport android.app.Activity\nimport android.content.Intent\n\nimport android.tex"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/common/BaseLogic.kt",
"chars": 381,
"preview": "package com.wiseassblog.spacenotes.common\n\nimport com.wiseassblog.domain.DispatcherProvider\nimport kotlinx.coroutines.Jo"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/common/Constants.kt",
"chars": 607,
"preview": "package com.wiseassblog.spacenotes.common\n\ninternal const val MESSAGE_DELETE_SUCCESSFUL = \"Note successfully deleted.\"\ni"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/common/Navigation.kt",
"chars": 994,
"preview": "package com.wiseassblog.spacenotes.common\n\nimport android.app.Activity\nimport android.content.Intent\nimport com.wiseassb"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/login/ILoginContract.kt",
"chars": 1509,
"preview": "package com.wiseassblog.spacenotes.login\n\nimport androidx.lifecycle.Observer\n\ninterface ILoginContract {\n\n interface "
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/login/LoginActivity.kt",
"chars": 4045,
"preview": "package com.wiseassblog.spacenotes.login\n\nimport android.content.Intent\nimport android.graphics.drawable.AnimationDrawab"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/login/LoginLogic.kt",
"chars": 3897,
"preview": "package com.wiseassblog.spacenotes.login\n\nimport androidx.lifecycle.Observer\nimport com.wiseassblog.domain.DispatcherPro"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/login/LoginResult.kt",
"chars": 322,
"preview": "package com.wiseassblog.spacenotes.login\n\nimport com.google.android.gms.auth.api.signin.GoogleSignInAccount\nimport com.g"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/login/buildlogic/LoginInjector.kt",
"chars": 1107,
"preview": "package com.wiseassblog.spacenotes.login.buildlogic\n\nimport android.app.Application\nimport androidx.lifecycle.AndroidVie"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notedetail/INoteDetailContract.kt",
"chars": 1254,
"preview": "package com.wiseassblog.spacenotes.notedetail\n\nimport androidx.lifecycle.Observer\nimport com.wiseassblog.domain.domainmo"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailActivity.kt",
"chars": 1681,
"preview": "package com.wiseassblog.spacenotes.notedetail\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.wid"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailLogic.kt",
"chars": 8306,
"preview": "package com.wiseassblog.spacenotes.notedetail\n\nimport androidx.lifecycle.Observer\nimport com.wiseassblog.domain.Dispatch"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailView.kt",
"chars": 3504,
"preview": "package com.wiseassblog.spacenotes.notedetail\n\n\nimport android.graphics.drawable.AnimationDrawable\nimport android.os.Bun"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notedetail/NoteDetailViewModel.kt",
"chars": 1164,
"preview": "package com.wiseassblog.spacenotes.notedetail\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveD"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notedetail/buildlogic/NoteDetailInjector.kt",
"chars": 3781,
"preview": "package com.wiseassblog.spacenotes.notedetail.buildlogic\n\nimport android.app.Application\nimport androidx.lifecycle.Andro"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notelist/INoteListContract.kt",
"chars": 1522,
"preview": "package com.wiseassblog.spacenotes.notelist\n\nimport androidx.lifecycle.Observer\nimport androidx.recyclerview.widget.List"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteDiffUtilCallback.kt",
"chars": 485,
"preview": "package com.wiseassblog.spacenotes.notelist\n\n\nimport androidx.recyclerview.widget.DiffUtil\nimport com.wiseassblog.domain"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListActivity.kt",
"chars": 994,
"preview": "package com.wiseassblog.spacenotes.notelist\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nim"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListAdapter.kt",
"chars": 2038,
"preview": "package com.wiseassblog.spacenotes.notelist\n\n\nimport android.view.LayoutInflater\nimport android.view.View\nimport android"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListLogic.kt",
"chars": 5089,
"preview": "package com.wiseassblog.spacenotes.notelist\n\nimport androidx.lifecycle.Observer\nimport com.wiseassblog.domain.Dispatcher"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListView.kt",
"chars": 4132,
"preview": "package com.wiseassblog.spacenotes.notelist\n\n\nimport android.graphics.drawable.AnimationDrawable\nimport android.os.Bundl"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notelist/NoteListViewModel.kt",
"chars": 1442,
"preview": "package com.wiseassblog.spacenotes.notelist\n\n\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.ViewMo"
},
{
"path": "app/src/main/java/com/wiseassblog/spacenotes/notelist/buildlogic/NoteListInjector.kt",
"chars": 3848,
"preview": "package com.wiseassblog.spacenotes.notelist.buildlogic\n\nimport android.app.Application\nimport androidx.lifecycle.Android"
},
{
"path": "app/src/main/res/drawable/antenna_loop.xml",
"chars": 404,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animation-list xmlns:android=\"http://schemas.android.com/apk/res/android\"\n an"
},
{
"path": "app/src/main/res/drawable/antenna_loop_fast.xml",
"chars": 401,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animation-list xmlns:android=\"http://schemas.android.com/apk/res/android\"\n an"
},
{
"path": "app/src/main/res/drawable/ic_access_time_black_24dp.xml",
"chars": 490,
"preview": "<vector android:alpha=\"0.86\" android:height=\"24dp\"\n android:tint=\"#FFFFFF\" android:viewportHeight=\"24.0\"\n android:"
},
{
"path": "app/src/main/res/drawable/ic_arrow_back_black_24dp.xml",
"chars": 356,
"preview": "<vector android:alpha=\"0.86\" android:height=\"24dp\"\n android:tint=\"#FFFFFF\" android:viewportHeight=\"24.0\"\n android:"
},
{
"path": "app/src/main/res/drawable/ic_baseline_add_24px.xml",
"chars": 302,
"preview": "<vector android:alpha=\"0.86\" android:height=\"24dp\"\n android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n androi"
},
{
"path": "app/src/main/res/drawable/ic_baseline_event_24px.xml",
"chars": 438,
"preview": "<vector android:alpha=\"0.86\" android:height=\"24dp\"\n android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n androi"
},
{
"path": "app/src/main/res/drawable/ic_delete_forever_black_24dp.xml",
"chars": 520,
"preview": "<vector android:alpha=\"0.86\" android:height=\"24dp\"\n android:tint=\"#FFFFFF\" android:viewportHeight=\"24.0\"\n android:"
},
{
"path": "app/src/main/res/drawable/ic_done_black_24dp.xml",
"chars": 345,
"preview": "<vector android:alpha=\"0.86\" android:height=\"24dp\"\n android:tint=\"#FFFFFF\" android:viewportHeight=\"24.0\"\n android:"
},
{
"path": "app/src/main/res/drawable/ic_launcher_background.xml",
"chars": 5606,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:wi"
},
{
"path": "app/src/main/res/drawable/ic_visibility_off_black_24dp.xml",
"chars": 886,
"preview": "<vector android:alpha=\"0.86\" android:height=\"24dp\"\n android:tint=\"#FFFFFF\" android:viewportHeight=\"24.0\"\n android:"
},
{
"path": "app/src/main/res/drawable/ic_vpn_key_black_24dp.xml",
"chars": 466,
"preview": "<vector android:alpha=\"0.86\" android:height=\"24dp\"\n android:tint=\"#FFFFFF\" android:viewportHeight=\"24.0\"\n android:"
},
{
"path": "app/src/main/res/drawable/satellite_beam.xml",
"chars": 392,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animation-list xmlns:android=\"http://schemas.android.com/apk/res/android\"\n an"
},
{
"path": "app/src/main/res/drawable/space_loop.xml",
"chars": 405,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animation-list xmlns:android=\"http://schemas.android.com/apk/res/android\"\n an"
},
{
"path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
"chars": 1880,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:aapt=\"http://schemas.android.com/aapt\"\n "
},
{
"path": "app/src/main/res/layout/activity_login.xml",
"chars": 3402,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/activity_note_detail.xml",
"chars": 462,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schema"
},
{
"path": "app/src/main/res/layout/activity_note_list.xml",
"chars": 416,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns"
},
{
"path": "app/src/main/res/layout/activity_user_auth.xml",
"chars": 465,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/fragment_note_detail.xml",
"chars": 4639,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/fragment_note_list.xml",
"chars": 4259,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/item_note.xml",
"chars": 3391,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout\n xmlns:android=\"http://sche"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 272,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 272,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 576,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"colorPrimary\">#121212</color>\n <color name=\"color"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 73,
"preview": "<resources>\n <string name=\"app_name\">SpaceNotes</string>\n</resources>\n"
},
{
"path": "app/src/main/res/values/styles.xml",
"chars": 458,
"preview": "<resources>\n\n <!-- Base application theme. -->\n <style name=\"AppTheme\" parent=\"Theme.AppCompat.NoActionBar\">\n "
},
{
"path": "app/src/main/res/values/view_styles.xml",
"chars": 1048,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n <style name=\"Text\">\n <item name=\"android:fontFamily\">sans"
},
{
"path": "app/src/test/java/com/wiseassblog/spacenotes/LoginLogicTest.kt",
"chars": 9426,
"preview": "package com.wiseassblog.spacenotes\n\nimport com.google.android.gms.auth.api.signin.GoogleSignInAccount\nimport com.wiseass"
},
{
"path": "app/src/test/java/com/wiseassblog/spacenotes/NoteDetailLogicTest.kt",
"chars": 18876,
"preview": "package com.wiseassblog.spacenotes\n\nimport com.wiseassblog.domain.DispatcherProvider\nimport com.wiseassblog.domain.domai"
},
{
"path": "app/src/test/java/com/wiseassblog/spacenotes/NoteListLogicTest.kt",
"chars": 15473,
"preview": "package com.wiseassblog.spacenotes\n\nimport com.wiseassblog.domain.DispatcherProvider\nimport com.wiseassblog.domain.servi"
},
{
"path": "build.gradle",
"chars": 598,
"preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n a"
},
{
"path": "data/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "data/build.gradle",
"chars": 1697,
"preview": "apply plugin: 'com.android.library'\n\napply plugin: 'kotlin-android'\n\napply plugin: 'kotlin-android-extensions'\n\napply pl"
},
{
"path": "data/proguard-rules.pro",
"chars": 751,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "data/src/main/AndroidManifest.xml",
"chars": 107,
"preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n package=\"com.wiseassblog.data\" />\n"
},
{
"path": "data/src/main/java/com/wiseassblog/data/DataExt.kt",
"chars": 4542,
"preview": "package com.wiseassblog.data\n\nimport android.net.Uri\nimport com.google.android.gms.tasks.Task\nimport com.wiseassblog.dat"
},
{
"path": "data/src/main/java/com/wiseassblog/data/auth/FirebaseAuthRepositoryImpl.kt",
"chars": 2045,
"preview": "package com.wiseassblog.data.auth\n\nimport com.google.android.gms.tasks.Tasks\nimport com.google.firebase.auth.FirebaseAut"
},
{
"path": "data/src/main/java/com/wiseassblog/data/datamodels/AnonymousRoomNote.kt",
"chars": 828,
"preview": "package com.wiseassblog.data.datamodels\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.roo"
},
{
"path": "data/src/main/java/com/wiseassblog/data/datamodels/FirebaseNote.kt",
"chars": 347,
"preview": "package com.wiseassblog.data.datamodels\n\n//var and default arguments used due to firestore requiring a no argument const"
},
{
"path": "data/src/main/java/com/wiseassblog/data/datamodels/RegisteredRoomNote.kt",
"chars": 830,
"preview": "package com.wiseassblog.data.datamodels\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.roo"
},
{
"path": "data/src/main/java/com/wiseassblog/data/datamodels/RegisteredRoomTransaction.kt",
"chars": 918,
"preview": "package com.wiseassblog.data.datamodels\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.roo"
},
{
"path": "data/src/main/java/com/wiseassblog/data/note/anonymous/AnonymousNoteDao.kt",
"chars": 770,
"preview": "package com.wiseassblog.data.note.anonymous\n\nimport androidx.room.*\nimport com.wiseassblog.data.datamodels.AnonymousRoom"
},
{
"path": "data/src/main/java/com/wiseassblog/data/note/anonymous/RoomAnonymousNoteDatabase.kt",
"chars": 1212,
"preview": "package com.wiseassblog.data.note.anonymous\n\nimport android.content.Context\nimport androidx.room.Database\nimport android"
},
{
"path": "data/src/main/java/com/wiseassblog/data/note/anonymous/RoomLocalAnonymousRepositoryImpl.kt",
"chars": 1809,
"preview": "package com.wiseassblog.data.note.anonymous\n\nimport com.wiseassblog.data.toAnonymousRoomNote\nimport com.wiseassblog.data"
},
{
"path": "data/src/main/java/com/wiseassblog/data/note/public/FirestorePublicNoteRepositoryImpl.kt",
"chars": 2837,
"preview": "package com.wiseassblog.data.note.public\n\nimport com.google.firebase.firestore.FirebaseFirestore\nimport com.google.fireb"
},
{
"path": "data/src/main/java/com/wiseassblog/data/note/registered/FirestorePrivateRemoteNoteImpl.kt",
"chars": 2942,
"preview": "package com.wiseassblog.data.note.registered\n\nimport com.google.firebase.firestore.FirebaseFirestore\nimport com.google.f"
},
{
"path": "data/src/main/java/com/wiseassblog/data/note/registered/RegisteredNoteDao.kt",
"chars": 762,
"preview": "package com.wiseassblog.data.note.registered\n\nimport androidx.room.*\nimport com.wiseassblog.data.datamodels.RegisteredRo"
},
{
"path": "data/src/main/java/com/wiseassblog/data/note/registered/RegisteredNoteRepositoryImpl.kt",
"chars": 2754,
"preview": "package com.wiseassblog.data.note.registered\n\nimport com.wiseassblog.data.toNote\nimport com.wiseassblog.domain.domainmod"
},
{
"path": "data/src/main/java/com/wiseassblog/data/note/registered/RegisteredTransactionDao.kt",
"chars": 675,
"preview": "package com.wiseassblog.data.note.registered\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room."
},
{
"path": "data/src/main/java/com/wiseassblog/data/note/registered/RoomLocalCacheImpl.kt",
"chars": 1730,
"preview": "package com.wiseassblog.data.note.registered\n\nimport com.wiseassblog.data.toNote\nimport com.wiseassblog.data.toNoteListF"
},
{
"path": "data/src/main/java/com/wiseassblog/data/note/registered/RoomRegisteredNoteDatabase.kt",
"chars": 1288,
"preview": "package com.wiseassblog.data.note.registered\n\nimport android.content.Context\nimport androidx.room.Database\nimport androi"
},
{
"path": "data/src/main/java/com/wiseassblog/data/transaction/RoomRegisteredTransactionDatabase.kt",
"chars": 1479,
"preview": "package com.wiseassblog.data.transaction\n\nimport android.content.Context\nimport androidx.room.Database\nimport androidx.r"
},
{
"path": "data/src/main/java/com/wiseassblog/data/transaction/RoomTransactionRepositoryImpl.kt",
"chars": 1353,
"preview": "package com.wiseassblog.data.transaction\n\nimport com.wiseassblog.data.note.registered.RegisteredTransactionDao\nimport co"
},
{
"path": "data/src/main/res/values/strings.xml",
"chars": 67,
"preview": "<resources>\n <string name=\"app_name\">data</string>\n</resources>\n"
},
{
"path": "data/src/test/java/com/wiseassblog/data/ExtTest.kt",
"chars": 2384,
"preview": "package com.wiseassblog.data\n\nimport com.wiseassblog.data.datamodels.AnonymousRoomNote\nimport com.wiseassblog.data.datam"
},
{
"path": "data/src/test/java/com/wiseassblog/data/RegisteredNoteRepositoryTest.kt",
"chars": 9339,
"preview": "package com.wiseassblog.data\n\nimport com.wiseassblog.data.note.registered.RegisteredNoteRepositoryImpl\nimport com.wiseas"
},
{
"path": "domain/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "domain/build.gradle",
"chars": 299,
"preview": "apply plugin: 'java-library'\napply plugin: 'kotlin'\n\ndependencies {\n implementation deps.kotlin.coroutines_core\n\n "
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/DispatcherProvider.kt",
"chars": 311,
"preview": "package com.wiseassblog.domain\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlin.coroutines.CoroutineContext\n\nobject "
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/domainmodel/Note.kt",
"chars": 326,
"preview": "package com.wiseassblog.domain.domainmodel\n\n\ndata class Note(val creationDate:String,\n val contents:Strin"
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/domainmodel/NoteTransaction.kt",
"chars": 552,
"preview": "package com.wiseassblog.domain.domainmodel\n\ndata class NoteTransaction(\n val creationDate:String,\n val con"
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/domainmodel/Result.kt",
"chars": 831,
"preview": "package com.wiseassblog.domain.domainmodel\n\n/**\n * Result Wrapper <Left = Exception, Right = Value/Success>\n */\nsealed c"
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/domainmodel/User.kt",
"chars": 177,
"preview": "package com.wiseassblog.domain.domainmodel\n\ndata class User(val uid: String,\n val name: String = \"\",\n "
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/error/SpaceNotesError.kt",
"chars": 494,
"preview": "package com.wiseassblog.domain.error\n\nimport java.lang.Exception\n\nsealed class SpaceNotesError: Exception() {\n\n objec"
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/interactor/AnonymousNoteSource.kt",
"chars": 789,
"preview": "package com.wiseassblog.domain.interactor\n\nimport com.wiseassblog.domain.servicelocator.NoteServiceLocator\nimport com.wi"
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/interactor/AuthSource.kt",
"chars": 831,
"preview": "package com.wiseassblog.domain.interactor\n\nimport com.wiseassblog.domain.servicelocator.UserServiceLocator\nimport com.wi"
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/interactor/PublicNoteSource.kt",
"chars": 937,
"preview": "package com.wiseassblog.domain.interactor\n\nimport com.wiseassblog.domain.servicelocator.NoteServiceLocator\nimport com.wi"
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/interactor/RegisteredNoteSource.kt",
"chars": 2580,
"preview": "package com.wiseassblog.domain.interactor\n\nimport com.wiseassblog.domain.servicelocator.NoteServiceLocator\nimport com.wi"
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/repository/IAuthRepository.kt",
"chars": 596,
"preview": "package com.wiseassblog.domain.repository\n\nimport com.wiseassblog.domain.domainmodel.Result\nimport com.wiseassblog.domai"
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/repository/ILocalNoteRepository.kt",
"chars": 549,
"preview": "package com.wiseassblog.domain.repository\n\nimport com.wiseassblog.domain.domainmodel.Note\nimport com.wiseassblog.domain."
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/repository/IPublicNoteRepository.kt",
"chars": 525,
"preview": "package com.wiseassblog.domain.repository\n\nimport com.wiseassblog.domain.domainmodel.Note\nimport com.wiseassblog.domain."
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/repository/IRemoteNoteRepository.kt",
"chars": 628,
"preview": "package com.wiseassblog.domain.repository\n\nimport com.wiseassblog.domain.domainmodel.Note\nimport com.wiseassblog.domain."
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/repository/ITransactionRepository.kt",
"chars": 415,
"preview": "package com.wiseassblog.domain.repository\n\nimport com.wiseassblog.domain.domainmodel.Result\nimport com.wiseassblog.domai"
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/servicelocator/NoteServiceLocator.kt",
"chars": 559,
"preview": "package com.wiseassblog.domain.servicelocator\n\nimport com.wiseassblog.domain.repository.ILocalNoteRepository\nimport com."
},
{
"path": "domain/src/main/java/com/wiseassblog/domain/servicelocator/UserServiceLocator.kt",
"chars": 166,
"preview": "package com.wiseassblog.domain.servicelocator\n\nimport com.wiseassblog.domain.repository.IAuthRepository\n\nclass UserServi"
},
{
"path": "domain/src/test/java/com/wiseassblog/domain/AnonymousNoteSourceTest.kt",
"chars": 7825,
"preview": "package com.wiseassblog.domain\n\nimport com.wiseassblog.domain.domainmodel.Note\nimport com.wiseassblog.domain.domainmodel"
},
{
"path": "domain/src/test/java/com/wiseassblog/domain/PublicNoteSourceTest.kt",
"chars": 3528,
"preview": "package com.wiseassblog.domain\n\nimport com.wiseassblog.domain.domainmodel.*\nimport com.wiseassblog.domain.error.SpaceNot"
},
{
"path": "domain/src/test/java/com/wiseassblog/domain/RegisteredNoteSourceTest.kt",
"chars": 13041,
"preview": "package com.wiseassblog.domain\n\nimport com.wiseassblog.domain.domainmodel.*\nimport com.wiseassblog.domain.error.SpaceNot"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 231,
"preview": "#Wed Oct 24 14:24:13 PDT 2018\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
},
{
"path": "gradle.properties",
"chars": 782,
"preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
},
{
"path": "gradlew",
"chars": 5296,
"preview": "#!/usr/bin/env sh\n\n##############################################################################\n##\n## Gradle start up"
},
{
"path": "gradlew.bat",
"chars": 2176,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem "
},
{
"path": "settings.gradle",
"chars": 35,
"preview": "include ':app', ':data', ':domain'\n"
},
{
"path": "versions.gradle",
"chars": 3913,
"preview": "/**\n *Source largely taken from this OS repo: https://github.com/googlesamples/android-architecture-components/blob/mast"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the BracketCove/SpaceNotes GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 115 files (230.6 KB), approximately 58.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.