summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore16
-rw-r--r--.idea/.gitignore3
-rw-r--r--.idea/.name1
-rw-r--r--.idea/compiler.xml6
-rw-r--r--.idea/gradle.xml22
-rw-r--r--.idea/jarRepositories.xml25
-rw-r--r--.idea/misc.xml9
-rw-r--r--.idea/render.experimental.xml6
-rw-r--r--.idea/vcs.xml6
-rw-r--r--app/.gitignore1
-rw-r--r--app/build.gradle44
-rw-r--r--app/proguard-rules.pro21
-rw-r--r--app/src/main/AndroidManifest.xml25
-rw-r--r--app/src/main/java/com/javispedro/rempe/Device.java231
-rw-r--r--app/src/main/java/com/javispedro/rempe/DeviceListRecyclerViewListAdapter.java44
-rw-r--r--app/src/main/java/com/javispedro/rempe/DeviceViewHolder.java123
-rw-r--r--app/src/main/java/com/javispedro/rempe/MainActivity.java264
-rw-r--r--app/src/main/java/com/javispedro/rempe/Preferences.java43
-rw-r--r--app/src/main/java/com/javispedro/rempe/Reading.java12
-rw-r--r--app/src/main/res/drawable/ic_baseline_add_24.xml10
-rw-r--r--app/src/main/res/drawable/ic_icon.xml24
-rw-r--r--app/src/main/res/layout/activity_main.xml46
-rw-r--r--app/src/main/res/layout/fragment_device.xml86
-rw-r--r--app/src/main/res/menu/menu_main.xml14
-rw-r--r--app/src/main/res/values-night/themes.xml16
-rw-r--r--app/src/main/res/values/colors.xml10
-rw-r--r--app/src/main/res/values/dimens.xml4
-rw-r--r--app/src/main/res/values/strings.xml39
-rw-r--r--app/src/main/res/values/themes.xml25
-rw-r--r--app/src/test/java/android/util/Log.java23
-rw-r--r--app/src/test/java/com/javispedro/rempe/MainActivityTest.java40
-rw-r--r--build.gradle24
-rw-r--r--gradle.properties19
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 54329 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew172
-rw-r--r--gradlew.bat84
-rw-r--r--imgs/icon.svg91
-rw-r--r--imgs/icon_opt.svg19
-rw-r--r--settings.gradle2
40 files changed, 1656 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f9bf186
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+*.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
+.cxx
+local.properties
+/ANT+_Android_SDK
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..660ab29
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+Rempe \ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..61a9130
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CompilerConfiguration">
+ <bytecodeTargetLevel target="1.8" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..22ffc66
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="GradleMigrationSettings" migrationVersion="1" />
+ <component name="GradleSettings">
+ <option name="linkedExternalProjectsSettings">
+ <GradleProjectSettings>
+ <option name="testRunner" value="PLATFORM" />
+ <option name="distributionType" value="DEFAULT_WRAPPED" />
+ <option name="externalProjectPath" value="$PROJECT_DIR$" />
+ <option name="gradleJvm" value="1.8 (2)" />
+ <option name="modules">
+ <set>
+ <option value="$PROJECT_DIR$" />
+ <option value="$PROJECT_DIR$/app" />
+ </set>
+ </option>
+ <option name="resolveModulePerSourceSet" value="false" />
+ <option name="useQualifiedModuleNames" value="true" />
+ </GradleProjectSettings>
+ </option>
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..a5f05cd
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="RemoteRepositoriesConfiguration">
+ <remote-repository>
+ <option name="id" value="central" />
+ <option name="name" value="Maven Central repository" />
+ <option name="url" value="https://repo1.maven.org/maven2" />
+ </remote-repository>
+ <remote-repository>
+ <option name="id" value="jboss.community" />
+ <option name="name" value="JBoss Community repository" />
+ <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+ </remote-repository>
+ <remote-repository>
+ <option name="id" value="BintrayJCenter" />
+ <option name="name" value="BintrayJCenter" />
+ <option name="url" value="https://jcenter.bintray.com/" />
+ </remote-repository>
+ <remote-repository>
+ <option name="id" value="Google" />
+ <option name="name" value="Google" />
+ <option name="url" value="https://dl.google.com/dl/android/maven2/" />
+ </remote-repository>
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..d5d35ec
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
+ <output url="file://$PROJECT_DIR$/build/classes" />
+ </component>
+ <component name="ProjectType">
+ <option name="id" value="Android" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml
new file mode 100644
index 0000000..dde5d2b
--- /dev/null
+++ b/.idea/render.experimental.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="RenderSettings">
+ <option name="useLiveRendering" value="false" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
+ </component>
+</project> \ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..0829758
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,44 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "30.0.3"
+
+ defaultConfig {
+ applicationId "com.javispedro.rempe"
+ minSdkVersion 23
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'com.google.android.material:material:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.navigation:navigation-fragment:2.2.2'
+ implementation 'androidx.navigation:navigation-ui:2.2.2'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'androidx.preference:preference:1.1.1'
+ implementation files('../ANT+_Android_SDK/API/antpluginlib_3-8-0.aar')
+ testImplementation 'junit:junit:4.+'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+} \ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /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 \ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..72f432c
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.javispedro.rempe">
+
+ <!--android:roundIcon="@mipmap/ic_launcher_round" -->
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_icon"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.Rempe">
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.Rempe.NoActionBar">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/app/src/main/java/com/javispedro/rempe/Device.java b/app/src/main/java/com/javispedro/rempe/Device.java
new file mode 100644
index 0000000..4622eed
--- /dev/null
+++ b/app/src/main/java/com/javispedro/rempe/Device.java
@@ -0,0 +1,231 @@
+package com.javispedro.rempe;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.dsi.ant.plugins.antplus.pcc.AntPlusEnvironmentPcc;
+import com.dsi.ant.plugins.antplus.pcc.defines.DeviceState;
+import com.dsi.ant.plugins.antplus.pcc.defines.EventFlag;
+import com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult;
+import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc;
+import com.dsi.ant.plugins.antplus.pccbase.AntPlusCommonPcc;
+import com.dsi.ant.plugins.antplus.pccbase.PccReleaseHandle;
+
+import java.math.BigDecimal;
+import java.util.EnumSet;
+
+public class Device {
+ private final static String TAG = "Device";
+
+ private final int mDeviceNumber;
+
+ private PccReleaseHandle<AntPlusEnvironmentPcc> mEnvPccHandle;
+ private AntPlusEnvironmentPcc mEnvPcc;
+
+ private String mDeviceName;
+
+ private final Reading mLastReading = new Reading();
+ private int mLastRssi;
+ private RequestAccessResult mConnectResult;
+ private DeviceState mCurState;
+
+ public interface DeviceObserver {
+ void onDeviceInfoChanged();
+ void onDeviceStateChanged();
+ void onDeviceNewReading();
+ void onDeviceRssiChanged();
+ }
+ private DeviceObserver mObserver;
+
+ public Device(int deviceNumber) {
+ mDeviceNumber = deviceNumber;
+ mDeviceName = "dev-" + deviceNumber;
+ mConnectResult = RequestAccessResult.SUCCESS;
+ mCurState = DeviceState.DEAD;
+ }
+
+ public void connect(Context context) {
+ Log.d(TAG, "connect (" + mDeviceNumber + ")");
+ if (mEnvPccHandle != null) {
+ Log.w(TAG, "Already connected");
+ }
+ mConnectResult = RequestAccessResult.SUCCESS; // Clear old connect result
+ mCurState = DeviceState.SEARCHING;
+ if (mObserver != null) {
+ mObserver.onDeviceStateChanged();
+ }
+ mEnvPccHandle = AntPlusEnvironmentPcc.requestAccess(context, mDeviceNumber, 0, mResultReceiver, mDeviceStateChangeReceiver);
+ }
+
+ public void close() {
+ Log.d(TAG, "close (" + mDeviceNumber + ")");
+ if (mEnvPccHandle != null) {
+ mEnvPccHandle.close();
+ mEnvPccHandle = null;
+ }
+ if (mEnvPcc != null) {
+ mEnvPcc = null;
+ }
+ mConnectResult = RequestAccessResult.SUCCESS;
+ mCurState = DeviceState.DEAD;
+ if (mObserver != null) {
+ mObserver.onDeviceStateChanged();
+ }
+ }
+
+ public void setObserver(DeviceObserver observer) {
+ mObserver = observer;
+ }
+
+ public boolean isOpen() {
+ return mEnvPccHandle != null;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "(" + mDeviceNumber + ")";
+ }
+
+ private void setEnvPcc(AntPlusEnvironmentPcc envPcc) {
+ if (mEnvPcc != null) {
+ mEnvPcc.releaseAccess();
+ mEnvPcc = null;
+ }
+ mEnvPcc = envPcc;
+ final String deviceName = mEnvPcc.getDeviceName();
+ final int deviceNumber = mEnvPcc.getAntDeviceNumber();
+ Log.d(TAG, "handleConnection deviceName=" + deviceName + " deviceNumber=" + deviceNumber);
+ if (deviceNumber != mDeviceNumber) {
+ Log.e(TAG, "device number mismatch");
+ }
+ mDeviceName = deviceName;
+ mEnvPcc.subscribeTemperatureDataEvent(mTemperatureDataReceiver);
+ mEnvPcc.subscribeRssiEvent(mRssiReceiver);
+ if (mObserver != null) {
+ mObserver.onDeviceInfoChanged();
+ }
+ }
+
+ public String getDeviceName() {
+ return mDeviceName;
+ }
+
+ public Reading getLastReading() {
+ return mLastReading;
+ }
+
+ public int getLastRssi() {
+ return mLastRssi;
+ }
+
+ public DeviceState getCurrentDeviceState() {
+ return mCurState;
+ }
+
+ public RequestAccessResult getConnectResult() {
+ return mConnectResult;
+ }
+
+ private AntPluginPcc.IPluginAccessResultReceiver<AntPlusEnvironmentPcc> mResultReceiver = new AntPluginPcc.IPluginAccessResultReceiver<AntPlusEnvironmentPcc>() {
+ @Override
+ public void onResultReceived(AntPlusEnvironmentPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
+ Log.d(TAG, "onResultReceived resultCode=" + resultCode + " initialDeviceState=" + initialDeviceState);
+ mConnectResult = resultCode;
+ mCurState = initialDeviceState;
+ if (resultCode.equals(RequestAccessResult.SUCCESS)) {
+ setEnvPcc(result);
+ }
+ if (mObserver != null) {
+ mObserver.onDeviceStateChanged();
+ }
+
+ if (resultCode.equals(RequestAccessResult.SEARCH_TIMEOUT)) {
+ Log.d(TAG, "timeout");
+ }
+ }
+ };
+
+ private AntPluginPcc.IDeviceStateChangeReceiver mDeviceStateChangeReceiver = new AntPluginPcc.IDeviceStateChangeReceiver() {
+ @Override
+ public void onDeviceStateChange(DeviceState newDeviceState) {
+ Log.d(TAG, "onDeviceStateChange newDeviceState=" + newDeviceState);
+ mCurState = newDeviceState;
+ if (mObserver != null) {
+ mObserver.onDeviceStateChanged();
+ }
+ }
+ };
+
+ private final AntPlusEnvironmentPcc.ITemperatureDataReceiver mTemperatureDataReceiver = new AntPlusEnvironmentPcc.ITemperatureDataReceiver() {
+ @Override
+ public void onNewTemperatureData(long estTimestamp, EnumSet<EventFlag> eventFlags, BigDecimal currentTemperature, long eventCount, BigDecimal lowLast24Hours, BigDecimal highLast24Hours) {
+ Log.d(TAG, "onNewTemperatureData");
+ mLastReading.timestamp = estTimestamp;
+ mLastReading.temperature = currentTemperature;
+ mLastReading.highLast24Hours = highLast24Hours;
+ mLastReading.lowLast24Hours = lowLast24Hours;
+ if (mObserver != null) {
+ mObserver.onDeviceNewReading();
+ }
+ }
+ };
+
+ private final AntPlusCommonPcc.IRssiReceiver mRssiReceiver = new AntPlusCommonPcc.IRssiReceiver() {
+ @Override
+ public void onRssiData(long estTimestamp, EnumSet<EventFlag> eventFlags, int rssi) {
+ Log.d(TAG, "onRssiData rssi=" + rssi);
+ mLastRssi = rssi;
+ if (mObserver != null) {
+ mObserver.onDeviceRssiChanged();
+ }
+ }
+ };
+
+ public static String deviceStateToString(Context context, DeviceState state) {
+ switch (state) {
+ case DEAD:
+ return context.getString(R.string.state_dead);
+ case CLOSED:
+ return context.getString(R.string.state_closed);
+ case SEARCHING:
+ return context.getString(R.string.state_searching);
+ case TRACKING:
+ return context.getString(R.string.state_tracking);
+ case PROCESSING_REQUEST:
+ return context.getString(R.string.state_processing);
+ case UNRECOGNIZED:
+ return context.getString(R.string.state_unrecognized);
+ default:
+ return context.getString(R.string.state_unknown);
+ }
+ }
+
+ public static String connectionRequestAccessResultToString(Context context, RequestAccessResult result) {
+ switch (result) {
+ case SUCCESS:
+ return context.getString(R.string.connection_result_success);
+ case USER_CANCELLED:
+ return context.getString(R.string.connection_result_user_cancelled);
+ case CHANNEL_NOT_AVAILABLE:
+ return context.getString(R.string.connection_result_channel_not_available);
+ case OTHER_FAILURE:
+ return context.getString(R.string.connection_result_other_failure);
+ case DEPENDENCY_NOT_INSTALLED:
+ return context.getString(R.string.connection_result_dependency_not_installed);
+ case DEVICE_ALREADY_IN_USE:
+ return context.getString(R.string.connection_result_device_already_in_use);
+ case SEARCH_TIMEOUT:
+ return context.getString(R.string.connection_result_search_timeout);
+ case ALREADY_SUBSCRIBED:
+ return context.getString(R.string.connection_result_already_subscribed);
+ case BAD_PARAMS:
+ return context.getString(R.string.connection_result_bad_params);
+ case ADAPTER_NOT_DETECTED:
+ return context.getString(R.string.connection_result_adapter_not_detected);
+ case UNRECOGNIZED:
+ return context.getString(R.string.connection_result_unrecognized);
+ default:
+ return context.getString(R.string.connection_result_unknown);
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/javispedro/rempe/DeviceListRecyclerViewListAdapter.java b/app/src/main/java/com/javispedro/rempe/DeviceListRecyclerViewListAdapter.java
new file mode 100644
index 0000000..2288f20
--- /dev/null
+++ b/app/src/main/java/com/javispedro/rempe/DeviceListRecyclerViewListAdapter.java
@@ -0,0 +1,44 @@
+package com.javispedro.rempe;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+/**
+ * {@link RecyclerView.Adapter} that can display a {@link Device}.
+ */
+public class DeviceListRecyclerViewListAdapter extends RecyclerView.Adapter<DeviceViewHolder> {
+
+ private List<Device> mList;
+
+ @Override
+ @NonNull
+ public DeviceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.fragment_device, parent, false);
+ return new DeviceViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(final DeviceViewHolder holder, int position) {
+ holder.setDevice(mList.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ if (mList != null) {
+ return mList.size();
+ } else {
+ return 0;
+ }
+ }
+
+ public void setDeviceList(List<Device> list) {
+ mList = list;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/javispedro/rempe/DeviceViewHolder.java b/app/src/main/java/com/javispedro/rempe/DeviceViewHolder.java
new file mode 100644
index 0000000..9731471
--- /dev/null
+++ b/app/src/main/java/com/javispedro/rempe/DeviceViewHolder.java
@@ -0,0 +1,123 @@
+package com.javispedro.rempe;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult;
+
+import java.math.BigDecimal;
+
+public class DeviceViewHolder extends RecyclerView.ViewHolder implements Device.DeviceObserver {
+ private static final String TAG = "DeviceViewHolder";
+
+ private final View mView;
+ private final Context mContext;
+
+ private final TextView mTemperatureView;
+ private final TextView mMinTemperatureView;
+ private final TextView mMaxTemperatureView;
+ private final TextView mNameView;
+ private final TextView mStatusView;
+ private final ProgressBar mSignalBar;
+ private final TextView mSignalLabel;
+
+ private Device mDevice;
+
+ public DeviceViewHolder(View view) {
+ super(view);
+ mView = view;
+ mContext = view.getContext();
+ mNameView = view.findViewById(R.id.nameView);
+ mStatusView = view.findViewById(R.id.statusView);
+ mTemperatureView = view.findViewById(R.id.temperatureView);
+ mMinTemperatureView = view.findViewById(R.id.minTemperatureView);
+ mMaxTemperatureView = view.findViewById(R.id.maxTemperatureView);
+ mSignalBar = view.findViewById(R.id.signalBar);
+ mSignalLabel = view.findViewById(R.id.signalLabel);
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " '" + mNameView.getText() + "'";
+ }
+
+ public void setDevice(Device device) {
+ resetDisplay();
+ if (mDevice != null) {
+ mDevice.setObserver(null);
+ }
+ mDevice = device;
+ if (mDevice != null) {
+ mNameView.setText(mDevice.getDeviceName());
+ mDevice.setObserver(this);
+ }
+ }
+
+ private void runOnUiThread(Runnable r) {
+ mView.post(r);
+ }
+
+ private void resetDisplay() {
+ mTemperatureView.setText(mContext.getString(R.string.temperature_nothing));
+ mMinTemperatureView.setText(mContext.getString(R.string.temperature_nothing));
+ mMaxTemperatureView.setText(mContext.getString(R.string.temperature_nothing));
+ mNameView.setText("");
+ mStatusView.setText("");
+ mSignalBar.setProgress(0);
+ mSignalLabel.setVisibility(View.INVISIBLE);
+ }
+
+ private String formatTemperature(BigDecimal temp) {
+ final String value = temp.setScale(1, BigDecimal.ROUND_HALF_EVEN).toPlainString();
+ return mContext.getString(R.string.temperature_celsius, value);
+ }
+
+ @Override
+ public void onDeviceInfoChanged() {
+ runOnUiThread(() -> {
+ mNameView.setText(mDevice.getDeviceName());
+ });
+ }
+
+ @Override
+ public void onDeviceStateChanged() {
+ runOnUiThread(() -> {
+ if (mDevice.getConnectResult() != RequestAccessResult.SUCCESS) {
+ mStatusView.setText(Device.connectionRequestAccessResultToString(mContext, mDevice.getConnectResult()));
+ } else {
+ mStatusView.setText(Device.deviceStateToString(mContext, mDevice.getCurrentDeviceState()));
+ }
+ });
+ }
+
+ @Override
+ public void onDeviceNewReading() {
+ runOnUiThread(() -> {
+ final Reading reading = mDevice.getLastReading();
+ mTemperatureView.setText(formatTemperature(reading.temperature));
+ mMinTemperatureView.setText(formatTemperature(reading.lowLast24Hours));
+ mMaxTemperatureView.setText(formatTemperature(reading.highLast24Hours));
+ });
+ }
+
+ private int rssiToMeterValue(float rssi) {
+ final float minThreshold = -100;
+ final float maxThreshold = 0;
+
+ final float step = (maxThreshold - minThreshold) / 100;
+
+ return Math.round((rssi - minThreshold) / step);
+ }
+
+ @Override
+ public void onDeviceRssiChanged() {
+ runOnUiThread(() -> {
+ mSignalBar.setProgress(rssiToMeterValue(mDevice.getLastRssi()));
+ mSignalLabel.setVisibility(View.VISIBLE);
+ });
+ }
+}
diff --git a/app/src/main/java/com/javispedro/rempe/MainActivity.java b/app/src/main/java/com/javispedro/rempe/MainActivity.java
new file mode 100644
index 0000000..e458468
--- /dev/null
+++ b/app/src/main/java/com/javispedro/rempe/MainActivity.java
@@ -0,0 +1,264 @@
+package com.javispedro.rempe;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.preference.PreferenceManager;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.ListUpdateCallback;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.dsi.ant.plugins.antplus.pcc.AntPlusEnvironmentPcc;
+import com.dsi.ant.plugins.antplus.pcc.defines.DeviceState;
+import com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult;
+import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc;
+import com.dsi.ant.plugins.antplus.pccbase.PccReleaseHandle;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.android.material.snackbar.Snackbar;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MainActivity extends AppCompatActivity {
+ private final static String TAG = "MainActivity";
+
+ private SharedPreferences mPrefs = null;
+ private SharedPreferences.OnSharedPreferenceChangeListener mPrefsListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ switch (key) {
+ case Preferences.PREFS_DEVICES:
+ refreshDevices();
+ break;
+ }
+ }
+ };
+
+ private final ArrayList<Integer> mDeviceNumbers = new ArrayList<Integer>();
+ private final ArrayList<Device> mDevices = new ArrayList<Device>();
+
+ private DeviceListRecyclerViewListAdapter mDeviceListAdapter;
+
+ private PccReleaseHandle<AntPlusEnvironmentPcc> mPccSearchHandle;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ setContentView(R.layout.activity_main);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ FloatingActionButton fab = findViewById(R.id.fabAddDevice);
+ fab.setOnClickListener(view -> onConnectButtonClicked());
+
+ RecyclerView list = findViewById(R.id.list);
+ list.setLayoutManager(new LinearLayoutManager(list.getContext()));
+ mDeviceListAdapter = new DeviceListRecyclerViewListAdapter();
+ list.setAdapter(mDeviceListAdapter);
+
+ refreshDevices();
+ }
+
+ @Override
+ protected void onDestroy() {
+ disconnectAll();
+ mPrefs = null;
+ mDeviceListAdapter = null;
+ super.onDestroy();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mPrefs.registerOnSharedPreferenceChangeListener(mPrefsListener);
+ connectToDevices();
+ }
+
+ @Override
+ public void onPause() {
+ disconnectAll();
+ mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefsListener);
+ super.onPause();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ @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();
+
+ switch (id) {
+ case R.id.action_remove_all:
+ removeAllDevices();
+ return true;
+ case R.id.action_settings:
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void onConnectButtonClicked() {
+ searchForNewDevice();
+ }
+
+ public void searchForNewDevice() {
+ Log.d(TAG, "searchForNewDevice");
+ if (mPccSearchHandle != null) {
+ mPccSearchHandle.close();
+ mPccSearchHandle = null;
+ }
+ mPccSearchHandle = AntPlusEnvironmentPcc.requestAccess(this, this, mResultReceiver, mDeviceStateChangeReceiver);
+ }
+
+ private AntPluginPcc.IPluginAccessResultReceiver<AntPlusEnvironmentPcc> mResultReceiver = new AntPluginPcc.IPluginAccessResultReceiver<AntPlusEnvironmentPcc>() {
+ @Override
+ public void onResultReceived(AntPlusEnvironmentPcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) {
+ Log.d(TAG, "onResultReceived resultCode=" + resultCode);
+ if (resultCode == RequestAccessResult.SUCCESS) {
+ int deviceNumber = result.getAntDeviceNumber();
+ result.releaseAccess();
+ runOnUiThread(() -> addDevice(result.getAntDeviceNumber()));
+ } else if (resultCode != RequestAccessResult.USER_CANCELLED) {
+ runOnUiThread(() -> {
+ final String resultText = Device.connectionRequestAccessResultToString(MainActivity.this, resultCode);
+ Snackbar.make(findViewById(R.id.fabAddDevice),
+ getString(R.string.add_device_failed, resultText), Snackbar.LENGTH_INDEFINITE).show();
+ });
+ }
+ mPccSearchHandle.close();
+ mPccSearchHandle = null;
+ }
+ };
+
+ private AntPluginPcc.IDeviceStateChangeReceiver mDeviceStateChangeReceiver = new AntPluginPcc.IDeviceStateChangeReceiver() {
+ @Override
+ public void onDeviceStateChange(DeviceState newDeviceState) {
+ Log.d(TAG, "onDeviceStateChange newDeviceState=" + newDeviceState);
+ }
+ };
+
+ public void addDevice(int deviceNumber) {
+ Log.d(TAG, "addDevice " + deviceNumber);
+ List<Integer> list = Preferences.getDeviceNumbers(mPrefs);
+ if (list.contains(deviceNumber)) {
+ Snackbar.make(findViewById(R.id.fabAddDevice),
+ getString(R.string.add_device_already), Snackbar.LENGTH_INDEFINITE).show();
+ return;
+ }
+ list.add(deviceNumber);
+ Preferences.saveDeviceNumbers(mPrefs, list);
+ }
+
+ public void removeAllDevices() {
+ List<Integer> list = new ArrayList<Integer>();
+ Preferences.saveDeviceNumbers(mPrefs, list);
+ }
+
+ public void refreshDevices() {
+ Log.d(TAG, "refreshDevices");
+
+ setDeviceNumberList(Preferences.getDeviceNumbers(mPrefs));
+ }
+
+ public void connectToDevices() {
+ Log.d(TAG, "connectToDevices");
+ for (Device dev : mDevices) {
+ if (!dev.isOpen()) {
+ dev.connect(this);
+ }
+ }
+ }
+
+ public void disconnectAll() {
+ Log.d(TAG, "disconnectAll");
+ for (Device dev : mDevices) {
+ dev.close();
+ }
+ }
+
+ void setDeviceNumberList(List<Integer> newDeviceNumbers) {
+ DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new DiffUtil.Callback() {
+ @Override
+ public int getOldListSize() {
+ return mDeviceNumbers.size();
+ }
+
+ @Override
+ public int getNewListSize() {
+ return newDeviceNumbers.size();
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+ return mDeviceNumbers.get(oldItemPosition).equals(newDeviceNumbers.get(newItemPosition));
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+ return areItemsTheSame(oldItemPosition, newItemPosition);
+ }
+ });
+
+ diff.dispatchUpdatesTo(new ListUpdateCallback() {
+ @Override
+ public void onInserted(int position, int count) {
+ for (int i = 0; i < count; ++i) {
+ final int deviceNumber = newDeviceNumbers.get(position + i);
+ Device device = new Device(deviceNumber);
+ mDeviceNumbers.add(position + i, deviceNumber);
+ mDevices.add(position + i, device);
+ }
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ for (int i = 0; i < count; ++i) {
+ mDevices.get(position + i).close();
+ }
+ mDevices.subList(position, position + count).clear();
+ mDeviceNumbers.subList(position, position + count).clear();
+ }
+
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ mDevices.set(toPosition, mDevices.get(fromPosition));
+ mDevices.set(fromPosition, null);
+ mDeviceNumbers.set(toPosition, mDeviceNumbers.get(fromPosition));
+ mDeviceNumbers.set(fromPosition, 0);
+ }
+
+ @Override
+ public void onChanged(int position, int count, @Nullable Object payload) {
+ // Nothing to be done
+ }
+ });
+
+ if (mDeviceListAdapter != null) {
+ mDeviceListAdapter.setDeviceList(mDevices);
+ diff.dispatchUpdatesTo(mDeviceListAdapter);
+ }
+ }
+
+ public List<Integer> getDeviceNumberList() {
+ return mDeviceNumbers;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/javispedro/rempe/Preferences.java b/app/src/main/java/com/javispedro/rempe/Preferences.java
new file mode 100644
index 0000000..41c943c
--- /dev/null
+++ b/app/src/main/java/com/javispedro/rempe/Preferences.java
@@ -0,0 +1,43 @@
+package com.javispedro.rempe;
+
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import static android.content.ContentValues.TAG;
+
+class Preferences {
+ public final static String PREFS_DEVICES = "devices";
+
+ public static List<Integer> getDeviceNumbers(SharedPreferences prefs) {
+ ArrayList<Integer> result = new ArrayList<Integer>();
+
+ try {
+ StringTokenizer st = new StringTokenizer(prefs.getString(PREFS_DEVICES, ""), ",");
+ while (st.hasMoreTokens()) {
+ result.add(Integer.parseInt(st.nextToken()));
+ }
+ } catch (java.lang.Exception ex) {
+ // Ensure that at least we can recover from a corrupted preferences situation...
+ Log.e(TAG, ex.toString());
+ }
+
+ return result;
+ }
+
+ public static void saveDeviceNumbers(SharedPreferences prefs, List<Integer> list) {
+ StringBuilder sb = new StringBuilder();
+ for (Integer i : list) {
+ sb.append(i.toString());
+ sb.append(",");
+ }
+ final String pref = sb.toString();
+ Log.d(TAG, "saveDeviceNumbers: " + pref);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(PREFS_DEVICES, pref);
+ editor.apply();
+ }
+}
diff --git a/app/src/main/java/com/javispedro/rempe/Reading.java b/app/src/main/java/com/javispedro/rempe/Reading.java
new file mode 100644
index 0000000..a49e29a
--- /dev/null
+++ b/app/src/main/java/com/javispedro/rempe/Reading.java
@@ -0,0 +1,12 @@
+package com.javispedro.rempe;
+
+import java.math.BigDecimal;
+
+public class Reading {
+ public long timestamp;
+
+ public BigDecimal temperature;
+
+ public BigDecimal lowLast24Hours;
+ public BigDecimal highLast24Hours;
+}
diff --git a/app/src/main/res/drawable/ic_baseline_add_24.xml b/app/src/main/res/drawable/ic_baseline_add_24.xml
new file mode 100644
index 0000000..eb23254
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_add_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_icon.xml b/app/src/main/res/drawable/ic_icon.xml
new file mode 100644
index 0000000..b9624de
--- /dev/null
+++ b/app/src/main/res/drawable/ic_icon.xml
@@ -0,0 +1,24 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:width="24dp"
+ android:height="24dp">
+ <path
+ android:pathData="M12 22c-5.52 0 -10 -4.48 -10 -10 0 -2.85 1.2 -5.41 3.11 -7.24l1.42 1.42c-1.55 1.46 -2.53 3.53 -2.53 5.82 0 4.41 3.59 8 8 8s8 -3.59 8 -8c0 -2.29 -0.98 -4.36 -2.53 -5.82l1.42 -1.42c1.91 1.83 3.11 4.39 3.11 7.24 0 5.52 -4.48 10 -10 10z"
+ android:fillColor="#000000" />
+ <path
+ android:pathData="M12 18c-3.31 0 -6 -2.69 -6 -6 0 -1.74 0.75 -3.31 1.94 -4.4l1.42 1.42c-0.83 0.73 -1.36 1.79 -1.36 2.98 0 2.21 1.79 4 4 4s4 -1.79 4 -4c0 -1.19 -0.53 -2.25 -1.36 -2.98l1.42 -1.42c1.19 1.09 1.94 2.66 1.94 4.4 0 3.31 -2.69 6 -6 6z"
+ android:fillColor="#000000" />
+ <path
+ android:pathData="M14 12c0 -0.74 -0.4 -1.38 -1 -1.72v-8.28h-2v8.28c-0.6 0.35 -1 0.98 -1 1.72 0 1.1 0.9 2 2 2s2 -0.9 2 -2z"
+ android:fillColor="#000000" />
+ <path
+ android:pathData="M13.1925 12A1.1925 1.1785 0 0 1 10.8075 12A1.1925 1.1785 0 0 1 13.1925 12Z"
+ android:fillColor="#FFFFFF" />
+ <path
+ android:pathData="M11.555 4.9574H12.44677V11.054H11.555V4.9574Z"
+ android:fillColor="#FFFFFF" />
+ <path
+ android:pathData="M13 2.1074A1 0.71138 0 0 1 11 2.1074A1 0.71138 0 0 1 13 2.1074Z"
+ android:fillColor="#000000" />
+</vector> \ No newline at end of file
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..fe4da78
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list"
+ android:name="com.javispedro.rempe.DeviceFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ app:layoutManager="LinearLayoutManager"
+ tools:context=".DeviceFragment"
+ tools:listitem="@layout/fragment_device">
+
+ </androidx.recyclerview.widget.RecyclerView>
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:theme="@style/Theme.Rempe.AppBarOverlay">
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ app:popupTheme="@style/Theme.Rempe.PopupOverlay" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/fabAddDevice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ android:contentDescription="@string/fab_add_device"
+ app:srcCompat="@drawable/ic_baseline_add_24" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_device.xml b/app/src/main/res/layout/fragment_device.xml
new file mode 100644
index 0000000..0a39ba4
--- /dev/null
+++ b/app/src/main/res/layout/fragment_device.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/nameView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/device_placeholder_name"
+ android:textAppearance="@style/TextAppearance.AppCompat.Large"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/statusView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/device_placeholder_status"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/nameView" />
+
+ <TextView
+ android:id="@+id/temperatureView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+
+ android:text="@string/temperature_nothing"
+ android:textAppearance="@style/TextAppearance.AppCompat.Large"
+ android:textSize="72sp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <TextView
+ android:id="@+id/maxTemperatureView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+
+ android:text="@string/temperature_nothing"
+ android:textAppearance="@style/TextAppearance.AppCompat.Large"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/temperatureView" />
+
+ <View
+ android:id="@+id/minmaxTemperatureSeparator"
+ android:layout_width="1dp"
+ android:layout_height="0dp"
+ android:layout_marginEnd="8dp"
+ android:background="?android:attr/dividerVertical"
+ app:layout_constraintBottom_toBottomOf="@+id/maxTemperatureView"
+ app:layout_constraintEnd_toStartOf="@+id/maxTemperatureView"
+ app:layout_constraintTop_toTopOf="@+id/maxTemperatureView" />
+
+ <TextView
+ android:id="@+id/minTemperatureView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+
+ android:layout_marginEnd="8dp"
+ android:text="@string/temperature_nothing"
+ android:textAppearance="@style/TextAppearance.AppCompat.Large"
+ app:layout_constraintEnd_toStartOf="@+id/minmaxTemperatureSeparator"
+ app:layout_constraintTop_toTopOf="@+id/maxTemperatureView" />
+
+ <ProgressBar
+ android:id="@+id/signalBar"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/maxTemperatureView" />
+
+ <TextView
+ android:id="@+id/signalLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/signal_level"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toTopOf="@+id/signalBar"
+ app:layout_constraintStart_toStartOf="@+id/signalBar" />
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..5320159
--- /dev/null
+++ b/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,14 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context="com.javispedro.rempe.MainActivity">
+ <item
+ android:id="@+id/action_remove_all"
+ android:orderInCategory="100"
+ android:title="Remove all" />
+ <item
+ android:id="@+id/action_settings"
+ android:orderInCategory="101"
+ android:title="@string/action_settings"
+ app:showAsAction="never" />
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..017308e
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Theme.Rempe" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+ <!-- Primary brand color. -->
+ <item name="colorPrimary">@color/purple_200</item>
+ <item name="colorPrimaryVariant">@color/purple_700</item>
+ <item name="colorOnPrimary">@color/black</item>
+ <!-- Secondary brand color. -->
+ <item name="colorSecondary">@color/teal_200</item>
+ <item name="colorSecondaryVariant">@color/teal_200</item>
+ <item name="colorOnSecondary">@color/black</item>
+ <!-- Status bar color. -->
+ <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+ <!-- Customize your theme here. -->
+ </style>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="purple_200">#FFBB86FC</color>
+ <color name="purple_500">#FF6200EE</color>
+ <color name="purple_700">#FF3700B3</color>
+ <color name="teal_200">#FF03DAC5</color>
+ <color name="teal_700">#FF018786</color>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..cf00d3f
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,4 @@
+<resources>
+ <dimen name="fab_margin">16dp</dimen>
+ <dimen name="text_margin">16dp</dimen>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..97e603d
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,39 @@
+<resources>
+ <string name="app_name">Rempe</string>
+ <string name="action_remove_all">Remove all</string>
+ <string name="action_settings">Settings</string>
+
+ <string name="fab_add_device">Add device...</string>
+ <string name="add_device_failed">Cannot connect to device: %1$s</string>
+ <string name="add_device_already">Device already on list</string>
+
+ <string name="device_placeholder_name">Device</string>
+ <string name="device_placeholder_status">Status</string>
+
+ <string name="temperature_nothing">--</string>
+ <string name="temperature_celsius">%1$s °C</string>
+
+ <string name="signal_level">Signal:</string>
+
+ <string name="state_dead">Disconnected</string>
+ <string name="state_closed">Disconnected</string>
+ <string name="state_searching">Searching</string>
+ <string name="state_tracking">Tracking</string>
+ <string name="state_processing">Processing request</string>
+ <string name="state_unrecognized">Unrecognized</string>
+ <string name="state_unknown">Unknown state</string>
+
+ <string name="connection_result_success">Success</string>
+ <string name="connection_result_user_cancelled">User cancelled</string>
+ <string name="connection_result_channel_not_available">Channel not available</string>
+ <string name="connection_result_other_failure">Unknown failure</string>
+ <string name="connection_result_dependency_not_installed">ANT+ dependencies not installed</string>
+ <string name="connection_result_device_already_in_use">Device already in use</string>
+ <string name="connection_result_search_timeout">Search timed out</string>
+ <string name="connection_result_already_subscribed">Already subscribed</string>
+ <string name="connection_result_bad_params">Bad parameters</string>
+ <string name="connection_result_adapter_not_detected">Adapter not detected</string>
+ <string name="connection_result_unrecognized">Unrecognized device</string>
+ <string name="connection_result_unknown">Unknown failure</string>
+
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..e59570a
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,25 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Theme.Rempe" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+ <!-- Primary brand color. -->
+ <item name="colorPrimary">@color/purple_500</item>
+ <item name="colorPrimaryVariant">@color/purple_700</item>
+ <item name="colorOnPrimary">@color/white</item>
+ <!-- Secondary brand color. -->
+ <item name="colorSecondary">@color/teal_200</item>
+ <item name="colorSecondaryVariant">@color/teal_700</item>
+ <item name="colorOnSecondary">@color/black</item>
+ <!-- Status bar color. -->
+ <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+ <!-- Customize your theme here. -->
+ </style>
+
+ <style name="Theme.Rempe.NoActionBar">
+ <item name="windowActionBar">false</item>
+ <item name="windowNoTitle">true</item>
+ </style>
+
+ <style name="Theme.Rempe.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
+
+ <style name="Theme.Rempe.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
+</resources> \ No newline at end of file
diff --git a/app/src/test/java/android/util/Log.java b/app/src/test/java/android/util/Log.java
new file mode 100644
index 0000000..e117d5d
--- /dev/null
+++ b/app/src/test/java/android/util/Log.java
@@ -0,0 +1,23 @@
+package android.util;
+
+public class Log {
+ public static int d(String tag, String msg) {
+ System.err.println("DEBUG: " + tag + ": " + msg);
+ return 0;
+ }
+
+ public static int i(String tag, String msg) {
+ System.err.println("INFO: " + tag + ": " + msg);
+ return 0;
+ }
+
+ public static int w(String tag, String msg) {
+ System.err.println("WARN: " + tag + ": " + msg);
+ return 0;
+ }
+
+ public static int e(String tag, String msg) {
+ System.err.println("ERROR: " + tag + ": " + msg);
+ return 0;
+ }
+}
diff --git a/app/src/test/java/com/javispedro/rempe/MainActivityTest.java b/app/src/test/java/com/javispedro/rempe/MainActivityTest.java
new file mode 100644
index 0000000..fe0f4c5
--- /dev/null
+++ b/app/src/test/java/com/javispedro/rempe/MainActivityTest.java
@@ -0,0 +1,40 @@
+package com.javispedro.rempe;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+import static org.junit.Assert.assertEquals;
+
+public class MainActivityTest {
+ private final static String TAG = "MainActivityTest";
+
+ @Test
+ public void setDeviceNumberList() {
+ final MainActivity activity = new MainActivity();
+ final Random r = new Random();
+
+ ArrayList<Integer> list = new ArrayList<Integer>();
+ activity.setDeviceNumberList(list);
+ assertEquals(list, activity.getDeviceNumberList());
+
+ list.add(r.nextInt(30000));
+ activity.setDeviceNumberList(list);
+ assertEquals(list, activity.getDeviceNumberList());
+
+ for (int i = 0; i < 400; ++i) {
+ list.add(r.nextInt(list.size()), r.nextInt(60000));
+ activity.setDeviceNumberList(list);
+ assertEquals(list, activity.getDeviceNumberList());
+
+ list.add(r.nextInt(list.size()), r.nextInt(60000));
+ activity.setDeviceNumberList(list);
+ assertEquals(list, activity.getDeviceNumberList());
+
+ list.remove(r.nextInt(list.size() - 1));
+ activity.setDeviceNumberList(list);
+ assertEquals(list, activity.getDeviceNumberList());
+ }
+ }
+} \ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..0363d65
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,24 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.3'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+} \ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..52f5917
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,19 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true \ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..bf7b8f8
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Mar 05 22:57:23 CET 2021
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/imgs/icon.svg b/imgs/icon.svg
new file mode 100644
index 0000000..60474a9
--- /dev/null
+++ b/imgs/icon.svg
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 24 24"
+ viewBox="0 0 24 24"
+ fill="black"
+ width="24px"
+ height="24px"
+ version="1.1"
+ id="svg70"
+ sodipodi:docname="icon.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata76">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs74" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1851"
+ inkscape:window-height="1016"
+ id="namedview72"
+ showgrid="false"
+ inkscape:snap-object-midpoints="true"
+ inkscape:zoom="34.125"
+ inkscape:cx="12"
+ inkscape:cy="11.090282"
+ inkscape:window-x="69"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg70" />
+ <g
+ id="g64">
+ <path
+ d="M0,0h24v24H0V0z"
+ fill="none"
+ id="path62" />
+ </g>
+ <path
+ d="M 12,22 C 6.48,22 2,17.52 2,12 2,9.15 3.2,6.59 5.11,4.76 L 6.53,6.18 C 4.98,7.64 4,9.71 4,12 c 0,4.41 3.59,8 8,8 4.41,0 8,-3.59 8,-8 C 20,9.71 19.02,7.64 17.47,6.18 L 18.89,4.76 C 20.8,6.59 22,9.15 22,12 22,17.52 17.52,22 12,22 Z"
+ id="path912" />
+ <path
+ d="M 12,18 C 8.69,18 6,15.31 6,12 6,10.26 6.75,8.69 7.94,7.6 L 9.36,9.02 C 8.53,9.75 8,10.81 8,12 c 0,2.21 1.79,4 4,4 2.21,0 4,-1.79 4,-4 0,-1.19 -0.53,-2.25 -1.36,-2.98 L 16.06,7.6 C 17.25,8.69 18,10.26 18,12 c 0,3.31 -2.69,6 -6,6 z"
+ id="path910" />
+ <path
+ d="m 14,12 c 0,-0.74 -0.4,-1.38 -1,-1.72 V 2 h -2 v 8.28 c -0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2 1.1,0 2,-0.9 2,-2 z"
+ id="path66" />
+ <ellipse
+ style="fill:#ffffff;stroke:none;stroke-width:1.1239"
+ id="path914"
+ cx="12"
+ cy="12"
+ rx="1.1925006"
+ ry="1.1785399" />
+ <rect
+ style="fill:#ffffff;stroke:none;stroke-width:0.880395"
+ id="rect916"
+ width="0.89177328"
+ height="6.0965505"
+ x="11.554827"
+ y="4.9574375" />
+ <ellipse
+ style="fill:#000000;stroke:none;stroke-width:1.71481"
+ id="path918-7"
+ cx="12"
+ cy="2.1073804"
+ rx="1"
+ ry="0.71138048" />
+</svg>
diff --git a/imgs/icon_opt.svg b/imgs/icon_opt.svg
new file mode 100644
index 0000000..6de59ee
--- /dev/null
+++ b/imgs/icon_opt.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" enable-background="new 0 0 24 24" fill="black" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <metadata>
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <dc:title/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <path d="M0,0h24v24H0V0z" fill="none"/>
+ <path d="m12 22c-5.52 0-10-4.48-10-10 0-2.85 1.2-5.41 3.11-7.24l1.42 1.42c-1.55 1.46-2.53 3.53-2.53 5.82 0 4.41 3.59 8 8 8s8-3.59 8-8c0-2.29-0.98-4.36-2.53-5.82l1.42-1.42c1.91 1.83 3.11 4.39 3.11 7.24 0 5.52-4.48 10-10 10z"/>
+ <path d="m12 18c-3.31 0-6-2.69-6-6 0-1.74 0.75-3.31 1.94-4.4l1.42 1.42c-0.83 0.73-1.36 1.79-1.36 2.98 0 2.21 1.79 4 4 4s4-1.79 4-4c0-1.19-0.53-2.25-1.36-2.98l1.42-1.42c1.19 1.09 1.94 2.66 1.94 4.4 0 3.31-2.69 6-6 6z"/>
+ <path d="m14 12c0-0.74-0.4-1.38-1-1.72v-8.28h-2v8.28c-0.6 0.35-1 0.98-1 1.72 0 1.1 0.9 2 2 2s2-0.9 2-2z"/>
+ <ellipse cx="12" cy="12" rx="1.1925" ry="1.1785" fill="#fff"/>
+ <rect x="11.555" y="4.9574" width=".89177" height="6.0966" fill="#fff"/>
+ <ellipse cx="12" cy="2.1074" rx="1" ry=".71138" fill="#000"/>
+</svg>
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..c6a6a6c
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name = "Rempe" \ No newline at end of file