Full Code of BracketCove/SpaceNotes for AI

master d799560fbb94 cached
115 files
230.6 KB
58.3k tokens
1 requests
Download .txt
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:
     * 
Download .txt
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.

Copied to clipboard!