mirror of
https://github.com/afollestad/nock-nock.git
synced 2025-09-27 19:58:45 +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 {
|
dependencies {
|
||||||
implementation project(':data')
|
|
||||||
implementation project(':utilities')
|
implementation project(':utilities')
|
||||||
implementation project(':engine')
|
implementation project(':engine')
|
||||||
|
implementation project(':data')
|
||||||
implementation project(':notifications')
|
implementation project(':notifications')
|
||||||
implementation project(':viewcomponents')
|
implementation project(':viewcomponents')
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
android:windowSoftInputMode="stateHidden"/>
|
android:windowSoftInputMode="stateHidden"/>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".engine.statuscheck.CheckStatusJob"
|
android:name=".engine.statuscheck.ValidationJob"
|
||||||
android:label="@string/check_service_name"
|
android:label="@string/check_service_name"
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
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.AppComponent
|
||||||
import com.afollestad.nocknock.di.DaggerAppComponent
|
import com.afollestad.nocknock.di.DaggerAppComponent
|
||||||
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
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.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||||
import com.afollestad.nocknock.ui.main.MainActivity
|
import com.afollestad.nocknock.ui.main.MainActivity
|
||||||
|
@ -82,7 +82,7 @@ class NockNockApp : Application(), Injector {
|
||||||
is MainActivity -> appComponent.inject(target)
|
is MainActivity -> appComponent.inject(target)
|
||||||
is ViewSiteActivity -> appComponent.inject(target)
|
is ViewSiteActivity -> appComponent.inject(target)
|
||||||
is AddSiteActivity -> appComponent.inject(target)
|
is AddSiteActivity -> appComponent.inject(target)
|
||||||
is CheckStatusJob -> appComponent.inject(target)
|
is ValidationJob -> appComponent.inject(target)
|
||||||
is BootReceiver -> appComponent.inject(target)
|
is BootReceiver -> appComponent.inject(target)
|
||||||
else -> throw IllegalStateException("Can't inject into $target")
|
else -> throw IllegalStateException("Can't inject into $target")
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,10 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.isPending
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
import com.afollestad.nocknock.data.textRes
|
import com.afollestad.nocknock.data.model.isPending
|
||||||
|
import com.afollestad.nocknock.data.model.textRes
|
||||||
import com.afollestad.nocknock.utilities.ui.onDebouncedClick
|
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.iconStatus
|
||||||
import kotlinx.android.synthetic.main.list_item_server.view.textInterval
|
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.textStatus
|
||||||
import kotlinx.android.synthetic.main.list_item_server.view.textUrl
|
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) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class ServerVH constructor(
|
class SiteViewHolder constructor(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
private val adapter: ServerAdapter
|
private val adapter: ServerAdapter
|
||||||
) : RecyclerView.ViewHolder(itemView), View.OnLongClickListener {
|
) : RecyclerView.ViewHolder(itemView), View.OnLongClickListener {
|
||||||
|
@ -45,24 +46,32 @@ class ServerVH constructor(
|
||||||
itemView.setOnLongClickListener(this)
|
itemView.setOnLongClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(model: ServerModel) {
|
fun bind(model: Site) {
|
||||||
|
requireNotNull(model.settings) { "Settings must be populated." }
|
||||||
|
|
||||||
itemView.textName.text = model.name
|
itemView.textName.text = model.name
|
||||||
itemView.textUrl.text = model.url
|
itemView.textUrl.text = model.url
|
||||||
itemView.iconStatus.setStatus(model.status)
|
|
||||||
|
|
||||||
val statusText = model.status.textRes()
|
val lastResult = model.lastResult
|
||||||
if (statusText == 0) {
|
if (lastResult != null) {
|
||||||
itemView.textStatus.text = model.reason
|
itemView.iconStatus.setStatus(lastResult.status)
|
||||||
|
val statusText = lastResult.status.textRes()
|
||||||
|
if (statusText == 0) {
|
||||||
|
itemView.textStatus.text = lastResult.reason
|
||||||
|
} else {
|
||||||
|
itemView.textStatus.setText(statusText)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
itemView.textStatus.setText(statusText)
|
itemView.iconStatus.setStatus(WAITING)
|
||||||
|
itemView.textStatus.setText(R.string.none)
|
||||||
}
|
}
|
||||||
|
|
||||||
val res = itemView.resources
|
val res = itemView.resources
|
||||||
when {
|
when {
|
||||||
model.disabled -> {
|
model.settings?.disabled == true -> {
|
||||||
itemView.textInterval.setText(R.string.checks_disabled)
|
itemView.textInterval.setText(R.string.checks_disabled)
|
||||||
}
|
}
|
||||||
model.status.isPending() -> {
|
model.lastResult?.status.isPending() -> {
|
||||||
itemView.textInterval.text = res.getString(
|
itemView.textInterval.text = res.getString(
|
||||||
R.string.next_check_x,
|
R.string.next_check_x,
|
||||||
res.getString(R.string.now)
|
res.getString(R.string.now)
|
||||||
|
@ -84,21 +93,21 @@ class ServerVH constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @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(
|
internal fun performClick(
|
||||||
index: Int,
|
index: Int,
|
||||||
longClick: Boolean
|
longClick: Boolean
|
||||||
) = listener.invoke(models[index], longClick)
|
) = listener.invoke(models[index], longClick)
|
||||||
|
|
||||||
fun add(model: ServerModel) {
|
fun add(model: Site) {
|
||||||
models.add(model)
|
models.add(model)
|
||||||
notifyItemInserted(models.size - 1)
|
notifyItemInserted(models.size - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(target: ServerModel) {
|
fun update(target: Site) {
|
||||||
for ((i, model) in models.withIndex()) {
|
for ((i, model) in models.withIndex()) {
|
||||||
if (model.id == target.id) {
|
if (model.id == target.id) {
|
||||||
update(i, target)
|
update(i, target)
|
||||||
|
@ -109,7 +118,7 @@ class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<Serve
|
||||||
|
|
||||||
private fun update(
|
private fun update(
|
||||||
index: Int,
|
index: Int,
|
||||||
model: ServerModel
|
model: Site
|
||||||
) {
|
) {
|
||||||
models[index] = model
|
models[index] = model
|
||||||
notifyItemChanged(index)
|
notifyItemChanged(index)
|
||||||
|
@ -120,7 +129,7 @@ class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<Serve
|
||||||
notifyItemRemoved(index)
|
notifyItemRemoved(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(target: ServerModel) {
|
fun remove(target: Site) {
|
||||||
for ((i, model) in models.withIndex()) {
|
for ((i, model) in models.withIndex()) {
|
||||||
if (model.id == target.id) {
|
if (model.id == target.id) {
|
||||||
remove(i)
|
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()
|
this.models.clear()
|
||||||
if (!newModels.isEmpty()) {
|
if (!newModels.isEmpty()) {
|
||||||
this.models.addAll(newModels)
|
this.models.addAll(newModels)
|
||||||
|
@ -140,14 +149,14 @@ class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<Serve
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
viewType: Int
|
viewType: Int
|
||||||
): ServerVH {
|
): SiteViewHolder {
|
||||||
val v = LayoutInflater.from(parent.context)
|
val v = LayoutInflater.from(parent.context)
|
||||||
.inflate(R.layout.list_item_server, parent, false)
|
.inflate(R.layout.list_item_server, parent, false)
|
||||||
return ServerVH(v, this)
|
return SiteViewHolder(v, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
override fun onBindViewHolder(
|
||||||
holder: ServerVH,
|
holder: SiteViewHolder,
|
||||||
position: Int
|
position: Int
|
||||||
) {
|
) {
|
||||||
val model = models[position]
|
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 android.widget.ArrayAdapter
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.ValidationMode
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.data.indexToValidationMode
|
import com.afollestad.nocknock.data.model.indexToValidationMode
|
||||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||||
import com.afollestad.nocknock.utilities.ext.injector
|
import com.afollestad.nocknock.utilities.ext.injector
|
||||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
||||||
|
@ -119,7 +119,7 @@ class AddSiteActivity : AppCompatActivity(), AddSiteView {
|
||||||
url = inputUrl.trimmedText(),
|
url = inputUrl.trimmedText(),
|
||||||
checkInterval = checkInterval,
|
checkInterval = checkInterval,
|
||||||
validationMode = validationMode,
|
validationMode = validationMode,
|
||||||
validationContent = validationMode.validationContent(),
|
validationArgs = validationMode.validationContent(),
|
||||||
networkTimeout = responseTimeoutInput.textAsInt(defaultValue = defaultTimeout)
|
networkTimeout = responseTimeoutInput.textAsInt(defaultValue = defaultTimeout)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,14 @@ package com.afollestad.nocknock.ui.addsite
|
||||||
|
|
||||||
import androidx.annotation.CheckResult
|
import androidx.annotation.CheckResult
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.ValidationMode
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
import com.afollestad.nocknock.data.putSite
|
||||||
|
import com.afollestad.nocknock.engine.statuscheck.ValidationManager
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
@ -63,7 +64,7 @@ interface AddSitePresenter {
|
||||||
url: String,
|
url: String,
|
||||||
checkInterval: Long,
|
checkInterval: Long,
|
||||||
validationMode: ValidationMode,
|
validationMode: ValidationMode,
|
||||||
validationContent: String?,
|
validationArgs: String?,
|
||||||
networkTimeout: Int
|
networkTimeout: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -72,8 +73,8 @@ interface AddSitePresenter {
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class RealAddSitePresenter @Inject constructor(
|
class RealAddSitePresenter @Inject constructor(
|
||||||
private val serverModelStore: ServerModelStore,
|
private val database: AppDatabase,
|
||||||
private val checkStatusManager: CheckStatusManager
|
private val checkStatusManager: ValidationManager
|
||||||
) : AddSitePresenter {
|
) : AddSitePresenter {
|
||||||
|
|
||||||
private var view: AddSiteView? = null
|
private var view: AddSiteView? = null
|
||||||
|
@ -118,7 +119,7 @@ class RealAddSitePresenter @Inject constructor(
|
||||||
url: String,
|
url: String,
|
||||||
checkInterval: Long,
|
checkInterval: Long,
|
||||||
validationMode: ValidationMode,
|
validationMode: ValidationMode,
|
||||||
validationContent: String?,
|
validationArgs: String?,
|
||||||
networkTimeout: Int
|
networkTimeout: Int
|
||||||
) {
|
) {
|
||||||
val inputErrors = InputErrors()
|
val inputErrors = InputErrors()
|
||||||
|
@ -134,9 +135,9 @@ class RealAddSitePresenter @Inject constructor(
|
||||||
if (checkInterval <= 0) {
|
if (checkInterval <= 0) {
|
||||||
inputErrors.checkInterval = R.string.please_enter_check_interval
|
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
|
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
|
inputErrors.javaScript = R.string.please_enter_javaScript
|
||||||
}
|
}
|
||||||
if (networkTimeout <= 0) {
|
if (networkTimeout <= 0) {
|
||||||
|
@ -148,14 +149,19 @@ class RealAddSitePresenter @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val newModel = ServerModel(
|
val newSettings = SiteSettings(
|
||||||
|
validationIntervalMs = checkInterval,
|
||||||
|
validationMode = validationMode,
|
||||||
|
validationArgs = validationArgs,
|
||||||
|
networkTimeout = networkTimeout,
|
||||||
|
disabled = false
|
||||||
|
)
|
||||||
|
val newModel = Site(
|
||||||
|
id = 0,
|
||||||
name = name,
|
name = name,
|
||||||
url = url,
|
url = url,
|
||||||
status = WAITING,
|
settings = newSettings,
|
||||||
checkInterval = checkInterval,
|
lastResult = null
|
||||||
validationMode = validationMode,
|
|
||||||
validationContent = validationContent,
|
|
||||||
networkTimeout = networkTimeout
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with(view!!) {
|
with(view!!) {
|
||||||
|
@ -163,7 +169,7 @@ class RealAddSitePresenter @Inject constructor(
|
||||||
launch(coroutineContext) {
|
launch(coroutineContext) {
|
||||||
setLoading()
|
setLoading()
|
||||||
val storedModel = async(IO) {
|
val storedModel = async(IO) {
|
||||||
serverModelStore.put(newModel)
|
database.putSite(newModel)
|
||||||
}.await()
|
}.await()
|
||||||
|
|
||||||
checkStatusManager.scheduleCheck(
|
checkStatusManager.scheduleCheck(
|
||||||
|
|
|
@ -28,9 +28,9 @@ import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
import com.afollestad.materialdialogs.list.listItems
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.adapter.ServerAdapter
|
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.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.ScopeReceiver
|
||||||
import com.afollestad.nocknock.utilities.ext.injector
|
import com.afollestad.nocknock.utilities.ext.injector
|
||||||
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
|
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 com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
||||||
import kotlinx.android.synthetic.main.activity_main.fab
|
import kotlinx.android.synthetic.main.activity_main.fab
|
||||||
import kotlinx.android.synthetic.main.activity_main.list
|
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.rootView
|
||||||
import kotlinx.android.synthetic.main.activity_main.toolbar
|
import kotlinx.android.synthetic.main.activity_main.toolbar
|
||||||
import kotlinx.android.synthetic.main.include_empty_view.emptyText
|
import kotlinx.android.synthetic.main.include_empty_view.emptyText
|
||||||
|
@ -109,18 +110,22 @@ class MainActivity : AppCompatActivity(), MainView {
|
||||||
super.onDestroy()
|
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 {
|
list.post {
|
||||||
adapter.set(models)
|
adapter.set(models)
|
||||||
emptyText.showOrHide(models.isEmpty())
|
emptyText.showOrHide(models.isEmpty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateModel(model: ServerModel) {
|
override fun updateModel(model: Site) {
|
||||||
list.post { adapter.update(model) }
|
list.post { adapter.update(model) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSiteDeleted(model: ServerModel) {
|
override fun onSiteDeleted(model: Site) {
|
||||||
list.post {
|
list.post {
|
||||||
adapter.remove(model)
|
adapter.remove(model)
|
||||||
emptyText.showOrHide(adapter.itemCount == 0)
|
emptyText.showOrHide(adapter.itemCount == 0)
|
||||||
|
@ -133,7 +138,7 @@ class MainActivity : AppCompatActivity(), MainView {
|
||||||
) = rootView.scopeWhileAttached(context, exec)
|
) = rootView.scopeWhileAttached(context, exec)
|
||||||
|
|
||||||
private fun onSiteSelected(
|
private fun onSiteSelected(
|
||||||
model: ServerModel,
|
model: Site,
|
||||||
longClick: Boolean
|
longClick: Boolean
|
||||||
) {
|
) {
|
||||||
if (longClick) {
|
if (longClick) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ package com.afollestad.nocknock.ui.main
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.nocknock.R
|
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.toHtml
|
||||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||||
import com.afollestad.nocknock.ui.addsite.KEY_FAB_SIZE
|
import com.afollestad.nocknock.ui.addsite.KEY_FAB_SIZE
|
||||||
|
@ -47,16 +47,16 @@ private fun MainActivity.intentToAdd(
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun MainActivity.viewSite(model: ServerModel) {
|
internal fun MainActivity.viewSite(model: Site) {
|
||||||
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
|
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MainActivity.intentToView(model: ServerModel) =
|
private fun MainActivity.intentToView(model: Site) =
|
||||||
Intent(this, ViewSiteActivity::class.java).apply {
|
Intent(this, ViewSiteActivity::class.java).apply {
|
||||||
putExtra(KEY_VIEW_MODEL, model)
|
putExtra(KEY_VIEW_MODEL, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun MainActivity.maybeRemoveSite(model: ServerModel) {
|
internal fun MainActivity.maybeRemoveSite(model: Site) {
|
||||||
MaterialDialog(this).show {
|
MaterialDialog(this).show {
|
||||||
title(R.string.remove_site)
|
title(R.string.remove_site)
|
||||||
message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
|
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) {
|
internal fun MainActivity.processIntent(intent: Intent) {
|
||||||
if (intent.hasExtra(KEY_VIEW_NOTIFICATION_MODEL)) {
|
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)
|
viewSite(model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,18 +15,25 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.ui.main
|
package com.afollestad.nocknock.ui.main
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context.MODE_PRIVATE
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
import com.afollestad.nocknock.data.allSites
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
import com.afollestad.nocknock.data.deleteSite
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
import com.afollestad.nocknock.data.legacy.DbMigrator
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
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.notifications.NockNotificationManager
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import timber.log.Timber.d as log
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
interface MainPresenter {
|
interface MainPresenter {
|
||||||
|
@ -37,18 +44,19 @@ interface MainPresenter {
|
||||||
|
|
||||||
fun resume()
|
fun resume()
|
||||||
|
|
||||||
fun refreshSite(site: ServerModel)
|
fun refreshSite(site: Site)
|
||||||
|
|
||||||
fun removeSite(site: ServerModel)
|
fun removeSite(site: Site)
|
||||||
|
|
||||||
fun dropView()
|
fun dropView()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class RealMainPresenter @Inject constructor(
|
class RealMainPresenter @Inject constructor(
|
||||||
private val serverModelStore: ServerModelStore,
|
private val app: Application,
|
||||||
|
private val database: AppDatabase,
|
||||||
private val notificationManager: NockNotificationManager,
|
private val notificationManager: NockNotificationManager,
|
||||||
private val checkStatusManager: CheckStatusManager
|
private val checkStatusManager: ValidationManager
|
||||||
) : MainPresenter {
|
) : MainPresenter {
|
||||||
|
|
||||||
private var view: MainView? = null
|
private var view: MainView? = null
|
||||||
|
@ -61,40 +69,46 @@ class RealMainPresenter @Inject constructor(
|
||||||
|
|
||||||
override fun onBroadcast(intent: Intent) {
|
override fun onBroadcast(intent: Intent) {
|
||||||
if (intent.action == ACTION_STATUS_UPDATE) {
|
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)
|
view?.updateModel(model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resume() {
|
override fun resume() {
|
||||||
notificationManager.cancelStatusNotifications()
|
notificationManager.cancelStatusNotifications()
|
||||||
|
|
||||||
view!!.run {
|
view!!.run {
|
||||||
setModels(listOf())
|
setModels(listOf())
|
||||||
|
setLoading()
|
||||||
|
|
||||||
scopeWhileAttached(Main) {
|
scopeWhileAttached(Main) {
|
||||||
launch(coroutineContext) {
|
launch(coroutineContext) {
|
||||||
val models = async(IO) {
|
doMigrationIfNeeded()
|
||||||
serverModelStore.get()
|
|
||||||
}.await()
|
val models = async(IO) { database.allSites() }.await()
|
||||||
|
|
||||||
setModels(models)
|
setModels(models)
|
||||||
|
setDoneLoading()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refreshSite(site: ServerModel) {
|
override fun refreshSite(site: Site) =
|
||||||
checkStatusManager.scheduleCheck(
|
checkStatusManager.scheduleCheck(
|
||||||
site = site,
|
site = site,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeSite(site: ServerModel) {
|
override fun removeSite(site: Site) {
|
||||||
checkStatusManager.cancelCheck(site)
|
checkStatusManager.cancelCheck(site)
|
||||||
notificationManager.cancelStatusNotification(site)
|
notificationManager.cancelStatusNotification(site)
|
||||||
|
|
||||||
view!!.scopeWhileAttached(Main) {
|
view!!.scopeWhileAttached(Main) {
|
||||||
launch(coroutineContext) {
|
launch(coroutineContext) {
|
||||||
async(IO) { serverModelStore.delete(site) }.await()
|
async(IO) { database.deleteSite(site) }.await()
|
||||||
view?.onSiteDeleted(site)
|
view?.onSiteDeleted(site)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,6 +118,28 @@ class RealMainPresenter @Inject constructor(
|
||||||
view = null
|
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() {
|
private fun ensureCheckJobs() {
|
||||||
view!!.scopeWhileAttached(IO) {
|
view!!.scopeWhileAttached(IO) {
|
||||||
launch(coroutineContext) {
|
launch(coroutineContext) {
|
||||||
|
|
|
@ -15,18 +15,22 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.ui.main
|
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 com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
interface MainView {
|
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(
|
fun scopeWhileAttached(
|
||||||
context: CoroutineContext,
|
context: CoroutineContext,
|
||||||
|
|
|
@ -24,15 +24,15 @@ import android.os.Bundle
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.LAST_CHECK_NONE
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
import com.afollestad.nocknock.data.ValidationMode
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.data.indexToValidationMode
|
import com.afollestad.nocknock.data.model.indexToValidationMode
|
||||||
import com.afollestad.nocknock.data.textRes
|
import com.afollestad.nocknock.data.model.textRes
|
||||||
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.ScopeReceiver
|
||||||
import com.afollestad.nocknock.utilities.ext.formatDate
|
import com.afollestad.nocknock.utilities.ext.formatDate
|
||||||
import com.afollestad.nocknock.utilities.ext.injector
|
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.textUrlWarning
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
|
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
||||||
|
import java.lang.System.currentTimeMillis
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
@ -132,7 +133,7 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
||||||
url = inputUrl.trimmedText(),
|
url = inputUrl.trimmedText(),
|
||||||
checkInterval = checkInterval,
|
checkInterval = checkInterval,
|
||||||
validationMode = validationMode,
|
validationMode = validationMode,
|
||||||
validationContent = validationMode.validationContent(),
|
validationArgs = validationMode.validationContent(),
|
||||||
networkTimeout = responseTimeoutInput.textAsInt(defaultValue = defaultTimeout)
|
networkTimeout = responseTimeoutInput.textAsInt(defaultValue = defaultTimeout)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -170,44 +171,48 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
||||||
|
|
||||||
override fun setValidationModeDescription(res: Int) = validationModeDescription.setText(res)
|
override fun setValidationModeDescription(res: Int) = validationModeDescription.setText(res)
|
||||||
|
|
||||||
override fun displayModel(model: ServerModel) = with(model) {
|
override fun displayModel(model: Site) = with(model) {
|
||||||
iconStatus.setStatus(this.status)
|
val siteSettings = this.settings
|
||||||
|
requireNotNull(siteSettings) { "Site settings must be populated." }
|
||||||
|
|
||||||
|
iconStatus.setStatus(this.lastResult?.status ?: WAITING)
|
||||||
inputName.setText(this.name)
|
inputName.setText(this.name)
|
||||||
inputUrl.setText(this.url)
|
inputUrl.setText(this.url)
|
||||||
|
|
||||||
if (this.lastCheck == LAST_CHECK_NONE) {
|
if (this.lastResult == null) {
|
||||||
textLastCheckResult.setText(R.string.none)
|
textLastCheckResult.setText(R.string.none)
|
||||||
} else {
|
} else {
|
||||||
val statusText = this.status.textRes()
|
val statusText = this.lastResult!!.status.textRes()
|
||||||
textLastCheckResult.text = if (statusText == 0) {
|
textLastCheckResult.text = if (statusText == 0) {
|
||||||
this.reason
|
this.lastResult!!.reason
|
||||||
} else {
|
} else {
|
||||||
getString(statusText)
|
getString(statusText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.disabled) {
|
if (siteSettings.disabled) {
|
||||||
textNextCheck.setText(R.string.auto_checks_disabled)
|
textNextCheck.setText(R.string.auto_checks_disabled)
|
||||||
} else {
|
} 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)
|
responseValidationMode.setSelection(siteSettings.validationMode.value - 1)
|
||||||
when (this.validationMode) {
|
when (siteSettings.validationMode) {
|
||||||
TERM_SEARCH -> responseValidationSearchTerm.setText(this.validationContent ?: "")
|
TERM_SEARCH -> responseValidationSearchTerm.setText(siteSettings.validationArgs ?: "")
|
||||||
JAVASCRIPT -> scriptInputLayout.setCode(this.validationContent)
|
JAVASCRIPT -> scriptInputLayout.setCode(siteSettings.validationArgs)
|
||||||
else -> {
|
else -> {
|
||||||
responseValidationSearchTerm.setText("")
|
responseValidationSearchTerm.setText("")
|
||||||
scriptInputLayout.clear()
|
scriptInputLayout.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
responseTimeoutInput.setText(model.networkTimeout.toString())
|
responseTimeoutInput.setText(siteSettings.networkTimeout.toString())
|
||||||
|
|
||||||
disableChecksButton.showOrHide(!this.disabled)
|
disableChecksButton.showOrHide(!siteSettings.disabled)
|
||||||
doneBtn.setText(
|
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
|
else R.string.save_changes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,8 @@ package com.afollestad.nocknock.ui.viewsite
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.isPending
|
import com.afollestad.nocknock.data.model.isPending
|
||||||
import com.afollestad.nocknock.toHtml
|
import com.afollestad.nocknock.toHtml
|
||||||
import com.afollestad.nocknock.utilities.ext.animateRotation
|
import com.afollestad.nocknock.utilities.ext.animateRotation
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
|
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)
|
val refreshIcon = toolbar.menu.findItem(R.id.refresh)
|
||||||
.actionView as ImageView
|
.actionView as ImageView
|
||||||
|
|
||||||
if (model.status.isPending()) {
|
if (model.lastResult?.status.isPending()) {
|
||||||
refreshIcon.animateRotation()
|
refreshIcon.animateRotation()
|
||||||
} else {
|
} else {
|
||||||
refreshIcon.run {
|
refreshIcon.run {
|
||||||
|
|
|
@ -18,15 +18,17 @@ package com.afollestad.nocknock.ui.viewsite
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.annotation.CheckResult
|
import androidx.annotation.CheckResult
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
import com.afollestad.nocknock.data.deleteSite
|
||||||
import com.afollestad.nocknock.data.ValidationMode
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
import com.afollestad.nocknock.data.updateSite
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
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.notifications.NockNotificationManager
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
|
@ -77,7 +79,7 @@ interface ViewSitePresenter {
|
||||||
url: String,
|
url: String,
|
||||||
checkInterval: Long,
|
checkInterval: Long,
|
||||||
validationMode: ValidationMode,
|
validationMode: ValidationMode,
|
||||||
validationContent: String?,
|
validationArgs: String?,
|
||||||
networkTimeout: Int
|
networkTimeout: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -87,26 +89,26 @@ interface ViewSitePresenter {
|
||||||
|
|
||||||
fun removeSite()
|
fun removeSite()
|
||||||
|
|
||||||
fun currentModel(): ServerModel
|
fun currentModel(): Site
|
||||||
|
|
||||||
fun dropView()
|
fun dropView()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class RealViewSitePresenter @Inject constructor(
|
class RealViewSitePresenter @Inject constructor(
|
||||||
private val serverModelStore: ServerModelStore,
|
private val database: AppDatabase,
|
||||||
private val checkStatusManager: CheckStatusManager,
|
private val checkStatusManager: ValidationManager,
|
||||||
private val notificationManager: NockNotificationManager
|
private val notificationManager: NockNotificationManager
|
||||||
) : ViewSitePresenter {
|
) : ViewSitePresenter {
|
||||||
|
|
||||||
private var view: ViewSiteView? = null
|
private var view: ViewSiteView? = null
|
||||||
private var currentModel: ServerModel? = null
|
private var currentModel: Site? = null
|
||||||
|
|
||||||
override fun takeView(
|
override fun takeView(
|
||||||
view: ViewSiteView,
|
view: ViewSiteView,
|
||||||
intent: Intent
|
intent: Intent
|
||||||
) {
|
) {
|
||||||
this.currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as ServerModel
|
this.currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as Site
|
||||||
this.view = view.apply {
|
this.view = view.apply {
|
||||||
displayModel(currentModel!!)
|
displayModel(currentModel!!)
|
||||||
}
|
}
|
||||||
|
@ -114,7 +116,8 @@ class RealViewSitePresenter @Inject constructor(
|
||||||
|
|
||||||
override fun onBroadcast(intent: Intent) {
|
override fun onBroadcast(intent: Intent) {
|
||||||
if (intent.action == ACTION_STATUS_UPDATE) {
|
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
|
this.currentModel = model
|
||||||
view?.displayModel(model)
|
view?.displayModel(model)
|
||||||
}
|
}
|
||||||
|
@ -122,7 +125,7 @@ class RealViewSitePresenter @Inject constructor(
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
if (intent != null && intent.hasExtra(KEY_VIEW_MODEL)) {
|
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!!)
|
view?.displayModel(currentModel!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,7 +166,7 @@ class RealViewSitePresenter @Inject constructor(
|
||||||
url: String,
|
url: String,
|
||||||
checkInterval: Long,
|
checkInterval: Long,
|
||||||
validationMode: ValidationMode,
|
validationMode: ValidationMode,
|
||||||
validationContent: String?,
|
validationArgs: String?,
|
||||||
networkTimeout: Int
|
networkTimeout: Int
|
||||||
) {
|
) {
|
||||||
val inputErrors = InputErrors()
|
val inputErrors = InputErrors()
|
||||||
|
@ -179,9 +182,9 @@ class RealViewSitePresenter @Inject constructor(
|
||||||
if (checkInterval <= 0) {
|
if (checkInterval <= 0) {
|
||||||
inputErrors.checkInterval = R.string.please_enter_check_interval
|
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
|
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
|
inputErrors.javaScript = R.string.please_enter_javaScript
|
||||||
}
|
}
|
||||||
if (networkTimeout <= 0) {
|
if (networkTimeout <= 0) {
|
||||||
|
@ -193,27 +196,34 @@ class RealViewSitePresenter @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val newModel = currentModel!!.copy(
|
val updatedSettings = currentModel!!.settings!!.copy(
|
||||||
name = name,
|
validationIntervalMs = checkInterval,
|
||||||
url = url,
|
|
||||||
status = WAITING,
|
|
||||||
checkInterval = checkInterval,
|
|
||||||
validationMode = validationMode,
|
validationMode = validationMode,
|
||||||
validationContent = validationContent,
|
validationArgs = validationArgs,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
networkTimeout = networkTimeout
|
networkTimeout = networkTimeout
|
||||||
)
|
)
|
||||||
|
val updatedModel = currentModel!!.copy(
|
||||||
|
name = name,
|
||||||
|
url = url,
|
||||||
|
settings = updatedSettings
|
||||||
|
)
|
||||||
|
.withStatus(status = WAITING)
|
||||||
|
|
||||||
with(view!!) {
|
with(view!!) {
|
||||||
scopeWhileAttached(Main) {
|
scopeWhileAttached(Main) {
|
||||||
launch(coroutineContext) {
|
launch(coroutineContext) {
|
||||||
setLoading()
|
setLoading()
|
||||||
async(IO) { serverModelStore.update(newModel) }.await()
|
async(IO) {
|
||||||
|
database.updateSite(updatedModel)
|
||||||
|
}.await()
|
||||||
|
|
||||||
checkStatusManager.scheduleCheck(
|
checkStatusManager.scheduleCheck(
|
||||||
site = newModel,
|
site = updatedModel,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
)
|
)
|
||||||
|
|
||||||
setDoneLoading()
|
setDoneLoading()
|
||||||
view?.finish()
|
view?.finish()
|
||||||
}
|
}
|
||||||
|
@ -222,7 +232,7 @@ class RealViewSitePresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun checkNow() = with(view!!) {
|
override fun checkNow() = with(view!!) {
|
||||||
val checkModel = currentModel!!.copy(
|
val checkModel = currentModel!!.withStatus(
|
||||||
status = WAITING
|
status = WAITING
|
||||||
)
|
)
|
||||||
view?.displayModel(checkModel)
|
view?.displayModel(checkModel)
|
||||||
|
@ -242,8 +252,16 @@ class RealViewSitePresenter @Inject constructor(
|
||||||
scopeWhileAttached(Main) {
|
scopeWhileAttached(Main) {
|
||||||
launch(coroutineContext) {
|
launch(coroutineContext) {
|
||||||
setLoading()
|
setLoading()
|
||||||
currentModel = currentModel!!.copy(disabled = true)
|
currentModel = currentModel!!.copy(
|
||||||
async(IO) { serverModelStore.update(currentModel!!) }.await()
|
settings = currentModel!!.settings!!.copy(
|
||||||
|
disabled = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async(IO) {
|
||||||
|
database.updateSite(currentModel!!)
|
||||||
|
}.await()
|
||||||
|
|
||||||
setDoneLoading()
|
setDoneLoading()
|
||||||
view?.displayModel(currentModel!!)
|
view?.displayModel(currentModel!!)
|
||||||
}
|
}
|
||||||
|
@ -260,7 +278,9 @@ class RealViewSitePresenter @Inject constructor(
|
||||||
scopeWhileAttached(Main) {
|
scopeWhileAttached(Main) {
|
||||||
launch(coroutineContext) {
|
launch(coroutineContext) {
|
||||||
setLoading()
|
setLoading()
|
||||||
async(IO) { serverModelStore.delete(site) }.await()
|
async(IO) {
|
||||||
|
database.deleteSite(site)
|
||||||
|
}.await()
|
||||||
setDoneLoading()
|
setDoneLoading()
|
||||||
view?.finish()
|
view?.finish()
|
||||||
}
|
}
|
||||||
|
@ -275,7 +295,7 @@ class RealViewSitePresenter @Inject constructor(
|
||||||
currentModel = null
|
currentModel = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestOnly fun setModel(model: ServerModel) {
|
@TestOnly fun setModel(model: Site) {
|
||||||
this.currentModel = model
|
this.currentModel = model
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
package com.afollestad.nocknock.ui.viewsite
|
package com.afollestad.nocknock.ui.viewsite
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
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 com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ interface ViewSiteView {
|
||||||
|
|
||||||
fun setDoneLoading()
|
fun setDoneLoading()
|
||||||
|
|
||||||
fun displayModel(model: ServerModel)
|
fun displayModel(model: Site)
|
||||||
|
|
||||||
fun showOrHideUrlSchemeWarning(show: Boolean)
|
fun showOrHideUrlSchemeWarning(show: Boolean)
|
||||||
|
|
||||||
|
|
|
@ -48,4 +48,12 @@
|
||||||
app:rippleColor="#40ffffff"
|
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>
|
</FrameLayout>
|
||||||
|
|
|
@ -22,33 +22,32 @@
|
||||||
<string name="please_enter_name">Please enter a name!</string>
|
<string name="please_enter_name">Please enter a name!</string>
|
||||||
<string name="please_enter_url">Please enter a URL.</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_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_search_term">Please input a search term.</string>
|
||||||
<string name="please_enter_javaScript">Please input a validation script.</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="please_enter_networkTimeout">Please enter a network timeout greater than 0.</string>
|
||||||
|
|
||||||
<string name="options">Options</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">Remove Site</string>
|
||||||
<string name="remove_site_prompt"><![CDATA[Remove <b>%1$s</b> from your sites?]]></string>
|
<string name="remove_site_prompt"><![CDATA[Remove <b>%1$s</b> from your sites?]]></string>
|
||||||
<string name="remove">Remove</string>
|
<string name="remove">Remove</string>
|
||||||
<string name="save_changes">Save Changes</string>
|
<string name="save_changes">Save Changes</string>
|
||||||
<string name="view_site">View Site</string>
|
<string name="view_site">View Site</string>
|
||||||
<string name="last_check_result">Last Check Result</string>
|
<string name="last_check_result">Last Validation Result</string>
|
||||||
<string name="next_check">Next Check</string>
|
<string name="next_check">Next Validation</string>
|
||||||
<string name="next_check_x">Next Check: %1$s</string>
|
<string name="next_check_x">Next Validation: %1$s</string>
|
||||||
<string name="now">Now</string>
|
<string name="now">Now</string>
|
||||||
<string name="none_turned_off">None (turned off)</string>
|
<string name="none_turned_off">None (turned off)</string>
|
||||||
<string name="none">None</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[
|
<string name="disable_automatic_checks_prompt"><![CDATA[
|
||||||
Disable automatic checks for <b>%1$s</b>? The site will not be validated in the background
|
Disable automatic validation for <b>%1$s</b>? The site will not be checked in the background
|
||||||
until you re-enable checks for it. You can still manually perform checks by tapping the
|
until you re-enable validation for it. You can still manually perform validation by tapping the
|
||||||
Refresh icon at the top of this page.
|
Refresh icon at the top of this page.
|
||||||
]]></string>
|
]]></string>
|
||||||
<string name="disable">Disable</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">Network Response Timeout</string>
|
||||||
<string name="response_timeout_default">10000</string>
|
<string name="response_timeout_default">10000</string>
|
||||||
|
@ -56,22 +55,22 @@
|
||||||
<string name="refresh_status">Refresh Status</string>
|
<string name="refresh_status">Refresh Status</string>
|
||||||
|
|
||||||
<string name="warning_http_url">
|
<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.
|
use an HTTP URL.
|
||||||
</string>
|
</string>
|
||||||
<string name="response_validation_mode">Response Validation Mode</string>
|
<string name="response_validation_mode">Response Validation Mode</string>
|
||||||
<string name="search_term">Search term…</string>
|
<string name="search_term">Search term…</string>
|
||||||
|
|
||||||
<string name="validation_mode_status_desc">
|
<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>
|
||||||
<string name="validation_mode_term_desc">
|
<string name="validation_mode_term_desc">
|
||||||
The status code check is done first. If it\'s successful, the response body is checked.
|
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>
|
||||||
<string name="validation_mode_javascript_desc">
|
<string name="validation_mode_javascript_desc">
|
||||||
The status code check is done first. If it\'s successful, the response body is passed to the
|
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.
|
exception to pass custom error messages to Nock Nock.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock
|
package com.afollestad.nocknock
|
||||||
|
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
import com.afollestad.nocknock.engine.statuscheck.ValidationManager
|
||||||
import com.afollestad.nocknock.ui.addsite.AddSiteView
|
import com.afollestad.nocknock.ui.addsite.AddSiteView
|
||||||
import com.afollestad.nocknock.ui.addsite.InputErrors
|
import com.afollestad.nocknock.ui.addsite.InputErrors
|
||||||
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter
|
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter
|
||||||
|
@ -42,16 +42,12 @@ import org.junit.Test
|
||||||
|
|
||||||
class AddSitePresenterTest {
|
class AddSitePresenterTest {
|
||||||
|
|
||||||
private val serverModelStore = mock<ServerModelStore> {
|
private val database = mockDatabase()
|
||||||
on { runBlocking { put(any()) } } doAnswer { inv ->
|
private val checkStatusManager = mock<ValidationManager>()
|
||||||
inv.getArgument<ServerModel>(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val checkStatusManager = mock<CheckStatusManager>()
|
|
||||||
private val view = mock<AddSiteView>()
|
private val view = mock<AddSiteView>()
|
||||||
|
|
||||||
private val presenter = RealAddSitePresenter(
|
private val presenter = RealAddSitePresenter(
|
||||||
serverModelStore,
|
database,
|
||||||
checkStatusManager
|
checkStatusManager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -260,10 +256,21 @@ class AddSitePresenterTest {
|
||||||
60000
|
60000
|
||||||
)
|
)
|
||||||
|
|
||||||
val modelCaptor = argumentCaptor<ServerModel>()
|
val siteCaptor = argumentCaptor<Site>()
|
||||||
|
val settingsCaptor = argumentCaptor<SiteSettings>()
|
||||||
|
|
||||||
verify(view).setLoading()
|
verify(view).setLoading()
|
||||||
verify(serverModelStore).put(modelCaptor.capture())
|
verify(database.siteDao()).insert(siteCaptor.capture())
|
||||||
val model = modelCaptor.firstValue
|
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(view, never()).setInputErrors(any())
|
||||||
verify(checkStatusManager).scheduleCheck(
|
verify(checkStatusManager).scheduleCheck(
|
||||||
site = model,
|
site = model,
|
||||||
|
@ -271,6 +278,7 @@ class AddSitePresenterTest {
|
||||||
cancelPrevious = true,
|
cancelPrevious = true,
|
||||||
fromFinishingJob = false
|
fromFinishingJob = false
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(view).setDoneLoading()
|
verify(view).setDoneLoading()
|
||||||
verify(view).onSiteAdded()
|
verify(view).onSiteAdded()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,14 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock
|
package com.afollestad.nocknock
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context.MODE_PRIVATE
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import android.content.SharedPreferences
|
||||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_UPDATE_MODEL
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
import com.afollestad.nocknock.engine.statuscheck.ValidationManager
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.ui.main.MainView
|
import com.afollestad.nocknock.ui.main.MainView
|
||||||
import com.afollestad.nocknock.ui.main.RealMainPresenter
|
import com.afollestad.nocknock.ui.main.RealMainPresenter
|
||||||
|
@ -42,13 +43,22 @@ import org.junit.Test
|
||||||
|
|
||||||
class MainPresenterTest {
|
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 notificationManager = mock<NockNotificationManager>()
|
||||||
private val checkStatusManager = mock<CheckStatusManager>()
|
private val checkStatusManager = mock<ValidationManager>()
|
||||||
private val view = mock<MainView>()
|
private val view = mock<MainView>()
|
||||||
|
|
||||||
private val presenter = RealMainPresenter(
|
private val presenter = RealMainPresenter(
|
||||||
serverModelStore,
|
app,
|
||||||
|
database,
|
||||||
notificationManager,
|
notificationManager,
|
||||||
checkStatusManager
|
checkStatusManager
|
||||||
)
|
)
|
||||||
|
@ -72,55 +82,45 @@ class MainPresenterTest {
|
||||||
val badIntent = fakeIntent("Hello World")
|
val badIntent = fakeIntent("Hello World")
|
||||||
presenter.onBroadcast(badIntent)
|
presenter.onBroadcast(badIntent)
|
||||||
|
|
||||||
val model = fakeModel()
|
|
||||||
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||||
whenever(goodIntent.getSerializableExtra(KEY_UPDATE_MODEL))
|
whenever(goodIntent.getSerializableExtra(KEY_UPDATE_MODEL))
|
||||||
.doReturn(model)
|
.doReturn(MOCK_MODEL_2)
|
||||||
|
|
||||||
presenter.onBroadcast(goodIntent)
|
presenter.onBroadcast(goodIntent)
|
||||||
verify(view, times(1)).updateModel(model)
|
verify(view, times(1)).updateModel(MOCK_MODEL_2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun resume() = runBlocking {
|
@Test fun resume() = runBlocking {
|
||||||
val model = fakeModel()
|
|
||||||
whenever(serverModelStore.get()).doReturn(listOf(model))
|
|
||||||
presenter.resume()
|
presenter.resume()
|
||||||
|
|
||||||
verify(notificationManager).cancelStatusNotifications()
|
verify(notificationManager).cancelStatusNotifications()
|
||||||
|
|
||||||
val modelsCaptor = argumentCaptor<List<ServerModel>>()
|
val modelsCaptor = argumentCaptor<List<Site>>()
|
||||||
verify(view, times(2)).setModels(modelsCaptor.capture())
|
verify(view, times(2)).setModels(modelsCaptor.capture())
|
||||||
assertThat(modelsCaptor.firstValue).isEmpty()
|
assertThat(modelsCaptor.firstValue).isEmpty()
|
||||||
assertThat(modelsCaptor.lastValue.single()).isEqualTo(model)
|
assertThat(modelsCaptor.lastValue).isEqualTo(ALL_MOCK_MODELS)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun refreshSite() {
|
@Test fun refreshSite() {
|
||||||
val model = fakeModel()
|
presenter.refreshSite(MOCK_MODEL_3)
|
||||||
presenter.refreshSite(model)
|
|
||||||
|
|
||||||
verify(checkStatusManager).scheduleCheck(
|
verify(checkStatusManager).scheduleCheck(
|
||||||
site = model,
|
site = MOCK_MODEL_3,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun removeSite() = runBlocking {
|
@Test fun removeSite() = runBlocking {
|
||||||
val model = fakeModel()
|
presenter.removeSite(MOCK_MODEL_1)
|
||||||
presenter.removeSite(model)
|
|
||||||
|
|
||||||
verify(checkStatusManager).cancelCheck(model)
|
verify(checkStatusManager).cancelCheck(MOCK_MODEL_1)
|
||||||
verify(notificationManager).cancelStatusNotification(model)
|
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
||||||
verify(serverModelStore).delete(model)
|
verify(database.siteDao()).delete(MOCK_MODEL_1)
|
||||||
verify(view).onSiteDeleted(model)
|
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 {
|
private fun fakeIntent(action: String): Intent {
|
||||||
return mock {
|
return mock {
|
||||||
on { getAction() } doReturn action
|
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
|
package com.afollestad.nocknock
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.afollestad.nocknock.data.LAST_CHECK_NONE
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
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.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.ui.viewsite.InputErrors
|
import com.afollestad.nocknock.ui.viewsite.InputErrors
|
||||||
import com.afollestad.nocknock.ui.viewsite.KEY_VIEW_MODEL
|
import com.afollestad.nocknock.ui.viewsite.KEY_VIEW_MODEL
|
||||||
|
@ -47,20 +48,17 @@ import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.lang.System.currentTimeMillis
|
||||||
|
|
||||||
class ViewSitePresenterTest {
|
class ViewSitePresenterTest {
|
||||||
|
|
||||||
private val serverModelStore = mock<ServerModelStore> {
|
private val database = mockDatabase()
|
||||||
on { runBlocking { put(any()) } } doAnswer { inv ->
|
private val checkStatusManager = mock<ValidationManager>()
|
||||||
inv.getArgument<ServerModel>(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val checkStatusManager = mock<CheckStatusManager>()
|
|
||||||
private val notificationManager = mock<NockNotificationManager>()
|
private val notificationManager = mock<NockNotificationManager>()
|
||||||
private val view = mock<ViewSiteView>()
|
private val view = mock<ViewSiteView>()
|
||||||
|
|
||||||
private val presenter = RealViewSitePresenter(
|
private val presenter = RealViewSitePresenter(
|
||||||
serverModelStore,
|
database,
|
||||||
checkStatusManager,
|
checkStatusManager,
|
||||||
notificationManager
|
notificationManager
|
||||||
)
|
)
|
||||||
|
@ -73,13 +71,12 @@ class ViewSitePresenterTest {
|
||||||
}.whenever(view)
|
}.whenever(view)
|
||||||
.scopeWhileAttached(any(), any())
|
.scopeWhileAttached(any(), any())
|
||||||
|
|
||||||
val model = fakeModel()
|
|
||||||
val intent = fakeIntent("")
|
val intent = fakeIntent("")
|
||||||
whenever(intent.getSerializableExtra(KEY_VIEW_MODEL))
|
whenever(intent.getSerializableExtra(KEY_VIEW_MODEL))
|
||||||
.doReturn(model)
|
.doReturn(MOCK_MODEL_1)
|
||||||
presenter.takeView(view, intent)
|
presenter.takeView(view, intent)
|
||||||
assertThat(presenter.currentModel()).isEqualTo(model)
|
assertThat(presenter.currentModel()).isEqualTo(MOCK_MODEL_1)
|
||||||
verify(view, times(1)).displayModel(model)
|
verify(view, times(1)).displayModel(MOCK_MODEL_1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After fun destroy() {
|
@After fun destroy() {
|
||||||
|
@ -90,27 +87,25 @@ class ViewSitePresenterTest {
|
||||||
val badIntent = fakeIntent("Hello World")
|
val badIntent = fakeIntent("Hello World")
|
||||||
presenter.onBroadcast(badIntent)
|
presenter.onBroadcast(badIntent)
|
||||||
|
|
||||||
val model = fakeModel().copy(lastCheck = 0)
|
|
||||||
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||||
whenever(goodIntent.getSerializableExtra(KEY_UPDATE_MODEL))
|
whenever(goodIntent.getSerializableExtra(KEY_UPDATE_MODEL))
|
||||||
.doReturn(model)
|
.doReturn(MOCK_MODEL_2)
|
||||||
|
|
||||||
presenter.onBroadcast(goodIntent)
|
presenter.onBroadcast(goodIntent)
|
||||||
assertThat(presenter.currentModel()).isEqualTo(model)
|
assertThat(presenter.currentModel()).isEqualTo(MOCK_MODEL_2)
|
||||||
verify(view, times(1)).displayModel(model)
|
verify(view, times(1)).displayModel(MOCK_MODEL_2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun onNewIntent() {
|
@Test fun onNewIntent() {
|
||||||
val badIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
val badIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||||
presenter.onBroadcast(badIntent)
|
presenter.onBroadcast(badIntent)
|
||||||
|
|
||||||
val model = fakeModel().copy(lastCheck = 0)
|
|
||||||
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||||
whenever(goodIntent.getSerializableExtra(KEY_VIEW_MODEL))
|
whenever(goodIntent.getSerializableExtra(KEY_VIEW_MODEL))
|
||||||
.doReturn(model)
|
.doReturn(MOCK_MODEL_3)
|
||||||
presenter.onBroadcast(goodIntent)
|
presenter.onBroadcast(goodIntent)
|
||||||
|
|
||||||
verify(view, times(1)).displayModel(model)
|
verify(view, times(1)).displayModel(MOCK_MODEL_3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun onUrlInputFocusChange_focused() {
|
@Test fun onUrlInputFocusChange_focused() {
|
||||||
|
@ -298,10 +293,19 @@ class ViewSitePresenterTest {
|
||||||
val url = "https://hello.com"
|
val url = "https://hello.com"
|
||||||
val checkInterval = 60000L
|
val checkInterval = 60000L
|
||||||
val validationMode = TERM_SEARCH
|
val validationMode = TERM_SEARCH
|
||||||
val validationContent = "Hello World"
|
val validationArgs = "Hello World"
|
||||||
|
|
||||||
val disabledModel = presenter.currentModel()
|
val currentModel = presenter.currentModel()
|
||||||
.copy(disabled = true)
|
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.setModel(disabledModel)
|
||||||
|
|
||||||
presenter.commit(
|
presenter.commit(
|
||||||
|
@ -309,21 +313,38 @@ class ViewSitePresenterTest {
|
||||||
url,
|
url,
|
||||||
checkInterval,
|
checkInterval,
|
||||||
validationMode,
|
validationMode,
|
||||||
validationContent,
|
validationArgs,
|
||||||
60000
|
60000
|
||||||
)
|
)
|
||||||
|
|
||||||
val modelCaptor = argumentCaptor<ServerModel>()
|
val siteCaptor = argumentCaptor<Site>()
|
||||||
verify(view).setLoading()
|
val settingsCaptor = argumentCaptor<SiteSettings>()
|
||||||
verify(serverModelStore).update(modelCaptor.capture())
|
val resultCaptor = argumentCaptor<ValidationResult>()
|
||||||
|
|
||||||
val model = modelCaptor.firstValue
|
verify(view).setLoading()
|
||||||
assertThat(model.name).isEqualTo(name)
|
verify(database.siteDao()).update(siteCaptor.capture())
|
||||||
assertThat(model.url).isEqualTo(url)
|
verify(database.siteSettingsDao()).update(settingsCaptor.capture())
|
||||||
assertThat(model.checkInterval).isEqualTo(checkInterval)
|
verify(database.validationResultsDao()).update(resultCaptor.capture())
|
||||||
assertThat(model.validationMode).isEqualTo(validationMode)
|
|
||||||
assertThat(model.validationContent).isEqualTo(validationContent)
|
val model = siteCaptor.firstValue
|
||||||
assertThat(model.disabled).isFalse()
|
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(view, never()).setInputErrors(any())
|
||||||
verify(checkStatusManager).scheduleCheck(
|
verify(checkStatusManager).scheduleCheck(
|
||||||
|
@ -338,9 +359,7 @@ class ViewSitePresenterTest {
|
||||||
|
|
||||||
@Test fun checkNow() {
|
@Test fun checkNow() {
|
||||||
val newModel = presenter.currentModel()
|
val newModel = presenter.currentModel()
|
||||||
.copy(
|
.withStatus(status = WAITING)
|
||||||
status = WAITING
|
|
||||||
)
|
|
||||||
presenter.checkNow()
|
presenter.checkNow()
|
||||||
|
|
||||||
verify(view, never()).setLoading()
|
verify(view, never()).setLoading()
|
||||||
|
@ -360,11 +379,18 @@ class ViewSitePresenterTest {
|
||||||
verify(notificationManager).cancelStatusNotification(model)
|
verify(notificationManager).cancelStatusNotification(model)
|
||||||
verify(view).setLoading()
|
verify(view).setLoading()
|
||||||
|
|
||||||
val modelCaptor = argumentCaptor<ServerModel>()
|
val modelCaptor = argumentCaptor<Site>()
|
||||||
verify(serverModelStore).update(modelCaptor.capture())
|
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
|
val newModel = modelCaptor.firstValue
|
||||||
assertThat(newModel.disabled).isTrue()
|
val newSettings = settingsCaptor.firstValue
|
||||||
assertThat(newModel.lastCheck).isEqualTo(LAST_CHECK_NONE)
|
val result = resultCaptor.firstValue
|
||||||
|
assertThat(newSettings.disabled).isTrue()
|
||||||
|
|
||||||
verify(view).setDoneLoading()
|
verify(view).setDoneLoading()
|
||||||
verify(view, times(1)).displayModel(newModel)
|
verify(view, times(1)).displayModel(newModel)
|
||||||
|
@ -377,18 +403,15 @@ class ViewSitePresenterTest {
|
||||||
verify(checkStatusManager).cancelCheck(model)
|
verify(checkStatusManager).cancelCheck(model)
|
||||||
verify(notificationManager).cancelStatusNotification(model)
|
verify(notificationManager).cancelStatusNotification(model)
|
||||||
verify(view).setLoading()
|
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).setDoneLoading()
|
||||||
verify(view).finish()
|
verify(view).finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fakeModel() = ServerModel(
|
|
||||||
id = 1,
|
|
||||||
name = "Test",
|
|
||||||
url = "https://test.com",
|
|
||||||
validationMode = STATUS_CODE
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun fakeIntent(action: String): Intent {
|
private fun fakeIntent(action: String): Intent {
|
||||||
return mock {
|
return mock {
|
||||||
on { getAction() } doReturn action
|
on { getAction() } doReturn action
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
apply from: '../dependencies.gradle'
|
apply from: '../dependencies.gradle'
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion versions.compileSdk
|
compileSdkVersion versions.compileSdk
|
||||||
|
@ -10,6 +11,8 @@ android {
|
||||||
targetSdkVersion versions.compileSdk
|
targetSdkVersion versions.compileSdk
|
||||||
versionCode versions.publishVersionCode
|
versionCode versions.publishVersionCode
|
||||||
versionName versions.publishVersion
|
versionName versions.publishVersion
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +20,17 @@ dependencies {
|
||||||
implementation project(':utilities')
|
implementation project(':utilities')
|
||||||
|
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
|
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'
|
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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.engine
|
package com.afollestad.nocknock.data
|
||||||
|
|
||||||
import com.afollestad.nocknock.engine.db.RealServerModelStore
|
import androidx.room.Dao
|
||||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
import androidx.room.Delete
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
import androidx.room.Insert
|
||||||
import com.afollestad.nocknock.engine.statuscheck.RealCheckStatusManager
|
import androidx.room.OnConflictStrategy.FAIL
|
||||||
import dagger.Binds
|
import androidx.room.Query
|
||||||
import dagger.Module
|
import androidx.room.Update
|
||||||
import javax.inject.Singleton
|
import com.afollestad.nocknock.data.model.Site
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
@Module
|
@Dao
|
||||||
abstract class EngineModule {
|
interface SiteDao {
|
||||||
|
|
||||||
@Binds
|
@Query("SELECT * FROM sites ORDER BY name ASC")
|
||||||
@Singleton
|
fun all(): List<Site>
|
||||||
abstract fun provideServerModelStore(
|
|
||||||
serverModelStore: RealServerModelStore
|
|
||||||
): ServerModelStore
|
|
||||||
|
|
||||||
@Binds
|
@Query("SELECT * FROM sites WHERE id = :id LIMIT 1")
|
||||||
@Singleton
|
fun one(id: Long): List<Site>
|
||||||
abstract fun provideCheckStatusManager(
|
|
||||||
checkStatusManager: RealCheckStatusManager
|
@Insert(onConflict = FAIL)
|
||||||
): CheckStatusManager
|
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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.data
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
|
package com.afollestad.nocknock.data.legacy
|
||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
import com.afollestad.nocknock.data.model.Status
|
||||||
import com.afollestad.nocknock.utilities.ext.timeString
|
import com.afollestad.nocknock.data.model.Status.OK
|
||||||
import com.afollestad.nocknock.utilities.providers.IdProvider
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import java.lang.System.currentTimeMillis
|
import com.afollestad.nocknock.data.model.toSiteStatus
|
||||||
import kotlin.math.max
|
import com.afollestad.nocknock.data.model.toValidationMode
|
||||||
|
|
||||||
const val CHECK_INTERVAL_UNSET = -1L
|
const val CHECK_INTERVAL_UNSET = -1L
|
||||||
const val LAST_CHECK_NONE = -1L
|
const val LAST_CHECK_NONE = -1L
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad)*/
|
/** @author Aidan Follestad (@afollestad)*/
|
||||||
|
@Deprecated("Deprecated in favor of Site.")
|
||||||
data class ServerModel(
|
data class ServerModel(
|
||||||
var id: Int = 0,
|
var id: Int = 0,
|
||||||
val name: String,
|
val name: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val status: ServerStatus = OK,
|
val status: Status = OK,
|
||||||
val checkInterval: Long = CHECK_INTERVAL_UNSET,
|
val checkInterval: Long = CHECK_INTERVAL_UNSET,
|
||||||
val lastCheck: Long = LAST_CHECK_NONE,
|
val lastCheck: Long = LAST_CHECK_NONE,
|
||||||
val reason: String? = null,
|
val reason: String? = null,
|
||||||
|
@ -39,7 +42,7 @@ data class ServerModel(
|
||||||
val validationContent: String? = null,
|
val validationContent: String? = null,
|
||||||
val disabled: Boolean = false,
|
val disabled: Boolean = false,
|
||||||
val networkTimeout: Int = 0
|
val networkTimeout: Int = 0
|
||||||
) : IdProvider {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TABLE_NAME = "server_models"
|
const val TABLE_NAME = "server_models"
|
||||||
|
@ -59,31 +62,65 @@ data class ServerModel(
|
||||||
|
|
||||||
fun pull(cursor: Cursor): ServerModel {
|
fun pull(cursor: Cursor): ServerModel {
|
||||||
return ServerModel(
|
return ServerModel(
|
||||||
id = cursor.getInt(cursor.getColumnIndex(COLUMN_ID)),
|
id = cursor.getInt(
|
||||||
name = cursor.getString(cursor.getColumnIndex(COLUMN_NAME)),
|
cursor.getColumnIndex(
|
||||||
url = cursor.getString(cursor.getColumnIndex(COLUMN_URL)),
|
COLUMN_ID
|
||||||
status = cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS)).toServerStatus(),
|
)
|
||||||
checkInterval = cursor.getLong(cursor.getColumnIndex(COLUMN_CHECK_INTERVAL)),
|
),
|
||||||
lastCheck = cursor.getLong(cursor.getColumnIndex(COLUMN_LAST_CHECK)),
|
name = cursor.getString(
|
||||||
reason = cursor.getString(cursor.getColumnIndex(COLUMN_REASON)),
|
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(
|
validationMode = cursor.getInt(
|
||||||
cursor.getColumnIndex(COLUMN_VALIDATION_MODE)
|
cursor.getColumnIndex(
|
||||||
|
COLUMN_VALIDATION_MODE
|
||||||
|
)
|
||||||
).toValidationMode(),
|
).toValidationMode(),
|
||||||
validationContent = cursor.getString(cursor.getColumnIndex(COLUMN_VALIDATION_CONTENT)),
|
validationContent = cursor.getString(
|
||||||
disabled = cursor.getInt(cursor.getColumnIndex(COLUMN_DISABLED)) == 1,
|
cursor.getColumnIndex(
|
||||||
networkTimeout = cursor.getInt(cursor.getColumnIndex(COLUMN_NETWORK_TIMEOUT))
|
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 {
|
fun toContentValues() = ContentValues().apply {
|
||||||
put(COLUMN_NAME, name)
|
put(COLUMN_NAME, name)
|
||||||
put(COLUMN_URL, url)
|
put(COLUMN_URL, url)
|
|
@ -13,12 +13,13 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.content.Context
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
|
||||||
|
|
||||||
private const val SQL_CREATE_ENTRIES =
|
private const val SQL_CREATE_ENTRIES =
|
||||||
"CREATE TABLE ${ServerModel.TABLE_NAME} (" +
|
"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}"
|
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${ServerModel.TABLE_NAME}"
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class ServerModelDbHelper(context: Context) : SQLiteOpenHelper(
|
@Deprecated("Use AppDatabase.")
|
||||||
context, DATABASE_NAME, null, DATABASE_VERSION
|
internal class ServerModelDbHelper(context: Context) : SQLiteOpenHelper(
|
||||||
|
context, DATABASE_NAME, null,
|
||||||
|
DATABASE_VERSION
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val DATABASE_VERSION = 3
|
const val DATABASE_VERSION = 3
|
||||||
|
@ -71,6 +74,5 @@ class ServerModelDbHelper(context: Context) : SQLiteOpenHelper(
|
||||||
|
|
||||||
fun wipe() {
|
fun wipe() {
|
||||||
this.writableDatabase.execSQL(SQL_DELETE_ENTRIES)
|
this.writableDatabase.execSQL(SQL_DELETE_ENTRIES)
|
||||||
onCreate(this.writableDatabase)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,42 +13,24 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.app.Application
|
||||||
|
import android.content.ContentValues
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.legacy.ServerModel.Companion.COLUMN_ID
|
||||||
import com.afollestad.nocknock.data.ServerModel.Companion.COLUMN_ID
|
import com.afollestad.nocknock.data.legacy.ServerModel.Companion.DEFAULT_SORT_ORDER
|
||||||
import com.afollestad.nocknock.data.ServerModel.Companion.DEFAULT_SORT_ORDER
|
import com.afollestad.nocknock.data.legacy.ServerModel.Companion.TABLE_NAME
|
||||||
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
|
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
interface ServerModelStore {
|
@Deprecated("Deprecated in favor of AppDatabase.")
|
||||||
|
internal class ServerModelStore(app: Application) {
|
||||||
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 {
|
|
||||||
|
|
||||||
private val dbHelper = ServerModelDbHelper(app)
|
private val dbHelper = ServerModelDbHelper(app)
|
||||||
|
|
||||||
override suspend fun get(id: Int?): List<ServerModel> {
|
fun get(id: Int? = null): List<ServerModel> {
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
return getAll()
|
return getAll()
|
||||||
}
|
}
|
||||||
|
@ -88,19 +70,16 @@ class RealServerModelStore @Inject constructor(app: Application) : ServerModelSt
|
||||||
cursor.use { return readModels(it) }
|
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." }
|
check(model.id == 0) { "Cannot put a model that already has an ID." }
|
||||||
|
|
||||||
val writer = dbHelper.writableDatabase
|
val writer = dbHelper.writableDatabase
|
||||||
val newId = writer.insert(TABLE_NAME, null, model.toContentValues())
|
val newId = writer.insert(TABLE_NAME, null, model.toContentValues())
|
||||||
|
|
||||||
return model.copy(id = newId.toInt())
|
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." }
|
check(model.id != 0) { "Cannot update a model that does not have an ID." }
|
||||||
|
|
||||||
val oldModel = get(model.id).single()
|
val oldModel = get(model.id).single()
|
||||||
|
@ -111,35 +90,27 @@ class RealServerModelStore @Inject constructor(app: Application) : ServerModelSt
|
||||||
val valuesDiff = oldValues.diffFrom(newValues)
|
val valuesDiff = oldValues.diffFrom(newValues)
|
||||||
|
|
||||||
if (valuesDiff.size() == 0) {
|
if (valuesDiff.size() == 0) {
|
||||||
warn("Nothing has changed - nothing to update!")
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val selection = "$COLUMN_ID = ?"
|
val selection = "$COLUMN_ID = ?"
|
||||||
val selectionArgs = arrayOf("${model.id}")
|
val selectionArgs = arrayOf("${model.id}")
|
||||||
|
|
||||||
log("Updated model: $model")
|
|
||||||
return writer.update(TABLE_NAME, valuesDiff, selection, selectionArgs)
|
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." }
|
check(id != 0) { "Cannot delete a model that doesn't have an ID." }
|
||||||
|
|
||||||
val selection = "$COLUMN_ID = ?"
|
val selection = "$COLUMN_ID = ?"
|
||||||
val selectionArgs = arrayOf("$id")
|
val selectionArgs = arrayOf("$id")
|
||||||
|
|
||||||
log("Deleted model: $id")
|
|
||||||
return dbHelper.writableDatabase.delete(TABLE_NAME, selection, selectionArgs)
|
return dbHelper.writableDatabase.delete(TABLE_NAME, selection, selectionArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteAll(): Int {
|
fun wipe() = dbHelper.wipe()
|
||||||
log("Deleted all models")
|
|
||||||
return dbHelper.writableDatabase.delete(TABLE_NAME, null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@TestOnly fun db() = dbHelper
|
|
||||||
|
|
||||||
private fun readModels(cursor: Cursor): List<ServerModel> {
|
private fun readModels(cursor: Cursor): List<ServerModel> {
|
||||||
val results = mutableListOf<ServerModel>()
|
val results = mutableListOf<ServerModel>()
|
||||||
|
@ -148,4 +119,45 @@ class RealServerModelStore @Inject constructor(app: Application) : ServerModelSt
|
||||||
}
|
}
|
||||||
return results
|
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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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) {
|
enum class ValidationMode(val value: Int) {
|
||||||
|
/** The site is running normally if its status code is successful. */
|
||||||
STATUS_CODE(1),
|
STATUS_CODE(1),
|
||||||
|
/** The site is running normally if a piece of text is found in its response body. */
|
||||||
TERM_SEARCH(2),
|
TERM_SEARCH(2),
|
||||||
|
/** The site is running normally if a block of given JavaScript executes successfully. */
|
||||||
JAVASCRIPT(3);
|
JAVASCRIPT(3);
|
||||||
|
|
||||||
companion object {
|
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',
|
dagger : '2.19',
|
||||||
kotlin : '1.3.10',
|
kotlin : '1.3.10',
|
||||||
coroutines : '1.0.1',
|
coroutines : '1.0.1',
|
||||||
|
|
||||||
androidx : '1.0.0',
|
androidx : '1.0.0',
|
||||||
|
room : '2.0.0',
|
||||||
|
|
||||||
rxBinding : '3.0.0-alpha1',
|
rxBinding : '3.0.0-alpha1',
|
||||||
|
|
||||||
|
@ -23,9 +25,10 @@ ext.versions = [
|
||||||
rxkPrefs : '1.2.0',
|
rxkPrefs : '1.2.0',
|
||||||
|
|
||||||
timber : '4.7.1',
|
timber : '4.7.1',
|
||||||
testRunner : '1.0.2',
|
|
||||||
junit : '4.12',
|
junit : '4.12',
|
||||||
mockito : '2.23.0',
|
mockito : '2.23.0',
|
||||||
mockitoKotlin : '2.0.0-RC1',
|
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
|
targetSdkVersion versions.compileSdk
|
||||||
versionCode versions.publishVersionCode
|
versionCode versions.publishVersionCode
|
||||||
versionName versions.publishVersion
|
versionName versions.publishVersion
|
||||||
|
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,9 +32,6 @@ dependencies {
|
||||||
testImplementation 'org.mockito:mockito-core:' + versions.mockito
|
testImplementation 'org.mockito:mockito-core:' + versions.mockito
|
||||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin
|
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin
|
||||||
testImplementation 'com.google.truth:truth:' + versions.truth
|
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'
|
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)
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -13,27 +13,19 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.di
|
package com.afollestad.nocknock.engine;
|
||||||
|
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.engine.statuscheck.RealValidationManager;
|
||||||
import com.afollestad.nocknock.ui.main.MainActivity
|
import com.afollestad.nocknock.engine.statuscheck.ValidationManager;
|
||||||
import com.afollestad.nocknock.utilities.qualifiers.AppIconRes
|
import dagger.Binds;
|
||||||
import com.afollestad.nocknock.utilities.qualifiers.MainActivityClass
|
import dagger.Module;
|
||||||
import dagger.Module
|
import javax.inject.Singleton;
|
||||||
import dagger.Provides
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
@Module
|
@Module
|
||||||
open class MainModule {
|
public abstract class EngineModule {
|
||||||
|
|
||||||
@Provides
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
@AppIconRes
|
abstract ValidationManager provideCheckStatusManager(RealValidationManager checkStatusManager);
|
||||||
fun provideAppIconRes(): Int = R.mipmap.ic_launcher
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
@MainActivityClass
|
|
||||||
fun provideMainActivityClass(): Class<*> = MainActivity::class.java
|
|
||||||
}
|
}
|
|
@ -31,7 +31,7 @@ import timber.log.Timber.d as log
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@Inject lateinit var checkStatusManager: CheckStatusManager
|
@Inject lateinit var checkStatusManager: ValidationManager
|
||||||
|
|
||||||
override fun onReceive(
|
override fun onReceive(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
|
@ -18,17 +18,20 @@ package com.afollestad.nocknock.engine.statuscheck
|
||||||
import android.app.job.JobParameters
|
import android.app.job.JobParameters
|
||||||
import android.app.job.JobService
|
import android.app.job.JobService
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.ServerStatus
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.ServerStatus.CHECKING
|
import com.afollestad.nocknock.data.model.Status
|
||||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
import com.afollestad.nocknock.data.model.Status.CHECKING
|
||||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.model.Status.OK
|
||||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.data.isPending
|
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.BuildConfig.APPLICATION_ID
|
||||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.utilities.ext.injector
|
import com.afollestad.nocknock.utilities.ext.injector
|
||||||
import com.afollestad.nocknock.utilities.js.JavaScript
|
import com.afollestad.nocknock.utilities.js.JavaScript
|
||||||
|
@ -42,8 +45,12 @@ import java.lang.System.currentTimeMillis
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import timber.log.Timber.d as log
|
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 {
|
companion object {
|
||||||
const val ACTION_STATUS_UPDATE = "$APPLICATION_ID.STATUS_UPDATE"
|
const val ACTION_STATUS_UPDATE = "$APPLICATION_ID.STATUS_UPDATE"
|
||||||
|
@ -52,47 +59,50 @@ class CheckStatusJob : JobService() {
|
||||||
const val KEY_SITE_ID = "site.id"
|
const val KEY_SITE_ID = "site.id"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject lateinit var modelStore: ServerModelStore
|
@Inject lateinit var database: AppDatabase
|
||||||
@Inject lateinit var checkStatusManager: CheckStatusManager
|
@Inject lateinit var checkStatusManager: ValidationManager
|
||||||
@Inject lateinit var notificationManager: NockNotificationManager
|
@Inject lateinit var notificationManager: NockNotificationManager
|
||||||
|
|
||||||
override fun onStartJob(params: JobParameters): Boolean {
|
override fun onStartJob(params: JobParameters): Boolean {
|
||||||
injector().injectInto(this)
|
injector().injectInto(this)
|
||||||
val siteId = params.extras.getInt(KEY_SITE_ID)
|
val siteId = params.extras.getLong(KEY_SITE_ID)
|
||||||
|
|
||||||
GlobalScope.launch(Main) {
|
GlobalScope.launch(Main) {
|
||||||
val sites = async(IO) { modelStore.get(id = siteId) }.await()
|
val site = async(IO) { database.getSite(siteId) }.await()
|
||||||
if (sites.isEmpty()) {
|
if (site == null) {
|
||||||
log("Unable to find any sites for ID $siteId, this job will not be rescheduled.")
|
log("Unable to find a site for ID $siteId, this job will not be rescheduled.")
|
||||||
return@launch jobFinished(params, false)
|
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}...")
|
log("Performing status checks on site ${site.id}...")
|
||||||
sendBroadcast(Intent(ACTION_JOB_RUNNING).apply { putExtra(KEY_SITE_ID, site.id) })
|
sendBroadcast(Intent(ACTION_JOB_RUNNING).apply { putExtra(KEY_SITE_ID, site.id) })
|
||||||
|
|
||||||
log("Checking ${site.name} (${site.url})...")
|
log("Checking ${site.name} (${site.url})...")
|
||||||
|
|
||||||
val result = async(IO) {
|
val jobResult = async(IO) {
|
||||||
updateStatus(site, CHECKING)
|
updateStatus(site, CHECKING)
|
||||||
val checkResult = checkStatusManager.performCheck(site)
|
val checkResult = checkStatusManager.performCheck(site)
|
||||||
val resultModel = checkResult.model
|
val resultModel = checkResult.model
|
||||||
val resultResponse = checkResult.response
|
val resultResponse = checkResult.response
|
||||||
|
val result = resultModel.lastResult!!
|
||||||
|
|
||||||
if (resultModel.status != OK) {
|
if (result.status != OK) {
|
||||||
log("Got unsuccessful check status back: ${resultModel.reason}")
|
log("Got unsuccessful check status back: ${result.reason}")
|
||||||
return@async updateStatus(site = resultModel)
|
return@async updateStatus(site = resultModel)
|
||||||
} else {
|
} else {
|
||||||
when (site.validationMode) {
|
when (siteSettings.validationMode) {
|
||||||
TERM_SEARCH -> {
|
TERM_SEARCH -> {
|
||||||
val body = resultResponse?.body()?.string() ?: ""
|
val body = resultResponse?.body()?.string() ?: ""
|
||||||
log("Using TERM_SEARCH validation mode on body of length: ${body.length}")
|
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(
|
updateStatus(
|
||||||
resultModel.copy(
|
resultModel.withStatus(
|
||||||
status = ERROR,
|
status = ERROR,
|
||||||
reason = "Term \"${site.validationContent}\" not found in response body."
|
reason = "Term \"${siteSettings.validationArgs}\" not found in response body."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -102,9 +112,9 @@ class CheckStatusJob : JobService() {
|
||||||
JAVASCRIPT -> {
|
JAVASCRIPT -> {
|
||||||
val body = resultResponse?.body()?.string() ?: ""
|
val body = resultResponse?.body()?.string() ?: ""
|
||||||
log("Using JAVASCRIPT validation mode on body of length: ${body.length}")
|
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) {
|
return@async if (reason != null) {
|
||||||
updateStatus(resultModel.copy(reason = reason), status = ERROR)
|
updateStatus(resultModel.withStatus(reason = reason), status = ERROR)
|
||||||
} else {
|
} else {
|
||||||
resultModel
|
resultModel
|
||||||
}
|
}
|
||||||
|
@ -113,27 +123,29 @@ class CheckStatusJob : JobService() {
|
||||||
// We already know the status code is successful because we are in this else branch
|
// We already know the status code is successful because we are in this else branch
|
||||||
log("Using STATUS_CODE validation, which has passed!")
|
log("Using STATUS_CODE validation, which has passed!")
|
||||||
updateStatus(
|
updateStatus(
|
||||||
resultModel.copy(
|
resultModel.withStatus(
|
||||||
status = OK,
|
status = OK,
|
||||||
reason = null
|
reason = null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw IllegalArgumentException("Unknown validation mode: ${site.validationMode}")
|
throw IllegalArgumentException(
|
||||||
|
"Unknown validation mode: ${siteSettings.validationArgs}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.await()
|
}.await()
|
||||||
|
|
||||||
if (result.status == OK) {
|
if (jobResult.lastResult!!.status == OK) {
|
||||||
notificationManager.cancelStatusNotification(result)
|
notificationManager.cancelStatusNotification(jobResult)
|
||||||
} else {
|
} else {
|
||||||
notificationManager.postStatusNotification(result)
|
notificationManager.postStatusNotification(jobResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkStatusManager.scheduleCheck(
|
checkStatusManager.scheduleCheck(
|
||||||
site = result,
|
site = jobResult,
|
||||||
fromFinishingJob = true
|
fromFinishingJob = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -148,28 +160,30 @@ class CheckStatusJob : JobService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateStatus(
|
private suspend fun updateStatus(
|
||||||
site: ServerModel,
|
site: Site,
|
||||||
status: ServerStatus = site.status
|
status: Status = site.lastResult?.status ?: WAITING
|
||||||
): ServerModel {
|
): Site {
|
||||||
log("Updating ${site.name} (${site.url}) status to $status...")
|
log("Updating ${site.name} (${site.url}) status to $status...")
|
||||||
|
|
||||||
val lastCheckTime =
|
val lastCheckTime =
|
||||||
if (status.isPending()) site.lastCheck
|
if (status.isPending()) site.lastResult?.timestampMs ?: -1
|
||||||
else currentTimeMillis()
|
else currentTimeMillis()
|
||||||
val reason =
|
val reason =
|
||||||
if (status == OK) null
|
if (status == OK) null
|
||||||
else site.reason
|
else site.lastResult?.reason ?: "Unknown"
|
||||||
|
|
||||||
val newSiteModel = site.copy(
|
val updatedModel = site.withStatus(
|
||||||
status = status,
|
status = status,
|
||||||
lastCheck = lastCheckTime,
|
timestamp = lastCheckTime,
|
||||||
reason = reason
|
reason = reason
|
||||||
)
|
)
|
||||||
modelStore.update(newSiteModel)
|
database.updateSite(updatedModel)
|
||||||
|
|
||||||
withContext(Main) {
|
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
|
||||||
import android.app.job.JobScheduler.RESULT_SUCCESS
|
import android.app.job.JobScheduler.RESULT_SUCCESS
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
import com.afollestad.nocknock.data.allSites
|
||||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
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.R
|
||||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_SITE_ID
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_SITE_ID
|
|
||||||
import com.afollestad.nocknock.utilities.providers.BundleProvider
|
import com.afollestad.nocknock.utilities.providers.BundleProvider
|
||||||
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
|
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
|
||||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||||
|
@ -37,37 +38,37 @@ import timber.log.Timber.d as log
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
data class CheckResult(
|
data class CheckResult(
|
||||||
val model: ServerModel,
|
val model: Site,
|
||||||
val response: Response? = null
|
val response: Response? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
|
typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
interface CheckStatusManager {
|
interface ValidationManager {
|
||||||
|
|
||||||
suspend fun ensureScheduledChecks()
|
suspend fun ensureScheduledChecks()
|
||||||
|
|
||||||
fun scheduleCheck(
|
fun scheduleCheck(
|
||||||
site: ServerModel,
|
site: Site,
|
||||||
rightNow: Boolean = false,
|
rightNow: Boolean = false,
|
||||||
cancelPrevious: Boolean = rightNow,
|
cancelPrevious: Boolean = rightNow,
|
||||||
fromFinishingJob: Boolean = false
|
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 jobScheduler: JobScheduler,
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val bundleProvider: BundleProvider,
|
private val bundleProvider: BundleProvider,
|
||||||
private val jobInfoProvider: JobInfoProvider,
|
private val jobInfoProvider: JobInfoProvider,
|
||||||
private val siteStore: ServerModelStore
|
private val database: AppDatabase
|
||||||
) : CheckStatusManager {
|
) : ValidationManager {
|
||||||
|
|
||||||
private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
|
private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
|
||||||
client.newBuilder()
|
client.newBuilder()
|
||||||
|
@ -76,12 +77,12 @@ class RealCheckStatusManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun ensureScheduledChecks() {
|
override suspend fun ensureScheduledChecks() {
|
||||||
val sites = siteStore.get()
|
val sites = database.allSites()
|
||||||
if (sites.isEmpty()) {
|
if (sites.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log("Ensuring enabled sites have scheduled checks.")
|
log("Ensuring enabled sites have scheduled checks.")
|
||||||
sites.filter { !it.disabled }
|
sites.filter { it.settings?.disabled != true }
|
||||||
.forEach { site ->
|
.forEach { site ->
|
||||||
val existingJob = jobForSite(site)
|
val existingJob = jobForSite(site)
|
||||||
if (existingJob == null) {
|
if (existingJob == null) {
|
||||||
|
@ -94,12 +95,15 @@ class RealCheckStatusManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun scheduleCheck(
|
override fun scheduleCheck(
|
||||||
site: ServerModel,
|
site: Site,
|
||||||
rightNow: Boolean,
|
rightNow: Boolean,
|
||||||
cancelPrevious: Boolean,
|
cancelPrevious: Boolean,
|
||||||
fromFinishingJob: 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) {
|
if (cancelPrevious) {
|
||||||
cancelCheck(site)
|
cancelCheck(site)
|
||||||
} else if (!fromFinishingJob) {
|
} else if (!fromFinishingJob) {
|
||||||
|
@ -111,18 +115,18 @@ class RealCheckStatusManager @Inject constructor(
|
||||||
|
|
||||||
log("Requesting a check job for site to be scheduled: $site")
|
log("Requesting a check job for site to be scheduled: $site")
|
||||||
val extras = bundleProvider.createPersistable {
|
val extras = bundleProvider.createPersistable {
|
||||||
putInt(KEY_SITE_ID, site.id)
|
putLong(KEY_SITE_ID, site.id)
|
||||||
}
|
}
|
||||||
val jobInfo = jobInfoProvider.createCheckJob(
|
val jobInfo = jobInfoProvider.createCheckJob(
|
||||||
id = site.id,
|
id = site.id.toInt(),
|
||||||
onlyUnmeteredNetwork = false,
|
onlyUnmeteredNetwork = false,
|
||||||
delayMs = if (rightNow) {
|
delayMs = if (rightNow) {
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
site.checkInterval
|
siteSettings.validationIntervalMs
|
||||||
},
|
},
|
||||||
extras = extras,
|
extras = extras,
|
||||||
target = CheckStatusJob::class.java
|
target = ValidationJob::class.java
|
||||||
)
|
)
|
||||||
|
|
||||||
val dispatchResult = jobScheduler.schedule(jobInfo)
|
val dispatchResult = jobScheduler.schedule(jobInfo)
|
||||||
|
@ -133,15 +137,17 @@ class RealCheckStatusManager @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelCheck(site: ServerModel) {
|
override fun cancelCheck(site: Site) {
|
||||||
check(site.id != 0) { "Cannot cancel scheduled checks for jobs with no ID." }
|
check(site.id != 0L) { "Cannot cancel scheduled checks for jobs with no ID." }
|
||||||
log("Cancelling scheduled checks for site: ${site.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 {
|
override suspend fun performCheck(site: Site): CheckResult {
|
||||||
check(site.id != 0) { "Cannot schedule checks for jobs with no ID." }
|
check(site.id != 0L) { "Cannot schedule checks for jobs with no ID." }
|
||||||
check(site.networkTimeout > 0) { "Network timeout not set for site ${site.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}")
|
log("performCheck(${site.id}) - GET ${site.url}")
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
|
@ -150,20 +156,20 @@ class RealCheckStatusManager @Inject constructor(
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val client = clientTimeoutChanger(okHttpClient, site.networkTimeout)
|
val client = clientTimeoutChanger(okHttpClient, siteSettings.networkTimeout)
|
||||||
val response = client.newCall(request)
|
val response = client.newCall(request)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
if (response.isSuccessful || response.code() == 401) {
|
if (response.isSuccessful || response.code() == 401) {
|
||||||
log("performCheck(${site.id}) = Successful")
|
log("performCheck(${site.id}) = Successful")
|
||||||
CheckResult(
|
CheckResult(
|
||||||
model = site.copy(status = OK, reason = null),
|
model = site.withStatus(status = OK, reason = null),
|
||||||
response = response
|
response = response
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
log("performCheck(${site.id}) = Failure, HTTP code ${response.code()}")
|
log("performCheck(${site.id}) = Failure, HTTP code ${response.code()}")
|
||||||
CheckResult(
|
CheckResult(
|
||||||
model = site.copy(
|
model = site.withStatus(
|
||||||
status = ERROR,
|
status = ERROR,
|
||||||
reason = "Response ${response.code()} - ${response.body()?.string() ?: "Unknown"}"
|
reason = "Response ${response.code()} - ${response.body()?.string() ?: "Unknown"}"
|
||||||
),
|
),
|
||||||
|
@ -173,20 +179,20 @@ class RealCheckStatusManager @Inject constructor(
|
||||||
} catch (timeoutEx: SocketTimeoutException) {
|
} catch (timeoutEx: SocketTimeoutException) {
|
||||||
log("performCheck(${site.id}) = Socket Timeout")
|
log("performCheck(${site.id}) = Socket Timeout")
|
||||||
CheckResult(
|
CheckResult(
|
||||||
model = site.copy(
|
model = site.withStatus(
|
||||||
status = ERROR,
|
status = ERROR,
|
||||||
reason = stringProvider.get(R.string.timeout)
|
reason = stringProvider.get(R.string.timeout)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
log("performCheck(${site.id}) = Error: ${ex.message}")
|
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
|
jobScheduler.allPendingJobs
|
||||||
.firstOrNull { job -> job.id == site.id }
|
.firstOrNull { job -> job.id == site.id.toInt() }
|
||||||
|
|
||||||
@TestOnly fun setClientTimeoutChanger(changer: ClientTimeoutChanger) {
|
@TestOnly fun setClientTimeoutChanger(changer: ClientTimeoutChanger) {
|
||||||
this.clientTimeoutChanger = changer
|
this.clientTimeoutChanger = changer
|
|
@ -17,13 +17,13 @@ package com.afollestad.nocknock.engine
|
||||||
|
|
||||||
import android.app.job.JobInfo
|
import android.app.job.JobInfo
|
||||||
import android.app.job.JobScheduler
|
import android.app.job.JobScheduler
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.legacy.ServerModel
|
||||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
import com.afollestad.nocknock.data.model.Status.OK
|
||||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
import com.afollestad.nocknock.data.legacy.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.engine.statuscheck.RealCheckStatusManager
|
import com.afollestad.nocknock.engine.statuscheck.RealValidationManager
|
||||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.nhaarman.mockitokotlin2.any
|
import com.nhaarman.mockitokotlin2.any
|
||||||
|
@ -58,7 +58,7 @@ class CheckStatusManagerTest {
|
||||||
private val jobInfoProvider = testJobInfoProvider()
|
private val jobInfoProvider = testJobInfoProvider()
|
||||||
private val store = mock<ServerModelStore>()
|
private val store = mock<ServerModelStore>()
|
||||||
|
|
||||||
private val manager = RealCheckStatusManager(
|
private val manager = RealValidationManager(
|
||||||
jobScheduler,
|
jobScheduler,
|
||||||
okHttpClient,
|
okHttpClient,
|
||||||
stringProvider,
|
stringProvider,
|
||||||
|
|
|
@ -14,7 +14,6 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':data')
|
|
||||||
implementation project(':utilities')
|
implementation project(':utilities')
|
||||||
|
|
||||||
api 'androidx.appcompat:appcompat:' + versions.androidx
|
api 'androidx.appcompat:appcompat:' + versions.androidx
|
||||||
|
|
|
@ -18,9 +18,9 @@ package com.afollestad.nocknock.notifications
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.os.Build.VERSION_CODES
|
import android.os.Build.VERSION_CODES
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
|
||||||
import com.afollestad.nocknock.notifications.Channel.CheckFailures
|
import com.afollestad.nocknock.notifications.Channel.CheckFailures
|
||||||
import com.afollestad.nocknock.utilities.providers.BitmapProvider
|
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.IntentProvider
|
||||||
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
|
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
|
||||||
import com.afollestad.nocknock.utilities.providers.NotificationProvider
|
import com.afollestad.nocknock.utilities.providers.NotificationProvider
|
||||||
|
@ -37,9 +37,9 @@ interface NockNotificationManager {
|
||||||
|
|
||||||
fun createChannels()
|
fun createChannels()
|
||||||
|
|
||||||
fun postStatusNotification(model: ServerModel)
|
fun postStatusNotification(model: CanNotifyModel)
|
||||||
|
|
||||||
fun cancelStatusNotification(model: ServerModel)
|
fun cancelStatusNotification(model: CanNotifyModel)
|
||||||
|
|
||||||
fun cancelStatusNotifications()
|
fun cancelStatusNotifications()
|
||||||
}
|
}
|
||||||
|
@ -65,32 +65,32 @@ class RealNockNotificationManager @Inject constructor(
|
||||||
override fun createChannels() =
|
override fun createChannels() =
|
||||||
Channel.values().forEach(this::createChannel)
|
Channel.values().forEach(this::createChannel)
|
||||||
|
|
||||||
override fun postStatusNotification(model: ServerModel) {
|
override fun postStatusNotification(model: CanNotifyModel) {
|
||||||
if (isAppOpen) {
|
if (isAppOpen) {
|
||||||
// Don't show notifications while the app is open
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Posting status notification for site ${model.id}...")
|
log("Posting status notification for site ${model.notiId()}...")
|
||||||
val intent = intentProvider.getPendingIntentForViewSite(model)
|
val intent = intentProvider.getPendingIntentForViewSite(model)
|
||||||
|
|
||||||
val newNotification = notificationProvider.create(
|
val newNotification = notificationProvider.create(
|
||||||
channelId = CheckFailures.id,
|
channelId = CheckFailures.id,
|
||||||
title = model.name,
|
title = model.notiName(),
|
||||||
content = stringProvider.get(R.string.something_wrong),
|
content = stringProvider.get(R.string.something_wrong),
|
||||||
intent = intent,
|
intent = intent,
|
||||||
smallIcon = R.drawable.ic_notification,
|
smallIcon = R.drawable.ic_notification,
|
||||||
largeIcon = bitmapProvider.get(appIconRes)
|
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()}.")
|
log("Posted status notification for site ${model.notificationId()}.")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelStatusNotification(model: ServerModel) {
|
override fun cancelStatusNotification(model: CanNotifyModel) {
|
||||||
stockManager.cancel(model.notificationId())
|
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()
|
override fun cancelStatusNotifications() = stockManager.cancelAll()
|
||||||
|
@ -107,5 +107,5 @@ class RealNockNotificationManager @Inject constructor(
|
||||||
log("Created notification channel ${channel.id}")
|
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)
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -13,19 +13,18 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.notifications
|
package com.afollestad.nocknock.notifications;
|
||||||
|
|
||||||
import dagger.Binds
|
import dagger.Binds;
|
||||||
import dagger.Module
|
import dagger.Module;
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
@Module
|
@Module
|
||||||
abstract class NotificationsModule {
|
public abstract class NotificationsModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun provideNockNotificationManager(
|
abstract NockNotificationManager provideNockNotificationManager(
|
||||||
notificationManager: RealNockNotificationManager
|
RealNockNotificationManager notificationManager);
|
||||||
): NockNotificationManager
|
|
||||||
}
|
}
|
|
@ -20,8 +20,8 @@ import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.legacy.ServerModel
|
||||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.notifications.Channel.CheckFailures
|
import com.afollestad.nocknock.notifications.Channel.CheckFailures
|
||||||
import com.afollestad.nocknock.utilities.providers.BitmapProvider
|
import com.afollestad.nocknock.utilities.providers.BitmapProvider
|
||||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
/**
|
/*
|
||||||
* Designed and developed by Aidan Follestad (@afollestad)
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* 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
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* 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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.BitmapProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.BundleProvider
|
import com.afollestad.nocknock.utilities.providers.BundleProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
import com.afollestad.nocknock.utilities.providers.IntentProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
|
import com.afollestad.nocknock.utilities.providers.JobInfoProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
|
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.NotificationProvider
|
import com.afollestad.nocknock.utilities.providers.NotificationProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.RealBitmapProvider
|
import com.afollestad.nocknock.utilities.providers.RealBitmapProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.RealBundleProvider
|
import com.afollestad.nocknock.utilities.providers.RealBundleProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.RealIntentProvider
|
import com.afollestad.nocknock.utilities.providers.RealIntentProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.RealJobInfoProvider
|
import com.afollestad.nocknock.utilities.providers.RealJobInfoProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.RealNotificationChannelProvider
|
import com.afollestad.nocknock.utilities.providers.RealNotificationChannelProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.RealNotificationProvider
|
import com.afollestad.nocknock.utilities.providers.RealNotificationProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.RealSdkProvider
|
import com.afollestad.nocknock.utilities.providers.RealSdkProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.RealStringProvider
|
import com.afollestad.nocknock.utilities.providers.RealStringProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.SdkProvider
|
import com.afollestad.nocknock.utilities.providers.SdkProvider;
|
||||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
import com.afollestad.nocknock.utilities.providers.StringProvider;
|
||||||
import dagger.Binds
|
import dagger.Binds;
|
||||||
import dagger.Module
|
import dagger.Module;
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
@Module
|
@Module
|
||||||
abstract class UtilitiesModule {
|
public abstract class UtilitiesModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun provideSdkProvider(
|
abstract SdkProvider provideSdkProvider(RealSdkProvider sdkProvider);
|
||||||
sdkProvider: RealSdkProvider
|
|
||||||
): SdkProvider
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun provideBitmapProvider(
|
abstract BitmapProvider provideBitmapProvider(RealBitmapProvider bitmapProvider);
|
||||||
bitmapProvider: RealBitmapProvider
|
|
||||||
): BitmapProvider
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun provideStringProvider(
|
abstract StringProvider provideStringProvider(RealStringProvider stringProvider);
|
||||||
stringProvider: RealStringProvider
|
|
||||||
): StringProvider
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun provideIntentProvider(
|
abstract IntentProvider provideIntentProvider(RealIntentProvider intentProvider);
|
||||||
intentProvider: RealIntentProvider
|
|
||||||
): IntentProvider
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun provideChannelProvider(
|
abstract NotificationChannelProvider provideChannelProvider(
|
||||||
channelProvider: RealNotificationChannelProvider
|
RealNotificationChannelProvider channelProvider);
|
||||||
): NotificationChannelProvider
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun provideNotificationProvider(
|
abstract NotificationProvider provideNotificationProvider(
|
||||||
notificationProvider: RealNotificationProvider
|
RealNotificationProvider notificationProvider);
|
||||||
): NotificationProvider
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun provideBundleProvider(
|
abstract BundleProvider provideBundleProvider(RealBundleProvider bundleProvider);
|
||||||
bundleProvider: RealBundleProvider
|
|
||||||
): BundleProvider
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun provideJobInfoProvider(
|
abstract JobInfoProvider provideJobInfoProvider(RealJobInfoProvider jobInfoProvider);
|
||||||
jobInfoProvider: 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
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface IBundle {
|
interface IBundle {
|
||||||
fun putInt(
|
fun putLong(
|
||||||
key: String,
|
key: String,
|
||||||
value: Int
|
value: Long
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ typealias IBundler = IBundle.() -> Unit
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
interface BundleProvider {
|
interface BundleProvider {
|
||||||
|
|
||||||
fun createPersistable(builder: IBundle.() -> Unit): PersistableBundle
|
fun createPersistable(bundler: IBundle.() -> Unit): PersistableBundle
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
@ -39,10 +39,10 @@ class RealBundleProvider @Inject constructor() : BundleProvider {
|
||||||
override fun createPersistable(bundler: IBundler): PersistableBundle {
|
override fun createPersistable(bundler: IBundler): PersistableBundle {
|
||||||
val realBundle = PersistableBundle()
|
val realBundle = PersistableBundle()
|
||||||
bundler(object : IBundle {
|
bundler(object : IBundle {
|
||||||
override fun putInt(
|
override fun putLong(
|
||||||
key: String,
|
key: String,
|
||||||
value: Int
|
value: Long
|
||||||
) = realBundle.putInt(key, value)
|
) = realBundle.putLong(key, value)
|
||||||
})
|
})
|
||||||
return realBundle
|
return realBundle
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,16 +24,20 @@ import java.io.Serializable
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @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) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
interface IntentProvider {
|
interface IntentProvider {
|
||||||
|
|
||||||
fun getPendingIntentForViewSite(
|
fun getPendingIntentForViewSite(
|
||||||
model: IdProvider
|
model: CanNotifyModel
|
||||||
): PendingIntent
|
): PendingIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,17 +52,17 @@ class RealIntentProvider @Inject constructor(
|
||||||
const val KEY_VIEW_NOTIFICATION_MODEL = "model"
|
const val KEY_VIEW_NOTIFICATION_MODEL = "model"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPendingIntentForViewSite(model: IdProvider): PendingIntent {
|
override fun getPendingIntentForViewSite(model: CanNotifyModel): PendingIntent {
|
||||||
val openIntent = getIntentForViewSite(model)
|
val openIntent = getIntentForViewSite(model)
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
app,
|
app,
|
||||||
BASE_NOTIFICATION_REQUEST_CODE + model.id(),
|
BASE_NOTIFICATION_REQUEST_CODE + model.notiId(),
|
||||||
openIntent,
|
openIntent,
|
||||||
FLAG_CANCEL_CURRENT
|
FLAG_CANCEL_CURRENT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getIntentForViewSite(model: IdProvider) =
|
private fun getIntentForViewSite(model: CanNotifyModel) =
|
||||||
Intent(app, mainActivity).apply {
|
Intent(app, mainActivity).apply {
|
||||||
putExtra(KEY_VIEW_NOTIFICATION_MODEL, model)
|
putExtra(KEY_VIEW_NOTIFICATION_MODEL, model)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package com.afollestad.nocknock.viewcomponents
|
package com.afollestad.nocknock.viewcomponents
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -32,6 +33,7 @@ class LoadingIndicatorFrame(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val showRunnable = Runnable { show() }
|
private val showRunnable = Runnable { show() }
|
||||||
|
private val delayHandler = Handler()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setBackgroundColor(ContextCompat.getColor(context, R.color.loading_indicator_frame_background))
|
setBackgroundColor(ContextCompat.getColor(context, R.color.loading_indicator_frame_background))
|
||||||
|
@ -42,11 +44,11 @@ class LoadingIndicatorFrame(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLoading() {
|
fun setLoading() {
|
||||||
handler.postDelayed(showRunnable, SHOW_DELAY_MS)
|
delayHandler.postDelayed(showRunnable, SHOW_DELAY_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDone() {
|
fun setDone() {
|
||||||
handler.removeCallbacks(showRunnable)
|
delayHandler.removeCallbacks(showRunnable)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,11 @@ package com.afollestad.nocknock.viewcomponents
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
import com.afollestad.nocknock.data.ServerStatus
|
import com.afollestad.nocknock.data.model.Status
|
||||||
import com.afollestad.nocknock.data.ServerStatus.CHECKING
|
import com.afollestad.nocknock.data.model.Status.CHECKING
|
||||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
import com.afollestad.nocknock.data.model.Status.OK
|
||||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class StatusImageView(
|
class StatusImageView(
|
||||||
|
@ -34,7 +34,7 @@ class StatusImageView(
|
||||||
setStatus(OK)
|
setStatus(OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setStatus(status: ServerStatus) = when (status) {
|
fun setStatus(status: Status) = when (status) {
|
||||||
CHECKING, WAITING -> {
|
CHECKING, WAITING -> {
|
||||||
setImageResource(R.drawable.status_progress)
|
setImageResource(R.drawable.status_progress)
|
||||||
setBackgroundResource(R.drawable.yellow_circle)
|
setBackgroundResource(R.drawable.yellow_circle)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue