From 5dfa37788c7f039eff00b27cc0ca8b9b9a71f60e Mon Sep 17 00:00:00 2001 From: Javier Date: Sat, 20 Mar 2021 23:57:51 +0100 Subject: Initial import --- .gitignore | 16 ++ .idea/.gitignore | 3 + .idea/.name | 1 + .idea/compiler.xml | 6 + .idea/gradle.xml | 22 ++ .idea/jarRepositories.xml | 25 ++ .idea/misc.xml | 9 + .idea/render.experimental.xml | 6 + .idea/vcs.xml | 6 + app/.gitignore | 1 + app/build.gradle | 44 ++++ app/proguard-rules.pro | 21 ++ app/src/main/AndroidManifest.xml | 25 ++ app/src/main/java/com/javispedro/rempe/Device.java | 231 ++++++++++++++++++ .../rempe/DeviceListRecyclerViewListAdapter.java | 44 ++++ .../com/javispedro/rempe/DeviceViewHolder.java | 123 ++++++++++ .../java/com/javispedro/rempe/MainActivity.java | 264 +++++++++++++++++++++ .../java/com/javispedro/rempe/Preferences.java | 43 ++++ .../main/java/com/javispedro/rempe/Reading.java | 12 + app/src/main/res/drawable/ic_baseline_add_24.xml | 10 + app/src/main/res/drawable/ic_icon.xml | 24 ++ app/src/main/res/layout/activity_main.xml | 46 ++++ app/src/main/res/layout/fragment_device.xml | 86 +++++++ app/src/main/res/menu/menu_main.xml | 14 ++ app/src/main/res/values-night/themes.xml | 16 ++ app/src/main/res/values/colors.xml | 10 + app/src/main/res/values/dimens.xml | 4 + app/src/main/res/values/strings.xml | 39 +++ app/src/main/res/values/themes.xml | 25 ++ app/src/test/java/android/util/Log.java | 23 ++ .../com/javispedro/rempe/MainActivityTest.java | 40 ++++ build.gradle | 24 ++ gradle.properties | 19 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++++++++++++ gradlew.bat | 84 +++++++ imgs/icon.svg | 91 +++++++ imgs/icon_opt.svg | 19 ++ settings.gradle | 2 + 40 files changed, 1656 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/render.experimental.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/javispedro/rempe/Device.java create mode 100644 app/src/main/java/com/javispedro/rempe/DeviceListRecyclerViewListAdapter.java create mode 100644 app/src/main/java/com/javispedro/rempe/DeviceViewHolder.java create mode 100644 app/src/main/java/com/javispedro/rempe/MainActivity.java create mode 100644 app/src/main/java/com/javispedro/rempe/Preferences.java create mode 100644 app/src/main/java/com/javispedro/rempe/Reading.java create mode 100644 app/src/main/res/drawable/ic_baseline_add_24.xml create mode 100644 app/src/main/res/drawable/ic_icon.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/fragment_device.xml create mode 100644 app/src/main/res/menu/menu_main.xml create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/test/java/android/util/Log.java create mode 100644 app/src/test/java/com/javispedro/rempe/MainActivityTest.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 imgs/icon.svg create mode 100644 imgs/icon_opt.svg create mode 100644 settings.gradle 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 @@ + + + + + + \ 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 @@ + + + + + + + \ 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 @@ + + + + + + + + + + + + + \ 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 @@ + + + + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + \ 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 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 mResultReceiver = new AntPluginPcc.IPluginAccessResultReceiver() { + @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 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 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 { + + private List 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 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 mDeviceNumbers = new ArrayList(); + private final ArrayList mDevices = new ArrayList(); + + private DeviceListRecyclerViewListAdapter mDeviceListAdapter; + + private PccReleaseHandle 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 mResultReceiver = new AntPluginPcc.IPluginAccessResultReceiver() { + @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 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 list = new ArrayList(); + 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 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 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 getDeviceNumbers(SharedPreferences prefs) { + ArrayList result = new ArrayList(); + + 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 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 @@ + + + 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 @@ + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + \ 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 @@ + + + + \ 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 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ 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 @@ + + 16dp + 16dp + \ 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 @@ + + Rempe + Remove all + Settings + + Add device... + Cannot connect to device: %1$s + Device already on list + + Device + Status + + -- + %1$s °C + + Signal: + + Disconnected + Disconnected + Searching + Tracking + Processing request + Unrecognized + Unknown state + + Success + User cancelled + Channel not available + Unknown failure + ANT+ dependencies not installed + Device already in use + Search timed out + Already subscribed + Bad parameters + Adapter not detected + Unrecognized device + Unknown failure + + \ 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 @@ + + + + + + +