diff options
author | Javier <dev.git@javispedro.com> | 2021-03-20 23:57:51 +0100 |
---|---|---|
committer | Javier <dev.git@javispedro.com> | 2021-03-20 23:58:23 +0100 |
commit | 5dfa37788c7f039eff00b27cc0ca8b9b9a71f60e (patch) | |
tree | 6aea7cd13dc6a8cbb771ec1b510a5ac3cf8200f0 | |
download | rempe-5dfa37788c7f039eff00b27cc0ca8b9b9a71f60e.tar.gz rempe-5dfa37788c7f039eff00b27cc0ca8b9b9a71f60e.zip |
Initial import
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 Binary files differnew file mode 100644 index 0000000..f6b961f --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar 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 @@ -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 |