commit b7b2eb0812738e3796188f01bce1c04a1b33c4c2 Author: Dimosthenis Kastrinakis Date: Wed Jan 22 03:28:32 2020 +0200 final android control client diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b75303 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..85950d9 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,58 @@ +apply plugin: 'com.android.application' +apply plugin: 'androidx.navigation.safeargs' +apply plugin: 'com.jaredsburrows.license' + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "com.skulixlabs.iotcontrol" + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + applicationVariants.all { variant -> + variant.resValue "string", "appVersion", variant.versionName + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +/*licenseReport { + generateHtmlReport = false + generateJsonReport = true + copyHtmlReportToAssets = true + copyJsonReportToAssets = false +}*/ + +dependencies { + def nav_version = "2.1.0" + def jackson_ver = "2.9.9" + + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + //implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.preference:preference:1.1.0-alpha05' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' + //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-3' + implementation "com.google.android.material:material:1.1.0-alpha10" + implementation "androidx.navigation:navigation-fragment:$nav_version" + implementation "androidx.navigation:navigation-ui:$nav_version" + implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_ver" + implementation "com.fasterxml.jackson.core:jackson-core:$jackson_ver" + implementation "com.fasterxml.jackson.core:jackson-annotations:$jackson_ver" + implementation 'commons-io:commons-io:2.6' + implementation 'com.google.android:flexbox:1.1.1' + + implementation 'br.com.simplepass:loading-button-android:2.1.5' + implementation 'com.github.StevenDXC:DxLoadingButton:2.2' +} diff --git a/app/licenses.yml b/app/licenses.yml new file mode 100644 index 0000000..6acdcc3 --- /dev/null +++ b/app/licenses.yml @@ -0,0 +1,333 @@ + +- artifact: androidx.slidingpanelayout:slidingpanelayout:+ + name: Android Support Library Sliding Pane Layout + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.appcompat:appcompat:+ + name: Android AppCompat Library v7 + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/jetpack/androidx +- artifact: androidx.appcompat:appcompat-resources:+ + name: Android Resources Library + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/jetpack/androidx +- artifact: androidx.customview:customview:+ + name: Android Support Library Custom View + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: org.jetbrains.kotlin:kotlin-stdlib-common:+ + name: org.jetbrains.kotlin:kotlin-stdlib-common + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://kotlinlang.org/ +- artifact: androidx.swiperefreshlayout:swiperefreshlayout:+ + name: Android Support Library Custom View + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.navigation:navigation-fragment:+ + name: Android Navigation Fragment + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: androidx.savedstate:savedstate:+ + name: Activity + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/jetpack/androidx +- artifact: com.google.android.material:material:+ + name: Material Components for Android + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.fragment:fragment:+ + name: Android Support Library fragment + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/jetpack/androidx +- artifact: androidx.lifecycle:lifecycle-livedata:+ + name: Android Lifecycle LiveData + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: androidx.constraintlayout:constraintlayout:+ + name: Android ConstraintLayout + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://tools.android.com +- artifact: androidx.localbroadcastmanager:localbroadcastmanager:+ + name: Android Support Library Local Broadcast Manager + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.preference:preference:+ + name: AndroidX Preference + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.legacy:legacy-support-core-utils:+ + name: Android Support Library core utils + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.coordinatorlayout:coordinatorlayout:+ + name: Android Support Library Coordinator Layout + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.legacy:legacy-support-v4:+ + name: Android Support Library v4 + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: commons-io:commons-io:+ + name: Apache Commons IO + copyrightHolder: #COPYRIGHT_HOLDER# + license: #LICENSE# + url: http://commons.apache.org/proper/commons-io/ +- artifact: androidx.navigation:navigation-common:+ + name: Android Navigation Common + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: androidx.versionedparcelable:versionedparcelable:+ + name: VersionedParcelable + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.print:print:+ + name: Android Support Library Print + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.drawerlayout:drawerlayout:+ + name: Android Support Library Drawer Layout + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.navigation:navigation-runtime:+ + name: Android Navigation Runtime + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: androidx.core:core:+ + name: Android Support Library compat + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.lifecycle:lifecycle-common:+ + name: Android Lifecycle-Common + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: androidx.interpolator:interpolator:+ + name: Android Support Library Interpolators + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: org.jetbrains.kotlin:kotlin-stdlib:+ + name: org.jetbrains.kotlin:kotlin-stdlib + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://kotlinlang.org/ +- artifact: androidx.viewpager:viewpager:+ + name: Android Support Library View Pager + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.lifecycle:lifecycle-runtime:+ + name: Android Lifecycle Runtime + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: androidx.arch.core:core-common:+ + name: Android Arch-Common + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: androidx.lifecycle:lifecycle-viewmodel:+ + name: Android Lifecycle ViewModel + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: androidx.vectordrawable:vectordrawable:+ + name: Android Support VectorDrawable + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/jetpack/androidx +- artifact: androidx.lifecycle:lifecycle-livedata-core:+ + name: Android Lifecycle LiveData Core + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: androidx.media:media:+ + name: Android Support Library media compat + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: br.com.simplepass:loading-button-android:+ + name: LoadingButtonAndroid + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://github.com/leandroBorgesFerreira/LoadingButtonAndroid +- artifact: androidx.documentfile:documentfile:+ + name: Android Support Library Document File + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.cursoradapter:cursoradapter:+ + name: Android Support Library Cursor Adapter + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: org.jetbrains:annotations:+ + name: IntelliJ IDEA Annotations + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://www.jetbrains.org +- artifact: androidx.arch.core:core-runtime:+ + name: Android Arch-Runtime + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: androidx.recyclerview:recyclerview:+ + name: Android Support RecyclerView v7 + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/jetpack/androidx +- artifact: com.fasterxml.jackson.core:jackson-core:+ + name: Jackson-core + copyrightHolder: #COPYRIGHT_HOLDER# + license: #LICENSE# + url: https://github.com/FasterXML/jackson-core +- artifact: androidx.cardview:cardview:+ + name: Android Support CardView v7 + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.legacy:legacy-support-core-ui:+ + name: Android Support Library core UI + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: com.fasterxml.jackson.core:jackson-databind:+ + name: jackson-databind + copyrightHolder: #COPYRIGHT_HOLDER# + license: #LICENSE# + url: http://github.com/FasterXML/jackson +- artifact: androidx.vectordrawable:vectordrawable-animated:+ + name: Android Support AnimatedVectorDrawable + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/jetpack/androidx +- artifact: androidx.annotation:annotation:+ + name: Android Support Library Annotations + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.transition:transition:+ + name: Android Transition Support Library + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.navigation:navigation-ui:+ + name: Android Navigation UI + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/topic/libraries/architecture/index.html +- artifact: com.fasterxml.jackson.core:jackson-annotations:+ + name: Jackson-annotations + copyrightHolder: #COPYRIGHT_HOLDER# + license: #LICENSE# + url: http://github.com/FasterXML/jackson +- artifact: androidx.constraintlayout:constraintlayout-solver:+ + name: Android ConstraintLayout Solver + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://tools.android.com +- artifact: androidx.viewpager2:viewpager2:+ + name: AndroidX Widget ViewPager2 + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/jetpack/androidx +- artifact: androidx.asynclayoutinflater:asynclayoutinflater:+ + name: Android Support Library Async Layout Inflater + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: com.github.StevenDXC:DxLoadingButton:+ + name: StevenDXC/DxLoadingButton + copyrightHolder: #COPYRIGHT_HOLDER# + license: Apache License 2.0 + licenseUrl: https://api.github.com/licenses/apache-2.0 + url: https://github.com/StevenDXC/DxLoadingButton +- artifact: androidx.loader:loader:+ + name: Android Support Library loader + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html +- artifact: androidx.activity:activity:+ + name: Activity + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://developer.android.com/jetpack/androidx +- artifact: androidx.collection:collection:+ + name: Android Support Library collections + copyrightHolder: #COPYRIGHT_HOLDER# + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/skulixlabs/iotcontrol/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/skulixlabs/iotcontrol/ExampleInstrumentedTest.java new file mode 100644 index 0000000..f71428f --- /dev/null +++ b/app/src/androidTest/java/com/skulixlabs/iotcontrol/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.skulixlabs.iotcontrol; + +import android.content.Context; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.example.gatecontrol", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c2471ce --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/open_source_licenses.html b/app/src/main/assets/open_source_licenses.html new file mode 100644 index 0000000..09fcf63 --- /dev/null +++ b/app/src/main/assets/open_source_licenses.html @@ -0,0 +1,385 @@ + + + + Open source licenses + + +

Notice for packages:

+ + + diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/AboutFragment.java b/app/src/main/java/com/skulixlabs/iotcontrol/AboutFragment.java new file mode 100644 index 0000000..9136e8e --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/AboutFragment.java @@ -0,0 +1,65 @@ +package com.skulixlabs.iotcontrol; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; + + +public class AboutFragment extends Fragment { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_about, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + view.findViewById(R.id.btnLicenses).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new OpenSourceLicensesDialog().newInstance().show(getChildFragmentManager(), "dialog_licenses"); + } + }); + } + + public static class OpenSourceLicensesDialog extends DialogFragment { + + public static OpenSourceLicensesDialog newInstance() { + OpenSourceLicensesDialog dialog = new OpenSourceLicensesDialog(); + Bundle args = new Bundle(); + dialog.setArguments(args); + return dialog; + } + + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + WebView webView = new WebView(requireActivity()); + webView.loadUrl("file:///android_asset/open_source_licenses.html"); + + return new AlertDialog.Builder(requireActivity()) + .setTitle("Open Source Licenses") + .setView(webView) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int i) { + dialog.dismiss(); + } + }) + .create(); + } + } +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/ConnectTaskFromAdapter.java b/app/src/main/java/com/skulixlabs/iotcontrol/ConnectTaskFromAdapter.java new file mode 100644 index 0000000..d16172b --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/ConnectTaskFromAdapter.java @@ -0,0 +1,231 @@ +package com.skulixlabs.iotcontrol; + +import android.graphics.Color; +import android.os.AsyncTask; +import android.os.Handler; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.util.Log; + +import com.dx.dxloadingbutton.lib.LoadingButton; +import com.skulixlabs.iotcontrol.adapter.ItemAdapter; +import com.skulixlabs.iotcontrol.model.Cmd; +import com.skulixlabs.iotcontrol.model.CmdResponse; +import com.skulixlabs.iotcontrol.util.Utils; + +import java.io.ByteArrayOutputStream; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import br.com.simplepass.loadingbutton.customViews.CircularProgressButton; + +public class ConnectTaskFromAdapter extends AsyncTask { + private final String TAG = ConnectTaskFromAdapter.class.getName(); + final ConnectTaskFromAdapter.ConnectTaskResult result = new ConnectTaskFromAdapter.ConnectTaskResult(); // If we were to save to a Byte var, then a final Byte array should be initiated + + WeakReference holder; + WeakReference fragment; + LoadingButton actionButton; + TcpClient mTcpClient = null; + Mac mac = null; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ByteBuffer resHash; + ByteBuffer gotBytes; + int gotBytesNum = 0; + Cmd cmd; + + public ConnectTaskFromAdapter(ItemAdapter.ItemViewHolder holder, WeakReference fragment, Cmd cmd, LoadingButton actionButton) { + this.holder = new WeakReference<>(holder); + this.fragment = fragment; + this.cmd = cmd; + this.actionButton = actionButton; + } + + @Override + protected void onPreExecute() { + try { + mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(cmd.getOptions().getHmacKey().getBytes(), cmd.getOptions().getHmacKey())); + } catch (Exception e) { + //onMessage(e.getMessage()); + cancel(true); + result.exception = e; + return; + } + + gotBytes = ByteBuffer.allocate(cmd.getOptions().getRandDataLength()); + resHash = ByteBuffer.allocate(cmd.getOptions().getHashByteLength()); + } + + @Override + protected ConnectTaskFromAdapter.ConnectTaskResult doInBackground(final Void... voids) { + if (isCancelled()) + return null; + Log.d(TAG, "Sending command: " + cmd.getValue()); + mTcpClient = new TcpClient(cmd.getOptions().getUrl(), new TcpClient.OnMessageReceived() { + @Override + public void messageReceived(byte[] data) { + try { + gotBytesNum += data.length; + + if (gotBytesNum <= cmd.getOptions().getRandDataLength()) { + mac.update(data); + for (int i = 0; i < data.length; i++) { + gotBytes.put(gotBytesNum - data.length + i, data[i]); + } + } + + if (gotBytesNum == cmd.getOptions().getRandDataLength()) { + Log.d(TAG, "Sending action with value: " + cmd.getValue()); + mac.update((byte) cmd.getValue()); + + byte[] reqHash = mac.doFinal(outputStream.toByteArray()); + + System.out.println(Utils.bytesToHex(reqHash)); + mTcpClient.sendMessage(reqHash); + } + + if (gotBytesNum > cmd.getOptions().getRandDataLength()) { + if (gotBytesNum > (cmd.getOptions().getRandDataLength() + cmd.getOptions().getHashByteLength())) + Log.d("test", "error!"); + + for (int i = 0; i < data.length; i++) + resHash.put(gotBytesNum - cmd.getOptions().getRandDataLength() - data.length + i, data[i]); + + if (gotBytesNum == (cmd.getOptions().getRandDataLength() + cmd.getOptions().getHashByteLength())) { + Object[] objects = cmd.getResponseList().keySet().toArray(); + Byte[] bytes = Arrays.copyOf(objects, objects.length, Byte[].class); + + byte b = Utils.analyzeResponse(cmd.getOptions().getHmacKey(), bytes, gotBytes, resHash); //cmds[0].getResponseList() + + CmdResponse cmdResponse = Utils.matchResponse(cmd, b); + if (cmdResponse != null) + publishProgress(updateState(cmdResponse.getDescription(), cmdResponse.getColor())); + else + publishProgress(updateState("Failed!", "#FF0000")); + + Log.d("test", "Response byte: " + b); + result.result = b; + + mTcpClient.stopClient(); + } + } + } catch (Exception e) { + publishProgress(updateState("Error: " + e.getMessage(), "#FF0000")); + Log.e(TAG, "An error occurred", e); + mTcpClient.stopClient(); + result.exception = e; + } + } + }, new TcpClient.OnClientError() { + @Override + public void onError(Exception e) { + + if (e instanceof java.net.ConnectException) + publishProgress(updateState("Error: Connection impossible", "#FF0000")); + else if (e instanceof java.net.SocketTimeoutException) + publishProgress(updateState("Error: Timeout", "#FF0000")); + else + publishProgress(updateState("Error: " + e.getMessage(), "#FF0000")); + mTcpClient.stopClient(); + result.exception = e; + + } + }); + + mTcpClient.run(); + return result; + } + + @Override + protected void onProgressUpdate(Spannable... values) { + try { + switch (cmd.getType()) { + case STATE: + holder.get().state.setText(values[0]); + break; + case FEEDBACK: + + } + } catch (NullPointerException e) { // holder may have been destroyed (null) + // Just stifle the exception, preventing sudden app termination + } + } + + @Override + protected void onPostExecute(ConnectTaskFromAdapter.ConnectTaskResult result) { + super.onPostExecute(result); + if (actionButton!= null) { + if (result.exception == null) { // A response should be returned if succeeded. Otherwise use custom object including an Exception field + actionSucceededAnimation(actionButton); + } else { + actionFailedAnimation(actionButton); + } + } + } + + /*private void onMessage(String message) { + Toast.makeText(fragment.get().getActivity(), message, Toast.LENGTH_SHORT).show(); + }*/ + + private void actionFailedAnimation(LoadingButton actionButton) { + /*Bitmap bitmap = Utils.drawableToBitmap(ContextCompat.getDrawable(holder.get().getContext(), R.drawable.ic_close_red_24dp)); + actionButton.doneLoadingAnimation(SettingsFragment.getPrimaryColor(holder.get().getContext()), bitmap); + revertAnimation(actionButton);*/ + actionButton.loadingFailed(); // Auto reset is enabled + } + + private void actionSucceededAnimation(final LoadingButton actionButton) { + /*Bitmap bitmap = Utils.drawableToBitmap(ContextCompat.getDrawable(holder.get().getContext(), R.drawable.ic_check_green_48dp)); + actionButton.doneLoadingAnimation(SettingsFragment.getPrimaryColor(holder.get().getContext()), bitmap); + revertAnimation(actionButton);*/ + + actionButton.loadingSuccessful(); + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + actionButton.reset(); + } + }, 750); + /*actionButton.setAnimationEndAction((Function1)(new Function1() { + public Object invoke(Object var1) { + this.invoke((AnimationType)var1); + return Unit.INSTANCE; + } + + public final void invoke(@NotNull AnimationType it) { + Intrinsics.checkParameterIsNotNull(it, "it"); + Toast.makeText(holder.get().getContext().getApplicationContext(), (CharSequence)("end:" + it), Toast.LENGTH_SHORT).show(); + } + }));*/ + } + + private void revertAnimation(final CircularProgressButton actionButton) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + actionButton.startMorphRevertAnimation(); + } + }, 500); + } + + private Spannable updateState(String str, String color) { + Spannable spannable = new SpannableString(str); + spannable.setSpan( + new ForegroundColorSpan(Color.parseColor(color)), + 0, str.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return spannable; + } + + public class ConnectTaskResult { + public byte result; + public Exception exception; + } +} + diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/MainActivity.java b/app/src/main/java/com/skulixlabs/iotcontrol/MainActivity.java new file mode 100644 index 0000000..2ab7f71 --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/MainActivity.java @@ -0,0 +1,69 @@ +package com.skulixlabs.iotcontrol; + +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.Toolbar; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; +import androidx.navigation.ui.NavigationUI; +import androidx.preference.PreferenceManager; + + +/** + * IMPORTANT NOTES + * + * The following applies only to custom toolbars. If we use NavigationUI's default + * toolbar then everything works by default. ConstraintLayout overlaps its children. + * That means that the Toolbar will be overlapped by the fragments' content (content + * of all the pages in the graph). To avoid this our top destination (home + * page in nav graph) must have a different type of layout (linear). The other + * solution is to add the margin_top=?attr/actionBarSize for the fragment of the + * home page, but that is not recommended. + * + */ + +// TODO: dynamic cmd and response equality check +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + theme(); // Set theme here, save as SharedPreference, recreate when changed + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + NavController navController = Navigation.findNavController(this, R.id.my_nav_host_fragment); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + NavigationUI.setupWithNavController(toolbar, navController); + } + + @Override + public boolean onSupportNavigateUp() { + Navigation.findNavController(this, R.id.my_nav_host_fragment).navigateUp(); + return super.onSupportNavigateUp(); + } + + public void theme() { + if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("dark_theme", false)) + getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES); + else { + getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_NO); + } + + /* Color even in dark mode, move to above else block if applied only when dark mode is off*/ + String themeColor = PreferenceManager.getDefaultSharedPreferences(this).getString("theme_color", null); + if (themeColor == null) themeColor = "default"; + switch (themeColor) { + case "red": + setTheme(R.style.AppTheme_RED); + break; + case "orange": + setTheme(R.style.AppTheme_ORANGE); + break; + default: + setTheme(R.style.AppTheme); + } + } +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/MainFragment.java b/app/src/main/java/com/skulixlabs/iotcontrol/MainFragment.java new file mode 100644 index 0000000..f8bbd57 --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/MainFragment.java @@ -0,0 +1,124 @@ +package com.skulixlabs.iotcontrol; + + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.skulixlabs.iotcontrol.adapter.ItemAdapter; +import com.skulixlabs.iotcontrol.util.jackson.JsonUtils; +import com.google.android.material.snackbar.Snackbar; + +import java.util.ArrayList; +import java.util.List; + +import com.skulixlabs.iotcontrol.model.CmdGroup; + +/** + * A simple {@link Fragment} subclass. + */ +public class MainFragment extends Fragment { + + private static final String TAG = MainFragment.class.getName(); + ItemAdapter adapter = null; + + public MainFragment() { + // Required empty public constructor + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + setHasOptionsMenu(true); + return inflater.inflate(R.layout.fragment_main, container, false); + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + RecyclerView recList = (RecyclerView) view.findViewById(R.id.cardList); + LinearLayoutManager llm = new LinearLayoutManager(getContext()); + llm.setOrientation(LinearLayoutManager.VERTICAL); + recList.setLayoutManager(llm); + + List groups = new ArrayList<>(); + try { + groups = new JsonUtils().getCommands(getActivity()); + } catch (Exception e) { + e.printStackTrace(); + Snackbar.make(view, getResources().getString(R.string.invalid_commands), Snackbar.LENGTH_LONG) + .setAction(getResources().getString(R.string.load), new View.OnClickListener() { + @Override + public void onClick(View view) { + NavGraphDirections.ActionOpenSettings actionOpenSettings = SettingsFragmentDirections.actionOpenSettings(); + actionOpenSettings.setImportCmds(true); // Using Safe-args + Navigation.findNavController(getActivity(), R.id.my_nav_host_fragment).navigate(actionOpenSettings); + } + }).show(); + } + + adapter = new ItemAdapter(null, groups); //this,groups + recList.setAdapter(adapter); + + Log.d(TAG, "oneviewcreated"); + } + + /** + * Runs when screen is off + */ + @Override + public void onResume() { + Log.d(TAG, "onResume"); + super.onResume(); + adapter.validateStateListeners(); + } + + @Override + public void onPause() { + Log.d(TAG, "onPause"); + super.onPause(); + adapter.invalidateStateListeners(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + adapter = null; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_main, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_settings) { + Navigation.findNavController(getActivity(), R.id.my_nav_host_fragment).navigate(R.id.action_open_settings); + return true; + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/SettingsFragment.java b/app/src/main/java/com/skulixlabs/iotcontrol/SettingsFragment.java new file mode 100644 index 0000000..2a7c760 --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/SettingsFragment.java @@ -0,0 +1,262 @@ +package com.skulixlabs.iotcontrol; + +import android.Manifest; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.navigation.Navigation; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreferenceCompat; + +import com.skulixlabs.iotcontrol.util.jackson.JsonUtils; + +import java.io.InputStream; + +import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN; +import static com.skulixlabs.iotcontrol.util.jackson.JsonUtils.REQUEST_READ_FILE; +import static com.skulixlabs.iotcontrol.util.jackson.JsonUtils.REQUEST_READ_STORAGE; + +/** Preference screens does not currently support Data Binding + * because it is not a xml layout file + */ +public class SettingsFragment extends PreferenceFragmentCompat { + @ColorInt public static Integer primaryColor = null; + + public SettingsFragment() { + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.root_preferences, rootKey); + + ((Preference) findPreference("load_json_commands")).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + showImportDialog(); + return true; + }; + }); + + ((SwitchPreferenceCompat) findPreference("dark_theme")).setOnPreferenceChangeListener(updateThemeListener(themeAction())); + ((ListPreference) findPreference("theme_color")).setOnPreferenceChangeListener(updateThemeListener(themeAction())); + + ((Preference) findPreference("version")).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Navigation.findNavController(getActivity(), R.id.my_nav_host_fragment).navigate(R.id.action_settingsFragment_to_aboutFragment); + return true; + }; + }); + + shouldShowImportDialog(); + } + + private void shouldShowImportDialog() { + if (SettingsFragmentArgs.fromBundle(getArguments()).getImportCmds()) { + getArguments().putBoolean("importCmds", false); + showImportDialog(); + } + } + + private void showImportDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = requireActivity().getLayoutInflater(); + + // Inflate and set the layout for the dialog + // Pass null as the parent view because its going in the dialog layout + builder.setView(inflater.inflate(R.layout.dialog_import_method, null)); + final AlertDialog alertDialog = builder.create(); + alertDialog.show(); + + alertDialog.findViewById(R.id.btnFromClipboard).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + String json = ""; + + // If the clipboard doesn't contain data, else start the process after validation + if (!(clipboard.hasPrimaryClip()) || !(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))) { + Toast.makeText(getActivity(), "Not valid!", Toast.LENGTH_SHORT).show(); + ((Preference) findPreference("load_json_commands")).setIcon(R.drawable.ic_error_outline_red_48dp); + } else { + try { + ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); // Since the clipboard contains plain text. + json = item.getText().toString(); // Gets the clipboard as text. + new JsonUtils().saveCommands(getActivity(), json); + ((Preference) findPreference("load_json_commands")).setIcon(R.drawable.ic_check_green_48dp); + alertDialog.cancel(); + } catch (Exception e) { + Toast.makeText(getActivity(), "Not valid!", Toast.LENGTH_SHORT).show(); + ((Preference) findPreference("load_json_commands")).setIcon(R.drawable.ic_error_outline_red_48dp); + } + } + } + }); + + alertDialog.findViewById(R.id.btnFromFile).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + selectFile(); + } + }); + } + + public Runnable themeAction() { + return new Runnable() { + @Override + public void run() { + primaryColor = null; + ((MainActivity) getActivity()).theme(); + getActivity().recreate(); + } + }; + } + + public static synchronized int getPrimaryColor(Context context) { + if (primaryColor == null) { + TypedValue typedValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorPrimary, typedValue, true); + primaryColor = typedValue.data; + } + return primaryColor; + } + + private String FRAGMENT_DIALOG = "dialog"; + + public void selectFile() { + if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + requestReadStoragePermission(); + return; + } + openFileManager(); + } + + private void openFileManager() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + startActivityForResult(intent, REQUEST_READ_FILE); + } + + private void requestReadStoragePermission() { + if (shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)) { + new ConfirmationDialog().show(getChildFragmentManager(), FRAGMENT_DIALOG); + } else { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_READ_STORAGE); + } + } + + public static class ConfirmationDialog extends DialogFragment { + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Fragment parent = getParentFragment(); + return new AlertDialog.Builder(getActivity()) + .setMessage(R.string.request_read_external_storage_permission) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + parent.requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + REQUEST_READ_STORAGE); + } + }) + .create(); + } + } + + public static class ErrorDialog extends DialogFragment { + + private static final String ARG_MESSAGE = "message"; + + public static ErrorDialog newInstance(String message) { + ErrorDialog dialog = new ErrorDialog(); + Bundle args = new Bundle(); + args.putString(ARG_MESSAGE, message); + dialog.setArguments(args); + return dialog; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + return new AlertDialog.Builder(activity) + .setMessage(getArguments().getString(ARG_MESSAGE)) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int i) { + getDialog().cancel(); + } + }) + .create(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + if (requestCode == REQUEST_READ_STORAGE) { + if (grantResults.length != 1 || grantResults[0] != PackageManager.PERMISSION_GRANTED) { // one requested permission, one result expected (in results array) + ErrorDialog.newInstance(getString(R.string.request_read_external_storage_permission)) + .show(getChildFragmentManager(), FRAGMENT_DIALOG); + } else { + openFileManager(); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == REQUEST_READ_FILE && resultCode == Activity.RESULT_OK) { + if (data != null) { + Uri uri = data.getData(); + + try { + InputStream is = getActivity().getContentResolver().openInputStream(uri); + JsonUtils jsonUtils = new JsonUtils(); + jsonUtils.saveCommands(getActivity(), is); + ((Preference) findPreference("load_json_commands")).setIcon(R.drawable.ic_check_green_48dp); + System.out.println(); + } catch (Exception e) { + ((Preference) findPreference("load_json_commands")).setIcon(R.drawable.ic_error_outline_red_48dp); + e.printStackTrace(); + } + } + } + } + + public static Preference.OnPreferenceChangeListener updateThemeListener(final Runnable action) { + return new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + action.run(); + return true; + } + }; + } +} + diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/TcpClient.java b/app/src/main/java/com/skulixlabs/iotcontrol/TcpClient.java new file mode 100644 index 0000000..44f16aa --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/TcpClient.java @@ -0,0 +1,163 @@ +package com.skulixlabs.iotcontrol; + +import android.util.Log; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Socket; + +public class TcpClient { + public static final String TAG = TcpClient.class.getName(); + public static final int TIMEOUT = 1500; + public String SERVER_IP; + public int SERVER_PORT; + + // sends message received notifications + private OnMessageReceived mMessageListener = null; + private OnClientError mErrorListener = null; + // while this is true, the server will continue running/listening + private boolean mRun = false; + // used to send data + DataOutputStream mBufferOut; + + /** + * Constructor of the class. OnMessagedReceived listens for the messages received from server + */ + public TcpClient(String ipAddressWithPort, OnMessageReceived listener, OnClientError errorListener) { + String[] ipAddressPort = ipAddressWithPort.split(":"); + SERVER_IP = ipAddressPort[0]; + SERVER_PORT = Integer.valueOf(ipAddressPort[1]); + mMessageListener = listener; + mErrorListener = errorListener; + } + + /** + * Sends the message entered by client to the server + * + * @param message text entered by client + */ + public void sendMessage(final byte[] message) { + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + if (mBufferOut != null) { + mBufferOut.write(message); + Log.d(TAG, "Sending data\n"); + mBufferOut.flush(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } + + /** + * Close the connection and release the members + */ + public void stopClient() { + mRun = false; + + if (mBufferOut != null) { + try { + mBufferOut.flush(); + mBufferOut.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + unregisterListeners(); + //mBufferIn = null; + mBufferOut = null; + + Log.d(TAG, "Client stopped successfully"); + } + + public void run() { + + mRun = true; + + try { + Log.d("TCP Client", "C: Connecting..."); + + //here you must put your computer's IP address. + /*InetAddress serverAddr = InetAddress.getByName(SERVER_IP); + + //create a socket to make the connection with the server + Socket socket = new Socket(serverAddr, SERVER_PORT);*/ + + InetSocketAddress sockAdr = new InetSocketAddress(SERVER_IP, SERVER_PORT); + Socket socket = new Socket(); + socket.setSoTimeout(TIMEOUT); + socket.connect(sockAdr, TIMEOUT); + + try { + + //sends the message to the server + mBufferOut = new DataOutputStream(socket.getOutputStream()); + + //receives the message which the server sends back + InputStream is = socket.getInputStream(); + DataInputStream dis = new DataInputStream(is); + + Log.d("test", "Client ready"); + sendMessage("X".getBytes()); // Send X and wait for response, then send command + + //in this while the client listens for the messages sent by the server + while (mRun) { + + // count the available bytes form the input stream + int count = is.available(); + + // create buffer + byte[] bs = new byte[count]; + + int read = dis.read(bs); + + if (count > 0 && mMessageListener != null) + mMessageListener.messageReceived(bs); + } + + Log.d("RESPONSE FROM SERVER", "S: Received Message"); + + } catch (Exception e) { + mErrorListener.onError(e); + Log.e(TAG, "S: Error", e); + } finally { + //the socket must be closed. It is not possible to reconnect to this socket + // after it is closed, which means a new socket instance has to be created. + socket.close(); + } + + } catch (Exception e) { + mErrorListener.onError(e); + Log.e(TAG, "C: Error", e); + } finally { + unregisterListeners(); + } + + } + + private void unregisterListeners() { + mMessageListener = null; + mErrorListener = null; + } + + //Declare the interface. The method messageReceived(String message) will must be implemented in the Activity + //class at on AsyncTask doInBackground + public interface OnMessageReceived { + public void messageReceived(byte[] message); + } + + public interface OnClientError { + public void onError(Exception e); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/Type.java b/app/src/main/java/com/skulixlabs/iotcontrol/Type.java new file mode 100644 index 0000000..08ec6d4 --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/Type.java @@ -0,0 +1,6 @@ +package com.skulixlabs.iotcontrol; + +public enum Type { + FEEDBACK, + STATE +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/adapter/ItemAdapter.java b/app/src/main/java/com/skulixlabs/iotcontrol/adapter/ItemAdapter.java new file mode 100644 index 0000000..a49e9cb --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/adapter/ItemAdapter.java @@ -0,0 +1,168 @@ +package com.skulixlabs.iotcontrol.adapter; + +import android.content.Context; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.dx.dxloadingbutton.lib.LoadingButton; +import com.google.android.flexbox.FlexboxLayout; +import com.skulixlabs.iotcontrol.ConnectTaskFromAdapter; +import com.skulixlabs.iotcontrol.MainFragment; +import com.skulixlabs.iotcontrol.R; +import com.skulixlabs.iotcontrol.bean.RunnableWithState; +import com.skulixlabs.iotcontrol.model.Cmd; +import com.skulixlabs.iotcontrol.model.CmdGroup; +import com.skulixlabs.iotcontrol.util.Utils; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import br.com.simplepass.loadingbutton.customViews.CircularProgressButton; + + +public class ItemAdapter extends RecyclerView.Adapter { + private List groupList; + private WeakReference fragment; + private Map stateHandlers = new HashMap<>(); + + public class ItemViewHolder extends RecyclerView.ViewHolder { + public Integer stateIndex; // Holds position (index) of state action + + public TextView title; + public TextView state; + public FlexboxLayout buttonLayout; + + public ItemViewHolder(@NonNull View itemView) { + super(itemView); + title = (TextView) itemView.findViewById(R.id.tvTitle); + state = (TextView) itemView.findViewById(R.id.tvState); + buttonLayout = (FlexboxLayout) itemView.findViewById(R.id.btnLayout); + + state.setAllCaps(false); + } + + public LoadingButton getStateButton() { + return stateIndex != null ? (LoadingButton) buttonLayout.getChildAt(stateIndex) : null; + } + + public Context getContext() { + return itemView.getContext(); + } + + private LoadingButton addButton() { + int viewId = View.generateViewId(); + LayoutInflater inflater = LayoutInflater.from(getContext()); + LoadingButton button = (LoadingButton) inflater.inflate(R.layout.material_morph_button_2, null, false); + FlexboxLayout.LayoutParams linearLayoutParams = new FlexboxLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + linearLayoutParams.setMargins(0, 0, Utils.calculatePixelsFromDp2(8, getContext()), 0); + button.setId(viewId); + buttonLayout.addView(button, linearLayoutParams); + return button; + } + } + + public ItemAdapter(MainFragment fragment, List groupList) { + this.fragment = new WeakReference<>(fragment); + this.groupList = groupList; + } + + @NonNull + @Override + public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View itemView = LayoutInflater. + from(parent.getContext()). + inflate(R.layout.row_item, parent, false); + + return new ItemViewHolder(itemView); + } + + @Override + public void onBindViewHolder(@NonNull final ItemViewHolder holder, int position) { + CmdGroup group = groupList.get(position); + holder.title.setText(group.getName()); + + for (final Cmd cmd : group.getCmdList()) { + + switch (cmd.getType()) { + case STATE: + // State actions are not shown if repeat is enabled + if (cmd.getRepeat() != null) { + break; // Else continue to next statement to create a button + } else { + holder.stateIndex = holder.buttonLayout.getChildCount(); + // And continue to next statement to create a button + } + case FEEDBACK: + final LoadingButton actionButton = holder.addButton(); + + actionButton.setText(cmd.getAction()); + actionButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + //actionButton.startMorphAnimation(); + actionButton.startLoading(); + new ConnectTaskFromAdapter(holder, fragment, cmd, actionButton).execute(); + } + }); + default: + + } + + if (cmd.getRepeat() != null) { + Handler handler = addStateListener(holder, cmd); + validateStateListener(handler); + } + } + } + + @Override + public int getItemCount() { + return groupList.size(); + } + + public List getCollection() { + return groupList; + } + + public void validateStateListeners() { + for (Map.Entry entry : stateHandlers.entrySet()) { + validateStateListener(entry.getKey()); + } + } + + public void validateStateListener(Handler handler) { + RunnableWithState runnableWithState = stateHandlers.get(handler); + runnableWithState.isRunning = true; + handler.post(runnableWithState.runnable); + } + + public void invalidateStateListeners() { + for (Map.Entry entry : stateHandlers.entrySet()) { + //entry.getKey().removeCallbacks(entry.getValue()); + entry.getValue().isRunning = false; + entry.getKey().removeCallbacksAndMessages(null); + } + } + + private Handler addStateListener(final ItemViewHolder holder, @NonNull final Cmd cmd) { + final Handler handler = new Handler(); + Runnable runnable = new Runnable() { + @Override + public void run() { + new ConnectTaskFromAdapter(holder, fragment, cmd, null).execute(); + if (stateHandlers.get(handler).isRunning) + handler.postDelayed(this, cmd.getRepeat()); + } + }; + stateHandlers.put(handler, new RunnableWithState(runnable, false)); + return handler; + } +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/bean/RunnableWithState.java b/app/src/main/java/com/skulixlabs/iotcontrol/bean/RunnableWithState.java new file mode 100644 index 0000000..804feb7 --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/bean/RunnableWithState.java @@ -0,0 +1,11 @@ +package com.skulixlabs.iotcontrol.bean; + +public class RunnableWithState { + public Runnable runnable; + public boolean isRunning; + + public RunnableWithState(Runnable runnable, boolean isRunning) { + this.runnable = runnable; + this.isRunning = isRunning; + } +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/model/Cmd.java b/app/src/main/java/com/skulixlabs/iotcontrol/model/Cmd.java new file mode 100644 index 0000000..c2685aa --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/model/Cmd.java @@ -0,0 +1,70 @@ +package com.skulixlabs.iotcontrol.model; + +import com.skulixlabs.iotcontrol.Type; +import com.skulixlabs.iotcontrol.util.jackson.CmdsToMapDeserializer; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.HashMap; + +public class Cmd { + private String action; + @JsonProperty("cmd") + private int value; + @JsonUnwrapped + private Options options; + @JsonDeserialize(using = CmdsToMapDeserializer.class) + @JsonProperty("responses") + private HashMap responseList; + private Integer repeat; + private Type type; + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + public Options getOptions() { + return options; + } + + public void setOptions(Options options) { + this.options = options; + } + + public HashMap getResponseList() { + return responseList; + } + + public void setResponseList(HashMap responseList) { + this.responseList = responseList; + } + + public Integer getRepeat() { + return repeat; + } + + public void setRepeat(Integer repeat) { + this.repeat = repeat; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/model/CmdGroup.java b/app/src/main/java/com/skulixlabs/iotcontrol/model/CmdGroup.java new file mode 100644 index 0000000..1babb57 --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/model/CmdGroup.java @@ -0,0 +1,28 @@ +package com.skulixlabs.iotcontrol.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class CmdGroup { + @JsonProperty("group") + private String name; + @JsonProperty("actions") + private List cmdList; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getCmdList() { + return cmdList; + } + + public void setCmdList(List cmdList) { + this.cmdList = cmdList; + } +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/model/CmdResponse.java b/app/src/main/java/com/skulixlabs/iotcontrol/model/CmdResponse.java new file mode 100644 index 0000000..471d13e --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/model/CmdResponse.java @@ -0,0 +1,40 @@ +package com.skulixlabs.iotcontrol.model; + +public class CmdResponse { + private byte value; + private String description; + private String color; + + public CmdResponse() { + } + + public CmdResponse(byte value, String description, String color) { + this.description = description; + this.value = value; + this.color = color; + } + + public byte getValue() { + return value; + } + + public void setValue(byte value) { + this.value = value; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/model/Options.java b/app/src/main/java/com/skulixlabs/iotcontrol/model/Options.java new file mode 100644 index 0000000..7fd3181 --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/model/Options.java @@ -0,0 +1,43 @@ +package com.skulixlabs.iotcontrol.model; + +public class Options { + private String url; + private String hmacKey; + private int hashByteLength; + private int randDataLength; + + public Options() { + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getHmacKey() { + return hmacKey; + } + + public void setHmacKey(String hmacKey) { + this.hmacKey = hmacKey; + } + + public int getHashByteLength() { + return hashByteLength; + } + + public void setHashByteLength(int hashByteLength) { + this.hashByteLength = hashByteLength; + } + + public int getRandDataLength() { + return randDataLength; + } + + public void setRandDataLength(int randDataLength) { + this.randDataLength = randDataLength; + } +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/util/AppUtils.java b/app/src/main/java/com/skulixlabs/iotcontrol/util/AppUtils.java new file mode 100644 index 0000000..8b3b7a7 --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/util/AppUtils.java @@ -0,0 +1,66 @@ +package com.skulixlabs.iotcontrol.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; + +import androidx.preference.PreferenceManager; + +import java.util.Locale; + +public class AppUtils { + public static String lang = null; + + /** + * Override {@link android.app.Activity#attachBaseContext(Context)} and pass this + * method as the Context parameter in order to change language in runtime. This + * will not work if dark mode is enabled, because the Context is rebuild by the + * Android Framework using default parameters (passed Context is ignored). + * + * @param context + * @return + */ + public static Context updateBaseContextLocale(Context context) { // The context probably always is the default one, even if previously modified + if (lang == null || "".equals(lang)) { + lang = PreferenceManager + .getDefaultSharedPreferences(context).getString("language", "system_default"); + } + + if (!lang.equals("system_default")) { + + String[] langSplit = lang.split("_"); + + Locale locale = new Locale(langSplit[0], langSplit[1]); + Locale.setDefault(locale); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return updateResourcesLocale(context, locale); + } + + return updateResourcesLocaleLegacy(context, locale); + } + return context; + } + + @TargetApi(Build.VERSION_CODES.N) + private static Context updateResourcesLocale(Context context, Locale locale) { + /*if (context.getResources().getConfiguration().getLocales().get(0).equals(locale)) // Even though locale=en + return context;*/ + Configuration configuration = context.getResources().getConfiguration(); + configuration.setLocale(locale); + return context.createConfigurationContext(configuration); + } + + @SuppressWarnings("deprecation") + private static Context updateResourcesLocaleLegacy(Context context, Locale locale) { + if (context.getResources().getConfiguration().locale.equals(locale)) + return context; + Resources resources = context.getResources(); + Configuration configuration = resources.getConfiguration(); + configuration.locale = locale; + resources.updateConfiguration(configuration, resources.getDisplayMetrics()); + return context; + } +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/util/Utils.java b/app/src/main/java/com/skulixlabs/iotcontrol/util/Utils.java new file mode 100644 index 0000000..1a7b562 --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/util/Utils.java @@ -0,0 +1,170 @@ +package com.skulixlabs.iotcontrol.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.InputType; +import android.util.TypedValue; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.preference.EditTextPreference; + +import com.skulixlabs.iotcontrol.model.Cmd; +import com.skulixlabs.iotcontrol.model.CmdResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +public class Utils { + private static final String TAG = Utils.class.getName(); + + public static byte[] generateHashWithHmac256ByByteValue(String hmacKey, byte[] data) { + try { + final String hashingAlgorithm = "HmacSHA256"; + + byte[] bytes = hmac(hashingAlgorithm, hmacKey.getBytes(), data); + + //Log.i(TAG, "message digest: " + bytes); + return bytes; + + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static byte[] generateHashWithHmac256ByValue(String hmacKey, String message) { + try { + final String hashingAlgorithm = "HmacSHA256"; + + byte[] bytes = hmac(hashingAlgorithm, hmacKey.getBytes(), message.getBytes()); + + //Log.i(TAG, "message digest: " + bytesToHex(bytes)); + return bytes; + + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static byte[] hmac(String algorithm, byte[] key, byte[] message) throws NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance(algorithm); + mac.init(new SecretKeySpec(key, algorithm)); + return mac.doFinal(message); + } + + public static String bytesToHex(byte[] bytes) { + final char[] hexArray = "0123456789abcdef".toCharArray(); + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0, v; j < bytes.length; j++) { + v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + + public static byte oneDigitHexToByte(String s) { + String[] split = s.split("x"); + int i = Integer.parseInt(split[1], 16); + return (byte) i; + } + + public static byte analyzeResponse(String hmacKey, Byte[] cmdSend, ByteBuffer gotBytes, ByteBuffer resHash) throws IOException { + gotBytes.position(0); + resHash.position(0); + + byte[] gotBytesArr = new byte[gotBytes.remaining()]; + gotBytes.get(gotBytesArr); + + for(int i=0; i deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + JsonNode node = p.getCodec().readTree(p); + + Map map = new HashMap<>(); + + for (JsonNode objectNode : node) { + byte value = oneDigitHexToByte(objectNode.get("value").asText()); + String description = objectNode.get("description").asText(); + String color = objectNode.get("color").asText(); + + map.put(value, new CmdResponse(value, description, color)); + } + + return map; + } +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/util/jackson/JsonUtils.java b/app/src/main/java/com/skulixlabs/iotcontrol/util/jackson/JsonUtils.java new file mode 100644 index 0000000..1d3ae1b --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/util/jackson/JsonUtils.java @@ -0,0 +1,53 @@ +package com.skulixlabs.iotcontrol.util.jackson; + +import android.content.Context; + +import com.skulixlabs.iotcontrol.model.CmdGroup; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import static android.content.Context.MODE_PRIVATE; + +public class JsonUtils { + public static final int REQUEST_READ_STORAGE = 7; + public static final int REQUEST_READ_FILE = 8; + + public boolean saveCommands(Context context, InputStream is) throws IOException { + String json = IOUtils.toString(is, "UTF-8"); + validateJson(json); + return context.getSharedPreferences("commands", MODE_PRIVATE).edit().putString("commands", json).commit(); + } + + public boolean saveCommands(Context context, String json) throws IOException { + validateJson(json); + return context.getSharedPreferences("commands", MODE_PRIVATE).edit().putString("commands", json).commit(); + } + + public void validateJson(String json) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.readTree(json); + } + + public List getCommands(Context context) throws IOException { + List cmdGroups = parseCommands(context.getSharedPreferences("commands", MODE_PRIVATE).getString("commands", null)); + return cmdGroups; + } + + public List parseCommands(String json) throws IOException { + if (json == null) throw new IllegalArgumentException("Content not valid"); + return new ObjectMapper() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .readValue(json, new TypeReference>(){}); + } + + + + +} diff --git a/app/src/main/java/com/skulixlabs/iotcontrol/util/legacy/TcpClient_original.java b/app/src/main/java/com/skulixlabs/iotcontrol/util/legacy/TcpClient_original.java new file mode 100644 index 0000000..1fe14e5 --- /dev/null +++ b/app/src/main/java/com/skulixlabs/iotcontrol/util/legacy/TcpClient_original.java @@ -0,0 +1,127 @@ +package com.skulixlabs.iotcontrol.util.legacy; + +import android.util.Log; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.Socket; + +public class TcpClient_original { + public static final String TAG = TcpClient_original.class.getSimpleName(); + public static final String SERVER_IP = "192.168.1.170"; //server IP address + public static final int SERVER_PORT = 3000; + + // message to send to the server + private String mServerMessage; + // sends message received notifications + private OnMessageReceived mMessageListener = null; + // while this is true, the server will continue running + private boolean mRun = false; + // used to send messages + private PrintWriter mBufferOut; + // used to read messages from the server + private BufferedReader mBufferIn; + + /** + * Constructor of the class. OnMessagedReceived listens for the messages received from server + */ + public TcpClient_original(OnMessageReceived listener) { + mMessageListener = listener; + } + + /** + * Sends the message entered by client to the server + * + * @param message text entered by client + */ + public void sendMessage(final String message) { + Runnable runnable = new Runnable() { + @Override + public void run() { + if (mBufferOut != null) { + Log.d(TAG, "Sending: " + message); + mBufferOut.println(message); + mBufferOut.flush(); + } + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } + + /** + * Close the connection and release the members + */ + public void stopClient() { + + mRun = false; + + if (mBufferOut != null) { + mBufferOut.flush(); + mBufferOut.close(); + } + + mMessageListener = null; + mBufferIn = null; + mBufferOut = null; + mServerMessage = null; + } + + public void run() { + + mRun = true; + + try { + //here you must put your computer's IP address. + InetAddress serverAddr = InetAddress.getByName(SERVER_IP); + + Log.d("TCP Client", "C: Connecting..."); + + //create a socket to make the connection with the server + Socket socket = new Socket(serverAddr, SERVER_PORT); + + try { + + //sends the message to the server + mBufferOut = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true); + + //receives the message which the server sends back + mBufferIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + + //in this while the client listens for the messages sent by the server + while (mRun) { + mServerMessage = mBufferIn.readLine(); + if (mServerMessage != null && mMessageListener != null) { + //sendAction the method messageReceived from MyActivity class + mMessageListener.messageReceived(mServerMessage); + } + } + + Log.d("RESPONSE FROM SERVER", "S: Received Message: '" + mServerMessage + "'"); + + } catch (Exception e) { + Log.e("TCP", "S: Error", e); + } finally { + //the socket must be closed. It is not possible to reconnect to this socket + // after it is closed, which means a new socket instance has to be created. + socket.close(); + } + + } catch (Exception e) { + Log.e("TCP", "C: Error", e); + } + + } + + //Declare the interface. The method messageReceived(String message) will must be implemented in the Activity + //class at on AsyncTask doInBackground + public interface OnMessageReceived { + public void messageReceived(String message); + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_check_green_48dp.xml b/app/src/main/res/drawable/ic_check_green_48dp.xml new file mode 100644 index 0000000..2326554 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_green_48dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_close_red_24dp.xml b/app/src/main/res/drawable/ic_close_red_24dp.xml new file mode 100644 index 0000000..3f4f904 --- /dev/null +++ b/app/src/main/res/drawable/ic_close_red_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_error_outline_red_48dp.xml b/app/src/main/res/drawable/ic_error_outline_red_48dp.xml new file mode 100644 index 0000000..8bd3566 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_outline_red_48dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..2971d8f --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_import_method.xml b/app/src/main/res/layout/dialog_import_method.xml new file mode 100644 index 0000000..0b07079 --- /dev/null +++ b/app/src/main/res/layout/dialog_import_method.xml @@ -0,0 +1,29 @@ + + + + +