diff options
Diffstat (limited to 'app/src')
19 files changed, 1079 insertions, 0 deletions
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 |