summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-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
22 files changed, 1145 insertions, 0 deletions
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