mirror of
https://github.com/afollestad/nock-nock.git
synced 2025-04-19 19:15:23 +00:00
Switch from SQLite to Room
This commit is contained in:
parent
cad589eebc
commit
88ae30c0c9
61 changed files with 2066 additions and 928 deletions
|
@ -18,9 +18,9 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':data')
|
||||
implementation project(':utilities')
|
||||
implementation project(':engine')
|
||||
implementation project(':data')
|
||||
implementation project(':notifications')
|
||||
implementation project(':viewcomponents')
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
android:windowSoftInputMode="stateHidden"/>
|
||||
|
||||
<service
|
||||
android:name=".engine.statuscheck.CheckStatusJob"
|
||||
android:name=".engine.statuscheck.ValidationJob"
|
||||
android:label="@string/check_service_name"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import android.app.Application
|
|||
import com.afollestad.nocknock.di.AppComponent
|
||||
import com.afollestad.nocknock.di.DaggerAppComponent
|
||||
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
|
@ -82,7 +82,7 @@ class NockNockApp : Application(), Injector {
|
|||
is MainActivity -> appComponent.inject(target)
|
||||
is ViewSiteActivity -> appComponent.inject(target)
|
||||
is AddSiteActivity -> appComponent.inject(target)
|
||||
is CheckStatusJob -> appComponent.inject(target)
|
||||
is ValidationJob -> appComponent.inject(target)
|
||||
is BootReceiver -> appComponent.inject(target)
|
||||
else -> throw IllegalStateException("Can't inject into $target")
|
||||
}
|
||||
|
|
|
@ -20,9 +20,10 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.isPending
|
||||
import com.afollestad.nocknock.data.textRes
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.isPending
|
||||
import com.afollestad.nocknock.data.model.textRes
|
||||
import com.afollestad.nocknock.utilities.ui.onDebouncedClick
|
||||
import kotlinx.android.synthetic.main.list_item_server.view.iconStatus
|
||||
import kotlinx.android.synthetic.main.list_item_server.view.textInterval
|
||||
|
@ -30,10 +31,10 @@ import kotlinx.android.synthetic.main.list_item_server.view.textName
|
|||
import kotlinx.android.synthetic.main.list_item_server.view.textStatus
|
||||
import kotlinx.android.synthetic.main.list_item_server.view.textUrl
|
||||
|
||||
typealias Listener = (model: ServerModel, longClick: Boolean) -> Unit
|
||||
typealias Listener = (model: Site, longClick: Boolean) -> Unit
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class ServerVH constructor(
|
||||
class SiteViewHolder constructor(
|
||||
itemView: View,
|
||||
private val adapter: ServerAdapter
|
||||
) : RecyclerView.ViewHolder(itemView), View.OnLongClickListener {
|
||||
|
@ -45,24 +46,32 @@ class ServerVH constructor(
|
|||
itemView.setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
fun bind(model: ServerModel) {
|
||||
fun bind(model: Site) {
|
||||
requireNotNull(model.settings) { "Settings must be populated." }
|
||||
|
||||
itemView.textName.text = model.name
|
||||
itemView.textUrl.text = model.url
|
||||
itemView.iconStatus.setStatus(model.status)
|
||||
|
||||
val statusText = model.status.textRes()
|
||||
if (statusText == 0) {
|
||||
itemView.textStatus.text = model.reason
|
||||
val lastResult = model.lastResult
|
||||
if (lastResult != null) {
|
||||
itemView.iconStatus.setStatus(lastResult.status)
|
||||
val statusText = lastResult.status.textRes()
|
||||
if (statusText == 0) {
|
||||
itemView.textStatus.text = lastResult.reason
|
||||
} else {
|
||||
itemView.textStatus.setText(statusText)
|
||||
}
|
||||
} else {
|
||||
itemView.textStatus.setText(statusText)
|
||||
itemView.iconStatus.setStatus(WAITING)
|
||||
itemView.textStatus.setText(R.string.none)
|
||||
}
|
||||
|
||||
val res = itemView.resources
|
||||
when {
|
||||
model.disabled -> {
|
||||
model.settings?.disabled == true -> {
|
||||
itemView.textInterval.setText(R.string.checks_disabled)
|
||||
}
|
||||
model.status.isPending() -> {
|
||||
model.lastResult?.status.isPending() -> {
|
||||
itemView.textInterval.text = res.getString(
|
||||
R.string.next_check_x,
|
||||
res.getString(R.string.now)
|
||||
|
@ -84,21 +93,21 @@ class ServerVH constructor(
|
|||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<ServerVH>() {
|
||||
class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<SiteViewHolder>() {
|
||||
|
||||
private val models = mutableListOf<ServerModel>()
|
||||
private val models = mutableListOf<Site>()
|
||||
|
||||
internal fun performClick(
|
||||
index: Int,
|
||||
longClick: Boolean
|
||||
) = listener.invoke(models[index], longClick)
|
||||
|
||||
fun add(model: ServerModel) {
|
||||
fun add(model: Site) {
|
||||
models.add(model)
|
||||
notifyItemInserted(models.size - 1)
|
||||
}
|
||||
|
||||
fun update(target: ServerModel) {
|
||||
fun update(target: Site) {
|
||||
for ((i, model) in models.withIndex()) {
|
||||
if (model.id == target.id) {
|
||||
update(i, target)
|
||||
|
@ -109,7 +118,7 @@ class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<Serve
|
|||
|
||||
private fun update(
|
||||
index: Int,
|
||||
model: ServerModel
|
||||
model: Site
|
||||
) {
|
||||
models[index] = model
|
||||
notifyItemChanged(index)
|
||||
|
@ -120,7 +129,7 @@ class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<Serve
|
|||
notifyItemRemoved(index)
|
||||
}
|
||||
|
||||
fun remove(target: ServerModel) {
|
||||
fun remove(target: Site) {
|
||||
for ((i, model) in models.withIndex()) {
|
||||
if (model.id == target.id) {
|
||||
remove(i)
|
||||
|
@ -129,7 +138,7 @@ class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<Serve
|
|||
}
|
||||
}
|
||||
|
||||
fun set(newModels: List<ServerModel>) {
|
||||
fun set(newModels: List<Site>) {
|
||||
this.models.clear()
|
||||
if (!newModels.isEmpty()) {
|
||||
this.models.addAll(newModels)
|
||||
|
@ -140,14 +149,14 @@ class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<Serve
|
|||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): ServerVH {
|
||||
): SiteViewHolder {
|
||||
val v = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.list_item_server, parent, false)
|
||||
return ServerVH(v, this)
|
||||
return SiteViewHolder(v, this)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ServerVH,
|
||||
holder: SiteViewHolder,
|
||||
position: Int
|
||||
) {
|
||||
val model = models[position]
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.di;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.job.JobScheduler;
|
||||
import com.afollestad.nocknock.NockNockApp;
|
||||
import com.afollestad.nocknock.engine.EngineModule;
|
||||
import com.afollestad.nocknock.engine.statuscheck.BootReceiver;
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob;
|
||||
import com.afollestad.nocknock.notifications.NotificationsModule;
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity;
|
||||
import com.afollestad.nocknock.ui.main.MainActivity;
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity;
|
||||
import com.afollestad.nocknock.utilities.UtilitiesModule;
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Component;
|
||||
import javax.inject.Singleton;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Singleton
|
||||
@Component(
|
||||
modules = {MainModule.class, EngineModule.class, NotificationsModule.class, UtilitiesModule.class}
|
||||
)
|
||||
public interface AppComponent {
|
||||
|
||||
void inject(NockNockApp app);
|
||||
|
||||
void inject(MainActivity activity);
|
||||
|
||||
void inject(ViewSiteActivity activity);
|
||||
|
||||
void inject(AddSiteActivity activity);
|
||||
|
||||
void inject(ValidationJob job);
|
||||
|
||||
void inject(BootReceiver bootReceiver);
|
||||
|
||||
@Component.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
Builder application(Application application);
|
||||
|
||||
@BindsInstance
|
||||
Builder okHttpClient(OkHttpClient okHttpClient);
|
||||
|
||||
@BindsInstance
|
||||
Builder jobScheduler(JobScheduler jobScheduler);
|
||||
|
||||
@BindsInstance
|
||||
Builder notificationManager(NotificationManager notificationManager);
|
||||
|
||||
AppComponent build();
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.di
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.app.job.JobScheduler
|
||||
import com.afollestad.nocknock.NockNockApp
|
||||
import com.afollestad.nocknock.engine.EngineModule
|
||||
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
||||
import com.afollestad.nocknock.notifications.NotificationsModule
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
||||
import com.afollestad.nocknock.utilities.UtilitiesModule
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Singleton
|
||||
@Component(
|
||||
modules = [
|
||||
MainModule::class,
|
||||
MainBindModule::class,
|
||||
EngineModule::class,
|
||||
NotificationsModule::class,
|
||||
UtilitiesModule::class
|
||||
]
|
||||
)
|
||||
interface AppComponent {
|
||||
|
||||
fun inject(app: NockNockApp)
|
||||
|
||||
fun inject(activity: MainActivity)
|
||||
|
||||
fun inject(activity: ViewSiteActivity)
|
||||
|
||||
fun inject(activity: AddSiteActivity)
|
||||
|
||||
fun inject(job: CheckStatusJob)
|
||||
|
||||
fun inject(bootReceiver: BootReceiver)
|
||||
|
||||
@Component.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance fun application(application: Application): Builder
|
||||
|
||||
@BindsInstance fun okHttpClient(okHttpClient: OkHttpClient): Builder
|
||||
|
||||
@BindsInstance fun jobScheduler(jobScheduler: JobScheduler): Builder
|
||||
|
||||
@BindsInstance fun notificationManager(notificationManager: NotificationManager): Builder
|
||||
|
||||
fun build(): AppComponent
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.di
|
||||
|
||||
import com.afollestad.nocknock.ui.addsite.AddSitePresenter
|
||||
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter
|
||||
import com.afollestad.nocknock.ui.main.MainPresenter
|
||||
import com.afollestad.nocknock.ui.main.RealMainPresenter
|
||||
import com.afollestad.nocknock.ui.viewsite.RealViewSitePresenter
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSitePresenter
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Module
|
||||
abstract class MainBindModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideMainPresenter(
|
||||
presenter: RealMainPresenter
|
||||
): MainPresenter
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideAddSitePresenter(
|
||||
presenter: RealAddSitePresenter
|
||||
): AddSitePresenter
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideViewSitePresenter(
|
||||
presenter: RealViewSitePresenter
|
||||
): ViewSitePresenter
|
||||
}
|
74
app/src/main/java/com/afollestad/nocknock/di/MainModule.java
Normal file
74
app/src/main/java/com/afollestad/nocknock/di/MainModule.java
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.di;
|
||||
|
||||
import static androidx.room.Room.databaseBuilder;
|
||||
|
||||
import android.app.Application;
|
||||
import com.afollestad.nocknock.R;
|
||||
import com.afollestad.nocknock.data.AppDatabase;
|
||||
import com.afollestad.nocknock.ui.addsite.AddSitePresenter;
|
||||
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter;
|
||||
import com.afollestad.nocknock.ui.main.MainActivity;
|
||||
import com.afollestad.nocknock.ui.main.MainPresenter;
|
||||
import com.afollestad.nocknock.ui.main.RealMainPresenter;
|
||||
import com.afollestad.nocknock.ui.viewsite.RealViewSitePresenter;
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSitePresenter;
|
||||
import com.afollestad.nocknock.utilities.qualifiers.AppIconRes;
|
||||
import com.afollestad.nocknock.utilities.qualifiers.MainActivityClass;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Module
|
||||
abstract class MainModule {
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private static String DATABASE_NAME = "NockNock.db";
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract MainPresenter provideMainPresenter(RealMainPresenter presenter);
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract AddSitePresenter provideAddSitePresenter(RealAddSitePresenter presenter);
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract ViewSitePresenter provideViewSitePresenter(RealViewSitePresenter presenter);
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@AppIconRes
|
||||
static int provideAppIconRes() {
|
||||
return R.mipmap.ic_launcher;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@MainActivityClass
|
||||
static Class<?> provideMainActivityClass() {
|
||||
return MainActivity.class;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static AppDatabase provideAppDatabase(Application app) {
|
||||
return databaseBuilder(app, AppDatabase.class, DATABASE_NAME).build();
|
||||
}
|
||||
}
|
|
@ -20,11 +20,11 @@ import android.os.Bundle
|
|||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ValidationMode
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.indexToValidationMode
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.model.indexToValidationMode
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
||||
|
@ -119,7 +119,7 @@ class AddSiteActivity : AppCompatActivity(), AddSiteView {
|
|||
url = inputUrl.trimmedText(),
|
||||
checkInterval = checkInterval,
|
||||
validationMode = validationMode,
|
||||
validationContent = validationMode.validationContent(),
|
||||
validationArgs = validationMode.validationContent(),
|
||||
networkTimeout = responseTimeoutInput.textAsInt(defaultValue = defaultTimeout)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -17,13 +17,14 @@ package com.afollestad.nocknock.ui.addsite
|
|||
|
||||
import androidx.annotation.CheckResult
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
import com.afollestad.nocknock.data.ValidationMode
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.putSite
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationManager
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
|
@ -63,7 +64,7 @@ interface AddSitePresenter {
|
|||
url: String,
|
||||
checkInterval: Long,
|
||||
validationMode: ValidationMode,
|
||||
validationContent: String?,
|
||||
validationArgs: String?,
|
||||
networkTimeout: Int
|
||||
)
|
||||
|
||||
|
@ -72,8 +73,8 @@ interface AddSitePresenter {
|
|||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class RealAddSitePresenter @Inject constructor(
|
||||
private val serverModelStore: ServerModelStore,
|
||||
private val checkStatusManager: CheckStatusManager
|
||||
private val database: AppDatabase,
|
||||
private val checkStatusManager: ValidationManager
|
||||
) : AddSitePresenter {
|
||||
|
||||
private var view: AddSiteView? = null
|
||||
|
@ -118,7 +119,7 @@ class RealAddSitePresenter @Inject constructor(
|
|||
url: String,
|
||||
checkInterval: Long,
|
||||
validationMode: ValidationMode,
|
||||
validationContent: String?,
|
||||
validationArgs: String?,
|
||||
networkTimeout: Int
|
||||
) {
|
||||
val inputErrors = InputErrors()
|
||||
|
@ -134,9 +135,9 @@ class RealAddSitePresenter @Inject constructor(
|
|||
if (checkInterval <= 0) {
|
||||
inputErrors.checkInterval = R.string.please_enter_check_interval
|
||||
}
|
||||
if (validationMode == TERM_SEARCH && validationContent.isNullOrEmpty()) {
|
||||
if (validationMode == TERM_SEARCH && validationArgs.isNullOrEmpty()) {
|
||||
inputErrors.termSearch = R.string.please_enter_search_term
|
||||
} else if (validationMode == JAVASCRIPT && validationContent.isNullOrEmpty()) {
|
||||
} else if (validationMode == JAVASCRIPT && validationArgs.isNullOrEmpty()) {
|
||||
inputErrors.javaScript = R.string.please_enter_javaScript
|
||||
}
|
||||
if (networkTimeout <= 0) {
|
||||
|
@ -148,14 +149,19 @@ class RealAddSitePresenter @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
val newModel = ServerModel(
|
||||
val newSettings = SiteSettings(
|
||||
validationIntervalMs = checkInterval,
|
||||
validationMode = validationMode,
|
||||
validationArgs = validationArgs,
|
||||
networkTimeout = networkTimeout,
|
||||
disabled = false
|
||||
)
|
||||
val newModel = Site(
|
||||
id = 0,
|
||||
name = name,
|
||||
url = url,
|
||||
status = WAITING,
|
||||
checkInterval = checkInterval,
|
||||
validationMode = validationMode,
|
||||
validationContent = validationContent,
|
||||
networkTimeout = networkTimeout
|
||||
settings = newSettings,
|
||||
lastResult = null
|
||||
)
|
||||
|
||||
with(view!!) {
|
||||
|
@ -163,7 +169,7 @@ class RealAddSitePresenter @Inject constructor(
|
|||
launch(coroutineContext) {
|
||||
setLoading()
|
||||
val storedModel = async(IO) {
|
||||
serverModelStore.put(newModel)
|
||||
database.putSite(newModel)
|
||||
}.await()
|
||||
|
||||
checkStatusManager.scheduleCheck(
|
||||
|
|
|
@ -28,9 +28,9 @@ import com.afollestad.materialdialogs.MaterialDialog
|
|||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.adapter.ServerAdapter
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.dialogs.AboutDialog
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
|
||||
|
@ -39,6 +39,7 @@ import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
|||
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
||||
import kotlinx.android.synthetic.main.activity_main.fab
|
||||
import kotlinx.android.synthetic.main.activity_main.list
|
||||
import kotlinx.android.synthetic.main.activity_main.loadingProgress
|
||||
import kotlinx.android.synthetic.main.activity_main.rootView
|
||||
import kotlinx.android.synthetic.main.activity_main.toolbar
|
||||
import kotlinx.android.synthetic.main.include_empty_view.emptyText
|
||||
|
@ -109,18 +110,22 @@ class MainActivity : AppCompatActivity(), MainView {
|
|||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun setModels(models: List<ServerModel>) {
|
||||
override fun setLoading() = loadingProgress.setLoading()
|
||||
|
||||
override fun setDoneLoading() = loadingProgress.setDone()
|
||||
|
||||
override fun setModels(models: List<Site>) {
|
||||
list.post {
|
||||
adapter.set(models)
|
||||
emptyText.showOrHide(models.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateModel(model: ServerModel) {
|
||||
override fun updateModel(model: Site) {
|
||||
list.post { adapter.update(model) }
|
||||
}
|
||||
|
||||
override fun onSiteDeleted(model: ServerModel) {
|
||||
override fun onSiteDeleted(model: Site) {
|
||||
list.post {
|
||||
adapter.remove(model)
|
||||
emptyText.showOrHide(adapter.itemCount == 0)
|
||||
|
@ -133,7 +138,7 @@ class MainActivity : AppCompatActivity(), MainView {
|
|||
) = rootView.scopeWhileAttached(context, exec)
|
||||
|
||||
private fun onSiteSelected(
|
||||
model: ServerModel,
|
||||
model: Site,
|
||||
longClick: Boolean
|
||||
) {
|
||||
if (longClick) {
|
||||
|
|
|
@ -18,7 +18,7 @@ package com.afollestad.nocknock.ui.main
|
|||
import android.content.Intent
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.toHtml
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.addsite.KEY_FAB_SIZE
|
||||
|
@ -47,16 +47,16 @@ private fun MainActivity.intentToAdd(
|
|||
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
}
|
||||
|
||||
internal fun MainActivity.viewSite(model: ServerModel) {
|
||||
internal fun MainActivity.viewSite(model: Site) {
|
||||
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
|
||||
}
|
||||
|
||||
private fun MainActivity.intentToView(model: ServerModel) =
|
||||
private fun MainActivity.intentToView(model: Site) =
|
||||
Intent(this, ViewSiteActivity::class.java).apply {
|
||||
putExtra(KEY_VIEW_MODEL, model)
|
||||
}
|
||||
|
||||
internal fun MainActivity.maybeRemoveSite(model: ServerModel) {
|
||||
internal fun MainActivity.maybeRemoveSite(model: Site) {
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.remove_site)
|
||||
message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
|
||||
|
@ -67,7 +67,7 @@ internal fun MainActivity.maybeRemoveSite(model: ServerModel) {
|
|||
|
||||
internal fun MainActivity.processIntent(intent: Intent) {
|
||||
if (intent.hasExtra(KEY_VIEW_NOTIFICATION_MODEL)) {
|
||||
val model = intent.getSerializableExtra(KEY_VIEW_NOTIFICATION_MODEL) as ServerModel
|
||||
val model = intent.getSerializableExtra(KEY_VIEW_NOTIFICATION_MODEL) as Site
|
||||
viewSite(model)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,18 +15,25 @@
|
|||
*/
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.Intent
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.allSites
|
||||
import com.afollestad.nocknock.data.deleteSite
|
||||
import com.afollestad.nocknock.data.legacy.DbMigrator
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import timber.log.Timber.d as log
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface MainPresenter {
|
||||
|
@ -37,18 +44,19 @@ interface MainPresenter {
|
|||
|
||||
fun resume()
|
||||
|
||||
fun refreshSite(site: ServerModel)
|
||||
fun refreshSite(site: Site)
|
||||
|
||||
fun removeSite(site: ServerModel)
|
||||
fun removeSite(site: Site)
|
||||
|
||||
fun dropView()
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class RealMainPresenter @Inject constructor(
|
||||
private val serverModelStore: ServerModelStore,
|
||||
private val app: Application,
|
||||
private val database: AppDatabase,
|
||||
private val notificationManager: NockNotificationManager,
|
||||
private val checkStatusManager: CheckStatusManager
|
||||
private val checkStatusManager: ValidationManager
|
||||
) : MainPresenter {
|
||||
|
||||
private var view: MainView? = null
|
||||
|
@ -61,40 +69,46 @@ class RealMainPresenter @Inject constructor(
|
|||
|
||||
override fun onBroadcast(intent: Intent) {
|
||||
if (intent.action == ACTION_STATUS_UPDATE) {
|
||||
val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? ServerModel ?: return
|
||||
val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? Site
|
||||
?: return
|
||||
view?.updateModel(model)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resume() {
|
||||
notificationManager.cancelStatusNotifications()
|
||||
|
||||
view!!.run {
|
||||
setModels(listOf())
|
||||
setLoading()
|
||||
|
||||
scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
val models = async(IO) {
|
||||
serverModelStore.get()
|
||||
}.await()
|
||||
doMigrationIfNeeded()
|
||||
|
||||
val models = async(IO) { database.allSites() }.await()
|
||||
|
||||
setModels(models)
|
||||
setDoneLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun refreshSite(site: ServerModel) {
|
||||
override fun refreshSite(site: Site) =
|
||||
checkStatusManager.scheduleCheck(
|
||||
site = site,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
}
|
||||
|
||||
override fun removeSite(site: ServerModel) {
|
||||
override fun removeSite(site: Site) {
|
||||
checkStatusManager.cancelCheck(site)
|
||||
notificationManager.cancelStatusNotification(site)
|
||||
|
||||
view!!.scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
async(IO) { serverModelStore.delete(site) }.await()
|
||||
async(IO) { database.deleteSite(site) }.await()
|
||||
view?.onSiteDeleted(site)
|
||||
}
|
||||
}
|
||||
|
@ -104,6 +118,28 @@ class RealMainPresenter @Inject constructor(
|
|||
view = null
|
||||
}
|
||||
|
||||
private suspend fun CoroutineScope.doMigrationIfNeeded() {
|
||||
if (needDbMigration()) {
|
||||
log("Doing database migration...")
|
||||
val migratedCount = async(IO) {
|
||||
DbMigrator(app, database).migrateAll()
|
||||
}.await()
|
||||
didDbMigration()
|
||||
log("Database migration done! Migrated $migratedCount models.")
|
||||
ensureCheckJobs()
|
||||
}
|
||||
}
|
||||
|
||||
private fun needDbMigration(): Boolean =
|
||||
!app.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
.getBoolean("did_db_migration", false)
|
||||
|
||||
private fun didDbMigration() =
|
||||
app.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
.edit()
|
||||
.putBoolean("did_db_migration", true)
|
||||
.apply()
|
||||
|
||||
private fun ensureCheckJobs() {
|
||||
view!!.scopeWhileAttached(IO) {
|
||||
launch(coroutineContext) {
|
||||
|
|
|
@ -15,18 +15,22 @@
|
|||
*/
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface MainView {
|
||||
|
||||
fun setModels(models: List<ServerModel>)
|
||||
fun setLoading()
|
||||
|
||||
fun updateModel(model: ServerModel)
|
||||
fun setDoneLoading()
|
||||
|
||||
fun onSiteDeleted(model: ServerModel)
|
||||
fun setModels(models: List<Site>)
|
||||
|
||||
fun updateModel(model: Site)
|
||||
|
||||
fun onSiteDeleted(model: Site)
|
||||
|
||||
fun scopeWhileAttached(
|
||||
context: CoroutineContext,
|
||||
|
|
|
@ -24,15 +24,15 @@ import android.os.Bundle
|
|||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.LAST_CHECK_NONE
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ValidationMode
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.indexToValidationMode
|
||||
import com.afollestad.nocknock.data.textRes
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.model.indexToValidationMode
|
||||
import com.afollestad.nocknock.data.model.textRes
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.formatDate
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
|
@ -63,6 +63,7 @@ import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
|||
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
||||
import java.lang.System.currentTimeMillis
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
|
@ -132,7 +133,7 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
|||
url = inputUrl.trimmedText(),
|
||||
checkInterval = checkInterval,
|
||||
validationMode = validationMode,
|
||||
validationContent = validationMode.validationContent(),
|
||||
validationArgs = validationMode.validationContent(),
|
||||
networkTimeout = responseTimeoutInput.textAsInt(defaultValue = defaultTimeout)
|
||||
)
|
||||
}
|
||||
|
@ -170,44 +171,48 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
|||
|
||||
override fun setValidationModeDescription(res: Int) = validationModeDescription.setText(res)
|
||||
|
||||
override fun displayModel(model: ServerModel) = with(model) {
|
||||
iconStatus.setStatus(this.status)
|
||||
override fun displayModel(model: Site) = with(model) {
|
||||
val siteSettings = this.settings
|
||||
requireNotNull(siteSettings) { "Site settings must be populated." }
|
||||
|
||||
iconStatus.setStatus(this.lastResult?.status ?: WAITING)
|
||||
inputName.setText(this.name)
|
||||
inputUrl.setText(this.url)
|
||||
|
||||
if (this.lastCheck == LAST_CHECK_NONE) {
|
||||
if (this.lastResult == null) {
|
||||
textLastCheckResult.setText(R.string.none)
|
||||
} else {
|
||||
val statusText = this.status.textRes()
|
||||
val statusText = this.lastResult!!.status.textRes()
|
||||
textLastCheckResult.text = if (statusText == 0) {
|
||||
this.reason
|
||||
this.lastResult!!.reason
|
||||
} else {
|
||||
getString(statusText)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.disabled) {
|
||||
if (siteSettings.disabled) {
|
||||
textNextCheck.setText(R.string.auto_checks_disabled)
|
||||
} else {
|
||||
textNextCheck.text = (this.lastCheck + this.checkInterval).formatDate()
|
||||
val lastCheck = this.lastResult?.timestampMs ?: currentTimeMillis()
|
||||
textNextCheck.text = (lastCheck + siteSettings.validationIntervalMs).formatDate()
|
||||
}
|
||||
checkIntervalLayout.set(this.checkInterval)
|
||||
checkIntervalLayout.set(siteSettings.validationIntervalMs)
|
||||
|
||||
responseValidationMode.setSelection(validationMode.value - 1)
|
||||
when (this.validationMode) {
|
||||
TERM_SEARCH -> responseValidationSearchTerm.setText(this.validationContent ?: "")
|
||||
JAVASCRIPT -> scriptInputLayout.setCode(this.validationContent)
|
||||
responseValidationMode.setSelection(siteSettings.validationMode.value - 1)
|
||||
when (siteSettings.validationMode) {
|
||||
TERM_SEARCH -> responseValidationSearchTerm.setText(siteSettings.validationArgs ?: "")
|
||||
JAVASCRIPT -> scriptInputLayout.setCode(siteSettings.validationArgs)
|
||||
else -> {
|
||||
responseValidationSearchTerm.setText("")
|
||||
scriptInputLayout.clear()
|
||||
}
|
||||
}
|
||||
|
||||
responseTimeoutInput.setText(model.networkTimeout.toString())
|
||||
responseTimeoutInput.setText(siteSettings.networkTimeout.toString())
|
||||
|
||||
disableChecksButton.showOrHide(!this.disabled)
|
||||
disableChecksButton.showOrHide(!siteSettings.disabled)
|
||||
doneBtn.setText(
|
||||
if (this.disabled) R.string.renable_and_save_changes
|
||||
if (siteSettings.disabled) R.string.renable_and_save_changes
|
||||
else R.string.save_changes
|
||||
)
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@ package com.afollestad.nocknock.ui.viewsite
|
|||
import android.widget.ImageView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.isPending
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.isPending
|
||||
import com.afollestad.nocknock.toHtml
|
||||
import com.afollestad.nocknock.utilities.ext.animateRotation
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
|
||||
|
@ -46,11 +46,11 @@ internal fun ViewSiteActivity.maybeDisableChecks() {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun ViewSiteActivity.invalidateMenuForStatus(model: ServerModel) {
|
||||
internal fun ViewSiteActivity.invalidateMenuForStatus(model: Site) {
|
||||
val refreshIcon = toolbar.menu.findItem(R.id.refresh)
|
||||
.actionView as ImageView
|
||||
|
||||
if (model.status.isPending()) {
|
||||
if (model.lastResult?.status.isPending()) {
|
||||
refreshIcon.animateRotation()
|
||||
} else {
|
||||
refreshIcon.run {
|
||||
|
|
|
@ -18,15 +18,17 @@ package com.afollestad.nocknock.ui.viewsite
|
|||
import android.content.Intent
|
||||
import androidx.annotation.CheckResult
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
import com.afollestad.nocknock.data.ValidationMode
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.deleteSite
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.updateSite
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
|
@ -77,7 +79,7 @@ interface ViewSitePresenter {
|
|||
url: String,
|
||||
checkInterval: Long,
|
||||
validationMode: ValidationMode,
|
||||
validationContent: String?,
|
||||
validationArgs: String?,
|
||||
networkTimeout: Int
|
||||
)
|
||||
|
||||
|
@ -87,26 +89,26 @@ interface ViewSitePresenter {
|
|||
|
||||
fun removeSite()
|
||||
|
||||
fun currentModel(): ServerModel
|
||||
fun currentModel(): Site
|
||||
|
||||
fun dropView()
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class RealViewSitePresenter @Inject constructor(
|
||||
private val serverModelStore: ServerModelStore,
|
||||
private val checkStatusManager: CheckStatusManager,
|
||||
private val database: AppDatabase,
|
||||
private val checkStatusManager: ValidationManager,
|
||||
private val notificationManager: NockNotificationManager
|
||||
) : ViewSitePresenter {
|
||||
|
||||
private var view: ViewSiteView? = null
|
||||
private var currentModel: ServerModel? = null
|
||||
private var currentModel: Site? = null
|
||||
|
||||
override fun takeView(
|
||||
view: ViewSiteView,
|
||||
intent: Intent
|
||||
) {
|
||||
this.currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as ServerModel
|
||||
this.currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as Site
|
||||
this.view = view.apply {
|
||||
displayModel(currentModel!!)
|
||||
}
|
||||
|
@ -114,7 +116,8 @@ class RealViewSitePresenter @Inject constructor(
|
|||
|
||||
override fun onBroadcast(intent: Intent) {
|
||||
if (intent.action == ACTION_STATUS_UPDATE) {
|
||||
val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? ServerModel ?: return
|
||||
val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? Site
|
||||
?: return
|
||||
this.currentModel = model
|
||||
view?.displayModel(model)
|
||||
}
|
||||
|
@ -122,7 +125,7 @@ class RealViewSitePresenter @Inject constructor(
|
|||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
if (intent != null && intent.hasExtra(KEY_VIEW_MODEL)) {
|
||||
currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as ServerModel
|
||||
currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as Site
|
||||
view?.displayModel(currentModel!!)
|
||||
}
|
||||
}
|
||||
|
@ -163,7 +166,7 @@ class RealViewSitePresenter @Inject constructor(
|
|||
url: String,
|
||||
checkInterval: Long,
|
||||
validationMode: ValidationMode,
|
||||
validationContent: String?,
|
||||
validationArgs: String?,
|
||||
networkTimeout: Int
|
||||
) {
|
||||
val inputErrors = InputErrors()
|
||||
|
@ -179,9 +182,9 @@ class RealViewSitePresenter @Inject constructor(
|
|||
if (checkInterval <= 0) {
|
||||
inputErrors.checkInterval = R.string.please_enter_check_interval
|
||||
}
|
||||
if (validationMode == TERM_SEARCH && validationContent.isNullOrEmpty()) {
|
||||
if (validationMode == TERM_SEARCH && validationArgs.isNullOrEmpty()) {
|
||||
inputErrors.termSearch = R.string.please_enter_search_term
|
||||
} else if (validationMode == JAVASCRIPT && validationContent.isNullOrEmpty()) {
|
||||
} else if (validationMode == JAVASCRIPT && validationArgs.isNullOrEmpty()) {
|
||||
inputErrors.javaScript = R.string.please_enter_javaScript
|
||||
}
|
||||
if (networkTimeout <= 0) {
|
||||
|
@ -193,27 +196,34 @@ class RealViewSitePresenter @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
val newModel = currentModel!!.copy(
|
||||
name = name,
|
||||
url = url,
|
||||
status = WAITING,
|
||||
checkInterval = checkInterval,
|
||||
val updatedSettings = currentModel!!.settings!!.copy(
|
||||
validationIntervalMs = checkInterval,
|
||||
validationMode = validationMode,
|
||||
validationContent = validationContent,
|
||||
validationArgs = validationArgs,
|
||||
disabled = false,
|
||||
networkTimeout = networkTimeout
|
||||
)
|
||||
val updatedModel = currentModel!!.copy(
|
||||
name = name,
|
||||
url = url,
|
||||
settings = updatedSettings
|
||||
)
|
||||
.withStatus(status = WAITING)
|
||||
|
||||
with(view!!) {
|
||||
scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
setLoading()
|
||||
async(IO) { serverModelStore.update(newModel) }.await()
|
||||
async(IO) {
|
||||
database.updateSite(updatedModel)
|
||||
}.await()
|
||||
|
||||
checkStatusManager.scheduleCheck(
|
||||
site = newModel,
|
||||
site = updatedModel,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
|
||||
setDoneLoading()
|
||||
view?.finish()
|
||||
}
|
||||
|
@ -222,7 +232,7 @@ class RealViewSitePresenter @Inject constructor(
|
|||
}
|
||||
|
||||
override fun checkNow() = with(view!!) {
|
||||
val checkModel = currentModel!!.copy(
|
||||
val checkModel = currentModel!!.withStatus(
|
||||
status = WAITING
|
||||
)
|
||||
view?.displayModel(checkModel)
|
||||
|
@ -242,8 +252,16 @@ class RealViewSitePresenter @Inject constructor(
|
|||
scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
setLoading()
|
||||
currentModel = currentModel!!.copy(disabled = true)
|
||||
async(IO) { serverModelStore.update(currentModel!!) }.await()
|
||||
currentModel = currentModel!!.copy(
|
||||
settings = currentModel!!.settings!!.copy(
|
||||
disabled = true
|
||||
)
|
||||
)
|
||||
|
||||
async(IO) {
|
||||
database.updateSite(currentModel!!)
|
||||
}.await()
|
||||
|
||||
setDoneLoading()
|
||||
view?.displayModel(currentModel!!)
|
||||
}
|
||||
|
@ -260,7 +278,9 @@ class RealViewSitePresenter @Inject constructor(
|
|||
scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
setLoading()
|
||||
async(IO) { serverModelStore.delete(site) }.await()
|
||||
async(IO) {
|
||||
database.deleteSite(site)
|
||||
}.await()
|
||||
setDoneLoading()
|
||||
view?.finish()
|
||||
}
|
||||
|
@ -275,7 +295,7 @@ class RealViewSitePresenter @Inject constructor(
|
|||
currentModel = null
|
||||
}
|
||||
|
||||
@TestOnly fun setModel(model: ServerModel) {
|
||||
@TestOnly fun setModel(model: Site) {
|
||||
this.currentModel = model
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
package com.afollestad.nocknock.ui.viewsite
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
|
@ -27,7 +27,7 @@ interface ViewSiteView {
|
|||
|
||||
fun setDoneLoading()
|
||||
|
||||
fun displayModel(model: ServerModel)
|
||||
fun displayModel(model: Site)
|
||||
|
||||
fun showOrHideUrlSchemeWarning(show: Boolean)
|
||||
|
||||
|
|
|
@ -48,4 +48,12 @@
|
|||
app:rippleColor="#40ffffff"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.LoadingIndicatorFrame
|
||||
android:id="@+id/loadingProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
||||
|
|
|
@ -22,33 +22,32 @@
|
|||
<string name="please_enter_name">Please enter a name!</string>
|
||||
<string name="please_enter_url">Please enter a URL.</string>
|
||||
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
||||
<string name="please_enter_check_interval">Please input a check interval.</string>
|
||||
<string name="please_enter_check_interval">Please input a validation interval.</string>
|
||||
<string name="please_enter_search_term">Please input a search term.</string>
|
||||
<string name="please_enter_javaScript">Please input a validation script.</string>
|
||||
<string name="please_enter_networkTimeout">Please enter a network timeout greater than 0.</string>
|
||||
|
||||
<string name="options">Options</string>
|
||||
<string name="already_checking_sites">Already checking sites!</string>
|
||||
<string name="remove_site">Remove Site</string>
|
||||
<string name="remove_site_prompt"><![CDATA[Remove <b>%1$s</b> from your sites?]]></string>
|
||||
<string name="remove">Remove</string>
|
||||
<string name="save_changes">Save Changes</string>
|
||||
<string name="view_site">View Site</string>
|
||||
<string name="last_check_result">Last Check Result</string>
|
||||
<string name="next_check">Next Check</string>
|
||||
<string name="next_check_x">Next Check: %1$s</string>
|
||||
<string name="last_check_result">Last Validation Result</string>
|
||||
<string name="next_check">Next Validation</string>
|
||||
<string name="next_check_x">Next Validation: %1$s</string>
|
||||
<string name="now">Now</string>
|
||||
<string name="none_turned_off">None (turned off)</string>
|
||||
<string name="none">None</string>
|
||||
|
||||
<string name="disable_automatic_checks">Disable Automatic Checks</string>
|
||||
<string name="disable_automatic_checks">Disable Automatic Validation</string>
|
||||
<string name="disable_automatic_checks_prompt"><![CDATA[
|
||||
Disable automatic checks for <b>%1$s</b>? The site will not be validated in the background
|
||||
until you re-enable checks for it. You can still manually perform checks by tapping the
|
||||
Disable automatic validation for <b>%1$s</b>? The site will not be checked in the background
|
||||
until you re-enable validation for it. You can still manually perform validation by tapping the
|
||||
Refresh icon at the top of this page.
|
||||
]]></string>
|
||||
<string name="disable">Disable</string>
|
||||
<string name="renable_and_save_changes">Enable Checks & Save Changes</string>
|
||||
<string name="renable_and_save_changes">Enable Auto Validation & Save Changes</string>
|
||||
|
||||
<string name="response_timeout">Network Response Timeout</string>
|
||||
<string name="response_timeout_default">10000</string>
|
||||
|
@ -56,22 +55,22 @@
|
|||
<string name="refresh_status">Refresh Status</string>
|
||||
|
||||
<string name="warning_http_url">
|
||||
Warning: this app checks for server availability with HTTP requests. It\'s recommended that you
|
||||
Warning: this app validates sites availability with HTTP requests. It\'s recommended that you
|
||||
use an HTTP URL.
|
||||
</string>
|
||||
<string name="response_validation_mode">Response Validation Mode</string>
|
||||
<string name="search_term">Search term…</string>
|
||||
|
||||
<string name="validation_mode_status_desc">
|
||||
The HTTP status code is checked. If it\'s a successful status code, the site passes the check.
|
||||
The HTTP status code is checked. If it\'s a successful status code, the site passes validation.
|
||||
</string>
|
||||
<string name="validation_mode_term_desc">
|
||||
The status code check is done first. If it\'s successful, the response body is checked.
|
||||
If it contains your search term, the site passes the check.
|
||||
If it contains your search term, the site passes validation.
|
||||
</string>
|
||||
<string name="validation_mode_javascript_desc">
|
||||
The status code check is done first. If it\'s successful, the response body is passed to the
|
||||
JavaScript function above. If the function returns true, the site passes the check. Throw an
|
||||
JavaScript function above. If the function returns true, the site passes validation. Throw an
|
||||
exception to pass custom error messages to Nock Nock.
|
||||
</string>
|
||||
|
||||
|
|
|
@ -15,12 +15,12 @@
|
|||
*/
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationManager
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteView
|
||||
import com.afollestad.nocknock.ui.addsite.InputErrors
|
||||
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter
|
||||
|
@ -42,16 +42,12 @@ import org.junit.Test
|
|||
|
||||
class AddSitePresenterTest {
|
||||
|
||||
private val serverModelStore = mock<ServerModelStore> {
|
||||
on { runBlocking { put(any()) } } doAnswer { inv ->
|
||||
inv.getArgument<ServerModel>(0)
|
||||
}
|
||||
}
|
||||
private val checkStatusManager = mock<CheckStatusManager>()
|
||||
private val database = mockDatabase()
|
||||
private val checkStatusManager = mock<ValidationManager>()
|
||||
private val view = mock<AddSiteView>()
|
||||
|
||||
private val presenter = RealAddSitePresenter(
|
||||
serverModelStore,
|
||||
database,
|
||||
checkStatusManager
|
||||
)
|
||||
|
||||
|
@ -260,10 +256,21 @@ class AddSitePresenterTest {
|
|||
60000
|
||||
)
|
||||
|
||||
val modelCaptor = argumentCaptor<ServerModel>()
|
||||
val siteCaptor = argumentCaptor<Site>()
|
||||
val settingsCaptor = argumentCaptor<SiteSettings>()
|
||||
|
||||
verify(view).setLoading()
|
||||
verify(serverModelStore).put(modelCaptor.capture())
|
||||
val model = modelCaptor.firstValue
|
||||
verify(database.siteDao()).insert(siteCaptor.capture())
|
||||
verify(database.siteSettingsDao()).insert(settingsCaptor.capture())
|
||||
verify(database.validationResultsDao(), never()).insert(any())
|
||||
|
||||
val settings = settingsCaptor.firstValue
|
||||
val model = siteCaptor.firstValue.copy(
|
||||
id = 1, // fill it in because our insert captor doesn't catch this
|
||||
settings = settings,
|
||||
lastResult = null
|
||||
)
|
||||
|
||||
verify(view, never()).setInputErrors(any())
|
||||
verify(checkStatusManager).scheduleCheck(
|
||||
site = model,
|
||||
|
@ -271,6 +278,7 @@ class AddSitePresenterTest {
|
|||
cancelPrevious = true,
|
||||
fromFinishingJob = false
|
||||
)
|
||||
|
||||
verify(view).setDoneLoading()
|
||||
verify(view).onSiteAdded()
|
||||
}
|
||||
|
|
|
@ -15,13 +15,14 @@
|
|||
*/
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.Intent
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import android.content.SharedPreferences
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.main.MainView
|
||||
import com.afollestad.nocknock.ui.main.RealMainPresenter
|
||||
|
@ -42,13 +43,22 @@ import org.junit.Test
|
|||
|
||||
class MainPresenterTest {
|
||||
|
||||
private val serverModelStore = mock<ServerModelStore>()
|
||||
private val prefs = mock<SharedPreferences> {
|
||||
on { getBoolean("did_db_migration", false) } doReturn true
|
||||
}
|
||||
private val app = mock<Application> {
|
||||
on { getSharedPreferences("settings", MODE_PRIVATE) } doReturn prefs
|
||||
}
|
||||
|
||||
private val database = mockDatabase()
|
||||
|
||||
private val notificationManager = mock<NockNotificationManager>()
|
||||
private val checkStatusManager = mock<CheckStatusManager>()
|
||||
private val checkStatusManager = mock<ValidationManager>()
|
||||
private val view = mock<MainView>()
|
||||
|
||||
private val presenter = RealMainPresenter(
|
||||
serverModelStore,
|
||||
app,
|
||||
database,
|
||||
notificationManager,
|
||||
checkStatusManager
|
||||
)
|
||||
|
@ -72,55 +82,45 @@ class MainPresenterTest {
|
|||
val badIntent = fakeIntent("Hello World")
|
||||
presenter.onBroadcast(badIntent)
|
||||
|
||||
val model = fakeModel()
|
||||
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||
whenever(goodIntent.getSerializableExtra(KEY_UPDATE_MODEL))
|
||||
.doReturn(model)
|
||||
.doReturn(MOCK_MODEL_2)
|
||||
|
||||
presenter.onBroadcast(goodIntent)
|
||||
verify(view, times(1)).updateModel(model)
|
||||
verify(view, times(1)).updateModel(MOCK_MODEL_2)
|
||||
}
|
||||
|
||||
@Test fun resume() = runBlocking {
|
||||
val model = fakeModel()
|
||||
whenever(serverModelStore.get()).doReturn(listOf(model))
|
||||
presenter.resume()
|
||||
|
||||
verify(notificationManager).cancelStatusNotifications()
|
||||
|
||||
val modelsCaptor = argumentCaptor<List<ServerModel>>()
|
||||
val modelsCaptor = argumentCaptor<List<Site>>()
|
||||
verify(view, times(2)).setModels(modelsCaptor.capture())
|
||||
assertThat(modelsCaptor.firstValue).isEmpty()
|
||||
assertThat(modelsCaptor.lastValue.single()).isEqualTo(model)
|
||||
assertThat(modelsCaptor.lastValue).isEqualTo(ALL_MOCK_MODELS)
|
||||
}
|
||||
|
||||
@Test fun refreshSite() {
|
||||
val model = fakeModel()
|
||||
presenter.refreshSite(model)
|
||||
presenter.refreshSite(MOCK_MODEL_3)
|
||||
|
||||
verify(checkStatusManager).scheduleCheck(
|
||||
site = model,
|
||||
site = MOCK_MODEL_3,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun removeSite() = runBlocking {
|
||||
val model = fakeModel()
|
||||
presenter.removeSite(model)
|
||||
presenter.removeSite(MOCK_MODEL_1)
|
||||
|
||||
verify(checkStatusManager).cancelCheck(model)
|
||||
verify(notificationManager).cancelStatusNotification(model)
|
||||
verify(serverModelStore).delete(model)
|
||||
verify(view).onSiteDeleted(model)
|
||||
verify(checkStatusManager).cancelCheck(MOCK_MODEL_1)
|
||||
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
||||
verify(database.siteDao()).delete(MOCK_MODEL_1)
|
||||
verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
|
||||
verify(view).onSiteDeleted(MOCK_MODEL_1)
|
||||
}
|
||||
|
||||
private fun fakeModel() = ServerModel(
|
||||
name = "Test",
|
||||
url = "https://test.com",
|
||||
validationMode = STATUS_CODE
|
||||
)
|
||||
|
||||
private fun fakeIntent(action: String): Intent {
|
||||
return mock {
|
||||
on { getAction() } doReturn action
|
||||
|
|
125
app/src/test/java/com/afollestad/nocknock/TestUtil.kt
Normal file
125
app/src/test/java/com/afollestad/nocknock/TestUtil.kt
Normal file
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.SiteDao
|
||||
import com.afollestad.nocknock.data.SiteSettingsDao
|
||||
import com.afollestad.nocknock.data.ValidationResultsDao
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.Status
|
||||
import com.afollestad.nocknock.data.model.Status.OK
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.isA
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import java.lang.System.currentTimeMillis
|
||||
|
||||
fun fakeSettingsModel(
|
||||
id: Long,
|
||||
validationMode: ValidationMode = STATUS_CODE
|
||||
) = SiteSettings(
|
||||
siteId = id,
|
||||
validationIntervalMs = 600000,
|
||||
validationMode = validationMode,
|
||||
validationArgs = null,
|
||||
disabled = false,
|
||||
networkTimeout = 10000
|
||||
)
|
||||
|
||||
fun fakeResultModel(
|
||||
id: Long,
|
||||
status: Status = OK,
|
||||
reason: String? = null
|
||||
) = ValidationResult(
|
||||
siteId = id,
|
||||
status = status,
|
||||
reason = reason,
|
||||
timestampMs = currentTimeMillis()
|
||||
)
|
||||
|
||||
fun fakeModel(id: Long) = Site(
|
||||
id = id,
|
||||
name = "Test",
|
||||
url = "https://test.com",
|
||||
settings = fakeSettingsModel(id),
|
||||
lastResult = fakeResultModel(id)
|
||||
)
|
||||
|
||||
val MOCK_MODEL_1 = fakeModel(1)
|
||||
val MOCK_MODEL_2 = fakeModel(2)
|
||||
val MOCK_MODEL_3 = fakeModel(3)
|
||||
val ALL_MOCK_MODELS = listOf(MOCK_MODEL_1, MOCK_MODEL_2, MOCK_MODEL_3)
|
||||
|
||||
fun mockDatabase(): AppDatabase {
|
||||
val siteDao = mock<SiteDao> {
|
||||
on { insert(isA()) } doReturn 1
|
||||
on { one(isA()) } doAnswer { inv ->
|
||||
val id = inv.getArgument<Long>(0)
|
||||
return@doAnswer when (id) {
|
||||
1L -> listOf(MOCK_MODEL_1)
|
||||
2L -> listOf(MOCK_MODEL_2)
|
||||
3L -> listOf(MOCK_MODEL_3)
|
||||
else -> listOf()
|
||||
}
|
||||
}
|
||||
on { all() } doReturn ALL_MOCK_MODELS
|
||||
on { update(isA()) } doAnswer { inv ->
|
||||
return@doAnswer inv.arguments.size
|
||||
}
|
||||
on { delete(isA()) } doAnswer { inv ->
|
||||
return@doAnswer inv.arguments.size
|
||||
}
|
||||
}
|
||||
val settingsDao = mock<SiteSettingsDao> {
|
||||
on { insert(isA()) } doReturn 1L
|
||||
on { forSite(isA()) } doAnswer { inv ->
|
||||
val id = inv.getArgument<Long>(0)
|
||||
return@doAnswer when (id) {
|
||||
1L -> listOf(MOCK_MODEL_1.settings!!)
|
||||
2L -> listOf(MOCK_MODEL_2.settings!!)
|
||||
3L -> listOf(MOCK_MODEL_3.settings!!)
|
||||
else -> listOf()
|
||||
}
|
||||
}
|
||||
on { update(isA()) } doReturn 1
|
||||
on { delete(isA()) } doReturn 1
|
||||
}
|
||||
val resultsDao = mock<ValidationResultsDao> {
|
||||
on { insert(isA()) } doReturn 1L
|
||||
on { forSite(isA()) } doAnswer { inv ->
|
||||
val id = inv.getArgument<Long>(0)
|
||||
return@doAnswer when (id) {
|
||||
1L -> listOf(MOCK_MODEL_1.lastResult!!)
|
||||
2L -> listOf(MOCK_MODEL_2.lastResult!!)
|
||||
3L -> listOf(MOCK_MODEL_3.lastResult!!)
|
||||
else -> listOf()
|
||||
}
|
||||
}
|
||||
on { update(isA()) } doReturn 1
|
||||
on { delete(isA()) } doReturn 1
|
||||
}
|
||||
|
||||
return mock {
|
||||
on { siteDao() } doReturn siteDao
|
||||
on { siteSettingsDao() } doReturn settingsDao
|
||||
on { validationResultsDao() } doReturn resultsDao
|
||||
}
|
||||
}
|
|
@ -16,16 +16,17 @@
|
|||
package com.afollestad.nocknock
|
||||
|
||||
import android.content.Intent
|
||||
import com.afollestad.nocknock.data.LAST_CHECK_NONE
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.viewsite.InputErrors
|
||||
import com.afollestad.nocknock.ui.viewsite.KEY_VIEW_MODEL
|
||||
|
@ -47,20 +48,17 @@ import kotlinx.coroutines.runBlocking
|
|||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.lang.System.currentTimeMillis
|
||||
|
||||
class ViewSitePresenterTest {
|
||||
|
||||
private val serverModelStore = mock<ServerModelStore> {
|
||||
on { runBlocking { put(any()) } } doAnswer { inv ->
|
||||
inv.getArgument<ServerModel>(0)
|
||||
}
|
||||
}
|
||||
private val checkStatusManager = mock<CheckStatusManager>()
|
||||
private val database = mockDatabase()
|
||||
private val checkStatusManager = mock<ValidationManager>()
|
||||
private val notificationManager = mock<NockNotificationManager>()
|
||||
private val view = mock<ViewSiteView>()
|
||||
|
||||
private val presenter = RealViewSitePresenter(
|
||||
serverModelStore,
|
||||
database,
|
||||
checkStatusManager,
|
||||
notificationManager
|
||||
)
|
||||
|
@ -73,13 +71,12 @@ class ViewSitePresenterTest {
|
|||
}.whenever(view)
|
||||
.scopeWhileAttached(any(), any())
|
||||
|
||||
val model = fakeModel()
|
||||
val intent = fakeIntent("")
|
||||
whenever(intent.getSerializableExtra(KEY_VIEW_MODEL))
|
||||
.doReturn(model)
|
||||
.doReturn(MOCK_MODEL_1)
|
||||
presenter.takeView(view, intent)
|
||||
assertThat(presenter.currentModel()).isEqualTo(model)
|
||||
verify(view, times(1)).displayModel(model)
|
||||
assertThat(presenter.currentModel()).isEqualTo(MOCK_MODEL_1)
|
||||
verify(view, times(1)).displayModel(MOCK_MODEL_1)
|
||||
}
|
||||
|
||||
@After fun destroy() {
|
||||
|
@ -90,27 +87,25 @@ class ViewSitePresenterTest {
|
|||
val badIntent = fakeIntent("Hello World")
|
||||
presenter.onBroadcast(badIntent)
|
||||
|
||||
val model = fakeModel().copy(lastCheck = 0)
|
||||
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||
whenever(goodIntent.getSerializableExtra(KEY_UPDATE_MODEL))
|
||||
.doReturn(model)
|
||||
.doReturn(MOCK_MODEL_2)
|
||||
|
||||
presenter.onBroadcast(goodIntent)
|
||||
assertThat(presenter.currentModel()).isEqualTo(model)
|
||||
verify(view, times(1)).displayModel(model)
|
||||
assertThat(presenter.currentModel()).isEqualTo(MOCK_MODEL_2)
|
||||
verify(view, times(1)).displayModel(MOCK_MODEL_2)
|
||||
}
|
||||
|
||||
@Test fun onNewIntent() {
|
||||
val badIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||
presenter.onBroadcast(badIntent)
|
||||
|
||||
val model = fakeModel().copy(lastCheck = 0)
|
||||
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||
whenever(goodIntent.getSerializableExtra(KEY_VIEW_MODEL))
|
||||
.doReturn(model)
|
||||
.doReturn(MOCK_MODEL_3)
|
||||
presenter.onBroadcast(goodIntent)
|
||||
|
||||
verify(view, times(1)).displayModel(model)
|
||||
verify(view, times(1)).displayModel(MOCK_MODEL_3)
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_focused() {
|
||||
|
@ -298,10 +293,19 @@ class ViewSitePresenterTest {
|
|||
val url = "https://hello.com"
|
||||
val checkInterval = 60000L
|
||||
val validationMode = TERM_SEARCH
|
||||
val validationContent = "Hello World"
|
||||
val validationArgs = "Hello World"
|
||||
|
||||
val disabledModel = presenter.currentModel()
|
||||
.copy(disabled = true)
|
||||
val currentModel = presenter.currentModel()
|
||||
val initialLastResult = ValidationResult(
|
||||
siteId = currentModel.id,
|
||||
timestampMs = currentTimeMillis() - 60000,
|
||||
status = ERROR,
|
||||
reason = "Oh no!"
|
||||
)
|
||||
val disabledModel = currentModel.copy(
|
||||
settings = currentModel.settings!!.copy(disabled = true),
|
||||
lastResult = initialLastResult
|
||||
)
|
||||
presenter.setModel(disabledModel)
|
||||
|
||||
presenter.commit(
|
||||
|
@ -309,21 +313,38 @@ class ViewSitePresenterTest {
|
|||
url,
|
||||
checkInterval,
|
||||
validationMode,
|
||||
validationContent,
|
||||
validationArgs,
|
||||
60000
|
||||
)
|
||||
|
||||
val modelCaptor = argumentCaptor<ServerModel>()
|
||||
verify(view).setLoading()
|
||||
verify(serverModelStore).update(modelCaptor.capture())
|
||||
val siteCaptor = argumentCaptor<Site>()
|
||||
val settingsCaptor = argumentCaptor<SiteSettings>()
|
||||
val resultCaptor = argumentCaptor<ValidationResult>()
|
||||
|
||||
val model = modelCaptor.firstValue
|
||||
assertThat(model.name).isEqualTo(name)
|
||||
assertThat(model.url).isEqualTo(url)
|
||||
assertThat(model.checkInterval).isEqualTo(checkInterval)
|
||||
assertThat(model.validationMode).isEqualTo(validationMode)
|
||||
assertThat(model.validationContent).isEqualTo(validationContent)
|
||||
assertThat(model.disabled).isFalse()
|
||||
verify(view).setLoading()
|
||||
verify(database.siteDao()).update(siteCaptor.capture())
|
||||
verify(database.siteSettingsDao()).update(settingsCaptor.capture())
|
||||
verify(database.validationResultsDao()).update(resultCaptor.capture())
|
||||
|
||||
val model = siteCaptor.firstValue
|
||||
model.apply {
|
||||
assertThat(this.name).isEqualTo(name)
|
||||
assertThat(this.url).isEqualTo(url)
|
||||
}
|
||||
|
||||
val settings = settingsCaptor.firstValue
|
||||
settings.apply {
|
||||
assertThat(this.validationIntervalMs).isEqualTo(checkInterval)
|
||||
assertThat(this.validationArgs).isEqualTo(validationArgs)
|
||||
assertThat(this.disabled).isFalse()
|
||||
}
|
||||
|
||||
val result = resultCaptor.firstValue
|
||||
result.apply {
|
||||
assertThat(this.status).isEqualTo(WAITING)
|
||||
assertThat(this.reason).isNull()
|
||||
assertThat(this.timestampMs).isGreaterThan(0)
|
||||
}
|
||||
|
||||
verify(view, never()).setInputErrors(any())
|
||||
verify(checkStatusManager).scheduleCheck(
|
||||
|
@ -338,9 +359,7 @@ class ViewSitePresenterTest {
|
|||
|
||||
@Test fun checkNow() {
|
||||
val newModel = presenter.currentModel()
|
||||
.copy(
|
||||
status = WAITING
|
||||
)
|
||||
.withStatus(status = WAITING)
|
||||
presenter.checkNow()
|
||||
|
||||
verify(view, never()).setLoading()
|
||||
|
@ -360,11 +379,18 @@ class ViewSitePresenterTest {
|
|||
verify(notificationManager).cancelStatusNotification(model)
|
||||
verify(view).setLoading()
|
||||
|
||||
val modelCaptor = argumentCaptor<ServerModel>()
|
||||
verify(serverModelStore).update(modelCaptor.capture())
|
||||
val modelCaptor = argumentCaptor<Site>()
|
||||
val settingsCaptor = argumentCaptor<SiteSettings>()
|
||||
val resultCaptor = argumentCaptor<ValidationResult>()
|
||||
|
||||
verify(database.siteDao()).update(modelCaptor.capture())
|
||||
verify(database.siteSettingsDao()).update(settingsCaptor.capture())
|
||||
verify(database.validationResultsDao()).update(resultCaptor.capture())
|
||||
|
||||
val newModel = modelCaptor.firstValue
|
||||
assertThat(newModel.disabled).isTrue()
|
||||
assertThat(newModel.lastCheck).isEqualTo(LAST_CHECK_NONE)
|
||||
val newSettings = settingsCaptor.firstValue
|
||||
val result = resultCaptor.firstValue
|
||||
assertThat(newSettings.disabled).isTrue()
|
||||
|
||||
verify(view).setDoneLoading()
|
||||
verify(view, times(1)).displayModel(newModel)
|
||||
|
@ -377,18 +403,15 @@ class ViewSitePresenterTest {
|
|||
verify(checkStatusManager).cancelCheck(model)
|
||||
verify(notificationManager).cancelStatusNotification(model)
|
||||
verify(view).setLoading()
|
||||
verify(serverModelStore).delete(model)
|
||||
|
||||
verify(database.siteSettingsDao()).delete(model.settings!!)
|
||||
verify(database.validationResultsDao()).delete(model.lastResult!!)
|
||||
verify(database.siteDao()).delete(model)
|
||||
|
||||
verify(view).setDoneLoading()
|
||||
verify(view).finish()
|
||||
}
|
||||
|
||||
private fun fakeModel() = ServerModel(
|
||||
id = 1,
|
||||
name = "Test",
|
||||
url = "https://test.com",
|
||||
validationMode = STATUS_CODE
|
||||
)
|
||||
|
||||
private fun fakeIntent(action: String): Intent {
|
||||
return mock {
|
||||
on { getAction() } doReturn action
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
apply from: '../dependencies.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
@ -10,6 +11,8 @@ android {
|
|||
targetSdkVersion versions.compileSdk
|
||||
versionCode versions.publishVersionCode
|
||||
versionName versions.publishVersion
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,6 +20,17 @@ dependencies {
|
|||
implementation project(':utilities')
|
||||
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
|
||||
|
||||
implementation 'com.google.dagger:dagger:' + versions.dagger
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:' + versions.dagger
|
||||
|
||||
api 'androidx.room:room-runtime:' + versions.room
|
||||
kapt 'androidx.room:room-compiler:' + versions.room
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:' + versions.androidxTestRunner
|
||||
androidTestImplementation 'androidx.test:rules:' + versions.androidxTestRunner
|
||||
androidTestImplementation 'androidx.test:core:' + versions.androidxTest
|
||||
androidTestImplementation 'com.google.truth:truth:' + versions.truth
|
||||
}
|
||||
|
||||
apply from: '../spotless.gradle'
|
10
data/src/androidTest/AndroidManifest.xml
Normal file
10
data/src/androidTest/AndroidManifest.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.afollestad.nocknock.data">
|
||||
|
||||
<application>
|
||||
<uses-library
|
||||
android:name="android.test.runner"
|
||||
android:required="false"/>
|
||||
</application>
|
||||
</manifest>
|
|
@ -0,0 +1,350 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* 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:Suppress("RemoveEmptyPrimaryConstructor")
|
||||
|
||||
package com.afollestad.nocknock.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room.inMemoryDatabaseBuilder
|
||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||
import com.afollestad.nocknock.data.model.Status.OK
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.IOException
|
||||
import java.lang.System.currentTimeMillis
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppDatabaseTest() {
|
||||
|
||||
private lateinit var db: AppDatabase
|
||||
private lateinit var sitesDao: SiteDao
|
||||
private lateinit var settingsDao: SiteSettingsDao
|
||||
private lateinit var resultsDao: ValidationResultsDao
|
||||
|
||||
@Before fun setup() {
|
||||
val context = getApplicationContext<Context>()
|
||||
db = inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
|
||||
sitesDao = db.siteDao()
|
||||
settingsDao = db.siteSettingsDao()
|
||||
resultsDao = db.validationResultsDao()
|
||||
}
|
||||
|
||||
@After
|
||||
@Throws(IOException::class)
|
||||
fun destroy() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
// SiteDao
|
||||
|
||||
@Test fun site_insert_and_get_all() {
|
||||
val model1 = Site(
|
||||
name = "Test 1",
|
||||
url = "https://test1.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
)
|
||||
val newId1 = sitesDao.insert(model1)
|
||||
assertThat(newId1).isGreaterThan(0)
|
||||
|
||||
val model2 = Site(
|
||||
name = "Test 2",
|
||||
url = "https://test2.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
)
|
||||
val newId2 = sitesDao.insert(model2)
|
||||
assertThat(newId2).isGreaterThan(newId1)
|
||||
|
||||
val models = sitesDao.all()
|
||||
assertThat(models.size).isEqualTo(2)
|
||||
assertThat(models[0]).isEqualTo(model1.copy(id = newId1))
|
||||
assertThat(models[1]).isEqualTo(model2.copy(id = newId2))
|
||||
}
|
||||
|
||||
@Test fun site_insert_and_get_one() {
|
||||
val model = Site(
|
||||
name = "Test",
|
||||
url = "https://test.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
)
|
||||
val newId = sitesDao.insert(model)
|
||||
assertThat(newId).isGreaterThan(0)
|
||||
|
||||
val models = sitesDao.all()
|
||||
assertThat(models.single()).isEqualTo(model.copy(id = newId))
|
||||
}
|
||||
|
||||
@Test fun site_insert_and_update() {
|
||||
val initialModel = Site(
|
||||
name = "Test 1",
|
||||
url = "https://test1.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
)
|
||||
val newId = sitesDao.insert(initialModel)
|
||||
assertThat(newId).isGreaterThan(0)
|
||||
|
||||
val insertedModel = sitesDao.all()
|
||||
.single()
|
||||
val updatedModel = insertedModel.copy(
|
||||
name = "Test 2",
|
||||
url = "https://hi.com"
|
||||
)
|
||||
assertThat(sitesDao.update(updatedModel)).isEqualTo(1)
|
||||
|
||||
val finalModel = sitesDao.all()
|
||||
.single()
|
||||
assertThat(finalModel).isNotEqualTo(initialModel.copy(id = newId))
|
||||
}
|
||||
|
||||
@Test fun site_insert_and_delete() {
|
||||
val model1 = Site(
|
||||
name = "Test 1",
|
||||
url = "https://test1.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
)
|
||||
val newId1 = sitesDao.insert(model1)
|
||||
assertThat(newId1).isGreaterThan(0)
|
||||
|
||||
val model2 = Site(
|
||||
name = "Test 2",
|
||||
url = "https://test2.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
)
|
||||
val newId2 = sitesDao.insert(model2)
|
||||
assertThat(newId2).isGreaterThan(newId1)
|
||||
|
||||
val models1 = sitesDao.all()
|
||||
sitesDao.delete(models1[0])
|
||||
|
||||
val models2 = sitesDao.all()
|
||||
assertThat(models2.single()).isEqualTo(models1[1])
|
||||
}
|
||||
|
||||
// SiteSettingsDao
|
||||
|
||||
@Test fun settings_insert_and_forSite() {
|
||||
val model = SiteSettings(
|
||||
siteId = 1,
|
||||
validationIntervalMs = 60000,
|
||||
validationMode = STATUS_CODE,
|
||||
validationArgs = null,
|
||||
disabled = false,
|
||||
networkTimeout = 10000
|
||||
)
|
||||
val newId = settingsDao.insert(model)
|
||||
assertThat(newId).isEqualTo(1)
|
||||
|
||||
val finalModel = settingsDao.forSite(newId)
|
||||
.single()
|
||||
assertThat(finalModel).isEqualTo(model.copy(siteId = newId))
|
||||
}
|
||||
|
||||
@Test fun settings_update() {
|
||||
settingsDao.insert(
|
||||
SiteSettings(
|
||||
siteId = 1,
|
||||
validationIntervalMs = 60000,
|
||||
validationMode = STATUS_CODE,
|
||||
validationArgs = null,
|
||||
disabled = false,
|
||||
networkTimeout = 10000
|
||||
)
|
||||
)
|
||||
|
||||
val insertedModel = settingsDao.forSite(1)
|
||||
.single()
|
||||
val updatedModel = insertedModel.copy(
|
||||
validationIntervalMs = 10000,
|
||||
validationMode = TERM_SEARCH,
|
||||
validationArgs = "test",
|
||||
disabled = false,
|
||||
networkTimeout = 1000
|
||||
)
|
||||
assertThat(settingsDao.update(updatedModel)).isEqualTo(1)
|
||||
|
||||
val finalModel = settingsDao.forSite(1)
|
||||
.single()
|
||||
assertThat(finalModel).isEqualTo(updatedModel)
|
||||
}
|
||||
|
||||
@Test fun settings_delete() {
|
||||
settingsDao.insert(
|
||||
SiteSettings(
|
||||
siteId = 1,
|
||||
validationIntervalMs = 60000,
|
||||
validationMode = STATUS_CODE,
|
||||
validationArgs = null,
|
||||
disabled = false,
|
||||
networkTimeout = 10000
|
||||
)
|
||||
)
|
||||
|
||||
val insertedModel = settingsDao.forSite(1)
|
||||
.single()
|
||||
settingsDao.delete(insertedModel)
|
||||
assertThat(settingsDao.forSite(1)).isEmpty()
|
||||
}
|
||||
|
||||
// ValidationResultsDao
|
||||
|
||||
@Test fun validation_insert_and_forSite() {
|
||||
val model = ValidationResult(
|
||||
siteId = 1,
|
||||
timestampMs = currentTimeMillis(),
|
||||
status = ERROR,
|
||||
reason = "Oh no"
|
||||
)
|
||||
val newId = resultsDao.insert(model)
|
||||
assertThat(newId).isEqualTo(1)
|
||||
|
||||
val finalModel = resultsDao.forSite(newId)
|
||||
.single()
|
||||
assertThat(finalModel).isEqualTo(model.copy(siteId = newId))
|
||||
}
|
||||
|
||||
@Test fun validation_update() {
|
||||
resultsDao.insert(
|
||||
ValidationResult(
|
||||
siteId = 1,
|
||||
timestampMs = currentTimeMillis(),
|
||||
status = ERROR,
|
||||
reason = "Oh no"
|
||||
)
|
||||
)
|
||||
|
||||
val insertedModel = resultsDao.forSite(1)
|
||||
.single()
|
||||
val updatedModel = insertedModel.copy(
|
||||
timestampMs = currentTimeMillis() + 1000,
|
||||
status = OK,
|
||||
reason = null
|
||||
)
|
||||
assertThat(resultsDao.update(updatedModel)).isEqualTo(1)
|
||||
|
||||
val finalModel = resultsDao.forSite(1)
|
||||
.single()
|
||||
assertThat(finalModel).isEqualTo(updatedModel)
|
||||
}
|
||||
|
||||
@Test fun validation_delete() {
|
||||
resultsDao.insert(
|
||||
ValidationResult(
|
||||
siteId = 1,
|
||||
timestampMs = currentTimeMillis(),
|
||||
status = ERROR,
|
||||
reason = "Oh no"
|
||||
)
|
||||
)
|
||||
|
||||
val insertedModel = resultsDao.forSite(1)
|
||||
.single()
|
||||
resultsDao.delete(insertedModel)
|
||||
assertThat(resultsDao.forSite(1)).isEmpty()
|
||||
}
|
||||
|
||||
// Extension Methods
|
||||
|
||||
@Test fun extension_put_and_allSites() {
|
||||
db.putSite(MOCK_MODEL_1)
|
||||
db.putSite(MOCK_MODEL_2)
|
||||
db.putSite(MOCK_MODEL_3)
|
||||
|
||||
val allSites = db.allSites()
|
||||
assertThat(allSites.size).isEqualTo(3)
|
||||
assertThat(allSites[0]).isEqualTo(MOCK_MODEL_1)
|
||||
assertThat(allSites[1]).isEqualTo(MOCK_MODEL_2)
|
||||
assertThat(allSites[2]).isEqualTo(MOCK_MODEL_3)
|
||||
}
|
||||
|
||||
@Test fun extension_put_getSite() {
|
||||
db.putSite(MOCK_MODEL_1)
|
||||
db.putSite(MOCK_MODEL_2)
|
||||
db.putSite(MOCK_MODEL_3)
|
||||
val allSites = db.allSites()
|
||||
|
||||
val site = db.getSite(2)
|
||||
assertThat(site).isEqualTo(allSites[1])
|
||||
}
|
||||
|
||||
@Test fun extension_put_updateSite() {
|
||||
db.putSite(MOCK_MODEL_1)
|
||||
db.putSite(MOCK_MODEL_2)
|
||||
db.putSite(MOCK_MODEL_3)
|
||||
val modelToUpdate = db.allSites()[1]
|
||||
|
||||
val updatedSettings = modelToUpdate.settings!!.copy(
|
||||
validationIntervalMs = 1,
|
||||
validationMode = JAVASCRIPT,
|
||||
validationArgs = "throw 'Hello World'",
|
||||
disabled = false,
|
||||
networkTimeout = 50
|
||||
)
|
||||
val updatedValidationResult = modelToUpdate.lastResult!!.copy(
|
||||
timestampMs = currentTimeMillis() + 10,
|
||||
status = ERROR,
|
||||
reason = "Oh no"
|
||||
)
|
||||
val updatedModel = modelToUpdate.copy(
|
||||
name = "Oijrfouhef",
|
||||
url = "https://iojfdfsdk.io",
|
||||
settings = updatedSettings,
|
||||
lastResult = updatedValidationResult
|
||||
)
|
||||
|
||||
db.updateSite(updatedModel)
|
||||
|
||||
val finalSite = db.getSite(modelToUpdate.id)!!
|
||||
assertThat(finalSite.settings).isEqualTo(updatedSettings)
|
||||
assertThat(finalSite.lastResult).isEqualTo(updatedValidationResult)
|
||||
assertThat(finalSite).isEqualTo(updatedModel)
|
||||
}
|
||||
|
||||
@Test fun extension_put_and_deleteSite() {
|
||||
db.putSite(MOCK_MODEL_1)
|
||||
db.putSite(MOCK_MODEL_2)
|
||||
db.putSite(MOCK_MODEL_3)
|
||||
val allSites = db.allSites()
|
||||
|
||||
db.deleteSite(MOCK_MODEL_2)
|
||||
|
||||
val remainingSettings = settingsDao.all()
|
||||
assertThat(remainingSettings.size).isEqualTo(2)
|
||||
assertThat(remainingSettings[0]).isEqualTo(allSites[0].settings!!)
|
||||
assertThat(remainingSettings[1]).isEqualTo(allSites[2].settings!!)
|
||||
|
||||
val remainingResults = resultsDao.all()
|
||||
assertThat(remainingResults.size).isEqualTo(2)
|
||||
assertThat(remainingResults[0]).isEqualTo(allSites[0].lastResult!!)
|
||||
assertThat(remainingResults[1]).isEqualTo(allSites[2].lastResult!!)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.data
|
||||
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.Status
|
||||
import com.afollestad.nocknock.data.model.Status.OK
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
import java.lang.System.currentTimeMillis
|
||||
|
||||
fun fakeSettingsModel(
|
||||
id: Long,
|
||||
validationMode: ValidationMode = STATUS_CODE
|
||||
) = SiteSettings(
|
||||
siteId = id,
|
||||
validationIntervalMs = 600000,
|
||||
validationMode = validationMode,
|
||||
validationArgs = null,
|
||||
disabled = false,
|
||||
networkTimeout = 10000
|
||||
)
|
||||
|
||||
fun fakeResultModel(
|
||||
id: Long,
|
||||
status: Status = OK,
|
||||
reason: String? = null
|
||||
) = ValidationResult(
|
||||
siteId = id,
|
||||
status = status,
|
||||
reason = reason,
|
||||
timestampMs = currentTimeMillis()
|
||||
)
|
||||
|
||||
fun fakeModel(id: Long) = Site(
|
||||
id = id,
|
||||
name = "Test",
|
||||
url = "https://test.com",
|
||||
settings = fakeSettingsModel(id),
|
||||
lastResult = fakeResultModel(id)
|
||||
)
|
||||
|
||||
val MOCK_MODEL_1 = fakeModel(1)
|
||||
val MOCK_MODEL_2 = fakeModel(2)
|
||||
val MOCK_MODEL_3 = fakeModel(3)
|
147
data/src/main/java/com/afollestad/nocknock/data/AppDatabase.kt
Normal file
147
data/src/main/java/com/afollestad/nocknock/data/AppDatabase.kt
Normal file
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.data
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.afollestad.nocknock.data.model.Converters
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Database(
|
||||
entities = [
|
||||
ValidationResult::class,
|
||||
SiteSettings::class,
|
||||
Site::class
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun siteDao(): SiteDao
|
||||
|
||||
abstract fun siteSettingsDao(): SiteSettingsDao
|
||||
|
||||
abstract fun validationResultsDao(): ValidationResultsDao
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all sites and maps their settings and last validation results.
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
fun AppDatabase.allSites(): List<Site> {
|
||||
return siteDao().all()
|
||||
.map {
|
||||
val settings = siteSettingsDao().forSite(it.id)
|
||||
.single()
|
||||
val lastResult = validationResultsDao().forSite(it.id)
|
||||
.singleOrNull()
|
||||
return@map it.copy(
|
||||
settings = settings,
|
||||
lastResult = lastResult
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single site and maps its settings and last validation result.
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
fun AppDatabase.getSite(id: Long): Site? {
|
||||
val result = siteDao().one(id)
|
||||
.singleOrNull() ?: return null
|
||||
val settings = siteSettingsDao().forSite(id)
|
||||
.single()
|
||||
val lastResult = validationResultsDao().forSite(id)
|
||||
.singleOrNull()
|
||||
return result.copy(
|
||||
settings = settings,
|
||||
lastResult = lastResult
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a site along with its settings and last result into the database.
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
fun AppDatabase.putSite(site: Site): Site {
|
||||
requireNotNull(site.settings) { "Settings must be populated." }
|
||||
val newId = siteDao().insert(site)
|
||||
val settingsWithSiteId =
|
||||
site.settings!!.copy(
|
||||
siteId = newId
|
||||
)
|
||||
siteSettingsDao().insert(settingsWithSiteId)
|
||||
site.lastResult?.let { validationResultsDao().insert(it) }
|
||||
return site.copy(
|
||||
id = newId,
|
||||
settings = settingsWithSiteId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a site, along with its settings and last result, in the database.
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
fun AppDatabase.updateSite(site: Site) {
|
||||
siteDao().update(site)
|
||||
if (site.settings != null) {
|
||||
val existing = siteSettingsDao().forSite(site.id)
|
||||
.singleOrNull()
|
||||
if (existing != null) {
|
||||
siteSettingsDao().update(site.settings!!)
|
||||
} else {
|
||||
siteSettingsDao().insert(
|
||||
site.settings!!.copy(
|
||||
siteId = site.id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (site.lastResult != null) {
|
||||
val existing = validationResultsDao().forSite(site.id)
|
||||
.singleOrNull()
|
||||
if (existing != null) {
|
||||
validationResultsDao().update(site.lastResult!!)
|
||||
} else {
|
||||
validationResultsDao().insert(site.lastResult!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a site along with its settings and last result from the database.
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
fun AppDatabase.deleteSite(site: Site) {
|
||||
if (site.settings != null) {
|
||||
siteSettingsDao().delete(site.settings!!)
|
||||
}
|
||||
if (site.lastResult != null) {
|
||||
validationResultsDao().delete(site.lastResult!!)
|
||||
}
|
||||
siteDao().delete(site)
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.data
|
||||
|
||||
import com.afollestad.nocknock.data.ServerStatus.CHECKING
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
enum class ServerStatus(val value: Int) {
|
||||
OK(1),
|
||||
WAITING(2),
|
||||
CHECKING(3),
|
||||
ERROR(4);
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromValue(value: Int) = when (value) {
|
||||
OK.value -> OK
|
||||
WAITING.value -> WAITING
|
||||
CHECKING.value -> CHECKING
|
||||
ERROR.value -> ERROR
|
||||
else -> throw IllegalArgumentException("Unknown validationMode: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ServerStatus.textRes() = when (this) {
|
||||
OK -> R.string.everything_checks_out
|
||||
WAITING -> R.string.waiting
|
||||
CHECKING -> R.string.checking_status
|
||||
else -> 0
|
||||
}
|
||||
|
||||
fun Int.toServerStatus() = ServerStatus.fromValue(this)
|
||||
|
||||
fun ServerStatus.isPending() = this == WAITING || this == CHECKING
|
|
@ -13,29 +13,32 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.engine
|
||||
package com.afollestad.nocknock.data
|
||||
|
||||
import com.afollestad.nocknock.engine.db.RealServerModelStore
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.engine.statuscheck.RealCheckStatusManager
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import javax.inject.Singleton
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy.FAIL
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Module
|
||||
abstract class EngineModule {
|
||||
@Dao
|
||||
interface SiteDao {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideServerModelStore(
|
||||
serverModelStore: RealServerModelStore
|
||||
): ServerModelStore
|
||||
@Query("SELECT * FROM sites ORDER BY name ASC")
|
||||
fun all(): List<Site>
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideCheckStatusManager(
|
||||
checkStatusManager: RealCheckStatusManager
|
||||
): CheckStatusManager
|
||||
@Query("SELECT * FROM sites WHERE id = :id LIMIT 1")
|
||||
fun one(id: Long): List<Site>
|
||||
|
||||
@Insert(onConflict = FAIL)
|
||||
fun insert(site: Site): Long
|
||||
|
||||
@Update(onConflict = FAIL)
|
||||
fun update(site: Site): Int
|
||||
|
||||
@Delete
|
||||
fun delete(site: Site): Int
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy.FAIL
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Dao
|
||||
interface SiteSettingsDao {
|
||||
|
||||
@Query("SELECT * FROM site_settings ORDER BY siteId ASC")
|
||||
fun all(): List<SiteSettings>
|
||||
|
||||
@Query("SELECT * FROM site_settings WHERE siteId = :siteId LIMIT 1")
|
||||
fun forSite(siteId: Long): List<SiteSettings>
|
||||
|
||||
@Insert(onConflict = FAIL)
|
||||
fun insert(siteSetting: SiteSettings): Long
|
||||
|
||||
@Update(onConflict = FAIL)
|
||||
fun update(siteSetting: SiteSettings): Int
|
||||
|
||||
@Delete
|
||||
fun delete(siteSetting: SiteSettings): Int
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy.FAIL
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Dao
|
||||
interface ValidationResultsDao {
|
||||
|
||||
@Query("SELECT * FROM validation_results ORDER BY siteId ASC")
|
||||
fun all(): List<ValidationResult>
|
||||
|
||||
@Query("SELECT * FROM validation_results WHERE siteId = :siteId LIMIT 1")
|
||||
fun forSite(siteId: Long): List<ValidationResult>
|
||||
|
||||
@Insert(onConflict = FAIL)
|
||||
fun insert(siteSetting: ValidationResult): Long
|
||||
|
||||
@Update(onConflict = FAIL)
|
||||
fun update(siteSetting: ValidationResult): Int
|
||||
|
||||
@Delete
|
||||
fun delete(siteSetting: ValidationResult): Int
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* 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:Suppress("DEPRECATION")
|
||||
|
||||
package com.afollestad.nocknock.data.legacy
|
||||
|
||||
import android.app.Application
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.Status.CHECKING
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Migrates from manual SQLite management to Room.
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
class DbMigrator @Inject constructor(
|
||||
app: Application,
|
||||
private val appDb: AppDatabase
|
||||
) {
|
||||
private val legacyStore = ServerModelStore(app)
|
||||
|
||||
fun migrateAll(): Int {
|
||||
val legacyModels = legacyStore.get()
|
||||
var count = 0
|
||||
|
||||
for (oldModel in legacyModels) {
|
||||
// Insert site
|
||||
val site = oldModel.toNewModel()
|
||||
val siteId = appDb.siteDao()
|
||||
.insert(site)
|
||||
|
||||
// Insert site settings
|
||||
val settingsWithId = site.settings!!.copy(
|
||||
siteId = siteId
|
||||
)
|
||||
appDb.siteSettingsDao()
|
||||
.insert(settingsWithId)
|
||||
|
||||
// Insert validation result
|
||||
site.lastResult?.let {
|
||||
val resultWithId = it.copy(
|
||||
siteId = siteId
|
||||
)
|
||||
appDb.validationResultsDao()
|
||||
.insert(resultWithId)
|
||||
}
|
||||
|
||||
count++
|
||||
}
|
||||
|
||||
legacyStore.wipe()
|
||||
return count
|
||||
}
|
||||
|
||||
private fun ServerModel.toNewModel(): Site {
|
||||
return Site(
|
||||
id = 0,
|
||||
name = this.name,
|
||||
url = this.url,
|
||||
settings = this.toSettingsModel(),
|
||||
lastResult = this.toValidationModel()
|
||||
)
|
||||
}
|
||||
|
||||
private fun ServerModel.toSettingsModel(): SiteSettings {
|
||||
return SiteSettings(
|
||||
siteId = 0,
|
||||
validationIntervalMs = this.checkInterval,
|
||||
validationMode = this.validationMode,
|
||||
validationArgs = this.validationContent,
|
||||
disabled = this.disabled,
|
||||
networkTimeout = this.networkTimeout
|
||||
)
|
||||
}
|
||||
|
||||
private fun ServerModel.toValidationModel(): ValidationResult? {
|
||||
if (this.lastCheck == LAST_CHECK_NONE) {
|
||||
return null
|
||||
}
|
||||
return ValidationResult(
|
||||
siteId = 0,
|
||||
timestampMs = this.lastCheck,
|
||||
status = if (this.status == CHECKING) WAITING else this.status,
|
||||
reason = this.reason
|
||||
)
|
||||
}
|
||||
}
|
|
@ -13,25 +13,28 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.data
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.afollestad.nocknock.data.legacy
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.utilities.ext.timeString
|
||||
import com.afollestad.nocknock.utilities.providers.IdProvider
|
||||
import java.lang.System.currentTimeMillis
|
||||
import kotlin.math.max
|
||||
import com.afollestad.nocknock.data.model.Status
|
||||
import com.afollestad.nocknock.data.model.Status.OK
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.data.model.toSiteStatus
|
||||
import com.afollestad.nocknock.data.model.toValidationMode
|
||||
|
||||
const val CHECK_INTERVAL_UNSET = -1L
|
||||
const val LAST_CHECK_NONE = -1L
|
||||
|
||||
/** @author Aidan Follestad (@afollestad)*/
|
||||
@Deprecated("Deprecated in favor of Site.")
|
||||
data class ServerModel(
|
||||
var id: Int = 0,
|
||||
val name: String,
|
||||
val url: String,
|
||||
val status: ServerStatus = OK,
|
||||
val status: Status = OK,
|
||||
val checkInterval: Long = CHECK_INTERVAL_UNSET,
|
||||
val lastCheck: Long = LAST_CHECK_NONE,
|
||||
val reason: String? = null,
|
||||
|
@ -39,7 +42,7 @@ data class ServerModel(
|
|||
val validationContent: String? = null,
|
||||
val disabled: Boolean = false,
|
||||
val networkTimeout: Int = 0
|
||||
) : IdProvider {
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val TABLE_NAME = "server_models"
|
||||
|
@ -59,31 +62,65 @@ data class ServerModel(
|
|||
|
||||
fun pull(cursor: Cursor): ServerModel {
|
||||
return ServerModel(
|
||||
id = cursor.getInt(cursor.getColumnIndex(COLUMN_ID)),
|
||||
name = cursor.getString(cursor.getColumnIndex(COLUMN_NAME)),
|
||||
url = cursor.getString(cursor.getColumnIndex(COLUMN_URL)),
|
||||
status = cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS)).toServerStatus(),
|
||||
checkInterval = cursor.getLong(cursor.getColumnIndex(COLUMN_CHECK_INTERVAL)),
|
||||
lastCheck = cursor.getLong(cursor.getColumnIndex(COLUMN_LAST_CHECK)),
|
||||
reason = cursor.getString(cursor.getColumnIndex(COLUMN_REASON)),
|
||||
id = cursor.getInt(
|
||||
cursor.getColumnIndex(
|
||||
COLUMN_ID
|
||||
)
|
||||
),
|
||||
name = cursor.getString(
|
||||
cursor.getColumnIndex(
|
||||
COLUMN_NAME
|
||||
)
|
||||
),
|
||||
url = cursor.getString(
|
||||
cursor.getColumnIndex(
|
||||
COLUMN_URL
|
||||
)
|
||||
),
|
||||
status = cursor.getInt(
|
||||
cursor.getColumnIndex(
|
||||
COLUMN_STATUS
|
||||
)
|
||||
).toSiteStatus(),
|
||||
checkInterval = cursor.getLong(
|
||||
cursor.getColumnIndex(
|
||||
COLUMN_CHECK_INTERVAL
|
||||
)
|
||||
),
|
||||
lastCheck = cursor.getLong(
|
||||
cursor.getColumnIndex(
|
||||
COLUMN_LAST_CHECK
|
||||
)
|
||||
),
|
||||
reason = cursor.getString(
|
||||
cursor.getColumnIndex(
|
||||
COLUMN_REASON
|
||||
)
|
||||
),
|
||||
validationMode = cursor.getInt(
|
||||
cursor.getColumnIndex(COLUMN_VALIDATION_MODE)
|
||||
cursor.getColumnIndex(
|
||||
COLUMN_VALIDATION_MODE
|
||||
)
|
||||
).toValidationMode(),
|
||||
validationContent = cursor.getString(cursor.getColumnIndex(COLUMN_VALIDATION_CONTENT)),
|
||||
disabled = cursor.getInt(cursor.getColumnIndex(COLUMN_DISABLED)) == 1,
|
||||
networkTimeout = cursor.getInt(cursor.getColumnIndex(COLUMN_NETWORK_TIMEOUT))
|
||||
validationContent = cursor.getString(
|
||||
cursor.getColumnIndex(
|
||||
COLUMN_VALIDATION_CONTENT
|
||||
)
|
||||
),
|
||||
disabled = cursor.getInt(
|
||||
cursor.getColumnIndex(
|
||||
COLUMN_DISABLED
|
||||
)
|
||||
) == 1,
|
||||
networkTimeout = cursor.getInt(
|
||||
cursor.getColumnIndex(
|
||||
COLUMN_NETWORK_TIMEOUT
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun id() = id
|
||||
|
||||
fun intervalText(): String {
|
||||
val now = currentTimeMillis()
|
||||
val nextCheck = max(lastCheck, 0) + checkInterval
|
||||
return (nextCheck - now).timeString()
|
||||
}
|
||||
|
||||
fun toContentValues() = ContentValues().apply {
|
||||
put(COLUMN_NAME, name)
|
||||
put(COLUMN_URL, url)
|
|
@ -13,12 +13,13 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.engine.db
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.afollestad.nocknock.data.legacy
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
|
||||
private const val SQL_CREATE_ENTRIES =
|
||||
"CREATE TABLE ${ServerModel.TABLE_NAME} (" +
|
||||
|
@ -38,8 +39,10 @@ private const val SQL_CREATE_ENTRIES =
|
|||
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${ServerModel.TABLE_NAME}"
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class ServerModelDbHelper(context: Context) : SQLiteOpenHelper(
|
||||
context, DATABASE_NAME, null, DATABASE_VERSION
|
||||
@Deprecated("Use AppDatabase.")
|
||||
internal class ServerModelDbHelper(context: Context) : SQLiteOpenHelper(
|
||||
context, DATABASE_NAME, null,
|
||||
DATABASE_VERSION
|
||||
) {
|
||||
companion object {
|
||||
const val DATABASE_VERSION = 3
|
||||
|
@ -71,6 +74,5 @@ class ServerModelDbHelper(context: Context) : SQLiteOpenHelper(
|
|||
|
||||
fun wipe() {
|
||||
this.writableDatabase.execSQL(SQL_DELETE_ENTRIES)
|
||||
onCreate(this.writableDatabase)
|
||||
}
|
||||
}
|
|
@ -13,42 +13,24 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.engine.db
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.afollestad.nocknock.data.legacy
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerModel.Companion.COLUMN_ID
|
||||
import com.afollestad.nocknock.data.ServerModel.Companion.DEFAULT_SORT_ORDER
|
||||
import com.afollestad.nocknock.data.ServerModel.Companion.TABLE_NAME
|
||||
import com.afollestad.nocknock.utilities.ext.diffFrom
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import javax.inject.Inject
|
||||
import timber.log.Timber.d as log
|
||||
import timber.log.Timber.w as warn
|
||||
import com.afollestad.nocknock.data.legacy.ServerModel.Companion.COLUMN_ID
|
||||
import com.afollestad.nocknock.data.legacy.ServerModel.Companion.DEFAULT_SORT_ORDER
|
||||
import com.afollestad.nocknock.data.legacy.ServerModel.Companion.TABLE_NAME
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface ServerModelStore {
|
||||
|
||||
suspend fun get(id: Int? = null): List<ServerModel>
|
||||
|
||||
suspend fun put(model: ServerModel): ServerModel
|
||||
|
||||
suspend fun update(model: ServerModel): Int
|
||||
|
||||
suspend fun delete(model: ServerModel): Int
|
||||
|
||||
suspend fun delete(id: Int): Int
|
||||
|
||||
suspend fun deleteAll(): Int
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class RealServerModelStore @Inject constructor(app: Application) : ServerModelStore {
|
||||
@Deprecated("Deprecated in favor of AppDatabase.")
|
||||
internal class ServerModelStore(app: Application) {
|
||||
|
||||
private val dbHelper = ServerModelDbHelper(app)
|
||||
|
||||
override suspend fun get(id: Int?): List<ServerModel> {
|
||||
fun get(id: Int? = null): List<ServerModel> {
|
||||
if (id == null) {
|
||||
return getAll()
|
||||
}
|
||||
|
@ -88,19 +70,16 @@ class RealServerModelStore @Inject constructor(app: Application) : ServerModelSt
|
|||
cursor.use { return readModels(it) }
|
||||
}
|
||||
|
||||
override suspend fun put(model: ServerModel): ServerModel {
|
||||
fun put(model: ServerModel): ServerModel {
|
||||
check(model.id == 0) { "Cannot put a model that already has an ID." }
|
||||
|
||||
val writer = dbHelper.writableDatabase
|
||||
val newId = writer.insert(TABLE_NAME, null, model.toContentValues())
|
||||
|
||||
return model.copy(id = newId.toInt())
|
||||
.apply {
|
||||
log("Inserted new site model: $this")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(model: ServerModel): Int {
|
||||
fun update(model: ServerModel): Int {
|
||||
check(model.id != 0) { "Cannot update a model that does not have an ID." }
|
||||
|
||||
val oldModel = get(model.id).single()
|
||||
|
@ -111,35 +90,27 @@ class RealServerModelStore @Inject constructor(app: Application) : ServerModelSt
|
|||
val valuesDiff = oldValues.diffFrom(newValues)
|
||||
|
||||
if (valuesDiff.size() == 0) {
|
||||
warn("Nothing has changed - nothing to update!")
|
||||
return 0
|
||||
}
|
||||
|
||||
val selection = "$COLUMN_ID = ?"
|
||||
val selectionArgs = arrayOf("${model.id}")
|
||||
|
||||
log("Updated model: $model")
|
||||
return writer.update(TABLE_NAME, valuesDiff, selection, selectionArgs)
|
||||
}
|
||||
|
||||
override suspend fun delete(model: ServerModel) = delete(model.id)
|
||||
fun delete(model: ServerModel) = delete(model.id)
|
||||
|
||||
override suspend fun delete(id: Int): Int {
|
||||
fun delete(id: Int): Int {
|
||||
check(id != 0) { "Cannot delete a model that doesn't have an ID." }
|
||||
|
||||
val selection = "$COLUMN_ID = ?"
|
||||
val selectionArgs = arrayOf("$id")
|
||||
|
||||
log("Deleted model: $id")
|
||||
return dbHelper.writableDatabase.delete(TABLE_NAME, selection, selectionArgs)
|
||||
}
|
||||
|
||||
override suspend fun deleteAll(): Int {
|
||||
log("Deleted all models")
|
||||
return dbHelper.writableDatabase.delete(TABLE_NAME, null, null)
|
||||
}
|
||||
|
||||
@TestOnly fun db() = dbHelper
|
||||
fun wipe() = dbHelper.wipe()
|
||||
|
||||
private fun readModels(cursor: Cursor): List<ServerModel> {
|
||||
val results = mutableListOf<ServerModel>()
|
||||
|
@ -148,4 +119,45 @@ class RealServerModelStore @Inject constructor(app: Application) : ServerModelSt
|
|||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [ContentValues] instance which contains only values that have changed between
|
||||
* the receiver (original) and parameter (new) instances.
|
||||
*/
|
||||
private fun ContentValues.diffFrom(contentValues: ContentValues): ContentValues {
|
||||
val diff = ContentValues()
|
||||
for ((name, oldValue) in this.valueSet()) {
|
||||
val newValue = contentValues.get(name)
|
||||
if (newValue != oldValue) {
|
||||
diff.putAny(name, newValue)
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto casts an [Any] value and uses the appropriate `put` method to store it
|
||||
* in the [ContentValues] instance.
|
||||
*/
|
||||
private fun ContentValues.putAny(
|
||||
name: String,
|
||||
value: Any?
|
||||
) {
|
||||
if (value == null) {
|
||||
putNull(name)
|
||||
return
|
||||
}
|
||||
when (value) {
|
||||
is String -> put(name, value)
|
||||
is Byte -> put(name, value)
|
||||
is Short -> put(name, value)
|
||||
is Int -> put(name, value)
|
||||
is Long -> put(name, value)
|
||||
is Float -> put(name, value)
|
||||
is Double -> put(name, value)
|
||||
is Boolean -> put(name, value)
|
||||
is ByteArray -> put(name, value)
|
||||
else -> throw IllegalArgumentException("ContentValues can't hold $value")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.data.model
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun fromStatus(status: Status): Int {
|
||||
return status.value
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toStatus(raw: Int): Status {
|
||||
return Status.fromValue(raw)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromValidationMode(mode: ValidationMode): Int {
|
||||
return mode.value
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toValidationMode(raw: Int): ValidationMode {
|
||||
return ValidationMode.fromValue(raw)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.utilities.ext.timeString
|
||||
import com.afollestad.nocknock.utilities.providers.CanNotifyModel
|
||||
import java.lang.System.currentTimeMillis
|
||||
import kotlin.math.max
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Entity(tableName = "sites")
|
||||
data class Site(
|
||||
/** The site's unique ID. */
|
||||
@PrimaryKey(autoGenerate = true) var id: Long = 0,
|
||||
/** The site's user-given name. */
|
||||
var name: String,
|
||||
/** The URl at which validation attempts are made to. */
|
||||
var url: String,
|
||||
/** Settings for the site. */
|
||||
@Ignore var settings: SiteSettings?,
|
||||
/** The last validation attempt result for the site, if any. */
|
||||
@Ignore var lastResult: ValidationResult?
|
||||
) : CanNotifyModel {
|
||||
|
||||
constructor() : this(0, "", "", null, null)
|
||||
|
||||
override fun notiId(): Int = id.toInt()
|
||||
|
||||
override fun notiName(): String = name
|
||||
|
||||
override fun notiTag(): String = url
|
||||
|
||||
fun intervalText(): String {
|
||||
requireNotNull(settings) { "Settings not queried." }
|
||||
val lastCheck = lastResult?.timestampMs ?: -1
|
||||
val checkInterval = settings!!.validationIntervalMs
|
||||
val now = System.currentTimeMillis()
|
||||
val nextCheck = max(lastCheck, 0) + checkInterval
|
||||
return (nextCheck - now).timeString()
|
||||
}
|
||||
|
||||
fun withStatus(
|
||||
status: Status? = null,
|
||||
reason: String? = null,
|
||||
timestamp: Long? = null
|
||||
): Site {
|
||||
val newLastResult = lastResult?.copy(
|
||||
status = status ?: lastResult!!.status,
|
||||
reason = reason,
|
||||
timestampMs = timestamp ?: lastResult!!.timestampMs
|
||||
) ?: ValidationResult(
|
||||
siteId = this.id,
|
||||
timestampMs = timestamp ?: currentTimeMillis(),
|
||||
status = status ?: WAITING,
|
||||
reason = reason
|
||||
)
|
||||
return this.copy(lastResult = newLastResult)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* 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:Suppress("unused")
|
||||
|
||||
package com.afollestad.nocknock.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Represents the current user configuration for a [Site].
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
@Entity(tableName = "site_settings")
|
||||
data class SiteSettings(
|
||||
/** The [Site] these settings belong to. */
|
||||
@PrimaryKey(autoGenerate = false) var siteId: Long = 0,
|
||||
/** How often a validation attempt is made, in milliseconds. */
|
||||
var validationIntervalMs: Long,
|
||||
/** The method of which is used to validate the [Site]. */
|
||||
var validationMode: ValidationMode,
|
||||
/** Args that are used for the [ValidationMode], e.g. a search term. */
|
||||
var validationArgs: String?,
|
||||
/** Whether or not the [Site] is enabled for automatic periodic checks. */
|
||||
var disabled: Boolean,
|
||||
/** The network response timeout for validation attempts. */
|
||||
var networkTimeout: Int
|
||||
) : Serializable {
|
||||
|
||||
constructor() : this(0, 0, STATUS_CODE, null, false, 0)
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.data.model
|
||||
|
||||
import com.afollestad.nocknock.data.R.string
|
||||
import com.afollestad.nocknock.data.model.Status.CHECKING
|
||||
import com.afollestad.nocknock.data.model.Status.OK
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
|
||||
/**
|
||||
* Represents the current status of a [Site] - or whether or not the
|
||||
* site passed its most recent check.
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
enum class Status(val value: Int) {
|
||||
/** The site has not been validated yet, pending the background job. */
|
||||
WAITING(1),
|
||||
/** The site is currently being validated. */
|
||||
CHECKING(2),
|
||||
/** The most recent validation attempt passed. */
|
||||
OK(3),
|
||||
/** The site did not pass a recent validation attempt. */
|
||||
ERROR(4);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int) = when (value) {
|
||||
OK.value -> OK
|
||||
WAITING.value -> WAITING
|
||||
CHECKING.value -> CHECKING
|
||||
ERROR.value -> ERROR
|
||||
else -> throw IllegalArgumentException("Unknown status: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Status.textRes() = when (this) {
|
||||
OK -> string.everything_checks_out
|
||||
WAITING -> string.waiting
|
||||
CHECKING -> string.checking_status
|
||||
else -> 0
|
||||
}
|
||||
|
||||
fun Status?.isPending() = this == WAITING || this == CHECKING
|
||||
|
||||
fun Int.toSiteStatus() = Status.fromValue(this)
|
|
@ -13,12 +13,20 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.data
|
||||
package com.afollestad.nocknock.data.model
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
/**
|
||||
* Represents the validation mode of a [Site] - this is the type of
|
||||
* check that is performed to get the site's current [Status].
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
enum class ValidationMode(val value: Int) {
|
||||
/** The site is running normally if its status code is successful. */
|
||||
STATUS_CODE(1),
|
||||
/** The site is running normally if a piece of text is found in its response body. */
|
||||
TERM_SEARCH(2),
|
||||
/** The site is running normally if a block of given JavaScript executes successfully. */
|
||||
JAVASCRIPT(3);
|
||||
|
||||
companion object {
|
||||
|
@ -39,6 +47,8 @@ enum class ValidationMode(val value: Int) {
|
|||
}
|
||||
}
|
||||
|
||||
fun Int.toValidationMode() = ValidationMode.fromValue(this)
|
||||
fun Int.toValidationMode() =
|
||||
ValidationMode.fromValue(this)
|
||||
|
||||
fun Int.indexToValidationMode() = ValidationMode.fromIndex(this)
|
||||
fun Int.indexToValidationMode() =
|
||||
ValidationMode.fromIndex(this)
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* 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:Suppress("unused")
|
||||
|
||||
package com.afollestad.nocknock.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.afollestad.nocknock.data.model.Status.OK
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Represents the most recent validation result for a [Site].
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
@Entity(tableName = "validation_results")
|
||||
data class ValidationResult(
|
||||
/** The [Site] that this result belongs to. */
|
||||
@PrimaryKey(autoGenerate = false) var siteId: Long = 0,
|
||||
/** The timestamp in milliseconds at which this attempt was made. */
|
||||
var timestampMs: Long,
|
||||
/** The result of this validation attempt. */
|
||||
var status: Status,
|
||||
/** If the attempt was not successful, why it was not successful. */
|
||||
var reason: String?
|
||||
) : Serializable {
|
||||
|
||||
constructor(): this(0, 0, OK, null)
|
||||
}
|
|
@ -15,7 +15,9 @@ ext.versions = [
|
|||
dagger : '2.19',
|
||||
kotlin : '1.3.10',
|
||||
coroutines : '1.0.1',
|
||||
|
||||
androidx : '1.0.0',
|
||||
room : '2.0.0',
|
||||
|
||||
rxBinding : '3.0.0-alpha1',
|
||||
|
||||
|
@ -23,9 +25,10 @@ ext.versions = [
|
|||
rxkPrefs : '1.2.0',
|
||||
|
||||
timber : '4.7.1',
|
||||
testRunner : '1.0.2',
|
||||
junit : '4.12',
|
||||
mockito : '2.23.0',
|
||||
mockitoKotlin : '2.0.0-RC1',
|
||||
truth : '0.42'
|
||||
truth : '0.42',
|
||||
androidxTestRunner: '1.1.0',
|
||||
androidxTest : '1.0.0',
|
||||
]
|
||||
|
|
|
@ -10,8 +10,6 @@ android {
|
|||
targetSdkVersion versions.compileSdk
|
||||
versionCode versions.publishVersionCode
|
||||
versionName versions.publishVersion
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,9 +32,6 @@ dependencies {
|
|||
testImplementation 'org.mockito:mockito-core:' + versions.mockito
|
||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin
|
||||
testImplementation 'com.google.truth:truth:' + versions.truth
|
||||
|
||||
androidTestImplementation 'com.android.support.test:runner:' + versions.testRunner
|
||||
androidTestImplementation 'com.google.truth:truth:' + versions.truth
|
||||
}
|
||||
|
||||
apply from: '../spotless.gradle'
|
|
@ -1,129 +0,0 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.engine
|
||||
|
||||
import android.app.Application
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.CHECKING
|
||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.RealServerModelStore
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import android.support.test.InstrumentationRegistry.getTargetContext as context
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ServerModelStoreTest {
|
||||
|
||||
private lateinit var store: RealServerModelStore
|
||||
|
||||
@Before fun setup() {
|
||||
store = RealServerModelStore(context().applicationContext as Application)
|
||||
store.db()
|
||||
.wipe()
|
||||
}
|
||||
|
||||
@Test fun get() = runBlocking {
|
||||
// Put some fake data to retrieve
|
||||
store.put(fakeModel(1))
|
||||
val model2 = store.put(fakeModel(2))
|
||||
|
||||
val model = store.get(2)
|
||||
.single()
|
||||
assertThat(model).isEqualTo(model2.copy(id = 2))
|
||||
}
|
||||
|
||||
@Test fun getAll() = runBlocking {
|
||||
// Put some fake data to retrieve
|
||||
val model1 = store.put(fakeModel(1))
|
||||
val model2 = store.put(fakeModel(2))
|
||||
|
||||
val models = store.get()
|
||||
assertThat(models.size).isEqualTo(2)
|
||||
assertThat(models[0]).isEqualTo(model1.copy(id = 1))
|
||||
assertThat(models[1]).isEqualTo(model2.copy(id = 2))
|
||||
}
|
||||
|
||||
@Test fun update() = runBlocking {
|
||||
store.put(
|
||||
ServerModel(
|
||||
name = "Wakanda Forever",
|
||||
url = "https://www.wakanda.gov",
|
||||
status = ERROR,
|
||||
checkInterval = 5,
|
||||
lastCheck = 10,
|
||||
reason = "Body doesn't contain your term.",
|
||||
validationMode = TERM_SEARCH,
|
||||
validationContent = "Vibranium",
|
||||
disabled = false
|
||||
)
|
||||
)
|
||||
store.put(fakeModel(2))
|
||||
|
||||
val originalModel1 = store.get(id = 1)
|
||||
.single()
|
||||
|
||||
val defaultJs = "var responseObj = JSON.parse(response);\\nreturn responseObj.success === true;"
|
||||
val newModel1 = originalModel1.copy(
|
||||
name = "HYDRA",
|
||||
url = "https://www.hyrda.dict",
|
||||
status = CHECKING,
|
||||
checkInterval = 10,
|
||||
lastCheck = 20,
|
||||
reason = "Evaluation failed.",
|
||||
validationMode = JAVASCRIPT,
|
||||
validationContent = defaultJs,
|
||||
disabled = true
|
||||
)
|
||||
assertThat(store.update(newModel1)).isEqualTo(1)
|
||||
|
||||
val newModels = store.get()
|
||||
assertThat(newModels.size).isEqualTo(2)
|
||||
assertThat(newModels.first()).isEqualTo(newModel1)
|
||||
}
|
||||
|
||||
@Test fun delete() = runBlocking {
|
||||
// Put some fake data to delete
|
||||
val model1 = store.put(fakeModel(1))
|
||||
val model2 = store.put(fakeModel(2))
|
||||
|
||||
assertThat(store.delete(model1)).isEqualTo(1)
|
||||
|
||||
val newModels = store.get()
|
||||
assertThat(newModels.single()).isEqualTo(model2)
|
||||
}
|
||||
|
||||
@Test fun deleteAll() = runBlocking {
|
||||
// Put some fake data to delete
|
||||
store.put(fakeModel(1))
|
||||
store.put(fakeModel(2))
|
||||
|
||||
store.deleteAll()
|
||||
assertThat(store.get()).isEmpty()
|
||||
}
|
||||
|
||||
private fun fakeModel(index: Int) = ServerModel(
|
||||
name = "Model $index",
|
||||
url = "https://hello.com/$index",
|
||||
validationMode = STATUS_CODE
|
||||
)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -13,27 +13,19 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.di
|
||||
package com.afollestad.nocknock.engine;
|
||||
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.utilities.qualifiers.AppIconRes
|
||||
import com.afollestad.nocknock.utilities.qualifiers.MainActivityClass
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
import com.afollestad.nocknock.engine.statuscheck.RealValidationManager;
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationManager;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Module
|
||||
open class MainModule {
|
||||
public abstract class EngineModule {
|
||||
|
||||
@Provides
|
||||
@Binds
|
||||
@Singleton
|
||||
@AppIconRes
|
||||
fun provideAppIconRes(): Int = R.mipmap.ic_launcher
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@MainActivityClass
|
||||
fun provideMainActivityClass(): Class<*> = MainActivity::class.java
|
||||
abstract ValidationManager provideCheckStatusManager(RealValidationManager checkStatusManager);
|
||||
}
|
|
@ -31,7 +31,7 @@ import timber.log.Timber.d as log
|
|||
/** @author Aidan Follestad (@afollestad) */
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var checkStatusManager: CheckStatusManager
|
||||
@Inject lateinit var checkStatusManager: ValidationManager
|
||||
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
|
|
|
@ -18,17 +18,20 @@ package com.afollestad.nocknock.engine.statuscheck
|
|||
import android.app.job.JobParameters
|
||||
import android.app.job.JobService
|
||||
import android.content.Intent
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus
|
||||
import com.afollestad.nocknock.data.ServerStatus.CHECKING
|
||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.isPending
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.Status
|
||||
import com.afollestad.nocknock.data.model.Status.CHECKING
|
||||
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||
import com.afollestad.nocknock.data.model.Status.OK
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.model.isPending
|
||||
import com.afollestad.nocknock.data.getSite
|
||||
import com.afollestad.nocknock.data.updateSite
|
||||
import com.afollestad.nocknock.engine.BuildConfig.APPLICATION_ID
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.js.JavaScript
|
||||
|
@ -42,8 +45,12 @@ import java.lang.System.currentTimeMillis
|
|||
import javax.inject.Inject
|
||||
import timber.log.Timber.d as log
|
||||
|
||||
/** @author Aidan Follestad (@afollestad)*/
|
||||
class CheckStatusJob : JobService() {
|
||||
/**
|
||||
* The job which is sent to the system JobScheduler to perform site validation in the background.
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
class ValidationJob : JobService() {
|
||||
|
||||
companion object {
|
||||
const val ACTION_STATUS_UPDATE = "$APPLICATION_ID.STATUS_UPDATE"
|
||||
|
@ -52,47 +59,50 @@ class CheckStatusJob : JobService() {
|
|||
const val KEY_SITE_ID = "site.id"
|
||||
}
|
||||
|
||||
@Inject lateinit var modelStore: ServerModelStore
|
||||
@Inject lateinit var checkStatusManager: CheckStatusManager
|
||||
@Inject lateinit var database: AppDatabase
|
||||
@Inject lateinit var checkStatusManager: ValidationManager
|
||||
@Inject lateinit var notificationManager: NockNotificationManager
|
||||
|
||||
override fun onStartJob(params: JobParameters): Boolean {
|
||||
injector().injectInto(this)
|
||||
val siteId = params.extras.getInt(KEY_SITE_ID)
|
||||
val siteId = params.extras.getLong(KEY_SITE_ID)
|
||||
|
||||
GlobalScope.launch(Main) {
|
||||
val sites = async(IO) { modelStore.get(id = siteId) }.await()
|
||||
if (sites.isEmpty()) {
|
||||
log("Unable to find any sites for ID $siteId, this job will not be rescheduled.")
|
||||
val site = async(IO) { database.getSite(siteId) }.await()
|
||||
if (site == null) {
|
||||
log("Unable to find a site for ID $siteId, this job will not be rescheduled.")
|
||||
return@launch jobFinished(params, false)
|
||||
}
|
||||
|
||||
val site = sites.single()
|
||||
val siteSettings = site.settings
|
||||
requireNotNull(siteSettings) { "Site settings must be populated." }
|
||||
|
||||
log("Performing status checks on site ${site.id}...")
|
||||
sendBroadcast(Intent(ACTION_JOB_RUNNING).apply { putExtra(KEY_SITE_ID, site.id) })
|
||||
|
||||
log("Checking ${site.name} (${site.url})...")
|
||||
|
||||
val result = async(IO) {
|
||||
val jobResult = async(IO) {
|
||||
updateStatus(site, CHECKING)
|
||||
val checkResult = checkStatusManager.performCheck(site)
|
||||
val resultModel = checkResult.model
|
||||
val resultResponse = checkResult.response
|
||||
val result = resultModel.lastResult!!
|
||||
|
||||
if (resultModel.status != OK) {
|
||||
log("Got unsuccessful check status back: ${resultModel.reason}")
|
||||
if (result.status != OK) {
|
||||
log("Got unsuccessful check status back: ${result.reason}")
|
||||
return@async updateStatus(site = resultModel)
|
||||
} else {
|
||||
when (site.validationMode) {
|
||||
when (siteSettings.validationMode) {
|
||||
TERM_SEARCH -> {
|
||||
val body = resultResponse?.body()?.string() ?: ""
|
||||
log("Using TERM_SEARCH validation mode on body of length: ${body.length}")
|
||||
|
||||
return@async if (!body.contains(site.validationContent ?: "")) {
|
||||
return@async if (!body.contains(siteSettings.validationArgs ?: "")) {
|
||||
updateStatus(
|
||||
resultModel.copy(
|
||||
resultModel.withStatus(
|
||||
status = ERROR,
|
||||
reason = "Term \"${site.validationContent}\" not found in response body."
|
||||
reason = "Term \"${siteSettings.validationArgs}\" not found in response body."
|
||||
)
|
||||
)
|
||||
} else {
|
||||
|
@ -102,9 +112,9 @@ class CheckStatusJob : JobService() {
|
|||
JAVASCRIPT -> {
|
||||
val body = resultResponse?.body()?.string() ?: ""
|
||||
log("Using JAVASCRIPT validation mode on body of length: ${body.length}")
|
||||
val reason = JavaScript.eval(resultModel.validationContent ?: "", body)
|
||||
val reason = JavaScript.eval(siteSettings.validationArgs ?: "", body)
|
||||
return@async if (reason != null) {
|
||||
updateStatus(resultModel.copy(reason = reason), status = ERROR)
|
||||
updateStatus(resultModel.withStatus(reason = reason), status = ERROR)
|
||||
} else {
|
||||
resultModel
|
||||
}
|
||||
|
@ -113,27 +123,29 @@ class CheckStatusJob : JobService() {
|
|||
// We already know the status code is successful because we are in this else branch
|
||||
log("Using STATUS_CODE validation, which has passed!")
|
||||
updateStatus(
|
||||
resultModel.copy(
|
||||
resultModel.withStatus(
|
||||
status = OK,
|
||||
reason = null
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException("Unknown validation mode: ${site.validationMode}")
|
||||
throw IllegalArgumentException(
|
||||
"Unknown validation mode: ${siteSettings.validationArgs}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.await()
|
||||
|
||||
if (result.status == OK) {
|
||||
notificationManager.cancelStatusNotification(result)
|
||||
if (jobResult.lastResult!!.status == OK) {
|
||||
notificationManager.cancelStatusNotification(jobResult)
|
||||
} else {
|
||||
notificationManager.postStatusNotification(result)
|
||||
notificationManager.postStatusNotification(jobResult)
|
||||
}
|
||||
|
||||
checkStatusManager.scheduleCheck(
|
||||
site = result,
|
||||
site = jobResult,
|
||||
fromFinishingJob = true
|
||||
)
|
||||
}
|
||||
|
@ -148,28 +160,30 @@ class CheckStatusJob : JobService() {
|
|||
}
|
||||
|
||||
private suspend fun updateStatus(
|
||||
site: ServerModel,
|
||||
status: ServerStatus = site.status
|
||||
): ServerModel {
|
||||
site: Site,
|
||||
status: Status = site.lastResult?.status ?: WAITING
|
||||
): Site {
|
||||
log("Updating ${site.name} (${site.url}) status to $status...")
|
||||
|
||||
val lastCheckTime =
|
||||
if (status.isPending()) site.lastCheck
|
||||
if (status.isPending()) site.lastResult?.timestampMs ?: -1
|
||||
else currentTimeMillis()
|
||||
val reason =
|
||||
if (status == OK) null
|
||||
else site.reason
|
||||
else site.lastResult?.reason ?: "Unknown"
|
||||
|
||||
val newSiteModel = site.copy(
|
||||
val updatedModel = site.withStatus(
|
||||
status = status,
|
||||
lastCheck = lastCheckTime,
|
||||
timestamp = lastCheckTime,
|
||||
reason = reason
|
||||
)
|
||||
modelStore.update(newSiteModel)
|
||||
database.updateSite(updatedModel)
|
||||
|
||||
withContext(Main) {
|
||||
sendBroadcast(Intent(ACTION_STATUS_UPDATE).apply { putExtra(KEY_UPDATE_MODEL, newSiteModel) })
|
||||
sendBroadcast(Intent(ACTION_STATUS_UPDATE).apply {
|
||||
putExtra(KEY_UPDATE_MODEL, updatedModel)
|
||||
})
|
||||
}
|
||||
return newSiteModel
|
||||
return updatedModel
|
||||
}
|
||||
}
|
|
@ -17,12 +17,13 @@ package com.afollestad.nocknock.engine.statuscheck
|
|||
|
||||
import android.app.job.JobScheduler
|
||||
import android.app.job.JobScheduler.RESULT_SUCCESS
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.allSites
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||
import com.afollestad.nocknock.data.model.Status.OK
|
||||
import com.afollestad.nocknock.engine.R
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_SITE_ID
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_SITE_ID
|
||||
import com.afollestad.nocknock.utilities.providers.BundleProvider
|
||||
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
|
||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||
|
@ -37,37 +38,37 @@ import timber.log.Timber.d as log
|
|||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
data class CheckResult(
|
||||
val model: ServerModel,
|
||||
val model: Site,
|
||||
val response: Response? = null
|
||||
)
|
||||
|
||||
typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface CheckStatusManager {
|
||||
interface ValidationManager {
|
||||
|
||||
suspend fun ensureScheduledChecks()
|
||||
|
||||
fun scheduleCheck(
|
||||
site: ServerModel,
|
||||
site: Site,
|
||||
rightNow: Boolean = false,
|
||||
cancelPrevious: Boolean = rightNow,
|
||||
fromFinishingJob: Boolean = false
|
||||
)
|
||||
|
||||
fun cancelCheck(site: ServerModel)
|
||||
fun cancelCheck(site: Site)
|
||||
|
||||
suspend fun performCheck(site: ServerModel): CheckResult
|
||||
suspend fun performCheck(site: Site): CheckResult
|
||||
}
|
||||
|
||||
class RealCheckStatusManager @Inject constructor(
|
||||
class RealValidationManager @Inject constructor(
|
||||
private val jobScheduler: JobScheduler,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val stringProvider: StringProvider,
|
||||
private val bundleProvider: BundleProvider,
|
||||
private val jobInfoProvider: JobInfoProvider,
|
||||
private val siteStore: ServerModelStore
|
||||
) : CheckStatusManager {
|
||||
private val database: AppDatabase
|
||||
) : ValidationManager {
|
||||
|
||||
private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
|
||||
client.newBuilder()
|
||||
|
@ -76,12 +77,12 @@ class RealCheckStatusManager @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun ensureScheduledChecks() {
|
||||
val sites = siteStore.get()
|
||||
val sites = database.allSites()
|
||||
if (sites.isEmpty()) {
|
||||
return
|
||||
}
|
||||
log("Ensuring enabled sites have scheduled checks.")
|
||||
sites.filter { !it.disabled }
|
||||
sites.filter { it.settings?.disabled != true }
|
||||
.forEach { site ->
|
||||
val existingJob = jobForSite(site)
|
||||
if (existingJob == null) {
|
||||
|
@ -94,12 +95,15 @@ class RealCheckStatusManager @Inject constructor(
|
|||
}
|
||||
|
||||
override fun scheduleCheck(
|
||||
site: ServerModel,
|
||||
site: Site,
|
||||
rightNow: Boolean,
|
||||
cancelPrevious: Boolean,
|
||||
fromFinishingJob: Boolean
|
||||
) {
|
||||
check(site.id != 0) { "Cannot schedule checks for jobs with no ID." }
|
||||
check(site.id != 0L) { "Cannot schedule checks for jobs with no ID." }
|
||||
val siteSettings = site.settings
|
||||
requireNotNull(siteSettings) { "Site settings must be populated." }
|
||||
|
||||
if (cancelPrevious) {
|
||||
cancelCheck(site)
|
||||
} else if (!fromFinishingJob) {
|
||||
|
@ -111,18 +115,18 @@ class RealCheckStatusManager @Inject constructor(
|
|||
|
||||
log("Requesting a check job for site to be scheduled: $site")
|
||||
val extras = bundleProvider.createPersistable {
|
||||
putInt(KEY_SITE_ID, site.id)
|
||||
putLong(KEY_SITE_ID, site.id)
|
||||
}
|
||||
val jobInfo = jobInfoProvider.createCheckJob(
|
||||
id = site.id,
|
||||
id = site.id.toInt(),
|
||||
onlyUnmeteredNetwork = false,
|
||||
delayMs = if (rightNow) {
|
||||
1
|
||||
} else {
|
||||
site.checkInterval
|
||||
siteSettings.validationIntervalMs
|
||||
},
|
||||
extras = extras,
|
||||
target = CheckStatusJob::class.java
|
||||
target = ValidationJob::class.java
|
||||
)
|
||||
|
||||
val dispatchResult = jobScheduler.schedule(jobInfo)
|
||||
|
@ -133,15 +137,17 @@ class RealCheckStatusManager @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun cancelCheck(site: ServerModel) {
|
||||
check(site.id != 0) { "Cannot cancel scheduled checks for jobs with no ID." }
|
||||
override fun cancelCheck(site: Site) {
|
||||
check(site.id != 0L) { "Cannot cancel scheduled checks for jobs with no ID." }
|
||||
log("Cancelling scheduled checks for site: ${site.id}")
|
||||
jobScheduler.cancel(site.id)
|
||||
jobScheduler.cancel(site.id.toInt())
|
||||
}
|
||||
|
||||
override suspend fun performCheck(site: ServerModel): CheckResult {
|
||||
check(site.id != 0) { "Cannot schedule checks for jobs with no ID." }
|
||||
check(site.networkTimeout > 0) { "Network timeout not set for site ${site.id}" }
|
||||
override suspend fun performCheck(site: Site): CheckResult {
|
||||
check(site.id != 0L) { "Cannot schedule checks for jobs with no ID." }
|
||||
val siteSettings = site.settings
|
||||
requireNotNull(siteSettings) { "Site settings must be populated." }
|
||||
check(siteSettings.networkTimeout > 0) { "Network timeout not set for site ${site.id}" }
|
||||
log("performCheck(${site.id}) - GET ${site.url}")
|
||||
|
||||
val request = Request.Builder()
|
||||
|
@ -150,20 +156,20 @@ class RealCheckStatusManager @Inject constructor(
|
|||
.build()
|
||||
|
||||
return try {
|
||||
val client = clientTimeoutChanger(okHttpClient, site.networkTimeout)
|
||||
val client = clientTimeoutChanger(okHttpClient, siteSettings.networkTimeout)
|
||||
val response = client.newCall(request)
|
||||
.execute()
|
||||
|
||||
if (response.isSuccessful || response.code() == 401) {
|
||||
log("performCheck(${site.id}) = Successful")
|
||||
CheckResult(
|
||||
model = site.copy(status = OK, reason = null),
|
||||
model = site.withStatus(status = OK, reason = null),
|
||||
response = response
|
||||
)
|
||||
} else {
|
||||
log("performCheck(${site.id}) = Failure, HTTP code ${response.code()}")
|
||||
CheckResult(
|
||||
model = site.copy(
|
||||
model = site.withStatus(
|
||||
status = ERROR,
|
||||
reason = "Response ${response.code()} - ${response.body()?.string() ?: "Unknown"}"
|
||||
),
|
||||
|
@ -173,20 +179,20 @@ class RealCheckStatusManager @Inject constructor(
|
|||
} catch (timeoutEx: SocketTimeoutException) {
|
||||
log("performCheck(${site.id}) = Socket Timeout")
|
||||
CheckResult(
|
||||
model = site.copy(
|
||||
model = site.withStatus(
|
||||
status = ERROR,
|
||||
reason = stringProvider.get(R.string.timeout)
|
||||
)
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
log("performCheck(${site.id}) = Error: ${ex.message}")
|
||||
CheckResult(model = site.copy(status = ERROR, reason = ex.message))
|
||||
CheckResult(model = site.withStatus(status = ERROR, reason = ex.message))
|
||||
}
|
||||
}
|
||||
|
||||
private fun jobForSite(site: ServerModel) =
|
||||
private fun jobForSite(site: Site) =
|
||||
jobScheduler.allPendingJobs
|
||||
.firstOrNull { job -> job.id == site.id }
|
||||
.firstOrNull { job -> job.id == site.id.toInt() }
|
||||
|
||||
@TestOnly fun setClientTimeoutChanger(changer: ClientTimeoutChanger) {
|
||||
this.clientTimeoutChanger = changer
|
|
@ -17,13 +17,13 @@ package com.afollestad.nocknock.engine
|
|||
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_SITE_ID
|
||||
import com.afollestad.nocknock.engine.statuscheck.RealCheckStatusManager
|
||||
import com.afollestad.nocknock.data.legacy.ServerModel
|
||||
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||
import com.afollestad.nocknock.data.model.Status.OK
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.legacy.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_SITE_ID
|
||||
import com.afollestad.nocknock.engine.statuscheck.RealValidationManager
|
||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
|
@ -58,7 +58,7 @@ class CheckStatusManagerTest {
|
|||
private val jobInfoProvider = testJobInfoProvider()
|
||||
private val store = mock<ServerModelStore>()
|
||||
|
||||
private val manager = RealCheckStatusManager(
|
||||
private val manager = RealValidationManager(
|
||||
jobScheduler,
|
||||
okHttpClient,
|
||||
stringProvider,
|
||||
|
|
|
@ -14,7 +14,6 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':data')
|
||||
implementation project(':utilities')
|
||||
|
||||
api 'androidx.appcompat:appcompat:' + versions.androidx
|
||||
|
|
|
@ -18,9 +18,9 @@ package com.afollestad.nocknock.notifications
|
|||
import android.annotation.TargetApi
|
||||
import android.app.NotificationManager
|
||||
import android.os.Build.VERSION_CODES
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.notifications.Channel.CheckFailures
|
||||
import com.afollestad.nocknock.utilities.providers.BitmapProvider
|
||||
import com.afollestad.nocknock.utilities.providers.CanNotifyModel
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
|
||||
import com.afollestad.nocknock.utilities.providers.NotificationProvider
|
||||
|
@ -37,9 +37,9 @@ interface NockNotificationManager {
|
|||
|
||||
fun createChannels()
|
||||
|
||||
fun postStatusNotification(model: ServerModel)
|
||||
fun postStatusNotification(model: CanNotifyModel)
|
||||
|
||||
fun cancelStatusNotification(model: ServerModel)
|
||||
fun cancelStatusNotification(model: CanNotifyModel)
|
||||
|
||||
fun cancelStatusNotifications()
|
||||
}
|
||||
|
@ -65,32 +65,32 @@ class RealNockNotificationManager @Inject constructor(
|
|||
override fun createChannels() =
|
||||
Channel.values().forEach(this::createChannel)
|
||||
|
||||
override fun postStatusNotification(model: ServerModel) {
|
||||
override fun postStatusNotification(model: CanNotifyModel) {
|
||||
if (isAppOpen) {
|
||||
// Don't show notifications while the app is open
|
||||
log("App is open, status notification for site ${model.id} won't be posted.")
|
||||
log("App is open, status notification for site ${model.notiId()} won't be posted.")
|
||||
return
|
||||
}
|
||||
|
||||
log("Posting status notification for site ${model.id}...")
|
||||
log("Posting status notification for site ${model.notiId()}...")
|
||||
val intent = intentProvider.getPendingIntentForViewSite(model)
|
||||
|
||||
val newNotification = notificationProvider.create(
|
||||
channelId = CheckFailures.id,
|
||||
title = model.name,
|
||||
title = model.notiName(),
|
||||
content = stringProvider.get(R.string.something_wrong),
|
||||
intent = intent,
|
||||
smallIcon = R.drawable.ic_notification,
|
||||
largeIcon = bitmapProvider.get(appIconRes)
|
||||
)
|
||||
|
||||
stockManager.notify(model.url, model.notificationId(), newNotification)
|
||||
stockManager.notify(model.notiTag(), model.notificationId(), newNotification)
|
||||
log("Posted status notification for site ${model.notificationId()}.")
|
||||
}
|
||||
|
||||
override fun cancelStatusNotification(model: ServerModel) {
|
||||
override fun cancelStatusNotification(model: CanNotifyModel) {
|
||||
stockManager.cancel(model.notificationId())
|
||||
log("Cancelled status notification for site ${model.id}.")
|
||||
log("Cancelled status notification for site ${model.notiId()}.")
|
||||
}
|
||||
|
||||
override fun cancelStatusNotifications() = stockManager.cancelAll()
|
||||
|
@ -107,5 +107,5 @@ class RealNockNotificationManager @Inject constructor(
|
|||
log("Created notification channel ${channel.id}")
|
||||
}
|
||||
|
||||
private fun ServerModel.notificationId() = BASE_NOTIFICATION_REQUEST_CODE + this.id
|
||||
private fun CanNotifyModel.notificationId() = BASE_NOTIFICATION_REQUEST_CODE + this.notiId()
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
/*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -13,19 +13,18 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.notifications
|
||||
package com.afollestad.nocknock.notifications;
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import javax.inject.Singleton
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Module
|
||||
abstract class NotificationsModule {
|
||||
public abstract class NotificationsModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideNockNotificationManager(
|
||||
notificationManager: RealNockNotificationManager
|
||||
): NockNotificationManager
|
||||
abstract NockNotificationManager provideNockNotificationManager(
|
||||
RealNockNotificationManager notificationManager);
|
||||
}
|
|
@ -20,8 +20,8 @@ import android.app.NotificationChannel
|
|||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.graphics.Bitmap
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.legacy.ServerModel
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.notifications.Channel.CheckFailures
|
||||
import com.afollestad.nocknock.utilities.providers.BitmapProvider
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
/*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* 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
|
||||
* 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,
|
||||
|
@ -13,77 +13,63 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.utilities
|
||||
package com.afollestad.nocknock.utilities;
|
||||
|
||||
import com.afollestad.nocknock.utilities.providers.BitmapProvider
|
||||
import com.afollestad.nocknock.utilities.providers.BundleProvider
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
|
||||
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
|
||||
import com.afollestad.nocknock.utilities.providers.NotificationProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealBitmapProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealBundleProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealIntentProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealJobInfoProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealNotificationChannelProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealNotificationProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealSdkProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealStringProvider
|
||||
import com.afollestad.nocknock.utilities.providers.SdkProvider
|
||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import javax.inject.Singleton
|
||||
import com.afollestad.nocknock.utilities.providers.BitmapProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.BundleProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.JobInfoProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.NotificationProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.RealBitmapProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.RealBundleProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.RealIntentProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.RealJobInfoProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.RealNotificationChannelProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.RealNotificationProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.RealSdkProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.RealStringProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.SdkProvider;
|
||||
import com.afollestad.nocknock.utilities.providers.StringProvider;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Module
|
||||
abstract class UtilitiesModule {
|
||||
public abstract class UtilitiesModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideSdkProvider(
|
||||
sdkProvider: RealSdkProvider
|
||||
): SdkProvider
|
||||
abstract SdkProvider provideSdkProvider(RealSdkProvider sdkProvider);
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideBitmapProvider(
|
||||
bitmapProvider: RealBitmapProvider
|
||||
): BitmapProvider
|
||||
abstract BitmapProvider provideBitmapProvider(RealBitmapProvider bitmapProvider);
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideStringProvider(
|
||||
stringProvider: RealStringProvider
|
||||
): StringProvider
|
||||
abstract StringProvider provideStringProvider(RealStringProvider stringProvider);
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideIntentProvider(
|
||||
intentProvider: RealIntentProvider
|
||||
): IntentProvider
|
||||
abstract IntentProvider provideIntentProvider(RealIntentProvider intentProvider);
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideChannelProvider(
|
||||
channelProvider: RealNotificationChannelProvider
|
||||
): NotificationChannelProvider
|
||||
abstract NotificationChannelProvider provideChannelProvider(
|
||||
RealNotificationChannelProvider channelProvider);
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideNotificationProvider(
|
||||
notificationProvider: RealNotificationProvider
|
||||
): NotificationProvider
|
||||
abstract NotificationProvider provideNotificationProvider(
|
||||
RealNotificationProvider notificationProvider);
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideBundleProvider(
|
||||
bundleProvider: RealBundleProvider
|
||||
): BundleProvider
|
||||
abstract BundleProvider provideBundleProvider(RealBundleProvider bundleProvider);
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideJobInfoProvider(
|
||||
jobInfoProvider: RealJobInfoProvider
|
||||
): JobInfoProvider
|
||||
abstract JobInfoProvider provideJobInfoProvider(RealJobInfoProvider jobInfoProvider);
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.utilities.ext
|
||||
|
||||
import android.content.ContentValues
|
||||
|
||||
/**
|
||||
* Returns a [ContentValues] instance which contains only values that have changed between
|
||||
* the receiver (original) and parameter (new) instances.
|
||||
*/
|
||||
fun ContentValues.diffFrom(contentValues: ContentValues): ContentValues {
|
||||
val diff = ContentValues()
|
||||
for ((name, oldValue) in this.valueSet()) {
|
||||
val newValue = contentValues.get(name)
|
||||
if (newValue != oldValue) {
|
||||
diff.putAny(name, newValue)
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto casts an [Any] value and uses the appropriate `put` method to store it
|
||||
* in the [ContentValues] instance.
|
||||
*/
|
||||
fun ContentValues.putAny(
|
||||
name: String,
|
||||
value: Any?
|
||||
) {
|
||||
if (value == null) {
|
||||
putNull(name)
|
||||
return
|
||||
}
|
||||
when (value) {
|
||||
is String -> put(name, value)
|
||||
is Byte -> put(name, value)
|
||||
is Short -> put(name, value)
|
||||
is Int -> put(name, value)
|
||||
is Long -> put(name, value)
|
||||
is Float -> put(name, value)
|
||||
is Double -> put(name, value)
|
||||
is Boolean -> put(name, value)
|
||||
is ByteArray -> put(name, value)
|
||||
else -> throw IllegalArgumentException("ContentValues can't hold $value")
|
||||
}
|
||||
}
|
|
@ -19,9 +19,9 @@ import android.os.PersistableBundle
|
|||
import javax.inject.Inject
|
||||
|
||||
interface IBundle {
|
||||
fun putInt(
|
||||
fun putLong(
|
||||
key: String,
|
||||
value: Int
|
||||
value: Long
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ typealias IBundler = IBundle.() -> Unit
|
|||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface BundleProvider {
|
||||
|
||||
fun createPersistable(builder: IBundle.() -> Unit): PersistableBundle
|
||||
fun createPersistable(bundler: IBundle.() -> Unit): PersistableBundle
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
|
@ -39,10 +39,10 @@ class RealBundleProvider @Inject constructor() : BundleProvider {
|
|||
override fun createPersistable(bundler: IBundler): PersistableBundle {
|
||||
val realBundle = PersistableBundle()
|
||||
bundler(object : IBundle {
|
||||
override fun putInt(
|
||||
override fun putLong(
|
||||
key: String,
|
||||
value: Int
|
||||
) = realBundle.putInt(key, value)
|
||||
value: Long
|
||||
) = realBundle.putLong(key, value)
|
||||
})
|
||||
return realBundle
|
||||
}
|
||||
|
|
|
@ -24,16 +24,20 @@ import java.io.Serializable
|
|||
import javax.inject.Inject
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface IdProvider : Serializable {
|
||||
interface CanNotifyModel : Serializable {
|
||||
|
||||
fun id(): Int
|
||||
fun notiId(): Int
|
||||
|
||||
fun notiName(): String
|
||||
|
||||
fun notiTag(): String
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface IntentProvider {
|
||||
|
||||
fun getPendingIntentForViewSite(
|
||||
model: IdProvider
|
||||
model: CanNotifyModel
|
||||
): PendingIntent
|
||||
}
|
||||
|
||||
|
@ -48,17 +52,17 @@ class RealIntentProvider @Inject constructor(
|
|||
const val KEY_VIEW_NOTIFICATION_MODEL = "model"
|
||||
}
|
||||
|
||||
override fun getPendingIntentForViewSite(model: IdProvider): PendingIntent {
|
||||
override fun getPendingIntentForViewSite(model: CanNotifyModel): PendingIntent {
|
||||
val openIntent = getIntentForViewSite(model)
|
||||
return PendingIntent.getActivity(
|
||||
app,
|
||||
BASE_NOTIFICATION_REQUEST_CODE + model.id(),
|
||||
BASE_NOTIFICATION_REQUEST_CODE + model.notiId(),
|
||||
openIntent,
|
||||
FLAG_CANCEL_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIntentForViewSite(model: IdProvider) =
|
||||
private fun getIntentForViewSite(model: CanNotifyModel) =
|
||||
Intent(app, mainActivity).apply {
|
||||
putExtra(KEY_VIEW_NOTIFICATION_MODEL, model)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package com.afollestad.nocknock.viewcomponents
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -32,6 +33,7 @@ class LoadingIndicatorFrame(
|
|||
}
|
||||
|
||||
private val showRunnable = Runnable { show() }
|
||||
private val delayHandler = Handler()
|
||||
|
||||
init {
|
||||
setBackgroundColor(ContextCompat.getColor(context, R.color.loading_indicator_frame_background))
|
||||
|
@ -42,11 +44,11 @@ class LoadingIndicatorFrame(
|
|||
}
|
||||
|
||||
fun setLoading() {
|
||||
handler.postDelayed(showRunnable, SHOW_DELAY_MS)
|
||||
delayHandler.postDelayed(showRunnable, SHOW_DELAY_MS)
|
||||
}
|
||||
|
||||
fun setDone() {
|
||||
handler.removeCallbacks(showRunnable)
|
||||
delayHandler.removeCallbacks(showRunnable)
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,11 @@ package com.afollestad.nocknock.viewcomponents
|
|||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import com.afollestad.nocknock.data.ServerStatus
|
||||
import com.afollestad.nocknock.data.ServerStatus.CHECKING
|
||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
import com.afollestad.nocknock.data.model.Status
|
||||
import com.afollestad.nocknock.data.model.Status.CHECKING
|
||||
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||
import com.afollestad.nocknock.data.model.Status.OK
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class StatusImageView(
|
||||
|
@ -34,7 +34,7 @@ class StatusImageView(
|
|||
setStatus(OK)
|
||||
}
|
||||
|
||||
fun setStatus(status: ServerStatus) = when (status) {
|
||||
fun setStatus(status: Status) = when (status) {
|
||||
CHECKING, WAITING -> {
|
||||
setImageResource(R.drawable.status_progress)
|
||||
setBackgroundResource(R.drawable.yellow_circle)
|
||||
|
|
Loading…
Add table
Reference in a new issue