diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index f519041..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -nock-nock \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 96cc43e..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index e7bedf3..0000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 97626ba..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index ecd817f..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 3b31283..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml deleted file mode 100644 index d819570..0000000 --- a/.idea/markdown-navigator.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/markdown-navigator/profiles_settings.xml b/.idea/markdown-navigator/profiles_settings.xml deleted file mode 100644 index 57927c5..0000000 --- a/.idea/markdown-navigator/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index fbb6828..06ee295 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,46 +1,38 @@ - - - - - - - - - - - - - - + - \ No newline at end of file + diff --git a/.idea/modules.xml b/.idea/modules.xml index afe79ae..22d4f44 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,11 +2,12 @@ - - + + - + + - \ No newline at end of file + diff --git a/.travis.yml b/.travis.yml index 9cd1ea0..c1eda38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ android: components: - tools - platform-tools - - build-tools-27.0.2 - - android-27 + - build-tools-28.0.3 + - android-28 - extra-android-support - extra-android-m2repository - extra-google-m2repository @@ -19,4 +19,4 @@ android: #- sys-img-x86-android-17 licenses: - - '.+' \ No newline at end of file + - '.+' diff --git a/NockNock-0.1.3.1.apk b/NockNock-0.1.3.1.apk deleted file mode 100644 index 7eb45b4..0000000 Binary files a/NockNock-0.1.3.1.apk and /dev/null differ diff --git a/app/build.gradle b/app/build.gradle index db9afa2..205652d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,40 +1,38 @@ +apply from: '../dependencies.gradle' apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion versions.compileSdk - buildToolsVersion versions.buildTools + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools - defaultConfig { - applicationId "com.afollestad.nocknock" - minSdkVersion versions.minSdk - targetSdkVersion versions.compileSdk - versionCode versions.publishVersionCode - versionName versions.publishVersion - - lintOptions { - abortOnError false - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } + defaultConfig { + applicationId "com.afollestad.nocknock" + minSdkVersion versions.minSdk + targetSdkVersion versions.compileSdk + versionCode versions.publishVersionCode + versionName versions.publishVersion + } } dependencies { - compile 'com.android.support:appcompat-v7:' + versions.supportLib - compile 'com.android.support:design:' + versions.supportLib - compile 'com.afollestad.material-dialogs:core:' + versions.materialDialogs - compile 'com.afollestad.material-dialogs:commons:' + versions.materialDialogs - compile 'com.afollestad:bridge:' + versions.bridge - compile 'com.afollestad:inquiry:' + versions.inquiry - compile files('libs/rhino-1.7.7.1.jar') -} \ No newline at end of file + implementation project(':data') + implementation project(':utilities') + implementation project(':engine') + implementation project(':notifications') + + implementation 'androidx.appcompat:appcompat:' + versions.androidx + implementation 'androidx.recyclerview:recyclerview:' + versions.androidx + implementation 'com.google.android.material:material:' + versions.androidx + + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin + + implementation 'com.google.dagger:dagger:' + versions.dagger + kapt 'com.google.dagger:dagger-compiler:' + versions.dagger + + implementation 'com.afollestad.material-dialogs:core:' + versions.materialDialogs +} + +apply from: '../spotless.gradle' \ No newline at end of file diff --git a/app/libs/rhino-1.7.7.1.jar b/app/libs/rhino-1.7.7.1.jar deleted file mode 100644 index a8b9417..0000000 Binary files a/app/libs/rhino-1.7.7.1.jar and /dev/null differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100644 index 666743b..0000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:\Users\drumm\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d6a9d32..cac0668 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,60 +3,49 @@ xmlns:tools="http://schemas.android.com/tools" package="com.afollestad.nocknock"> - - - + + + - + - - - + + + - - - + + + - + - + - + - - - - - + - - - - - - - - - - \ No newline at end of file + diff --git a/app/src/main/java/com/afollestad/nocknock/App.kt b/app/src/main/java/com/afollestad/nocknock/App.kt new file mode 100644 index 0000000..06e17bd --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/App.kt @@ -0,0 +1,56 @@ +/* + * Licensed under Apache-2.0 + * + * Designed and developed by Aidan Follestad (@afollestad) + */ +package com.afollestad.nocknock + +import android.app.Application +import android.app.NotificationManager +import android.app.job.JobScheduler +import android.content.Context +import com.afollestad.nocknock.di.AppComponent +import com.afollestad.nocknock.di.DaggerAppComponent +import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob +import com.afollestad.nocknock.ui.AddSiteActivity +import com.afollestad.nocknock.ui.MainActivity +import com.afollestad.nocknock.ui.ViewSiteActivity +import com.afollestad.nocknock.utilities.Injector +import okhttp3.OkHttpClient + +/** @author Aidan Follestad (afollestad) */ +class App : Application(), Injector { + + lateinit var appComponent: AppComponent + + override fun onCreate() { + super.onCreate() + + val okHttpClient = OkHttpClient.Builder() + .addNetworkInterceptor { chain -> + val request = chain.request() + .newBuilder() + .addHeader("User-Agent", "com.afollestad.nocknock") + .build() + chain.proceed(request) + } + .build() + val jobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + appComponent = DaggerAppComponent.builder() + .application(this) + .okHttpClient(okHttpClient) + .jobScheduler(jobScheduler) + .notificationManager(notificationManager) + .build() + } + + override fun injectInto(target: Any) = when (target) { + is MainActivity -> appComponent.inject(target) + is ViewSiteActivity -> appComponent.inject(target) + is AddSiteActivity -> appComponent.inject(target) + is CheckStatusJob -> appComponent.inject(target) + else -> throw IllegalStateException("Can't inject into $target") + } +} diff --git a/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.java b/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.java deleted file mode 100644 index e64c570..0000000 --- a/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.afollestad.nocknock.adapter; - -import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import com.afollestad.nocknock.R; -import com.afollestad.nocknock.api.ServerModel; -import com.afollestad.nocknock.api.ServerStatus; -import com.afollestad.nocknock.util.TimeUtil; -import com.afollestad.nocknock.views.StatusImageView; -import java.util.ArrayList; -import java.util.Collections; - -/** @author Aidan Follestad (afollestad) */ -public class ServerAdapter extends RecyclerView.Adapter { - - private final Object LOCK = new Object(); - private ArrayList mServers; - private ClickListener mListener; - - public interface ClickListener { - void onSiteSelected(int index, ServerModel model, boolean longClick); - } - - void performClick(int index, boolean longClick) { - if (mListener != null) { - mListener.onSiteSelected(index, mServers.get(index), longClick); - } - } - - public ServerAdapter(ClickListener listener) { - mListener = listener; - mServers = new ArrayList<>(2); - } - - public void add(ServerModel model) { - mServers.add(model); - notifyItemInserted(mServers.size() - 1); - } - - private void update(int index, ServerModel model) { - mServers.set(index, model); - notifyItemChanged(index); - } - - public void update(ServerModel model) { - synchronized (LOCK) { - for (int i = 0; i < mServers.size(); i++) { - if (mServers.get(i).id == model.id) { - update(i, model); - break; - } - } - } - } - - public void remove(int index) { - mServers.remove(index); - notifyItemRemoved(index); - } - - public void remove(ServerModel model) { - synchronized (LOCK) { - for (int i = 0; i < mServers.size(); i++) { - if (mServers.get(i).id == model.id) { - remove(i); - break; - } - } - } - } - - public void set(ServerModel[] models) { - if (models == null || models.length == 0) { - mServers.clear(); - return; - } - mServers = new ArrayList<>(models.length); - Collections.addAll(mServers, models); - notifyDataSetChanged(); - } - - public void clear() { - mServers.clear(); - notifyDataSetChanged(); - } - - @Override - public ServerAdapter.ServerVH onCreateViewHolder(ViewGroup parent, int viewType) { - final View v = - LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_server, parent, false); - return new ServerVH(v, this); - } - - @Override - public void onBindViewHolder(ServerAdapter.ServerVH holder, int position) { - final ServerModel model = mServers.get(position); - - holder.textName.setText(model.name); - holder.textUrl.setText(model.url); - holder.iconStatus.setStatus(model.status); - - switch (model.status) { - case ServerStatus.OK: - holder.textStatus.setText(R.string.everything_checks_out); - break; - case ServerStatus.WAITING: - holder.textStatus.setText(R.string.waiting); - break; - case ServerStatus.CHECKING: - holder.textStatus.setText(R.string.checking_status); - break; - case ServerStatus.ERROR: - holder.textStatus.setText(model.reason); - break; - } - - if (model.checkInterval <= 0) { - holder.textInterval.setText(""); - } else { - final long now = System.currentTimeMillis(); - final long nextCheck = model.lastCheck + model.checkInterval; - final long difference = nextCheck - now; - holder.textInterval.setText(TimeUtil.str(difference)); - } - } - - @Override - public int getItemCount() { - return mServers.size(); - } - - public static class ServerVH extends RecyclerView.ViewHolder - implements View.OnClickListener, View.OnLongClickListener { - - final StatusImageView iconStatus; - final TextView textName; - final TextView textInterval; - final TextView textUrl; - final TextView textStatus; - final ServerAdapter adapter; - - ServerVH(View itemView, ServerAdapter adapter) { - super(itemView); - iconStatus = itemView.findViewById(R.id.iconStatus); - textName = itemView.findViewById(R.id.textName); - textInterval = itemView.findViewById(R.id.textInterval); - textUrl = itemView.findViewById(R.id.textUrl); - textStatus = itemView.findViewById(R.id.textStatus); - this.adapter = adapter; - - itemView.setOnClickListener(this); - itemView.setOnLongClickListener(this); - } - - @Override - public void onClick(View view) { - adapter.performClick(getAdapterPosition(), false); - } - - @Override - public boolean onLongClick(View view) { - adapter.performClick(getAdapterPosition(), true); - return false; - } - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.kt b/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.kt new file mode 100644 index 0000000..c390b81 --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.kt @@ -0,0 +1,137 @@ +/* + * Licensed under Apache-2.0 + * + * Designed and developed by Aidan Follestad (@afollestad) + */ +package com.afollestad.nocknock.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.afollestad.nocknock.R +import com.afollestad.nocknock.data.ServerModel +import com.afollestad.nocknock.data.textRes +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.textName +import kotlinx.android.synthetic.main.list_item_server.view.textStatus +import kotlinx.android.synthetic.main.list_item_server.view.textUrl + +typealias Listener = (model: ServerModel, longClick: Boolean) -> Unit + +/** @author Aidan Follestad (afollestad) */ +class ServerVH constructor( + itemView: View, + private val adapter: ServerAdapter +) : RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener { + + init { + itemView.setOnClickListener(this) + itemView.setOnLongClickListener(this) + } + + fun bind(model: ServerModel) { + itemView.textName.text = model.name + itemView.textUrl.text = model.url + itemView.iconStatus.setStatus(model.status) + + val statusText = model.status.textRes() + if (statusText == 0) { + itemView.textStatus.text = model.reason + } else { + itemView.textStatus.setText(statusText) + } + + itemView.textInterval.text = model.intervalText() + } + + override fun onClick(view: View) { + adapter.performClick(adapterPosition, false) + } + + override fun onLongClick(view: View): Boolean { + adapter.performClick(adapterPosition, true) + return false + } +} + +/** @author Aidan Follestad (afollestad) */ +class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter() { + + private val models = mutableListOf() + + internal fun performClick( + index: Int, + longClick: Boolean + ) = listener.invoke(models[index], longClick) + + fun add(model: ServerModel) { + models.add(model) + notifyItemInserted(models.size - 1) + } + + fun update(target: ServerModel) { + for ((i, model) in models.withIndex()) { + if (model.id == target.id) { + update(i, target) + break + } + } + } + + private fun update( + index: Int, + model: ServerModel + ) { + models[index] = model + notifyItemChanged(index) + } + + fun remove(index: Int) { + models.removeAt(index) + notifyItemRemoved(index) + } + + fun remove(target: ServerModel) { + for ((i, model) in models.withIndex()) { + if (model.id == target.id) { + remove(i) + break + } + } + } + + fun set(newModels: List) { + this.models.clear() + if (newModels.isEmpty()) { + return + } + this.models.addAll(newModels) + notifyDataSetChanged() + } + + fun clear() { + models.clear() + notifyDataSetChanged() + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ServerVH { + val v = LayoutInflater.from(parent.context) + .inflate(R.layout.list_item_server, parent, false) + return ServerVH(v, this) + } + + override fun onBindViewHolder( + holder: ServerVH, + position: Int + ) { + val model = models[position] + holder.bind(model) + } + + override fun getItemCount() = models.size +} diff --git a/app/src/main/java/com/afollestad/nocknock/api/ServerModel.java b/app/src/main/java/com/afollestad/nocknock/api/ServerModel.java deleted file mode 100644 index de6c2a6..0000000 --- a/app/src/main/java/com/afollestad/nocknock/api/ServerModel.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.afollestad.nocknock.api; - -import static com.afollestad.nocknock.ui.MainActivity.SITES_TABLE_NAME; - -import com.afollestad.inquiry.annotations.Column; -import com.afollestad.inquiry.annotations.Table; -import java.io.Serializable; - -/** @author Aidan Follestad (afollestad) */ -@Table(name = SITES_TABLE_NAME) -public class ServerModel implements Serializable { - - public ServerModel() {} - - @Column(name = "_id", primaryKey = true, notNull = true, autoIncrement = true) - public long id; - - @Column public String name; - @Column public String url; - @Column @ServerStatus.Enum public int status; - @Column public long checkInterval; - @Column public long lastCheck; - @Column public String reason; - - @Column @ValidationMode.Enum public int validationMode; - @Column public String validationContent; -} diff --git a/app/src/main/java/com/afollestad/nocknock/api/ServerStatus.java b/app/src/main/java/com/afollestad/nocknock/api/ServerStatus.java deleted file mode 100644 index 93bf2c8..0000000 --- a/app/src/main/java/com/afollestad/nocknock/api/ServerStatus.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.afollestad.nocknock.api; - -import android.support.annotation.IntDef; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** @author Aidan Follestad (afollestad) */ -public final class ServerStatus { - - public static final int OK = 1; - public static final int WAITING = 2; - public static final int CHECKING = 3; - public static final int ERROR = 4; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({OK, WAITING, CHECKING, ERROR}) - public @interface Enum {} -} diff --git a/app/src/main/java/com/afollestad/nocknock/api/ValidationMode.java b/app/src/main/java/com/afollestad/nocknock/api/ValidationMode.java deleted file mode 100644 index bf1a524..0000000 --- a/app/src/main/java/com/afollestad/nocknock/api/ValidationMode.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.afollestad.nocknock.api; - -import android.support.annotation.IntDef; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** @author Aidan Follestad (afollestad) */ -public final class ValidationMode { - - public static final int STATUS_CODE = 1; - public static final int TERM_SEARCH = 2; - public static final int JAVASCRIPT = 3; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({STATUS_CODE, TERM_SEARCH, JAVASCRIPT}) - public @interface Enum {} -} diff --git a/app/src/main/java/com/afollestad/nocknock/di/AppComponent.kt b/app/src/main/java/com/afollestad/nocknock/di/AppComponent.kt new file mode 100644 index 0000000..1346a67 --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/di/AppComponent.kt @@ -0,0 +1,56 @@ +/* + * Licensed under Apache-2.0 + * + * Designed and developed by Aidan Follestad (@afollestad) + */ +package com.afollestad.nocknock.di + +import android.app.Application +import android.app.NotificationManager +import android.app.job.JobScheduler +import com.afollestad.nocknock.engine.EngineModule +import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob +import com.afollestad.nocknock.notifications.NotificationsModule +import com.afollestad.nocknock.ui.AddSiteActivity +import com.afollestad.nocknock.ui.MainActivity +import com.afollestad.nocknock.ui.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, + EngineModule::class, + NotificationsModule::class, + UtilitiesModule::class + ] +) +interface AppComponent { + + fun inject(activity: MainActivity) + + fun inject(activity: ViewSiteActivity) + + fun inject(activity: AddSiteActivity) + + fun inject(job: CheckStatusJob) + + @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 + } +} diff --git a/app/src/main/java/com/afollestad/nocknock/di/MainModule.kt b/app/src/main/java/com/afollestad/nocknock/di/MainModule.kt new file mode 100644 index 0000000..97f850b --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/di/MainModule.kt @@ -0,0 +1,22 @@ +/* + * Licensed under Apache-2.0 + * + * Designed and developed by Aidan Follestad (@afollestad) + */ +package com.afollestad.nocknock.di + +import com.afollestad.nocknock.R +import com.afollestad.nocknock.utilities.qualifiers.AppIconRes +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +/** @author Aidan Follestad (afollestad) */ +@Module +open class MainModule { + + @Provides + @Singleton + @AppIconRes + fun provideAppIconRes(): Int = R.mipmap.ic_launcher +} diff --git a/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.java b/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.java deleted file mode 100644 index e61ed75..0000000 --- a/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.afollestad.nocknock.dialogs; - -import android.app.Dialog; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v7.app.AppCompatActivity; -import android.text.Html; -import com.afollestad.materialdialogs.MaterialDialog; -import com.afollestad.nocknock.R; - -/** @author Aidan Follestad (afollestad) */ -public class AboutDialog extends DialogFragment { - - public static void show(AppCompatActivity context) { - AboutDialog dialog = new AboutDialog(); - dialog.show(context.getSupportFragmentManager(), "[ABOUT_DIALOG]"); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - return new MaterialDialog.Builder(getActivity()) - .title(R.string.about) - .positiveText(R.string.dismiss) - .content(Html.fromHtml(getString(R.string.about_body))) - .contentLineSpacing(1.6f) - .build(); - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.kt b/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.kt new file mode 100644 index 0000000..2577e86 --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.kt @@ -0,0 +1,32 @@ +/* + * Licensed under Apache-2.0 + * + * Designed and developed by Aidan Follestad (@afollestad) + */ +package com.afollestad.nocknock.dialogs + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.DialogFragment +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.nocknock.R + +/** @author Aidan Follestad (afollestad) */ +class AboutDialog : DialogFragment() { + companion object { + private const val TAG = "[ABOUT_DIALOG]" + + fun show(context: AppCompatActivity) { + val dialog = AboutDialog() + dialog.show(context.supportFragmentManager, TAG) + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialDialog(activity!!) + .title(R.string.about) + .positiveButton(R.string.dismiss) + .message(R.string.about_body, html = true, lineHeightMultiplier = 1.4f) + } +} diff --git a/app/src/main/java/com/afollestad/nocknock/receivers/BootReceiver.java b/app/src/main/java/com/afollestad/nocknock/receivers/BootReceiver.java deleted file mode 100644 index 29b70cf..0000000 --- a/app/src/main/java/com/afollestad/nocknock/receivers/BootReceiver.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.afollestad.nocknock.receivers; - -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import com.afollestad.inquiry.Inquiry; -import com.afollestad.nocknock.api.ServerModel; -import com.afollestad.nocknock.ui.MainActivity; -import com.afollestad.nocknock.util.AlarmUtil; - -/** @author Aidan Follestad (afollestad) */ -public class BootReceiver extends BroadcastReceiver { - - @SuppressLint("VisibleForTests") - @Override - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { - final Inquiry inq = Inquiry.newInstance(context, MainActivity.DB_NAME).build(false); - ServerModel[] models = inq.select(ServerModel.class).all(); - AlarmUtil.setSiteChecks(context, models); - inq.destroyInstance(); - } - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/receivers/ConnectivityReceiver.java b/app/src/main/java/com/afollestad/nocknock/receivers/ConnectivityReceiver.java deleted file mode 100644 index 3417ce3..0000000 --- a/app/src/main/java/com/afollestad/nocknock/receivers/ConnectivityReceiver.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.afollestad.nocknock.receivers; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import com.afollestad.nocknock.services.CheckService; -import com.afollestad.nocknock.util.NetworkUtil; - -/** @author Aidan Follestad (afollestad) */ -public class ConnectivityReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - final boolean hasInternet = NetworkUtil.hasInternet(context); - Log.v("ConnectivityReceiver", "Connectivity state changed... has internet? " + hasInternet); - if (hasInternet) { - context.startService( - new Intent(context, CheckService.class).putExtra(CheckService.ONLY_WAITING, true)); - } - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/services/CheckService.java b/app/src/main/java/com/afollestad/nocknock/services/CheckService.java deleted file mode 100644 index 9924733..0000000 --- a/app/src/main/java/com/afollestad/nocknock/services/CheckService.java +++ /dev/null @@ -1,235 +0,0 @@ -package com.afollestad.nocknock.services; - -import android.annotation.SuppressLint; -import android.app.IntentService; -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.graphics.BitmapFactory; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.support.annotation.Nullable; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationManagerCompat; -import android.util.Log; -import com.afollestad.bridge.Bridge; -import com.afollestad.bridge.BridgeException; -import com.afollestad.bridge.Response; -import com.afollestad.inquiry.Inquiry; -import com.afollestad.inquiry.Query; -import com.afollestad.nocknock.BuildConfig; -import com.afollestad.nocknock.R; -import com.afollestad.nocknock.api.ServerModel; -import com.afollestad.nocknock.api.ServerStatus; -import com.afollestad.nocknock.api.ValidationMode; -import com.afollestad.nocknock.ui.MainActivity; -import com.afollestad.nocknock.ui.ViewSiteActivity; -import com.afollestad.nocknock.util.JsUtil; -import com.afollestad.nocknock.util.NetworkUtil; -import java.util.Locale; - -/** @author Aidan Follestad (afollestad) */ -@SuppressWarnings("CheckResult") -@SuppressLint("VisibleForTests") -public class CheckService extends IntentService { - - public static String ACTION_CHECK_UPDATE = BuildConfig.APPLICATION_ID + ".CHECK_UPDATE"; - public static String ACTION_RUNNING = BuildConfig.APPLICATION_ID + ".CHECK_RUNNING"; - public static String MODEL_ID = "model_id"; - public static String ONLY_WAITING = "only_waiting"; - public static int NOTI_ID = 3456; - - public CheckService() { - super("NockNockCheckService"); - } - - private static void LOG(String msg, Object... format) { - if (format != null) { - msg = String.format(Locale.getDefault(), msg, format); - } - Log.v("NockNockService", msg); - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - protected void onHandleIntent(Intent intent) { - Inquiry.newInstance(this, MainActivity.DB_NAME).build(); - isRunning(true); - Bridge.config().defaultHeader("User-Agent", getString(R.string.app_name) + " (Android)"); - - final Query query = - Inquiry.get(this).selectFrom(MainActivity.SITES_TABLE_NAME, ServerModel.class); - if (intent != null && intent.hasExtra(MODEL_ID)) { - query.where("_id = ?", intent.getLongExtra(MODEL_ID, -1)); - } else if (intent != null && intent.getBooleanExtra(ONLY_WAITING, false)) { - query.where("status = ?", ServerStatus.WAITING); - } - final ServerModel[] sites = query.all(); - - if (sites == null || sites.length == 0) { - LOG("No sites added to check, service will terminate."); - isRunning(false); - stopSelf(); - return; - } - - LOG("Checking %d sites...", sites.length); - sendBroadcast(new Intent(ACTION_RUNNING)); - - for (ServerModel site : sites) { - LOG("Updating %s (%s) status to WAITING...", site.name, site.url); - site.status = ServerStatus.WAITING; - updateStatus(site); - } - - if (NetworkUtil.hasInternet(this)) { - for (ServerModel site : sites) { - LOG("Checking %s (%s)...", site.name, site.url); - site.status = ServerStatus.CHECKING; - site.lastCheck = System.currentTimeMillis(); - updateStatus(site); - - try { - final Response response = - Bridge.get(site.url).throwIfNotSuccess().cancellable(false).request().response(); - - site.reason = null; - site.status = ServerStatus.OK; - - if (site.validationMode == ValidationMode.TERM_SEARCH) { - final String body = response.asString(); - if (body == null || !body.contains(site.validationContent)) { - site.status = ServerStatus.ERROR; - site.reason = "Term \"" + site.validationContent + "\" not found in response body."; - } - } else if (site.validationMode == ValidationMode.JAVASCRIPT) { - final String body = response.asString(); - site.reason = JsUtil.exec(site.validationContent, body); - if (site.reason != null && !site.toString().isEmpty()) site.status = ServerStatus.ERROR; - } - - if (site.status == ServerStatus.ERROR) showNotification(this, site); - } catch (BridgeException e) { - processError(e, site); - } - updateStatus(site); - } - } else { - LOG("No internet connection, waiting."); - } - - isRunning(false); - LOG("Service is finished!"); - } - - private void processError(BridgeException e, ServerModel site) { - site.status = ServerStatus.OK; - site.reason = null; - - switch (e.reason()) { - case BridgeException.REASON_REQUEST_CANCELLED: - // Shouldn't happen - break; - case BridgeException.REASON_REQUEST_FAILED: - case BridgeException.REASON_RESPONSE_UNPARSEABLE: - case BridgeException.REASON_RESPONSE_UNSUCCESSFUL: - case BridgeException.REASON_RESPONSE_IOERROR: - //noinspection ConstantConditions - if (e.response() != null && e.response().code() == 401) { - // Don't consider 401 unsuccessful here - site.reason = null; - } else { - site.status = ServerStatus.ERROR; - site.reason = e.getMessage(); - } - break; - case BridgeException.REASON_REQUEST_TIMEOUT: - site.status = ServerStatus.ERROR; - site.reason = getString(R.string.timeout); - break; - case BridgeException.REASON_RESPONSE_VALIDATOR_ERROR: - case BridgeException.REASON_RESPONSE_VALIDATOR_FALSE: - // Not used - break; - } - - if (site.status != ServerStatus.OK) { - LOG("%s error: %s", site.name, site.reason); - showNotification(this, site); - } - } - - private void updateStatus(ServerModel site) { - Inquiry.get(this).update(ServerModel.class).values(new ServerModel[] {site}).run(); - sendBroadcast(new Intent(ACTION_CHECK_UPDATE).putExtra("model", site)); - } - - private void isRunning(boolean running) { - PreferenceManager.getDefaultSharedPreferences(this) - .edit() - .putBoolean("check_service_running", running) - .apply(); - } - - public static boolean isRunning(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean("check_service_running", false); - } - - public static void isAppOpen(Context context, boolean open) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putBoolean("is_app_open", open) - .apply(); - } - - public static boolean isAppOpen(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("is_app_open", false); - } - - private static void showNotification(Context context, ServerModel site) { - if (isAppOpen(context)) { - // Don't show notifications while the app is open - return; - } - - final NotificationManagerCompat nm = NotificationManagerCompat.from(context); - final PendingIntent openIntent = - PendingIntent.getActivity( - context, - 9669, - new Intent(context, ViewSiteActivity.class) - .putExtra("model", site) - .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), - PendingIntent.FLAG_CANCEL_CURRENT); - final Notification noti = - new NotificationCompat.Builder(context) - .setContentTitle(site.name) - .setContentText(context.getString(R.string.something_wrong)) - .setContentIntent(openIntent) - .setSmallIcon(R.drawable.ic_notification) - .setLargeIcon( - BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher)) - .setPriority(Notification.PRIORITY_HIGH) - .setAutoCancel(true) - .setDefaults(Notification.DEFAULT_VIBRATE) - .build(); - nm.notify(site.url, NOTI_ID, noti); - } - - @Override - public void onDestroy() { - try { - Inquiry.destroy(this); - } catch (Throwable t2) { - t2.printStackTrace(); - } - super.onDestroy(); - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.java b/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.java deleted file mode 100644 index 0b30163..0000000 --- a/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.java +++ /dev/null @@ -1,275 +0,0 @@ -package com.afollestad.nocknock.ui; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.annotation.SuppressLint; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TextInputLayout; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.util.Patterns; -import android.view.View; -import android.view.ViewAnimationUtils; -import android.view.ViewTreeObserver; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; -import com.afollestad.nocknock.R; -import com.afollestad.nocknock.api.ServerModel; -import com.afollestad.nocknock.api.ServerStatus; -import com.afollestad.nocknock.api.ValidationMode; - -/** @author Aidan Follestad (afollestad) */ -public class AddSiteActivity extends AppCompatActivity implements View.OnClickListener { - - private View rootLayout; - private Toolbar toolbar; - - private TextInputLayout nameTiLayout; - private EditText inputName; - private TextInputLayout urlTiLayout; - private EditText inputUrl; - private EditText inputInterval; - private Spinner spinnerInterval; - private TextView textUrlWarning; - private Spinner responseValidationSpinner; - - private boolean isClosing; - - @SuppressLint("SetTextI18n") - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_addsite); - - rootLayout = findViewById(R.id.rootView); - nameTiLayout = findViewById(R.id.nameTiLayout); - inputName = findViewById(R.id.inputName); - urlTiLayout = findViewById(R.id.urlTiLayout); - inputUrl = findViewById(R.id.inputUrl); - textUrlWarning = findViewById(R.id.textUrlWarning); - inputInterval = findViewById(R.id.checkIntervalInput); - spinnerInterval = findViewById(R.id.checkIntervalSpinner); - responseValidationSpinner = findViewById(R.id.responseValidationMode); - - toolbar = findViewById(R.id.toolbar); - toolbar.setNavigationOnClickListener(view -> closeActivityWithReveal()); - - if (savedInstanceState == null) { - rootLayout.setVisibility(View.INVISIBLE); - ViewTreeObserver viewTreeObserver = rootLayout.getViewTreeObserver(); - if (viewTreeObserver.isAlive()) { - viewTreeObserver.addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - circularRevealActivity(); - rootLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - }); - } - } - - ArrayAdapter intervalOptionsAdapter = - new ArrayAdapter<>( - this, - R.layout.list_item_spinner, - getResources().getStringArray(R.array.interval_options)); - intervalOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown); - spinnerInterval.setAdapter(intervalOptionsAdapter); - - inputUrl.setOnFocusChangeListener( - (view, hasFocus) -> { - if (!hasFocus) { - final String inputStr = inputUrl.getText().toString().trim(); - if (inputStr.isEmpty()) return; - final Uri uri = Uri.parse(inputStr); - if (uri.getScheme() == null) { - inputUrl.setText("http://" + inputStr); - textUrlWarning.setVisibility(View.GONE); - } else if (!"http".equals(uri.getScheme()) && !"https".equals(uri.getScheme())) { - textUrlWarning.setVisibility(View.VISIBLE); - textUrlWarning.setText(R.string.warning_http_url); - } else { - textUrlWarning.setVisibility(View.GONE); - } - } - }); - - ArrayAdapter validationOptionsAdapter = - new ArrayAdapter<>( - this, - R.layout.list_item_spinner, - getResources().getStringArray(R.array.response_validation_options)); - validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown); - responseValidationSpinner.setAdapter(validationOptionsAdapter); - responseValidationSpinner.setOnItemSelectedListener( - new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - final View searchTerm = findViewById(R.id.responseValidationSearchTerm); - final View javascript = findViewById(R.id.responseValidationScript); - final TextView modeDesc = findViewById(R.id.validationModeDescription); - - searchTerm.setVisibility(i == 1 ? View.VISIBLE : View.GONE); - javascript.setVisibility(i == 2 ? View.VISIBLE : View.GONE); - - switch (i) { - case 0: - modeDesc.setText(R.string.validation_mode_status_desc); - break; - case 1: - modeDesc.setText(R.string.validation_mode_term_desc); - break; - case 2: - modeDesc.setText(R.string.validation_mode_javascript_desc); - break; - } - } - - @Override - public void onNothingSelected(AdapterView adapterView) {} - }); - - findViewById(R.id.doneBtn).setOnClickListener(this); - } - - @Override - public void onBackPressed() { - closeActivityWithReveal(); - } - - private void closeActivityWithReveal() { - if (isClosing) return; - isClosing = true; - final int fabSize = getIntent().getIntExtra("fab_size", toolbar.getMeasuredHeight()); - final int cx = - (int) getIntent().getFloatExtra("fab_x", rootLayout.getMeasuredWidth() / 2) + (fabSize / 2); - final int cy = - (int) getIntent().getFloatExtra("fab_y", rootLayout.getMeasuredHeight() / 2) - + toolbar.getMeasuredHeight() - + (fabSize / 2); - float initialRadius = Math.max(cx, cy); - - final Animator circularReveal = - ViewAnimationUtils.createCircularReveal(rootLayout, cx, cy, initialRadius, 0); - circularReveal.setDuration(300); - circularReveal.setInterpolator(new AccelerateInterpolator()); - circularReveal.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - rootLayout.setVisibility(View.INVISIBLE); - finish(); - overridePendingTransition(0, 0); - } - }); - - circularReveal.start(); - } - - private void circularRevealActivity() { - final int cx = rootLayout.getMeasuredWidth() / 2; - final int cy = rootLayout.getMeasuredHeight() / 2; - final float finalRadius = Math.max(cx, cy); - final Animator circularReveal = - ViewAnimationUtils.createCircularReveal(rootLayout, cx, cy, 0, finalRadius); - - circularReveal.setDuration(300); - circularReveal.setInterpolator(new DecelerateInterpolator()); - - rootLayout.setVisibility(View.VISIBLE); - circularReveal.start(); - } - - // Done button - @Override - public void onClick(View view) { - isClosing = true; - - ServerModel model = new ServerModel(); - model.name = inputName.getText().toString().trim(); - model.url = inputUrl.getText().toString().trim(); - model.status = ServerStatus.WAITING; - - if (model.name.isEmpty()) { - nameTiLayout.setError(getString(R.string.please_enter_name)); - isClosing = false; - return; - } else { - nameTiLayout.setError(null); - } - - if (model.url.isEmpty()) { - urlTiLayout.setError(getString(R.string.please_enter_url)); - isClosing = false; - return; - } else { - urlTiLayout.setError(null); - if (!Patterns.WEB_URL.matcher(model.url).find()) { - urlTiLayout.setError(getString(R.string.please_enter_valid_url)); - isClosing = false; - return; - } else { - final Uri uri = Uri.parse(model.url); - if (uri.getScheme() == null) model.url = "http://" + model.url; - } - } - - String intervalStr = inputInterval.getText().toString().trim(); - if (intervalStr.isEmpty()) intervalStr = "0"; - model.checkInterval = Integer.parseInt(intervalStr); - - switch (spinnerInterval.getSelectedItemPosition()) { - case 0: // minutes - model.checkInterval *= (60 * 1000); - break; - case 1: // hours - model.checkInterval *= (60 * 60 * 1000); - break; - case 2: // days - model.checkInterval *= (60 * 60 * 24 * 1000); - break; - default: // weeks - model.checkInterval *= (60 * 60 * 24 * 7 * 1000); - break; - } - - model.lastCheck = System.currentTimeMillis() - model.checkInterval; - - switch (responseValidationSpinner.getSelectedItemPosition()) { - case 0: - model.validationMode = ValidationMode.STATUS_CODE; - model.validationContent = null; - break; - case 1: - model.validationMode = ValidationMode.TERM_SEARCH; - model.validationContent = - ((EditText) findViewById(R.id.responseValidationSearchTerm)) - .getText() - .toString() - .trim(); - break; - case 2: - model.validationMode = ValidationMode.JAVASCRIPT; - model.validationContent = - ((EditText) findViewById(R.id.responseValidationScriptInput)) - .getText() - .toString() - .trim(); - break; - } - - setResult(RESULT_OK, new Intent().putExtra("model", model)); - finish(); - overridePendingTransition(R.anim.fade_out, R.anim.fade_out); - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.kt new file mode 100644 index 0000000..580b693 --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.kt @@ -0,0 +1,278 @@ +/* + * Licensed under Apache-2.0 + * + * Designed and developed by Aidan Follestad (@afollestad) + */ +package com.afollestad.nocknock.ui + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION +import android.net.Uri +import android.os.Bundle +import android.util.Patterns.WEB_URL +import android.view.View +import android.view.ViewAnimationUtils.createCircularReveal +import android.view.animation.AccelerateInterpolator +import android.view.animation.DecelerateInterpolator +import android.widget.ArrayAdapter +import androidx.appcompat.app.AppCompatActivity +import com.afollestad.nocknock.R +import com.afollestad.nocknock.data.ServerModel +import com.afollestad.nocknock.data.ServerStatus.WAITING +import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT +import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE +import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH +import com.afollestad.nocknock.engine.db.ServerModelStore +import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager +import com.afollestad.nocknock.utilities.ext.conceal +import com.afollestad.nocknock.utilities.ext.hide +import com.afollestad.nocknock.utilities.ext.injector +import com.afollestad.nocknock.utilities.ext.onEnd +import com.afollestad.nocknock.utilities.ext.onItemSelected +import com.afollestad.nocknock.utilities.ext.onLayout +import com.afollestad.nocknock.utilities.ext.scopeWhileAttached +import com.afollestad.nocknock.utilities.ext.show +import com.afollestad.nocknock.utilities.ext.showOrHide +import com.afollestad.nocknock.utilities.ext.textAsLong +import com.afollestad.nocknock.utilities.ext.trimmedText +import kotlinx.android.synthetic.main.activity_addsite.checkIntervalInput +import kotlinx.android.synthetic.main.activity_addsite.checkIntervalSpinner +import kotlinx.android.synthetic.main.activity_addsite.doneBtn +import kotlinx.android.synthetic.main.activity_addsite.inputName +import kotlinx.android.synthetic.main.activity_addsite.inputUrl +import kotlinx.android.synthetic.main.activity_addsite.nameTiLayout +import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode +import kotlinx.android.synthetic.main.activity_addsite.responseValidationScript +import kotlinx.android.synthetic.main.activity_addsite.responseValidationScriptInput +import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm +import kotlinx.android.synthetic.main.activity_addsite.rootView +import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning +import kotlinx.android.synthetic.main.activity_addsite.toolbar +import kotlinx.android.synthetic.main.activity_addsite.urlTiLayout +import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import java.lang.System.currentTimeMillis +import javax.inject.Inject +import kotlin.math.max + +private const val KEY_FAB_X = "fab_x" +private const val KEY_FAB_Y = "fab_y" +private const val KEY_FAB_SIZE = "fab_size" + +/** @author Aidan Follestad (afollestad) */ +fun MainActivity.intentToAdd( + x: Float, + y: Float, + size: Int +) = Intent(this, AddSiteActivity::class.java).apply { + putExtra(KEY_FAB_X, x) + putExtra(KEY_FAB_Y, y) + putExtra(KEY_FAB_SIZE, size) + addFlags(FLAG_ACTIVITY_NO_ANIMATION) +} + +/** @author Aidan Follestad (afollestad) */ +class AddSiteActivity : AppCompatActivity(), View.OnClickListener { + + private var isClosing: Boolean = false + + @Inject lateinit var serverModelStore: ServerModelStore + @Inject lateinit var checkStatusManager: CheckStatusManager + + @SuppressLint("SetTextI18n") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + injector().injectInto(this) + + setContentView(R.layout.activity_addsite) + toolbar.setNavigationOnClickListener { closeActivityWithReveal() } + + if (savedInstanceState == null) { + rootView.conceal() + rootView.onLayout { circularRevealActivity() } + } + + val intervalOptionsAdapter = ArrayAdapter( + this, + R.layout.list_item_spinner, + resources.getStringArray(R.array.interval_options) + ) + intervalOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown) + checkIntervalSpinner.adapter = intervalOptionsAdapter + + inputUrl.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val inputStr = inputUrl.text + .toString() + .trim() + if (inputStr.isEmpty()) { + return@setOnFocusChangeListener + } + + val uri = Uri.parse(inputStr) + if (uri.scheme == null) { + inputUrl.setText("http://$inputStr") + textUrlWarning.hide() + } else if ("http" != uri.scheme && "https" != uri.scheme) { + textUrlWarning.show() + textUrlWarning.setText(R.string.warning_http_url) + } else { + textUrlWarning.hide() + } + } + } + + val validationOptionsAdapter = ArrayAdapter( + this, + R.layout.list_item_spinner, + resources.getStringArray(R.array.response_validation_options) + ) + validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown) + + responseValidationMode.adapter = validationOptionsAdapter + responseValidationMode.onItemSelected { pos -> + responseValidationSearchTerm.showOrHide(pos == 1) + responseValidationScript.showOrHide(pos == 2) + + validationModeDescription.setText( + when (pos) { + 0 -> R.string.validation_mode_status_desc + 1 -> R.string.validation_mode_term_desc + 2 -> R.string.validation_mode_javascript_desc + else -> throw IllegalStateException("Unknown validation mode position: $pos") + } + ) + } + + doneBtn.setOnClickListener(this) + } + + private fun closeActivityWithReveal() { + if (isClosing) { + return + } + + isClosing = true + val fabSize = intent.getIntExtra(KEY_FAB_SIZE, toolbar!!.measuredHeight) + + val defaultCx = rootView.measuredWidth / 2f + val cx = + intent.getFloatExtra(KEY_FAB_X, defaultCx).toInt() + fabSize / 2 + + val defaultCy = rootView.measuredHeight / 2f + val cy = (intent.getFloatExtra(KEY_FAB_Y, defaultCy).toInt() + + toolbar!!.measuredHeight + + fabSize / 2) + + val initialRadius = max(cx, cy).toFloat() + createCircularReveal(rootView, cx, cy, initialRadius, 0f) + .apply { + duration = 300 + interpolator = AccelerateInterpolator() + onEnd { + rootView.conceal() + finish() + overridePendingTransition(0, 0) + } + start() + } + } + + private fun circularRevealActivity() { + val cx = rootView.measuredWidth / 2 + val cy = rootView.measuredHeight / 2 + val finalRadius = Math.max(cx, cy) + .toFloat() + val circularReveal = createCircularReveal(rootView, cx, cy, 0f, finalRadius) + .apply { + duration = 300 + interpolator = DecelerateInterpolator() + } + rootView.show() + circularReveal.start() + } + + // Done button + override fun onClick(view: View) { + isClosing = true + + var model = ServerModel( + name = inputName.trimmedText(), + url = inputUrl.trimmedText(), + status = WAITING, + validationMode = STATUS_CODE + ) + + if (model.name.isEmpty()) { + nameTiLayout.error = getString(R.string.please_enter_name) + isClosing = false + return + } else { + nameTiLayout.error = null + } + + if (model.url.isEmpty()) { + urlTiLayout.error = getString(R.string.please_enter_url) + isClosing = false + return + } else { + urlTiLayout.error = null + if (!WEB_URL.matcher(model.url).find()) { + urlTiLayout.error = getString(R.string.please_enter_valid_url) + isClosing = false + return + } else { + val uri = Uri.parse(model.url) + if (uri.scheme == null) { + model = model.copy(url = "http://${model.url}") + } + } + } + + val intervalValue = checkIntervalInput.textAsLong() + + model = when (checkIntervalSpinner.selectedItemPosition) { + 0 -> model.copy(checkInterval = intervalValue * (60 * 1000)) + 1 -> model.copy(checkInterval = intervalValue * (60 * 60 * 1000)) + 2 -> model.copy(checkInterval = intervalValue * (60 * 60 * 24 * 1000)) + else -> model.copy(checkInterval = intervalValue * (60 * 60 * 24 * 7 * 1000)) + } + model = model.copy(lastCheck = currentTimeMillis() - model.checkInterval) + + when (responseValidationMode.selectedItemPosition) { + 0 -> { + model = model.copy(validationMode = STATUS_CODE, validationContent = null) + } + 1 -> { + model = model.copy( + validationMode = TERM_SEARCH, + validationContent = responseValidationSearchTerm.trimmedText() + ) + } + 2 -> { + model = model.copy( + validationMode = JAVASCRIPT, + validationContent = responseValidationScriptInput.trimmedText() + ) + } + } + + rootView.scopeWhileAttached(Main) { + launch(coroutineContext) { + val storedModel = async(IO) { serverModelStore.put(model) }.await() + checkStatusManager.cancelCheck(storedModel) + checkStatusManager.scheduleCheck(storedModel, rightNow = true) + + setResult(RESULT_OK) + finish() + overridePendingTransition(R.anim.fade_out, R.anim.fade_out) + } + } + } + + override fun onBackPressed() = closeActivityWithReveal() +} diff --git a/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.java b/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.java deleted file mode 100644 index 64a0add..0000000 --- a/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.java +++ /dev/null @@ -1,319 +0,0 @@ -package com.afollestad.nocknock.ui; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.annotation.SuppressLint; -import android.app.ActivityOptions; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.graphics.Path; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.app.NotificationManagerCompat; -import android.support.v4.content.ContextCompat; -import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.text.Html; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.animation.PathInterpolator; -import android.widget.TextView; -import com.afollestad.bridge.Bridge; -import com.afollestad.inquiry.Inquiry; -import com.afollestad.materialdialogs.MaterialDialog; -import com.afollestad.nocknock.R; -import com.afollestad.nocknock.adapter.ServerAdapter; -import com.afollestad.nocknock.api.ServerModel; -import com.afollestad.nocknock.dialogs.AboutDialog; -import com.afollestad.nocknock.services.CheckService; -import com.afollestad.nocknock.util.AlarmUtil; -import com.afollestad.nocknock.util.MathUtil; - -@SuppressLint("VisibleForTests") -public class MainActivity extends AppCompatActivity - implements SwipeRefreshLayout.OnRefreshListener, - View.OnClickListener, - ServerAdapter.ClickListener { - - private static final int ADD_SITE_RQ = 6969; - private static final int VIEW_SITE_RQ = 6923; - public static final String DB_NAME = "nock_nock"; - public static final String SITES_TABLE_NAME = "site_models"; - - private FloatingActionButton mFab; - private RecyclerView mList; - private ServerAdapter mAdapter; - private TextView mEmptyText; - private SwipeRefreshLayout mRefreshLayout; - - private ObjectAnimator mFabAnimator; - private float mOrigFabX; - private float mOrigFabY; - - private final BroadcastReceiver mReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Log.v("MainActivity", "Received " + intent.getAction()); - if (CheckService.ACTION_RUNNING.equals(intent.getAction())) { - if (mRefreshLayout != null) mRefreshLayout.setRefreshing(false); - } else { - final ServerModel model = (ServerModel) intent.getSerializableExtra("model"); - if (mAdapter != null && mList != null && model != null) { - mList.post(() -> mAdapter.update(model)); - } - } - } - }; - - @SuppressLint("CommitPrefEdits") - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - mAdapter = new ServerAdapter(this); - mEmptyText = findViewById(R.id.emptyText); - - mList = findViewById(R.id.list); - mList.setLayoutManager(new LinearLayoutManager(this)); - mList.setAdapter(mAdapter); - mList.addItemDecoration( - new android.support.v7.widget.DividerItemDecoration( - this, android.support.v7.widget.DividerItemDecoration.VERTICAL)); - - mRefreshLayout = findViewById(R.id.swipeRefreshLayout); - mRefreshLayout.setOnRefreshListener(this); - mRefreshLayout.setColorSchemeColors( - ContextCompat.getColor(this, R.color.md_green), - ContextCompat.getColor(this, R.color.md_yellow), - ContextCompat.getColor(this, R.color.md_red)); - - mFab = findViewById(R.id.fab); - mFab.setOnClickListener(this); - - Inquiry.newInstance(this, DB_NAME).build(); - Bridge.config().defaultHeader("User-Agent", getString(R.string.app_name) + " (Android)"); - } - - private void showRefreshTutorial() { - if (mAdapter.getItemCount() == 0) return; - final SharedPreferences pr = PreferenceManager.getDefaultSharedPreferences(this); - if (pr.getBoolean("shown_swipe_refresh_tutorial", false)) return; - - mFab.hide(); - final View tutorialView = findViewById(R.id.swipeRefreshTutorial); - tutorialView.setVisibility(View.VISIBLE); - tutorialView.setAlpha(0f); - tutorialView.animate().cancel(); - tutorialView.animate().setDuration(300).alpha(1f).start(); - - findViewById(R.id.understoodBtn) - .setOnClickListener( - view -> { - view.setOnClickListener(null); - findViewById(R.id.swipeRefreshTutorial).setVisibility(View.GONE); - pr.edit().putBoolean("shown_swipe_refresh_tutorial", true).apply(); - mFab.show(); - }); - } - - @Override - protected void onResume() { - super.onResume(); - CheckService.isAppOpen(this, true); - - try { - final IntentFilter filter = new IntentFilter(); - filter.addAction(CheckService.ACTION_CHECK_UPDATE); - filter.addAction(CheckService.ACTION_RUNNING); - registerReceiver(mReceiver, filter); - } catch (Throwable t) { - t.printStackTrace(); - } - - refreshModels(); - } - - @Override - protected void onPause() { - super.onPause(); - CheckService.isAppOpen(this, false); - - if (isFinishing()) { - Inquiry.destroy(this); - } - - NotificationManagerCompat.from(this).cancel(CheckService.NOTI_ID); - try { - unregisterReceiver(mReceiver); - } catch (Throwable t) { - t.printStackTrace(); - } - } - - private void refreshModels() { - mAdapter.clear(); - mEmptyText.setVisibility(View.VISIBLE); - Inquiry.get(this).selectFrom(SITES_TABLE_NAME, ServerModel.class).all(this::setModels); - } - - private void setModels(ServerModel[] models) { - mAdapter.set(models); - mEmptyText.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); - AlarmUtil.setSiteChecks(this, models); - showRefreshTutorial(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_main, menu); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.about) { - AboutDialog.show(this); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onRefresh() { - if (CheckService.isRunning(this)) { - mRefreshLayout.setRefreshing(false); - return; - } - startService(new Intent(this, CheckService.class)); - } - - // FAB clicked - @Override - public void onClick(View view) { - mOrigFabX = mFab.getX(); - mOrigFabY = mFab.getY(); - final Path curve = MathUtil.bezierCurve(mFab, mList); - if (mFabAnimator != null) mFabAnimator.cancel(); - mFabAnimator = ObjectAnimator.ofFloat(view, View.X, View.Y, curve); - mFabAnimator.setInterpolator(new PathInterpolator(.5f, .5f)); - mFabAnimator.setDuration(300); - mFabAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - startActivityForResult( - new Intent(MainActivity.this, AddSiteActivity.class) - .putExtra("fab_x", mOrigFabX) - .putExtra("fab_y", mOrigFabY) - .putExtra("fab_size", mFab.getMeasuredWidth()) - .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION), - ADD_SITE_RQ); - mFab.postDelayed( - () -> { - mFab.setX(mOrigFabX); - mFab.setY(mOrigFabY); - }, - 600); - } - }); - mFabAnimator.start(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (resultCode == RESULT_OK) { - final ServerModel model = (ServerModel) data.getSerializableExtra("model"); - if (requestCode == ADD_SITE_RQ) { - mAdapter.add(model); - mEmptyText.setVisibility(View.GONE); - Inquiry.get(this) - .insertInto(SITES_TABLE_NAME, ServerModel.class) - .values(new ServerModel[] {model}) - .run( - inserted -> { - AlarmUtil.setSiteChecks(MainActivity.this, model); - checkSite(MainActivity.this, model); - }); - } else if (requestCode == VIEW_SITE_RQ) { - mAdapter.update(model); - AlarmUtil.setSiteChecks(MainActivity.this, model); - checkSite(MainActivity.this, model); - } - } - } - - public static void removeSite( - final Context context, final ServerModel model, final Runnable onRemoved) { - new MaterialDialog.Builder(context) - .title(R.string.remove_site) - .content(Html.fromHtml(context.getString(R.string.remove_site_prompt, model.name))) - .positiveText(R.string.remove) - .negativeText(android.R.string.cancel) - .onPositive( - (dialog, which) -> { - AlarmUtil.cancelSiteChecks(context, model); - final NotificationManagerCompat nm = NotificationManagerCompat.from(context); - nm.cancel(model.url, CheckService.NOTI_ID); - //noinspection CheckResult - final Inquiry rinq = - Inquiry.newInstance(context, DB_NAME).instanceName("remove_site").build(false); - //noinspection CheckResult - rinq.delete(ServerModel.class).where("_id = ?", model.id).run(); - rinq.destroyInstance(); - if (onRemoved != null) { - onRemoved.run(); - } - }) - .show(); - } - - public static void checkSite(Context context, ServerModel model) { - context.startService( - new Intent(context, CheckService.class).putExtra(CheckService.MODEL_ID, model.id)); - } - - @SuppressLint("RestrictedApi") - @Override - public void onSiteSelected(final int index, final ServerModel model, boolean longClick) { - if (longClick) { - new MaterialDialog.Builder(this) - .title(R.string.options) - .items(R.array.site_long_options) - .negativeText(android.R.string.cancel) - .itemsCallback( - (dialog, itemView, which, text) -> { - if (which == 0) { - checkSite(MainActivity.this, model); - } else { - removeSite( - MainActivity.this, - model, - () -> { - mAdapter.remove(index); - mEmptyText.setVisibility( - mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); - }); - } - }) - .show(); - } else { - startActivityForResult( - new Intent(this, ViewSiteActivity.class).putExtra("model", model), - VIEW_SITE_RQ, - ActivityOptions.makeSceneTransitionAnimation(this).toBundle()); - } - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.kt new file mode 100644 index 0000000..adb119b --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.kt @@ -0,0 +1,260 @@ +/* + * Licensed under Apache-2.0 + * + * Designed and developed by Aidan Follestad (@afollestad) + */ +package com.afollestad.nocknock.ui + +import android.animation.ObjectAnimator +import android.animation.ObjectAnimator.ofFloat +import android.annotation.SuppressLint +import android.app.ActivityOptions.makeSceneTransitionAnimation +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.View.X +import android.view.View.Y +import android.view.animation.PathInterpolator +import androidx.appcompat.app.AppCompatActivity +import androidx.core.text.HtmlCompat +import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL +import androidx.recyclerview.widget.LinearLayoutManager +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.list.listItems +import com.afollestad.nocknock.BuildConfig +import com.afollestad.nocknock.R +import com.afollestad.nocknock.adapter.ServerAdapter +import com.afollestad.nocknock.data.ServerModel +import com.afollestad.nocknock.dialogs.AboutDialog +import com.afollestad.nocknock.engine.db.ServerModelStore +import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE +import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL +import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager +import com.afollestad.nocknock.notifications.NockNotificationManager +import com.afollestad.nocknock.utilities.ext.injector +import com.afollestad.nocknock.utilities.ext.onEnd +import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver +import com.afollestad.nocknock.utilities.ext.safeUnregisterReceiver +import com.afollestad.nocknock.utilities.ext.scopeWhileAttached +import com.afollestad.nocknock.utilities.ext.show +import com.afollestad.nocknock.utilities.ext.showOrHide +import com.afollestad.nocknock.utilities.util.MathUtil.bezierCurve +import kotlinx.android.synthetic.main.activity_main.emptyText +import kotlinx.android.synthetic.main.activity_main.fab +import kotlinx.android.synthetic.main.activity_main.list +import kotlinx.android.synthetic.main.activity_main.rootView +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** @author Aidan Follestad (afollestad) */ +class MainActivity : AppCompatActivity(), View.OnClickListener { + + companion object { + private const val ADD_SITE_RQ = 6969 + private const val VIEW_SITE_RQ = 6923 + private const val REVEAL_DURATION = 250L + + private fun log(message: String) { + if (BuildConfig.DEBUG) { + Log.d("MainActivity", message) + } + } + } + + private var fabAnimator: ObjectAnimator? = null + private var originalFabX: Float = 0.toFloat() + private var originalFabY: Float = 0.toFloat() + + private val intentReceiver = object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent + ) { + log("Received broadcast ${intent.action}") + when (intent.action) { + ACTION_STATUS_UPDATE -> { + val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? ServerModel ?: return + list.post { adapter.update(model) } + } + else -> throw IllegalStateException("Unexpected intent: ${intent.action}") + } + } + } + + @Inject lateinit var serverModelStore: ServerModelStore + @Inject lateinit var notificationManager: NockNotificationManager + @Inject lateinit var checkStatusManager: CheckStatusManager + + private lateinit var adapter: ServerAdapter + + @SuppressLint("CommitPrefEdits") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + injector().injectInto(this) + setContentView(R.layout.activity_main) + + adapter = ServerAdapter(this::onSiteSelected) + + list.layoutManager = LinearLayoutManager(this) + list.adapter = adapter + list.addItemDecoration(DividerItemDecoration(this, VERTICAL)) + + fab.setOnClickListener(this) + } + + override fun onResume() { + super.onResume() + notificationManager.setIsAppOpen(true) + + val filter = IntentFilter().apply { + addAction(ACTION_STATUS_UPDATE) + } + safeRegisterReceiver(intentReceiver, filter) + + refreshModels() + } + + override fun onPause() { + super.onPause() + notificationManager.setIsAppOpen(false) + safeUnregisterReceiver(intentReceiver) + } + + private fun refreshModels() { + adapter.clear() + emptyText.show() + rootView.scopeWhileAttached(Main) { + launch(coroutineContext) { + val models = async(IO) { serverModelStore.get() }.await() + adapter.set(models) + emptyText.showOrHide(adapter.itemCount == 0) + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.about) { + AboutDialog.show(this) + return true + } + return super.onOptionsItemSelected(item) + } + + // FAB clicked + override fun onClick(view: View) { + originalFabX = fab.x + originalFabY = fab.y + + fabAnimator?.cancel() + fabAnimator = ofFloat(view, X, Y, bezierCurve(fab, list)) + .apply { + interpolator = PathInterpolator(.5f, .5f) + duration = REVEAL_DURATION + onEnd { + startActivityForResult( + intentToAdd(originalFabX, originalFabY, fab.measuredWidth), + ADD_SITE_RQ + ) + fab.postDelayed( + { + fab.x = originalFabX + fab.y = originalFabY + }, + REVEAL_DURATION * 2 + ) + } + start() + } + } + + override fun onActivityResult( + requestCode: Int, + resultCode: Int, + data: Intent? + ) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == RESULT_OK) { + refreshModels() + } + } + + private fun onSiteSelected( + model: ServerModel, + longClick: Boolean + ) { + if (longClick) { + MaterialDialog(this).show { + title(R.string.options) + listItems(R.array.site_long_options) { _, i, _ -> + when (i) { + 0 -> { + checkStatusManager.cancelCheck(model) + checkStatusManager.scheduleCheck(model) + } + 1 -> maybeRemoveSite(model) { + adapter.remove(i) + emptyText.showOrHide(adapter.itemCount == 0) + } + else -> throw IllegalStateException("Unexpected index: $i") + } + } + negativeButton(android.R.string.cancel) + } + return + } + + startActivityForResult( + intentToView(model), + VIEW_SITE_RQ, + makeSceneTransitionAnimation(this).toBundle() + ) + } + + private fun maybeRemoveSite( + model: ServerModel, + onRemoved: (() -> Unit)? + ) { + MaterialDialog(this).show { + title(R.string.remove_site) + message( + text = HtmlCompat.fromHtml( + context.getString(R.string.remove_site_prompt, model.name), FROM_HTML_MODE_LEGACY + ) + ) + positiveButton(R.string.remove) { + checkStatusManager.cancelCheck(model) + notificationManager.cancelStatusNotifications() + performRemoveSite(model, onRemoved) + } + negativeButton(android.R.string.cancel) + } + } + + private fun performRemoveSite( + model: ServerModel, + onRemoved: (() -> Unit)? + ) { + rootView.scopeWhileAttached(Main) { + launch(coroutineContext) { + async(IO) { serverModelStore.delete(model) }.await() + onRemoved?.invoke() + } + } + } +} diff --git a/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.java b/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.java deleted file mode 100644 index f3e6b60..0000000 --- a/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.java +++ /dev/null @@ -1,359 +0,0 @@ -package com.afollestad.nocknock.ui; - -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.util.Log; -import android.util.Patterns; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; -import com.afollestad.bridge.Bridge; -import com.afollestad.inquiry.Inquiry; -import com.afollestad.nocknock.R; -import com.afollestad.nocknock.api.ServerModel; -import com.afollestad.nocknock.api.ServerStatus; -import com.afollestad.nocknock.api.ValidationMode; -import com.afollestad.nocknock.services.CheckService; -import com.afollestad.nocknock.util.TimeUtil; -import com.afollestad.nocknock.views.StatusImageView; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -/** @author Aidan Follestad (afollestad) */ -public class ViewSiteActivity extends AppCompatActivity - implements View.OnClickListener, Toolbar.OnMenuItemClickListener { - - private StatusImageView iconStatus; - private EditText inputName; - private EditText inputUrl; - private EditText inputCheckInterval; - private Spinner checkIntervalSpinner; - private TextView textLastCheckResult; - private TextView textNextCheck; - private TextView textUrlWarning; - private Spinner responseValidationSpinner; - - private ServerModel mModel; - - private final BroadcastReceiver mReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Log.v("ViewSiteActivity", "Received " + intent.getAction()); - final ServerModel model = (ServerModel) intent.getSerializableExtra("model"); - if (model != null) { - mModel = model; - update(); - } - } - }; - - @SuppressLint("SetTextI18n") - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_viewsite); - - final Toolbar toolbar = findViewById(R.id.toolbar); - toolbar.setNavigationOnClickListener(view -> finish()); - toolbar.inflateMenu(R.menu.menu_viewsite); - toolbar.setOnMenuItemClickListener(this); - - iconStatus = findViewById(R.id.iconStatus); - inputName = findViewById(R.id.inputName); - inputUrl = findViewById(R.id.inputUrl); - textUrlWarning = findViewById(R.id.textUrlWarning); - inputCheckInterval = findViewById(R.id.checkIntervalInput); - checkIntervalSpinner = findViewById(R.id.checkIntervalSpinner); - textLastCheckResult = findViewById(R.id.textLastCheckResult); - textNextCheck = findViewById(R.id.textNextCheck); - responseValidationSpinner = findViewById(R.id.responseValidationMode); - - ArrayAdapter intervalOptionsAdapter = - new ArrayAdapter<>( - this, - R.layout.list_item_spinner, - getResources().getStringArray(R.array.interval_options)); - intervalOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown); - checkIntervalSpinner.setAdapter(intervalOptionsAdapter); - - inputUrl.setOnFocusChangeListener( - (view, hasFocus) -> { - if (!hasFocus) { - final String inputStr = inputUrl.getText().toString().trim(); - if (inputStr.isEmpty()) return; - final Uri uri = Uri.parse(inputStr); - if (uri.getScheme() == null) { - inputUrl.setText("http://" + inputStr); - textUrlWarning.setVisibility(View.GONE); - } else if (!"http".equals(uri.getScheme()) && !"https".equals(uri.getScheme())) { - textUrlWarning.setVisibility(View.VISIBLE); - textUrlWarning.setText(R.string.warning_http_url); - } else { - textUrlWarning.setVisibility(View.GONE); - } - } - }); - - ArrayAdapter validationOptionsAdapter = - new ArrayAdapter<>( - this, - R.layout.list_item_spinner, - getResources().getStringArray(R.array.response_validation_options)); - validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown); - responseValidationSpinner.setAdapter(validationOptionsAdapter); - responseValidationSpinner.setOnItemSelectedListener( - new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - final View searchTerm = findViewById(R.id.responseValidationSearchTerm); - final View javascript = findViewById(R.id.responseValidationScript); - final TextView modeDesc = findViewById(R.id.validationModeDescription); - - searchTerm.setVisibility(i == 1 ? View.VISIBLE : View.GONE); - javascript.setVisibility(i == 2 ? View.VISIBLE : View.GONE); - - switch (i) { - case 0: - modeDesc.setText(R.string.validation_mode_status_desc); - break; - case 1: - modeDesc.setText(R.string.validation_mode_term_desc); - break; - case 2: - modeDesc.setText(R.string.validation_mode_javascript_desc); - break; - } - } - - @Override - public void onNothingSelected(AdapterView adapterView) {} - }); - - mModel = (ServerModel) getIntent().getSerializableExtra("model"); - update(); - - Bridge.config().defaultHeader("User-Agent", getString(R.string.app_name) + " (Android)"); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - if (intent != null && intent.hasExtra("model")) { - mModel = (ServerModel) intent.getSerializableExtra("model"); - update(); - } - } - - @SuppressLint({"SetTextI18n", "SwitchIntDef"}) - private void update() { - final SimpleDateFormat df = new SimpleDateFormat("MMMM dd, hh:mm:ss a", Locale.getDefault()); - - iconStatus.setStatus(mModel.status); - inputName.setText(mModel.name); - inputUrl.setText(mModel.url); - - if (mModel.lastCheck == 0) { - textLastCheckResult.setText(R.string.none); - } else { - switch (mModel.status) { - case ServerStatus.CHECKING: - textLastCheckResult.setText(R.string.checking_status); - break; - case ServerStatus.ERROR: - textLastCheckResult.setText(mModel.reason); - break; - case ServerStatus.OK: - textLastCheckResult.setText(R.string.everything_checks_out); - break; - case ServerStatus.WAITING: - textLastCheckResult.setText(R.string.waiting); - break; - } - } - - if (mModel.checkInterval == 0) { - textNextCheck.setText(R.string.none_turned_off); - inputCheckInterval.setText(""); - checkIntervalSpinner.setSelection(0); - } else { - long lastCheck = mModel.lastCheck; - if (lastCheck == 0) lastCheck = System.currentTimeMillis(); - textNextCheck.setText(df.format(new Date(lastCheck + mModel.checkInterval))); - - if (mModel.checkInterval >= TimeUtil.WEEK) { - inputCheckInterval.setText( - Integer.toString( - (int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.WEEK)))); - checkIntervalSpinner.setSelection(3); - } else if (mModel.checkInterval >= TimeUtil.DAY) { - inputCheckInterval.setText( - Integer.toString( - (int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.DAY)))); - checkIntervalSpinner.setSelection(2); - } else if (mModel.checkInterval >= TimeUtil.HOUR) { - inputCheckInterval.setText( - Integer.toString( - (int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.HOUR)))); - checkIntervalSpinner.setSelection(1); - } else if (mModel.checkInterval >= TimeUtil.MINUTE) { - inputCheckInterval.setText( - Integer.toString( - (int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.MINUTE)))); - checkIntervalSpinner.setSelection(0); - } else { - inputCheckInterval.setText("0"); - checkIntervalSpinner.setSelection(0); - } - } - - responseValidationSpinner.setSelection(mModel.validationMode - 1); - switch (mModel.validationMode) { - case ValidationMode.TERM_SEARCH: - ((TextView) findViewById(R.id.responseValidationSearchTerm)) - .setText(mModel.validationContent); - break; - case ValidationMode.JAVASCRIPT: - ((TextView) findViewById(R.id.responseValidationScriptInput)) - .setText(mModel.validationContent); - break; - } - - findViewById(R.id.doneBtn).setOnClickListener(this); - } - - @Override - protected void onResume() { - super.onResume(); - try { - final IntentFilter filter = new IntentFilter(); - filter.addAction(CheckService.ACTION_CHECK_UPDATE); - // filter.addAction(CheckService.ACTION_RUNNING); - registerReceiver(mReceiver, filter); - } catch (Throwable t) { - t.printStackTrace(); - } - } - - @Override - protected void onPause() { - super.onPause(); - try { - unregisterReceiver(mReceiver); - } catch (Throwable t) { - t.printStackTrace(); - } - } - - @SuppressLint("VisibleForTests") - private void performSave(boolean withValidation) { - mModel.name = inputName.getText().toString().trim(); - mModel.url = inputUrl.getText().toString().trim(); - mModel.status = ServerStatus.WAITING; - - if (withValidation && mModel.name.isEmpty()) { - inputName.setError(getString(R.string.please_enter_name)); - return; - } else { - inputName.setError(null); - } - - if (withValidation && mModel.url.isEmpty()) { - inputUrl.setError(getString(R.string.please_enter_url)); - return; - } else { - inputUrl.setError(null); - if (withValidation && !Patterns.WEB_URL.matcher(mModel.url).find()) { - inputUrl.setError(getString(R.string.please_enter_valid_url)); - return; - } else { - final Uri uri = Uri.parse(mModel.url); - if (uri.getScheme() == null) mModel.url = "http://" + mModel.url; - } - } - - String intervalStr = inputCheckInterval.getText().toString().trim(); - if (intervalStr.isEmpty()) intervalStr = "0"; - mModel.checkInterval = Integer.parseInt(intervalStr); - - switch (checkIntervalSpinner.getSelectedItemPosition()) { - case 0: // minutes - mModel.checkInterval *= (60 * 1000); - break; - case 1: // hours - mModel.checkInterval *= (60 * 60 * 1000); - break; - case 2: // days - mModel.checkInterval *= (60 * 60 * 24 * 1000); - break; - default: // weeks - mModel.checkInterval *= (60 * 60 * 24 * 7 * 1000); - break; - } - - mModel.lastCheck = System.currentTimeMillis() - mModel.checkInterval; - - switch (responseValidationSpinner.getSelectedItemPosition()) { - case 0: - mModel.validationMode = ValidationMode.STATUS_CODE; - mModel.validationContent = null; - break; - case 1: - mModel.validationMode = ValidationMode.TERM_SEARCH; - mModel.validationContent = - ((EditText) findViewById(R.id.responseValidationSearchTerm)) - .getText() - .toString() - .trim(); - break; - case 2: - mModel.validationMode = ValidationMode.JAVASCRIPT; - mModel.validationContent = - ((EditText) findViewById(R.id.responseValidationScriptInput)) - .getText() - .toString() - .trim(); - break; - } - - final Inquiry inq = Inquiry.newInstance(this, MainActivity.DB_NAME).build(false); - //noinspection CheckResult - inq.update(ServerModel.class).values(new ServerModel[] {mModel}).run(); - inq.destroyInstance(); - } - - // Save button - @Override - public void onClick(View view) { - performSave(true); - setResult(RESULT_OK, new Intent().putExtra("model", mModel)); - finish(); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case R.id.refresh: - performSave(false); - MainActivity.checkSite(this, mModel); - return true; - case R.id.remove: - MainActivity.removeSite(this, mModel, this::finish); - return true; - } - return false; - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.kt new file mode 100644 index 0000000..df5f19f --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.kt @@ -0,0 +1,416 @@ +/* + * Licensed under Apache-2.0 + * + * Designed and developed by Aidan Follestad (@afollestad) + */ +package com.afollestad.nocknock.ui + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.util.Patterns.WEB_URL +import android.view.MenuItem +import android.view.View +import android.widget.ArrayAdapter +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.core.text.HtmlCompat +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.nocknock.BuildConfig +import com.afollestad.nocknock.R +import com.afollestad.nocknock.data.ServerModel +import com.afollestad.nocknock.data.ServerStatus.WAITING +import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT +import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE +import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH +import com.afollestad.nocknock.data.textRes +import com.afollestad.nocknock.engine.db.ServerModelStore +import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE +import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager +import com.afollestad.nocknock.notifications.NockNotificationManager +import com.afollestad.nocknock.utilities.ext.DAY +import com.afollestad.nocknock.utilities.ext.HOUR +import com.afollestad.nocknock.utilities.ext.MINUTE +import com.afollestad.nocknock.utilities.ext.WEEK +import com.afollestad.nocknock.utilities.ext.formatDate +import com.afollestad.nocknock.utilities.ext.hide +import com.afollestad.nocknock.utilities.ext.injector +import com.afollestad.nocknock.utilities.ext.isHttpOrHttps +import com.afollestad.nocknock.utilities.ext.onItemSelected +import com.afollestad.nocknock.utilities.ext.scopeWhileAttached +import com.afollestad.nocknock.utilities.ext.show +import com.afollestad.nocknock.utilities.ext.showOrHide +import com.afollestad.nocknock.utilities.ext.textAsLong +import com.afollestad.nocknock.utilities.ext.trimmedText +import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalInput +import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalSpinner +import kotlinx.android.synthetic.main.activity_viewsite.doneBtn +import kotlinx.android.synthetic.main.activity_viewsite.iconStatus +import kotlinx.android.synthetic.main.activity_viewsite.inputName +import kotlinx.android.synthetic.main.activity_viewsite.inputUrl +import kotlinx.android.synthetic.main.activity_viewsite.responseValidationMode +import kotlinx.android.synthetic.main.activity_viewsite.responseValidationScript +import kotlinx.android.synthetic.main.activity_viewsite.responseValidationScriptInput +import kotlinx.android.synthetic.main.activity_viewsite.responseValidationSearchTerm +import kotlinx.android.synthetic.main.activity_viewsite.rootView +import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult +import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck +import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning +import kotlinx.android.synthetic.main.activity_viewsite.toolbar +import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import java.lang.System.currentTimeMillis +import javax.inject.Inject +import kotlin.math.ceil + +private const val KEY_VIEW_MODEL = "site_model" + +/** @author Aidan Follestad (afViewSiteActivityollestad) */ +fun MainActivity.intentToView(model: ServerModel) = + Intent(this, ViewSiteActivity::class.java).apply { + putExtra(KEY_VIEW_MODEL, model) + } + +/** @author Aidan Follestad (afollestad) */ +class ViewSiteActivity : AppCompatActivity(), + View.OnClickListener, + Toolbar.OnMenuItemClickListener { + companion object { + private fun log(message: String) { + if (BuildConfig.DEBUG) { + Log.d("ViewSiteActivity", message) + } + } + } + + private lateinit var currentModel: ServerModel + + @Inject lateinit var serverModelStore: ServerModelStore + @Inject lateinit var notificationManager: NockNotificationManager + @Inject lateinit var checkStatusManager: CheckStatusManager + + private val intentReceiver = object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent + ) { + log("Received broadcast ${intent.action}") + val model = intent.getSerializableExtra(KEY_VIEW_MODEL) as? ServerModel + if (model != null) { + this@ViewSiteActivity.currentModel = model + update() + } + } + } + + @SuppressLint("SetTextI18n") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + injector().injectInto(this) + setContentView(R.layout.activity_viewsite) + + toolbar.run { + setNavigationOnClickListener { finish() } + inflateMenu(R.menu.menu_viewsite) + setOnMenuItemClickListener(this@ViewSiteActivity) + } + + val intervalOptionsAdapter = ArrayAdapter( + this, + R.layout.list_item_spinner, + resources.getStringArray(R.array.interval_options) + ) + intervalOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown) + checkIntervalSpinner.adapter = intervalOptionsAdapter + + inputUrl.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val inputStr = inputUrl.text + .toString() + .trim() + if (inputStr.isEmpty()) { + return@setOnFocusChangeListener + } + + val uri = Uri.parse(inputStr) + if (uri.scheme == null) { + inputUrl.setText("http://$inputStr") + textUrlWarning.hide() + } else if (!uri.isHttpOrHttps()) { + textUrlWarning.show() + textUrlWarning.setText(R.string.warning_http_url) + } else { + textUrlWarning.hide() + } + } + } + + val validationOptionsAdapter = ArrayAdapter( + this, + R.layout.list_item_spinner, + resources.getStringArray(R.array.response_validation_options) + ) + validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown) + responseValidationMode.adapter = validationOptionsAdapter + + responseValidationMode.onItemSelected { pos -> + responseValidationSearchTerm.showOrHide(pos == 1) + responseValidationScript.showOrHide(pos == 2) + + validationModeDescription.setText( + when (pos) { + 0 -> R.string.validation_mode_status_desc + 1 -> R.string.validation_mode_term_desc + 2 -> R.string.validation_mode_javascript_desc + else -> throw IllegalStateException("Unexpected position: $pos") + } + ) + } + + currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as ServerModel + update() + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + if (intent != null && intent.hasExtra(KEY_VIEW_MODEL)) { + currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as ServerModel + update() + } + } + + @SuppressLint("SetTextI18n") + private fun update() = with(currentModel) { + iconStatus.setStatus(this.status) + inputName.setText(this.name) + inputUrl.setText(this.url) + + if (this.lastCheck == 0L) { + textLastCheckResult.setText(R.string.none) + } else { + val statusText = this.status.textRes() + textLastCheckResult.text = if (statusText == 0) { + this.reason + } else { + getString(statusText) + } + } + + if (this.checkInterval == 0L) { + textNextCheck.setText(R.string.none_turned_off) + checkIntervalInput.setText("") + checkIntervalSpinner.setSelection(0) + } else { + var lastCheck = this.lastCheck + if (lastCheck == 0L) { + lastCheck = currentTimeMillis() + } + textNextCheck.text = (lastCheck + this.checkInterval).formatDate() + + when { + this.checkInterval >= WEEK -> { + checkIntervalInput.setText( + ceil((this.checkInterval.toFloat() / WEEK).toDouble()).toInt().toString() + ) + checkIntervalSpinner.setSelection(3) + } + this.checkInterval >= DAY -> { + checkIntervalInput.setText( + ceil((this.checkInterval.toFloat() / DAY.toFloat()).toDouble()).toInt().toString() + ) + checkIntervalSpinner.setSelection(2) + } + this.checkInterval >= HOUR -> { + checkIntervalInput.setText( + ceil((this.checkInterval.toFloat() / HOUR.toFloat()).toDouble()).toInt().toString() + ) + checkIntervalSpinner.setSelection(1) + } + this.checkInterval >= MINUTE -> { + checkIntervalInput.setText( + ceil((this.checkInterval.toFloat() / MINUTE.toFloat()).toDouble()).toInt().toString() + ) + checkIntervalSpinner.setSelection(0) + } + else -> { + checkIntervalInput.setText("0") + checkIntervalSpinner.setSelection(0) + } + } + } + + responseValidationMode.setSelection(validationMode.value - 1) + + when (this.validationMode) { + TERM_SEARCH -> responseValidationSearchTerm.setText(this.validationContent ?: "") + JAVASCRIPT -> responseValidationScriptInput.setText(this.validationContent ?: "") + else -> { + responseValidationSearchTerm.setText("") + responseValidationScriptInput.setText("") + } + } + + doneBtn.setOnClickListener(this@ViewSiteActivity) + } + + override fun onResume() { + super.onResume() + try { + val filter = IntentFilter() + filter.addAction(ACTION_STATUS_UPDATE) + // filter.addAction(CheckService.ACTION_JOB_RUNNING); + registerReceiver(intentReceiver, filter) + } catch (t: Throwable) { + t.printStackTrace() + } + } + + override fun onPause() { + super.onPause() + try { + unregisterReceiver(intentReceiver) + } catch (t: Throwable) { + t.printStackTrace() + } + } + + private fun updateModelFromInput(withValidation: Boolean) { + currentModel = currentModel.copy( + name = inputName.trimmedText(), + url = inputUrl.trimmedText(), + status = WAITING + ) + + if (withValidation && currentModel.name.isEmpty()) { + inputName.error = getString(R.string.please_enter_name) + return + } else { + inputName.error = null + } + + if (withValidation && currentModel.url.isEmpty()) { + inputUrl.error = getString(R.string.please_enter_url) + return + } else { + inputUrl.error = null + if (withValidation && !WEB_URL.matcher(currentModel.url).find()) { + inputUrl.error = getString(R.string.please_enter_valid_url) + return + } else { + val uri = Uri.parse(currentModel.url) + if (uri.scheme == null) { + currentModel = currentModel.copy(url = "http://${currentModel.url}") + } + } + } + + val intervalValue = checkIntervalInput.textAsLong() + + currentModel = when (checkIntervalSpinner.selectedItemPosition) { + 0 -> currentModel.copy(checkInterval = intervalValue * (60 * 1000)) + 1 -> currentModel.copy(checkInterval = intervalValue * (60 * 60 * 1000)) + 2 -> currentModel.copy(checkInterval = intervalValue * (60 * 60 * 24 * 1000)) + else -> currentModel.copy(checkInterval = intervalValue * (60 * 60 * 24 * 7 * 1000)) + } + + currentModel = currentModel.copy( + lastCheck = currentTimeMillis() - currentModel.checkInterval + ) + + when (responseValidationMode.selectedItemPosition) { + 0 -> { + currentModel = currentModel.copy( + validationMode = STATUS_CODE, + validationContent = null + ) + } + 1 -> { + currentModel = currentModel.copy( + validationMode = TERM_SEARCH, + validationContent = responseValidationSearchTerm.trimmedText() + ) + } + 2 -> { + currentModel = currentModel.copy( + validationMode = JAVASCRIPT, + validationContent = responseValidationScriptInput.trimmedText() + ) + } + } + } + + // Save button + override fun onClick(view: View) { + rootView.scopeWhileAttached(Main) { + launch(coroutineContext) { + updateModelFromInput(true) + async(IO) { serverModelStore.update(currentModel) }.await() + setResult(RESULT_OK) + finish() + } + } + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.refresh -> { + rootView.scopeWhileAttached(Main) { + launch(coroutineContext) { + updateModelFromInput(false) + async(IO) { serverModelStore.update(currentModel) }.await() + checkStatusManager.cancelCheck(currentModel) + checkStatusManager.scheduleCheck(currentModel, rightNow = true) + } + } + return true + } + R.id.remove -> { + maybeRemoveSite(currentModel) { finish() } + return true + } + } + return false + } + + private fun maybeRemoveSite( + model: ServerModel, + onRemoved: (() -> Unit)? + ) { + MaterialDialog(this).show { + title(R.string.remove_site) + message( + text = HtmlCompat.fromHtml( + context.getString(R.string.remove_site_prompt, model.name), + HtmlCompat.FROM_HTML_MODE_LEGACY + ) + ) + positiveButton(R.string.remove) { + checkStatusManager.cancelCheck(model) + notificationManager.cancelStatusNotifications() + performRemoveSite(model, onRemoved) + } + negativeButton(android.R.string.cancel) + } + } + + private fun performRemoveSite( + model: ServerModel, + onRemoved: (() -> Unit)? + ) { + rootView.scopeWhileAttached(Main) { + launch(coroutineContext) { + async(IO) { serverModelStore.delete(model) }.await() + onRemoved?.invoke() + } + } + } +} diff --git a/app/src/main/java/com/afollestad/nocknock/util/AlarmUtil.java b/app/src/main/java/com/afollestad/nocknock/util/AlarmUtil.java deleted file mode 100644 index a527898..0000000 --- a/app/src/main/java/com/afollestad/nocknock/util/AlarmUtil.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.afollestad.nocknock.util; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import com.afollestad.nocknock.api.ServerModel; -import com.afollestad.nocknock.services.CheckService; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -/** @author Aidan Follestad (afollestad) */ -public class AlarmUtil { - - private static final int BASE_RQC = 69; - - public static PendingIntent getSiteIntent(Context context, ServerModel site) { - return PendingIntent.getService( - context, - BASE_RQC + (int) site.id, - new Intent(context, CheckService.class).putExtra(CheckService.MODEL_ID, site.id), - PendingIntent.FLAG_UPDATE_CURRENT); - } - - private static AlarmManager am(Context context) { - return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - } - - public static void cancelSiteChecks(Context context, ServerModel site) { - PendingIntent pi = getSiteIntent(context, site); - am(context).cancel(pi); - } - - public static void setSiteChecks(Context context, ServerModel site) { - cancelSiteChecks(context, site); - if (site.checkInterval <= 0) return; - if (site.lastCheck <= 0) site.lastCheck = System.currentTimeMillis(); - final long nextCheck = site.lastCheck + site.checkInterval; - final AlarmManager aMgr = am(context); - final PendingIntent serviceIntent = getSiteIntent(context, site); - aMgr.setRepeating(AlarmManager.RTC_WAKEUP, nextCheck, site.checkInterval, serviceIntent); - final SimpleDateFormat df = - new SimpleDateFormat("EEE MMM dd hh:mm:ssa z yyyy", Locale.getDefault()); - Log.d( - "AlarmUtil", - String.format( - Locale.getDefault(), - "Set site check alarm for %s (%s), check interval: %d, next check: %s", - site.name, - site.url, - site.checkInterval, - df.format(new Date(nextCheck)))); - } - - public static void setSiteChecks(Context context, ServerModel[] sites) { - if (sites == null || sites.length == 0) return; - for (ServerModel site : sites) setSiteChecks(context, site); - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/util/JsUtil.java b/app/src/main/java/com/afollestad/nocknock/util/JsUtil.java deleted file mode 100644 index 3c7c462..0000000 --- a/app/src/main/java/com/afollestad/nocknock/util/JsUtil.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.afollestad.nocknock.util; - -import android.util.Log; -import org.mozilla.javascript.Context; -import org.mozilla.javascript.EvaluatorException; -import org.mozilla.javascript.Function; -import org.mozilla.javascript.Scriptable; - -/** @author Aidan Follestad (afollestad) */ -public class JsUtil { - - public static String exec(String code, String response) { - try { - final String func = - String.format( - "function validate(response) { " - + "try { " - + "%s " - + "} catch(e) { " - + "return e; " - + "} " - + "}", - code.replace("\n", " ")); - - // Every Rhino VM begins with the enter() - // This Context is not Android's Context - Context rhino = Context.enter(); - - // Turn off optimization to make Rhino Android compatible - rhino.setOptimizationLevel(-1); - try { - Scriptable scope = rhino.initStandardObjects(); - - // Note the forth argument is 1, which means the JavaScript source has - // been compressed to only one line using something like YUI - rhino.evaluateString(scope, func, "JavaScript", 1, null); - - // Get the functionName defined in JavaScriptCode - Function jsFunction = (Function) scope.get("validate", scope); - - // Call the function with params - Object jsResult = jsFunction.call(rhino, scope, scope, new Object[] {response}); - - // Parse the jsResult object to a String - String result = Context.toString(jsResult); - - boolean success = result != null && result.equals("true"); - String message = "The script returned a value other than true!"; - if (!success && result != null && !result.equals("false")) { - if (result.equals("undefined")) { - message = "The script did not return or throw anything!"; - } else { - message = result; - } - } - - Log.d("JsUtil", "Evaluated to " + message + " (" + success + "): " + code); - return !success ? message : null; - } finally { - Context.exit(); - } - } catch (EvaluatorException e) { - return e.getMessage(); - } - } - - private JsUtil() {} -} diff --git a/app/src/main/java/com/afollestad/nocknock/util/MathUtil.java b/app/src/main/java/com/afollestad/nocknock/util/MathUtil.java deleted file mode 100644 index efe418b..0000000 --- a/app/src/main/java/com/afollestad/nocknock/util/MathUtil.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.afollestad.nocknock.util; - -import android.graphics.Path; -import android.support.design.widget.FloatingActionButton; -import android.view.View; - -/** @author Aidan Follestad (afollestad) */ -public final class MathUtil { - - public static Path bezierCurve(FloatingActionButton fab, View rootView) { - final int fabCenterX = (int) (fab.getX() + fab.getMeasuredWidth() / 2); - final int fabCenterY = (int) (fab.getY() + fab.getMeasuredHeight() / 2); - - final int endCenterX = (rootView.getMeasuredWidth() / 2) - (fab.getMeasuredWidth() / 2); - final int endCenterY = (rootView.getMeasuredHeight() / 2) - (fab.getMeasuredHeight() / 2); - - final int halfX = (fabCenterX - endCenterX) / 2; - final int halfY = (fabCenterY - endCenterY) / 2; - int mControlX = endCenterX + halfX; - int mControlY = endCenterY + halfY; - mControlY -= halfY; - mControlX += halfX; - - Path path = new Path(); - path.moveTo(fab.getX(), fab.getY()); - path.quadTo(mControlX, mControlY, endCenterX, endCenterY); - - return path; - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/util/NetworkUtil.java b/app/src/main/java/com/afollestad/nocknock/util/NetworkUtil.java deleted file mode 100644 index c67e57a..0000000 --- a/app/src/main/java/com/afollestad/nocknock/util/NetworkUtil.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.afollestad.nocknock.util; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; - -/** @author Aidan Follestad (afollestad) */ -public class NetworkUtil { - - public static boolean hasInternet(Context context) { - final ConnectivityManager cm = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - final NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); - return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/util/TimeUtil.java b/app/src/main/java/com/afollestad/nocknock/util/TimeUtil.java deleted file mode 100644 index bc0b604..0000000 --- a/app/src/main/java/com/afollestad/nocknock/util/TimeUtil.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.afollestad.nocknock.util; - -/** @author Aidan Follestad (afollestad) */ -public class TimeUtil { - - public static final long SECOND = 1000; - public static final long MINUTE = SECOND * 60; - public static final long HOUR = MINUTE * 60; - public static final long DAY = HOUR * 24; - public static final long WEEK = DAY * 7; - public static final long MONTH = WEEK * 4; - - public static String str(long duration) { - if (duration <= 0) { - return ""; - } else if (duration >= MONTH) { - return (int) Math.ceil(((float) duration / (float) MONTH)) + "mo"; - } else if (duration >= WEEK) { - return (int) Math.ceil(((float) duration / (float) WEEK)) + "w"; - } else if (duration >= DAY) { - return (int) Math.ceil(((float) duration / (float) DAY)) + "d"; - } else if (duration >= HOUR) { - return (int) Math.ceil(((float) duration / (float) HOUR)) + "h"; - } else if (duration >= MINUTE) { - return (int) Math.ceil(((float) duration / (float) MINUTE)) + "m"; - } else { - return "<1m"; - } - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/views/StatusImageView.java b/app/src/main/java/com/afollestad/nocknock/views/StatusImageView.java deleted file mode 100644 index a9a7b21..0000000 --- a/app/src/main/java/com/afollestad/nocknock/views/StatusImageView.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.afollestad.nocknock.views; - -import android.content.Context; -import android.support.v7.widget.AppCompatImageView; -import android.util.AttributeSet; -import com.afollestad.nocknock.R; -import com.afollestad.nocknock.api.ServerStatus; - -/** @author Aidan Follestad (afollestad) */ -public class StatusImageView extends AppCompatImageView { - - public StatusImageView(Context context) { - super(context); - setStatus(ServerStatus.OK); - } - - public StatusImageView(Context context, AttributeSet attrs) { - super(context, attrs); - setStatus(ServerStatus.OK); - } - - public StatusImageView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setStatus(ServerStatus.OK); - } - - public void setStatus(@ServerStatus.Enum int status) { - switch (status) { - case ServerStatus.CHECKING: - case ServerStatus.WAITING: - setImageResource(R.drawable.status_progress); - setBackgroundResource(R.drawable.yellow_circle); - break; - case ServerStatus.ERROR: - setImageResource(R.drawable.status_error); - setBackgroundResource(R.drawable.red_circle); - break; - case ServerStatus.OK: - setImageResource(R.drawable.status_ok); - setBackgroundResource(R.drawable.green_circle); - break; - } - } -} diff --git a/app/src/main/java/com/afollestad/nocknock/views/StatusImageView.kt b/app/src/main/java/com/afollestad/nocknock/views/StatusImageView.kt new file mode 100644 index 0000000..a118d04 --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/views/StatusImageView.kt @@ -0,0 +1,42 @@ +/* + * Licensed under Apache-2.0 + * + * Designed and developed by Aidan Follestad (@afollestad) + */ +package com.afollestad.nocknock.views + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView +import com.afollestad.nocknock.R +import com.afollestad.nocknock.data.ServerStatus +import com.afollestad.nocknock.data.ServerStatus.CHECKING +import com.afollestad.nocknock.data.ServerStatus.ERROR +import com.afollestad.nocknock.data.ServerStatus.OK +import com.afollestad.nocknock.data.ServerStatus.WAITING + +/** @author Aidan Follestad (afollestad) */ +class StatusImageView( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { + + init { + setStatus(OK) + } + + fun setStatus(status: ServerStatus) = when (status) { + CHECKING, WAITING -> { + setImageResource(R.drawable.status_progress) + setBackgroundResource(R.drawable.yellow_circle) + } + ERROR -> { + setImageResource(R.drawable.status_error) + setBackgroundResource(R.drawable.red_circle) + } + OK -> { + setImageResource(R.drawable.status_ok) + setBackgroundResource(R.drawable.green_circle) + } + } +} diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml index 900d3cc..da8bafd 100644 --- a/app/src/main/res/anim/fade_out.xml +++ b/app/src/main/res/anim/fade_out.xml @@ -1,11 +1,9 @@ - - - - \ No newline at end of file + + diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml index 98beb0c..9be83b4 100644 --- a/app/src/main/res/drawable/divider.xml +++ b/app/src/main/res/drawable/divider.xml @@ -1,4 +1,4 @@ - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/green_circle.xml b/app/src/main/res/drawable/green_circle.xml index 31bc563..e02aec2 100644 --- a/app/src/main/res/drawable/green_circle.xml +++ b/app/src/main/res/drawable/green_circle.xml @@ -2,11 +2,11 @@ - + - + - - \ No newline at end of file + + diff --git a/app/src/main/res/drawable/ic_action_close.xml b/app/src/main/res/drawable/ic_action_close.xml index f4977c9..369498e 100644 --- a/app/src/main/res/drawable/ic_action_close.xml +++ b/app/src/main/res/drawable/ic_action_close.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0"> - + diff --git a/app/src/main/res/drawable/ic_action_delete.xml b/app/src/main/res/drawable/ic_action_delete.xml index d85947c..7539f69 100644 --- a/app/src/main/res/drawable/ic_action_delete.xml +++ b/app/src/main/res/drawable/ic_action_delete.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0"> - - \ No newline at end of file + + diff --git a/app/src/main/res/drawable/ic_action_refresh.xml b/app/src/main/res/drawable/ic_action_refresh.xml index 85eb103..0240dcb 100644 --- a/app/src/main/res/drawable/ic_action_refresh.xml +++ b/app/src/main/res/drawable/ic_action_refresh.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0"> - - \ No newline at end of file + + diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml index 7b18128..c0ca276 100644 --- a/app/src/main/res/drawable/ic_add.xml +++ b/app/src/main/res/drawable/ic_add.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0"> - + diff --git a/app/src/main/res/drawable/ic_down_arrow.xml b/app/src/main/res/drawable/ic_down_arrow.xml deleted file mode 100644 index 2341755..0000000 --- a/app/src/main/res/drawable/ic_down_arrow.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/red_circle.xml b/app/src/main/res/drawable/red_circle.xml index d6bdd26..d2aecb2 100644 --- a/app/src/main/res/drawable/red_circle.xml +++ b/app/src/main/res/drawable/red_circle.xml @@ -2,11 +2,11 @@ - + - + - - \ No newline at end of file + + diff --git a/app/src/main/res/drawable/status_error.xml b/app/src/main/res/drawable/status_error.xml index 6f67037..e913358 100644 --- a/app/src/main/res/drawable/status_error.xml +++ b/app/src/main/res/drawable/status_error.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0"> - - \ No newline at end of file + + diff --git a/app/src/main/res/drawable/status_ok.xml b/app/src/main/res/drawable/status_ok.xml index 3ec2bc9..fb118ed 100644 --- a/app/src/main/res/drawable/status_ok.xml +++ b/app/src/main/res/drawable/status_ok.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0"> - - \ No newline at end of file + + diff --git a/app/src/main/res/drawable/status_progress.xml b/app/src/main/res/drawable/status_progress.xml index c100e60..ecdb7b3 100644 --- a/app/src/main/res/drawable/status_progress.xml +++ b/app/src/main/res/drawable/status_progress.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0"> - - \ No newline at end of file + + diff --git a/app/src/main/res/drawable/yellow_circle.xml b/app/src/main/res/drawable/yellow_circle.xml index 1201e65..dfff479 100644 --- a/app/src/main/res/drawable/yellow_circle.xml +++ b/app/src/main/res/drawable/yellow_circle.xml @@ -2,11 +2,11 @@ - + - + - - \ No newline at end of file + + diff --git a/app/src/main/res/layout/activity_addsite.xml b/app/src/main/res/layout/activity_addsite.xml index 4bf5e29..a03096c 100644 --- a/app/src/main/res/layout/activity_addsite.xml +++ b/app/src/main/res/layout/activity_addsite.xml @@ -1,242 +1,270 @@ - + android:orientation="vertical" + > - + + + + + android:orientation="vertical" + android:paddingBottom="@dimen/content_inset" + android:paddingLeft="@dimen/content_inset" + android:paddingRight="@dimen/content_inset" + > - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + > - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -