diff --git a/app/build.gradle b/app/build.gradle index 1ae0cc7..bc4a365 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,9 +82,9 @@ android { defaultConfig { applicationId "eu.pkgsoftware.babybuddywidgets" minSdkVersion 24 - targetSdkVersion 33 - versionCode 38 - versionName "2.3.3" + targetSdk 34 + versionCode 40 + versionName "2.4.0b" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -105,28 +105,34 @@ android { namespace 'eu.pkgsoftware.babybuddywidgets' } -ext.camerax_version = "1.3.1" +ext.camerax_version = "1.3.4" dependencies { implementation 'com.squareup.phrase:phrase:1.2.0' + // Retrofit and jackson need to be compatible. Maybe(?) the matching the major versions + // is enough, but I'm not sure. This version works. implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-jackson:2.9.0' - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.11.0' + implementation 'com.fasterxml.jackson.core:jackson-core:2.10.1' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.10.1' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.9.10' + + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7' implementation 'androidx.navigation:navigation-ui-ktx:2.7.7' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.concurrent:concurrent-futures:1.1.0' + implementation 'androidx.concurrent:concurrent-futures:1.2.0' implementation "androidx.camera:camera-core:$camerax_version" implementation "androidx.camera:camera-camera2:$camerax_version" implementation "androidx.camera:camera-lifecycle:$camerax_version" implementation "androidx.camera:camera-view:$camerax_version" - implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.core:core-ktx:1.13.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" @@ -136,9 +142,9 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" testImplementation 'org.json:json:20230618' - androidTestImplementation 'androidx.test:core-ktx:1.5.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test:core-ktx:1.6.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' androidTestImplementation 'tools.fastlane:screengrab:2.1.1' implementation project(':zxing-cpp') diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/ActivityStorage.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/ActivityStorage.kt new file mode 100644 index 0000000..52ab5e7 --- /dev/null +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/ActivityStorage.kt @@ -0,0 +1,97 @@ +package eu.pkgsoftware.babybuddywidgets + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import eu.pkgsoftware.babybuddywidgets.debugging.GlobalDebugObject +import java.io.IOException + +class ActivityDatabaseV1(context: Context) : SQLiteOpenHelper( + context, "store", null, 1 +) { + override fun onCreate(db: SQLiteDatabase) { + db.execSQL("create table global_kv (key_name text primary key, value text)") + db.execSQL("create table login_kv (key_name text primary key, value text)") + db.execSQL("create table child_kv (child INTEGER, key_name text, value text, primary key (child, key_name))") + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + TODO("Not yet implemented") + } +} + +class ActivityStore(context: Context) { + val openHelper = ActivityDatabaseV1(context) + val database = openHelper.writableDatabase + val jackOM = jacksonObjectMapper() + + inline fun genSet(table: String, value: K?, selectors: Map) { + if (value == null) { + val query = "delete from $table where " + selectors.keys.joinToString(" and ") { "$it = ?" } + val args = selectors.values.toTypedArray() + database.execSQL(query, args) + } else { + val query = "insert or replace into $table (" + + selectors.keys.joinToString(", ") + ", value) values (" + + selectors.keys.joinToString(", ") { "?" } + ", ?)" + val valueString = jackOM.writeValueAsString(value) + database.execSQL(query, selectors.values.toTypedArray() + valueString) + } + } + + inline fun genGet(table: String, selectors: Map): K? { + val query = "select value from $table where " + selectors.keys.joinToString(" and ") { "$it = ?" } + val cursor = database.rawQuery(query, selectors.values.toTypedArray()) + var value = "" + try { + if (cursor.moveToNext()) { + value = cursor.getString(0) + cursor.close() + return jackOM.readValue(value, K::class.java) + } + } + catch (e: IOException) { + GlobalDebugObject.log("Failed to deserialize value from table $table: '$value'") + } + finally { + cursor.close() + } + return null + } + + inline fun globals(key: String): K? { + return genGet("global_kv", mapOf("key_name" to key)) + } + + inline fun globals(key: String, value: K) { + genSet("global_kv", value, mapOf("key_name" to key)) + } + + inline fun login(key: String): K? { + return genGet("login_kv", mapOf("key_name" to key)) + } + + inline fun login(key: String, value: K) { + genSet("login_kv", value, mapOf("key_name" to key)) + } + + inline fun child(child: Int, key: String): K? { + return genGet("child_kv", mapOf("child" to child.toString(), "key_name" to key)) + } + + inline fun child(child: Int, key: String, value: K) { + genSet("child_kv", value, mapOf("child" to child.toString(), "key_name" to key)) + } + + fun close() { + database.close() + openHelper.close() + } + + fun deleteAllData() { + database.delete("global_kv", "1", arrayOf()) + database.delete("login_kv", "1", arrayOf()) + database.delete("child_kv", "1", arrayOf()) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/BabyLayoutHolder.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/BabyLayoutHolder.java index 9afa879..7d86f55 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/BabyLayoutHolder.java +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/BabyLayoutHolder.java @@ -6,50 +6,45 @@ import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import eu.pkgsoftware.babybuddywidgets.activitycomponents.TimerControl; +import eu.pkgsoftware.babybuddywidgets.timers.FragmentCallbacks; +import eu.pkgsoftware.babybuddywidgets.timers.LoggingButtonController; import eu.pkgsoftware.babybuddywidgets.databinding.BabyManagerBinding; -import eu.pkgsoftware.babybuddywidgets.databinding.NotesEditorBinding; import eu.pkgsoftware.babybuddywidgets.history.ChildEventHistoryLoader; -import eu.pkgsoftware.babybuddywidgets.history.ShowErrorPill; import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient; import eu.pkgsoftware.babybuddywidgets.networking.ChildrenStateTracker; -import eu.pkgsoftware.babybuddywidgets.timers.EmptyTimerListProvider; -import eu.pkgsoftware.babybuddywidgets.timers.StoreActivityRouter; +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.TimeEntry; import eu.pkgsoftware.babybuddywidgets.timers.TimerControlInterface; -import eu.pkgsoftware.babybuddywidgets.timers.TimerListProvider; import eu.pkgsoftware.babybuddywidgets.timers.TimersUpdatedCallback; import eu.pkgsoftware.babybuddywidgets.timers.TranslatedException; import eu.pkgsoftware.babybuddywidgets.utils.Promise; -import eu.pkgsoftware.babybuddywidgets.widgets.SwitchButtonLogic; public class BabyLayoutHolder extends RecyclerView.ViewHolder implements TimerControlInterface { private final BabyManagerBinding binding; private final BaseFragment baseFragment; private final BabyBuddyClient client; - private TimerListProvider timerListProvider = null; - private BabyBuddyClient.Child child = null; - private boolean changeWet = false; - private boolean changeSolid = false; + private BabyBuddyClient.Child child = null; - private NotesEditorLogic notesEditor; - private SwitchButtonLogic notesSwitch; private ChildEventHistoryLoader childHistoryLoader = null; private ChildrenStateTracker.ChildObserver childObserver = null; - private StoreActivityRouter storeActivityRouter; private BabyBuddyClient.Timer[] cachedTimers = null; - private TimersUpdatedCallback updateTimersCallback = null; + private List updateTimersCallbacks = new ArrayList<>(10); private int pendingTimerModificationCalls = 0; + private LoggingButtonController loggingButtonController = null; + public BabyLayoutHolder(BaseFragment fragment, BabyManagerBinding bmb) { super(bmb.getRoot()); binding = bmb; @@ -57,40 +52,6 @@ public BabyLayoutHolder(BaseFragment fragment, BabyManagerBinding bmb) { baseFragment = fragment; client = fragment.getMainActivity().getClient(); - storeActivityRouter = new StoreActivityRouter(baseFragment.getMainActivity()); - - GridLayoutManager l = new GridLayoutManager(binding.timersList.getContext(), 1); - binding.timersList.setLayoutManager(l); - - View.OnClickListener invertSolid = view -> { - changeSolid = !changeSolid; - updateDiaperBar(); - }; - binding.solidEnabledButton.setOnClickListener(invertSolid); - binding.solidDisabledButton.setOnClickListener(invertSolid); - - View.OnClickListener invertWet = view -> { - changeWet = !changeWet; - updateDiaperBar(); - }; - binding.wetEnabledButton.setOnClickListener(invertWet); - binding.wetDisabledButton.setOnClickListener(invertWet); - binding.sendChangeButton.setOnClickListener(view -> storeDiaperChange()); - - notesSwitch = new SwitchButtonLogic( - binding.addNoteButton, - binding.removeNoteButton, - false - ); - - NotesEditorBinding notesEditorBinding = NotesEditorBinding.inflate( - fragment.getMainActivity().getLayoutInflater() - ); - binding.diaperNotesSlot.addView(notesEditorBinding.getRoot()); - - notesEditor = new NotesEditorLogic(notesEditorBinding, false); - notesSwitch.addStateListener((b, userInduced) -> notesEditor.setVisible(b)); - binding.mainScrollView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { if (childHistoryLoader != null) { childHistoryLoader.updateTop(); @@ -102,46 +63,6 @@ public BabyBuddyClient.Child getChild() { return child; } - private void resetDiaperUi() { - changeSolid = false; - changeWet = false; - updateDiaperBar(); - } - - private void updateDiaperBar() { - binding.sendChangeButton.setVisibility((changeSolid || changeWet) ? View.VISIBLE : View.INVISIBLE); - binding.solidEnabledButton.setVisibility(changeSolid ? View.VISIBLE : View.GONE); - binding.solidDisabledButton.setVisibility(!changeSolid ? View.VISIBLE : View.GONE); - binding.wetEnabledButton.setVisibility(changeWet ? View.VISIBLE : View.GONE); - binding.wetDisabledButton.setVisibility(!changeWet ? View.VISIBLE : View.GONE); - } - - private void storeDiaperChange() { - client.createChangeRecord(child, changeWet, changeSolid, notesEditor.getText(), - new BabyBuddyClient.RequestCallback() { - @Override - public void error(@NonNull Exception error) { - baseFragment.showError( - true, - "Failed to save", - "Diaper change not saved" - ); - } - - @Override - public void response(Boolean response) { - notesEditor.clearText(); - notesSwitch.setState(false); - if (childHistoryLoader != null) { - childHistoryLoader.forceRefresh(); - } - } - } - ); - - resetDiaperUi(); - } - private void requeueImmediateTimerListRefresh() { client.listTimers(child.id, new BabyBuddyClient.RequestCallback() { @Override @@ -160,16 +81,15 @@ private void resetChildHistoryLoader() { childHistoryLoader.close(); } childHistoryLoader = null; - binding.timersList.setAdapter(new EmptyTimerListProvider()); } public void updateChild(BabyBuddyClient.Child c, ChildrenStateTracker stateTracker) { + if (childObserver != null && child == c && stateTracker == childObserver.getTracker()) { + return; + } + clear(); this.child = c; - notesEditor.setNotes(new CredStoreNotes( - "diaper_" + c.slug, baseFragment.getMainActivity().getCredStore() - )); - notesSwitch.setState(notesEditor.isVisible()); if (child != null) { if (stateTracker == null) { @@ -194,8 +114,40 @@ public void updateChild(BabyBuddyClient.Child c, ChildrenStateTracker stateTrack } ); - timerListProvider = new TimerListProvider(baseFragment, this); - binding.timersList.setAdapter(timerListProvider); + + loggingButtonController = new LoggingButtonController( + baseFragment, + binding, + new FragmentCallbacks() { + @Override + public void insertControls(@NonNull View view) { + if (view.getParent() != null) { + return; + } + binding.loggingEditors.addView(view); + } + + @Override + public void removeControls(@NonNull View view) { + if (view.getParent() == null) { + return; + } + binding.loggingEditors.removeView(view); + } + + @Override + public void updateTimeline(@Nullable TimeEntry newEntry) { + if (childHistoryLoader != null) { + if (newEntry != null) { + childHistoryLoader.addEntryToTop(newEntry); + } + childHistoryLoader.forceRefresh(); + } + } + }, + child, + this + ); } } @@ -223,9 +175,11 @@ public void updateTimerList(BabyBuddyClient.Timer[] timers) { } public void onViewDeselected() { + if (loggingButtonController != null) { + loggingButtonController.storeStateForSuspend(); + } resetChildObserver(); resetChildHistoryLoader(); - resetDiaperUi(); } private void resetChildObserver() { @@ -236,16 +190,19 @@ private void resetChildObserver() { } public void clear() { + if (loggingButtonController != null) { + loggingButtonController.storeStateForSuspend(); + loggingButtonController.destroy(); + loggingButtonController = null; + } resetChildObserver(); resetChildHistoryLoader(); - resetDiaperUi(); child = null; cachedTimers = null; } public void close() { clear(); - timerListProvider.close(); } private class UpdateBufferingPromise implements Promise { @@ -291,32 +248,28 @@ public void stopTimer(@NotNull BabyBuddyClient.Timer timer, @NonNull Promise cb - ) { - baseFragment.getMainActivity().getChildTimerControl(child).storeActivity( - timer, - activity, - notes, - new UpdateBufferingPromise<>(cb) { - @Override - public void succeeded(Boolean aBoolean) { - super.succeeded(aBoolean); - if (childHistoryLoader != null) { - childHistoryLoader.forceRefresh(); - } + public void registerTimersUpdatedCallback(@NonNull TimersUpdatedCallback callback) { + if (updateTimersCallbacks.contains(callback)) { + return; + } + updateTimersCallbacks.add(callback); + + baseFragment.getMainActivity().getChildTimerControl(child).registerTimersUpdatedCallback( + timers -> { + for (TimersUpdatedCallback c : updateTimersCallbacks) { + c.newTimerListLoaded(timers); } } ); + callTimerUpdateCallback(); } @Override - public void registerTimersUpdatedCallback(@NonNull TimersUpdatedCallback callback) { - baseFragment.getMainActivity().getChildTimerControl(child).registerTimersUpdatedCallback(callback); - callTimerUpdateCallback(); + public void unregisterTimersUpdatedCallback(@NonNull TimersUpdatedCallback callback) { + if (!updateTimersCallbacks.contains(callback)) { + return; + } + updateTimersCallbacks.remove(callback); } private void callTimerUpdateCallback() { diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/CredStore.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/CredStore.java index 62588f1..d2baeb5 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/CredStore.java +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/CredStore.java @@ -319,7 +319,7 @@ public boolean isStoredVersionOutdated() { return !CURRENT_VERSION.equals(storedVersion); } - public Map getAuthCookies() { + public @NotNull Map getAuthCookies() { String encodedMap = decryptMessage(encryptedCookies); if (encodedMap == null) { return new HashMap<>(); diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/FeedingFragment.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/FeedingFragment.java deleted file mode 100644 index 7640585..0000000 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/FeedingFragment.java +++ /dev/null @@ -1,481 +0,0 @@ -package eu.pkgsoftware.babybuddywidgets; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.Spinner; - -import com.squareup.phrase.Phrase; - -import org.jetbrains.annotations.NotNull; -import org.json.JSONException; -import org.json.JSONObject; - -import java.text.DecimalFormat; -import java.text.NumberFormat; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.navigation.NavController; -import androidx.navigation.Navigation; -import eu.pkgsoftware.babybuddywidgets.activitycomponents.TimerControl; -import eu.pkgsoftware.babybuddywidgets.compat.BabyBuddyV2TimerAdapter; -import eu.pkgsoftware.babybuddywidgets.databinding.FeedingFragmentBinding; -import eu.pkgsoftware.babybuddywidgets.databinding.NotesEditorBinding; -import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient; -import eu.pkgsoftware.babybuddywidgets.networking.RequestCodeFailure; -import eu.pkgsoftware.babybuddywidgets.timers.ResolveConflicts; -import eu.pkgsoftware.babybuddywidgets.timers.TimerControlInterface; -import eu.pkgsoftware.babybuddywidgets.timers.TranslatedException; -import eu.pkgsoftware.babybuddywidgets.utils.Promise; -import eu.pkgsoftware.babybuddywidgets.widgets.HorizontalNumberPicker; - -public class FeedingFragment extends BaseFragment { - public interface ButtonListCallback { - void onSelectionChanged(int i); - } - - public class AmountValuesGenerator implements HorizontalNumberPicker.ValueGenerator { - public static final NumberFormat FORMAT_VALUE = DecimalFormat.getNumberInstance(); - - public long minValue() { - return -1L; - } - - public long maxValue() { - return 1 + 5 * 9; - } // 5 orders of magnitude - - private long calcBaseValue(long index) { - return (long) Math.round(Math.pow(10, (double) Math.max(0, (index / 9)))); - } - - public String getValue(long index) { - if (index < 0) { - return getString(R.string.store_feeding_amount_none); - } else { - return FORMAT_VALUE.format(getRawValue(index, 0f)); - } - } - - public Double getRawValue(long index, float offset) { - if (index + offset < -0.001) { - return null; - } else { - if (offset < 0) { - index--; - offset += 1.0f; - if (index < 0) { - index = 0; - offset = 0.0f; - } - } - - if (index == 0) { - return (double) offset; - } - return (double) calcBaseValue(index - 1) * (((index - 1) % 9 + 1) + offset); - } - } - - public long getValueIndex(Double value) { - if (value == null) { - return -1L; - } else { - long exp = (long) Math.max(0, Math.floor(Math.log10(value))); - long base10 = Math.round(Math.pow(10, exp)); - double relativeRest = value / base10; - - long index = (long) Math.floor(relativeRest); - double offset = relativeRest - index; - if (offset >= 0.5) { - offset -= 1.0; - index += 1; - } - index += 9 * exp; - - return Math.max(minValue(), Math.min(maxValue(), index)); - } - } - - public double getValueOffset(Double value) { - if (value == null) { - return 0.0d; - } else { - long exp = (long) Math.max(0, Math.floor(Math.log10(value))); - long base10 = Math.round(Math.pow(10, exp)); - double relativeRest = value / base10; - - long index = (long) Math.floor(relativeRest); - double offset = relativeRest - index; - if (offset >= 0.5) { - offset -= 1.0; - index += 1; - } - index += 9 * exp; - - return Math.max(-0.5, Math.min(0.5, offset)); - } - } - } - - private FeedingFragmentBinding binding = null; - private Double amount = -1.0; - private NotesEditorBinding notesEditor = null; - private AmountValuesGenerator amountValuesGenerator = new AmountValuesGenerator(); - private BabyBuddyClient.Timer selectedTimer = null; - private BabyBuddyClient.Timer virtTimer = null; - private boolean restoredPreviousState = false; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FeedingFragmentBinding.inflate(inflater, container, false); - View view = binding.getRoot(); - - binding.submitButton.setOnClickListener(view1 -> submitFeeding()); - binding.feedingTypeSpinner.setOnItemSelectedListener( - new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - setupFeedingMethodButtons(Constants.FeedingTypeEnumValues.get((int) l)); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - } - } - ); - - binding.amountNumberPicker.setValueGenerator(amountValuesGenerator); - binding.amountNumberPicker.setValueUpdatedListener( - new HorizontalNumberPicker.ValueUpdatedListener() { - @Override - public void valueChangeChanging(long valueIndex, float relativeOffset) { - amount = amountValuesGenerator.getRawValue(valueIndex, relativeOffset); - updateAmount(); - } - - @Override - public void valueChangeFinished(long valueIndex, float relativeOffset) { - valueChangeChanging(valueIndex, relativeOffset); - } - } - ); - - notesEditor = NotesEditorBinding.inflate(mainActivity().getLayoutInflater()); - binding.notes.addView(notesEditor.getRoot()); - - String notes = ""; - final Bundle args = getArguments(); - if (args != null) { - notes = args.getString("notes", ""); - } - notesEditor.noteEditor.setText(notes); - - resetVisibilityState(); - - return view; - } - - @Override - public void onViewStateRestored(@Nullable Bundle savedInstanceState) { - super.onViewStateRestored(savedInstanceState); - - restoredPreviousState = savedInstanceState != null; - if (restoredPreviousState) { - final String timerIdString = savedInstanceState.getString("timerId", null); - if (timerIdString != null) { - try { - selectedTimer = BabyBuddyClient.Timer.fromJSON(new JSONObject(timerIdString)); - virtTimer = timerControl().timerToVirtualTimer( - selectedTimer - ); - } catch (ParseException | JSONException e) { - selectedTimer = null; - virtTimer = null; - System.err.println("Could not decode selected timer data"); - } - } - amount = savedInstanceState.getDouble("amount", -1.0); - final String notes = savedInstanceState.getString("notes"); - notesEditor.noteEditor.setText(notes); - } - } - - @Override - public void onStart() { - super.onStart(); - - if (!restoredPreviousState) { - Double lastUsedAmount = mainActivity().getCredStore().getLastUsedAmount(); - if (lastUsedAmount != null) { - amount = lastUsedAmount; - } - } - - if (amount == null) { - amount = -1.0; - } - final double newAmountValue = amount; - binding.amountNumberPicker.setValueIndex(amountValuesGenerator.getValueIndex(newAmountValue)); - binding.amountNumberPicker.setRelativeValueIndexOffset(amountValuesGenerator.getValueOffset(newAmountValue)); - - resetVisibilityState(); - } - - @Override - public void onResume() { - super.onResume(); - - if (!restoredPreviousState) { - if (mainActivity().selectedTimer != null) { - selectedTimer = mainActivity().selectedTimer; - virtTimer = timerControl().timerToVirtualTimer( - selectedTimer - ); - } - } - } - - @Override - public void onPause() { - super.onPause(); - - if (selectedTimer != null) { - CredStore.Notes notes = timerControl().getNotes(virtTimer); - notes.note = notesEditor.noteEditor.getText().toString(); - timerControl().setNotes(virtTimer, notes); - mainActivity().getCredStore().storePrefs(); - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - if (selectedTimer == null) { - outState.putString("timerId", null); - } else { - outState.putString("timerId", selectedTimer.toJSON().toString()); - } - if (amount == null) { - outState.putDouble("amount", -1.0); - } else { - outState.putDouble("amount", amount); - } - outState.putString("notes", notesEditor.noteEditor.getText().toString()); - } - - private void resetVisibilityState() { - populateButtonList( - getResources().getTextArray(R.array.feedingTypes), - binding.feedingTypeButtons, - binding.feedingTypeSpinner, - i -> setupFeedingMethodButtons(Constants.FeedingTypeEnumValues.get(i)) - ); - binding.feedingMethodSpinner.setVisibility(View.GONE); - binding.feedingMethodButtons.setVisibility(View.GONE); - updateAmount(); - } - - private void updateAmount() { - String text = "(" + getString(R.string.store_feeding_amount_none) + ")"; - if (amount != null) { - text = AmountValuesGenerator.FORMAT_VALUE.format(amount); - } - binding.amountText.setText( - Phrase.from(requireContext(), R.string.store_feeding_amount_string) - .put("quantity", text) - .format() - ); - } - - private static class ButtonListOnClickListener implements View.OnClickListener { - private int i; - private ButtonListCallback cb; - - public ButtonListOnClickListener(ButtonListCallback cb, int i) { - this.i = i; - this.cb = cb; - } - - public void onClick(View view) { - cb.onSelectionChanged(i); - } - } - - private void populateButtonList(CharSequence[] textArray, LinearLayout buttons, Spinner spinner, ButtonListCallback callback) { - spinner.setVisibility(View.GONE); - buttons.setVisibility(View.VISIBLE); - - buttons.removeAllViewsInLayout(); - for (int i = 0; i < textArray.length; i++) { - Button button = new Button(getContext()); - button.setOnClickListener( - new ButtonListOnClickListener( - i0 -> { - spinner.setSelection(i0); - spinner.setVisibility(View.VISIBLE); - buttons.setVisibility(View.GONE); - callback.onSelectionChanged(i0); - }, i) - ); - button.setText(textArray[i]); - button.setLayoutParams( - new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT, - 1)); - buttons.addView(button); - } - } - - private List assignedMethodButtons = null; - - private void setupFeedingMethodButtons(Constants.FeedingTypeEnum type) { - binding.submitButton.setVisibility(View.GONE); - assignedMethodButtons = new ArrayList<>(10); - - switch (type) { - case BREAST_MILK: - assignedMethodButtons.add(Constants.FeedingMethodEnum.LEFT_BREAST); - assignedMethodButtons.add(Constants.FeedingMethodEnum.RIGHT_BREAST); - assignedMethodButtons.add(Constants.FeedingMethodEnum.BOTH_BREASTS); - assignedMethodButtons.add(Constants.FeedingMethodEnum.BOTTLE); - assignedMethodButtons.add(Constants.FeedingMethodEnum.PARENT_FED); - assignedMethodButtons.add(Constants.FeedingMethodEnum.SELF_FED); - break; - default: - assignedMethodButtons.add(Constants.FeedingMethodEnum.BOTTLE); - assignedMethodButtons.add(Constants.FeedingMethodEnum.PARENT_FED); - assignedMethodButtons.add(Constants.FeedingMethodEnum.SELF_FED); - } - - CharSequence[] orgItems = getResources().getTextArray(R.array.feedingMethods); - List textItems = new ArrayList<>(10); - for (int i = 0; i < assignedMethodButtons.size(); i++) { - textItems.add(orgItems[assignedMethodButtons.get(i).value]); - } - - binding.feedingMethodSpinner.setAdapter( - new ArrayAdapter( - requireContext(), android.R.layout.simple_spinner_dropdown_item, textItems - ) - ); - - populateButtonList( - textItems.toArray( - new CharSequence[0]), - binding.feedingMethodButtons, - binding.feedingMethodSpinner, - i -> binding.submitButton.setVisibility(View.VISIBLE) - ); - } - - private MainActivity mainActivity() { - return (MainActivity) getActivity(); - } - - private BabyBuddyV2TimerAdapter timerControl() { - return mainActivity().getChildTimerControl(selectedTimer.child_id); - } - - private void submitFeeding() { - long feedingTypeId = binding.feedingTypeSpinner.getSelectedItemId(); - Constants.FeedingTypeEnum feedingType = Constants.FeedingTypeEnumValues.get((int) feedingTypeId); - long feedingMethodId = binding.feedingMethodSpinner.getSelectedItemId(); - Constants.FeedingMethodEnum feedingMethod = assignedMethodButtons.get((int) feedingMethodId); - - Float fAmount = amount == null ? null : (float) (amount * 1.0d); - if (fAmount != null) { - fAmount = Math.round(fAmount * 10.0f) / 10.0f; - } - final Float finalFloatAmount = fAmount; - - mainActivity().storeActivity(selectedTimer, new StoreFunction() { - @Override - public void error(@NonNull Exception error) { - ResolveConflicts resolveConflicts = new ResolveConflicts( - FeedingFragment.this, virtTimer, timerControl() - ) { - @Override - protected void updateTimerActiveState() { - } - - @Override - protected void finished() { - navUp(); - } - }; - resolveConflicts.tryResolveStoreError(error); - } - - @Override - public void response(Boolean response) { - MainActivity ma = mainActivity(); - ma.getCredStore().storeLastUsedAmount(amount); - notesEditor.noteEditor.setText(""); - - timerControl().setNotes(virtTimer, CredStore.EMPTY_NOTES); - navUp(); - } - - @Override - public void cancel() { - navUp(); - } - - @Override - public void timerStopped() { - timerControl().stopTimer(virtTimer, new Promise<>() { - @Override - public void succeeded(Object o) { - navUp(); - } - - @Override - public void failed(TranslatedException s) { - showError( - true, - R.string.activity_store_failure_failed_to_stop_title, - R.string.activity_store_failure_failed_to_stop_message - ); - navUp(); - } - }); - } - - @NonNull - @Override - public String name() { - return BabyBuddyClient.ACTIVITIES.FEEDING; - } - - @Override - public void store( - @NonNull BabyBuddyClient.Timer timer, - @NonNull BabyBuddyClient.RequestCallback callback - ) { - mainActivity().getClient().createFeedingRecordFromTimer( - selectedTimer, - feedingType.post_name, - feedingMethod.post_name, - finalFloatAmount, - notesEditor.noteEditor.getText().toString().trim(), - callback - ); - } - }); - } - - private void navUp() { - NavController nav = Navigation.findNavController(requireView()); - nav.navigateUp(); - } -} diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/JacksonSerializers.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/JacksonSerializers.kt new file mode 100644 index 0000000..e85dae9 --- /dev/null +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/JacksonSerializers.kt @@ -0,0 +1,57 @@ +package eu.pkgsoftware.babybuddywidgets + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.DATE_ONLY_FORMAT_STRING +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.DATE_TIME_FORMAT_STRING +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.formatDate +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.parseNullOrDate +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.serverTimeToClientTime +import java.io.IOException +import java.util.Date + +class DateTimeDeserializer : StdDeserializer(Date::class.java) { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): Date { + p.text?.let { + parseNullOrDate(it, DATE_TIME_FORMAT_STRING)?.let { + return serverTimeToClientTime(it) + } + } + throw IOException("Invalid date string ${p.text}") + } +} + +class DateOnlyDeserializer : StdDeserializer(Date::class.java) { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): Date { + p.text?.let { + parseNullOrDate(it, DATE_ONLY_FORMAT_STRING)?.let { + return it + } + } + throw IOException("Invalid date string ${p.text}") + } +} + +class AnyDateTimeDeserializer : StdDeserializer(Date::class.java) { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): Date { + p.text?.let { + parseNullOrDate(it, DATE_TIME_FORMAT_STRING)?.let { + return serverTimeToClientTime(it) + } + parseNullOrDate(it, DATE_ONLY_FORMAT_STRING)?.let { + return it + } + } + throw IOException("Invalid date string ${p.text}") + } +} + +class DateTimeSerializer : StdSerializer(Date::class.java) { + override fun serialize(value: Date, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString(formatDate(value, DATE_TIME_FORMAT_STRING)) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/MainActivity.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/MainActivity.kt index d6db0cf..a51ddf8 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/MainActivity.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/MainActivity.kt @@ -29,11 +29,10 @@ import kotlin.coroutines.suspendCoroutine interface StoreFunction : BabyBuddyClient.RequestCallback { fun store(timer: BabyBuddyClient.Timer, callback: BabyBuddyClient.RequestCallback) fun name(): String - fun timerStopped() + fun stopTimer(timer: BabyBuddyClient.Timer) fun cancel() } - enum class ConflictResolutionOptions { CANCEL, RESOLVE, STOP_TIMER } @@ -50,6 +49,20 @@ class MainActivity : AppCompatActivity() { val scope = MainScope() val inputEventListeners = mutableListOf() + internal var internalStorage: ActivityStore? = null + val storage: ActivityStore + get() { + internalStorage.let { + if (it == null) { + val newStorage = ActivityStore(this) + internalStorage = newStorage + return newStorage + } else { + return it + } + } + } + internal var internalCredStore: CredStore? = null val credStore: CredStore get() { @@ -171,11 +184,11 @@ class MainActivity : AppCompatActivity() { return super.dispatchTouchEvent(ev) } - override fun dispatchKeyEvent(ev: KeyEvent?): Boolean { - ev?.let { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + event?.let { invokeInputEventListeners(it) } - return super.dispatchKeyEvent(ev) + return super.dispatchKeyEvent(event) } fun storeActivity( @@ -249,7 +262,11 @@ class MainActivity : AppCompatActivity() { val endTime = timer.computeCurrentServerEndTime(client) for (c in conflicts) { val values = BabyBuddyClient.QueryValues() - values.add("start", timer.start) + if (c.start < timer.start) { + values.add("start", c.start) + } else { + values.add("start", timer.start) + } values.add("end", timer.start) try { patchEntry(c, values) @@ -301,7 +318,7 @@ class MainActivity : AppCompatActivity() { progressDialog.show() if (resolution == ConflictResolutionOptions.STOP_TIMER) { progressDialog.cancel() - storeInterface.timerStopped() + storeInterface.stopTimer(timer) } else if (resolution == ConflictResolutionOptions.RESOLVE) { var retries = 3 while (retries > 0) { @@ -322,7 +339,6 @@ class MainActivity : AppCompatActivity() { storeInterface.cancel() } } catch (e: Exception) { - e.printStackTrace() storeInterface.error(e) } finally { progressDialog.cancel() @@ -352,6 +368,10 @@ class MainActivity : AppCompatActivity() { fun logout() { credStore.clearLoginData() + timerControls.clear() internalClient = null + // Storage gets cleaned in the resume function of the login fragment + // because otherwise the cleanup functions of the LoggedIn fragment + // will repopulate the storage with the old data that should be deleted } } \ No newline at end of file diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/activitycomponents/TimerControl.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/activitycomponents/TimerControl.kt index 437d50d..099c308 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/activitycomponents/TimerControl.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/activitycomponents/TimerControl.kt @@ -5,14 +5,12 @@ import eu.pkgsoftware.babybuddywidgets.MainActivity import eu.pkgsoftware.babybuddywidgets.R import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient.RequestCallback import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient.Timer -import eu.pkgsoftware.babybuddywidgets.timers.StoreActivityRouter import eu.pkgsoftware.babybuddywidgets.timers.TimerControlInterface import eu.pkgsoftware.babybuddywidgets.timers.TimersUpdatedCallback import eu.pkgsoftware.babybuddywidgets.timers.TranslatedException import eu.pkgsoftware.babybuddywidgets.utils.Promise class TimerControl(val mainActivity: MainActivity, val childId: Int) : TimerControlInterface { - private val storeActivityRouter = StoreActivityRouter(mainActivity) private val client = mainActivity.client var updateTimersCallback: TimersUpdatedCallback? = null @@ -72,27 +70,16 @@ class TimerControl(val mainActivity: MainActivity, val childId: Int) : TimerCont }) } - override fun storeActivity( - timer: Timer, - activity: String, - notes: String, - cb: Promise - ) { - storeActivityRouter.store(activity, notes, timer, object : Promise { - override fun succeeded(aBoolean: Boolean) { - cb.succeeded(aBoolean) - } - - override fun failed(e: Exception) { - cb.failed(e) - } - }) - } - override fun registerTimersUpdatedCallback(callback: TimersUpdatedCallback) { updateTimersCallback = callback } + override fun unregisterTimersUpdatedCallback(callback: TimersUpdatedCallback) { + if (updateTimersCallback == callback) { + updateTimersCallback = null + } + } + override fun getNotes(timer: Timer): CredStore.Notes { val credStore: CredStore = mainActivity.credStore return credStore.getObjectNotes("timer_" + timer.id) diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/compat/BabyBuddyV2TimerAdapter.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/compat/BabyBuddyV2TimerAdapter.kt index 11d5b42..af2b515 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/compat/BabyBuddyV2TimerAdapter.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/compat/BabyBuddyV2TimerAdapter.kt @@ -15,7 +15,9 @@ import java.util.Locale data class WrappedTimer(val mappedActivityIndex: Int, val timer: Timer) { } -val IMPLEMENTED_ACTIVITIES = listOf(ACTIVITIES.FEEDING, ACTIVITIES.SLEEP, ACTIVITIES.TUMMY_TIME) +val IMPLEMENTED_ACTIVITIES = listOf( + ACTIVITIES.FEEDING, ACTIVITIES.SLEEP, ACTIVITIES.TUMMY_TIME, ACTIVITIES.PUMPING +) class BabyBuddyV2TimerAdapter( val childId: Int, @@ -236,24 +238,17 @@ class BabyBuddyV2TimerAdapter( } } - override fun storeActivity( - timer: Timer, - activity: String, - notes: String, - cb: Promise - ) { - virtualToActualTimer(timer)?.let { - wrap.storeActivity(it, activity, notes, cb) - } ?: { - cb.failed(java.lang.Exception("Timer ${timer.name} does not exist")) - } - } - override fun registerTimersUpdatedCallback(callback: TimersUpdatedCallback) { timersCallback = callback triggerTimerCallback() } + override fun unregisterTimersUpdatedCallback(callback: TimersUpdatedCallback) { + if (timersCallback == callback) { + timersCallback = null + } + } + override fun getNotes(timer: Timer): CredStore.Notes { virtualToActualTimer(timer)?.let { return wrap.getNotes(it) diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/debugging/GlobalDebugObject.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/debugging/GlobalDebugObject.kt index 7ba0f14..4ed31cd 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/debugging/GlobalDebugObject.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/debugging/GlobalDebugObject.kt @@ -5,7 +5,7 @@ import android.util.Log class GlobalDebugObject { companion object { @JvmStatic - val ENABLED = false + val ENABLED = true @JvmStatic val DO_PRINT = true @JvmStatic @@ -38,4 +38,4 @@ class GlobalDebugObject { return result } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/history/ChildEventHistoryLoader.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/history/ChildEventHistoryLoader.kt index b48e1db..d9252f6 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/history/ChildEventHistoryLoader.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/history/ChildEventHistoryLoader.kt @@ -14,6 +14,7 @@ import eu.pkgsoftware.babybuddywidgets.logic.EndAwareContinuousListIntegrator import eu.pkgsoftware.babybuddywidgets.networking.RequestCodeFailure import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.ConnectingDialogInterface import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.InterruptedException +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.PaginatedResult import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.exponentialBackoff import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.ChangeEntry import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.SleepEntry @@ -49,7 +50,7 @@ class ChildEventHistoryLoader( private val progressBar: ProgressBar, private val errorPill: ShowErrorPill, ) { - val HISTORY_ITEM_COUNT = 50 + val HISTORY_ITEM_COUNT = 150 / IMPLEMENTED_EVENT_CLASSES.size val POLL_INTERVAL = 5000 private val activityCollectionGate = IMPLEMENTED_EVENT_CLASSES.toMutableList() @@ -97,8 +98,12 @@ class ChildEventHistoryLoader( val activityName = classActivityName(it) try { val conInterface = BackoffConnectionInterface(activityName) + val mainActivity = fragment.mainActivity val r = exponentialBackoff(conInterface) { - fragment.mainActivity.client.v2client.getEntries( + if (fragment.isDetached) { + throw InterruptedException() + } + mainActivity.client.v2client.getEntries( it, offset = queryOffsets.getOrDefault(it, 0), limit = HISTORY_ITEM_COUNT, @@ -140,7 +145,7 @@ class ChildEventHistoryLoader( private fun timeEntryToContinuousListItem(e: TimeEntry): ContinuousListItem { val result = ContinuousListItem( -e.start.time, - e.type, + e.appType, e.id.toString(), ) timeEntryLookup[result] = e @@ -246,7 +251,6 @@ class ChildEventHistoryLoader( override val position: PointF? get() { val r = Rect() container.getGlobalVisibleRect(r) - println(r) if (r.isEmpty) return null return PointF((r.left + r.right) / 2f, r.top.toFloat()) } @@ -267,6 +271,21 @@ class ChildEventHistoryLoader( tutorialMessageAdded = false } + fun addEntryToTop(entry: TimeEntry) { + val cls = entry::class + val activityName = classActivityName(cls) + + val currentList = listIntegrator.items.filter { it.className == activityName }.toMutableList() + currentList.add(0, timeEntryToContinuousListItem(entry)) + + listIntegrator.updateItemsWithCount( + 0, currentList.size, activityName, currentList.toTypedArray() + ) + fragment.mainActivity.scope.launch { + deferredUpdate() + } + } + fun updateTop() { var i = 0 listIntegrator.top = null diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/history/TimelineEntry.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/history/TimelineEntry.kt index 1108cb3..0f98c73 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/history/TimelineEntry.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/history/TimelineEntry.kt @@ -2,7 +2,6 @@ package eu.pkgsoftware.babybuddywidgets.history import android.view.MotionEvent import android.view.View -import android.view.ViewConfiguration import com.squareup.phrase.Phrase import eu.pkgsoftware.babybuddywidgets.BaseFragment import eu.pkgsoftware.babybuddywidgets.Constants.FeedingMethodEnum @@ -55,17 +54,17 @@ class TimelineEntry(private val fragment: BaseFragment, private var _entry: Time binding.root.visibility = View.INVISIBLE } else { binding.root.visibility = View.VISIBLE - if (BabyBuddyClient.ACTIVITIES.TUMMY_TIME == entry.type) { + if (BabyBuddyClient.ACTIVITIES.TUMMY_TIME == entry.appType) { configureTummyTime() - } else if (BabyBuddyClient.EVENTS.CHANGE == entry.type) { + } else if (BabyBuddyClient.EVENTS.CHANGE == entry.appType) { configureChange() - } else if (BabyBuddyClient.ACTIVITIES.SLEEP == entry.type) { + } else if (BabyBuddyClient.ACTIVITIES.SLEEP == entry.appType) { configureSleep() - } else if (BabyBuddyClient.ACTIVITIES.FEEDING == entry.type) { + } else if (BabyBuddyClient.ACTIVITIES.FEEDING == entry.appType) { configureFeeding() - } else if (BabyBuddyClient.EVENTS.NOTE == entry.type) { + } else if (BabyBuddyClient.EVENTS.NOTE == entry.appType) { configureNote() - } else if (BabyBuddyClient.ACTIVITIES.PUMPING == entry.type) { + } else if (BabyBuddyClient.ACTIVITIES.PUMPING == entry.appType) { configurePumping() } else { configureDefaultView() @@ -84,12 +83,16 @@ class TimelineEntry(private val fragment: BaseFragment, private var _entry: Time } private fun defaultPhraseFields(phrase: Phrase): Phrase { + val start_time = TIME_FORMAT.format(entry!!.start) + val end_time = TIME_FORMAT.format(entry!!.end) + val opt_time_range = if (start_time == end_time) start_time else "$start_time - $end_time" return phrase - .putOptional("type", entry!!.type) + .putOptional("type", entry!!.appType) .putOptional("start_date", DATE_FORMAT.format(entry!!.start)) .putOptional("start_time", TIME_FORMAT.format(entry!!.start)) .putOptional("end_date", DATE_FORMAT.format(entry!!.end)) .putOptional("end_time", TIME_FORMAT.format(entry!!.end)) + .putOptional("opt_time_range", opt_time_range) .putOptional("notes", entry!!.notes.trim { it <= ' ' }) } @@ -97,7 +100,7 @@ class TimelineEntry(private val fragment: BaseFragment, private var _entry: Time hideAllSubviews() binding.viewGroup.getChildAt(0).visibility = View.VISIBLE val message = defaultPhraseFields( - Phrase.from("{type}\n{start_date} {start_time} - {end_time}") + Phrase.from("{type}\n{start_date} {opt_time_range}") ).format().toString() binding.defaultContent.text = message } @@ -106,7 +109,7 @@ class TimelineEntry(private val fragment: BaseFragment, private var _entry: Time hideAllSubviews() binding.tummyTimeView.visibility = View.VISIBLE val message = defaultPhraseFields( - Phrase.from("{start_date} {start_time} - {end_time}\n{notes}") + Phrase.from("{start_date} {opt_time_range}\n{notes}") ).format().toString().trim { it <= ' ' } binding.tummytimeMilestoneText.text = message } @@ -128,7 +131,7 @@ class TimelineEntry(private val fragment: BaseFragment, private var _entry: Time hideAllSubviews() binding.sleepView.visibility = View.VISIBLE val message = defaultPhraseFields( - Phrase.from("{start_date} {start_time} - {end_time}\n{notes}") + Phrase.from("{start_date} {opt_time_range}\n{notes}") ).format().toString().trim { it <= ' ' } binding.sleepText.text = message.trim { it <= ' ' } } @@ -146,7 +149,7 @@ class TimelineEntry(private val fragment: BaseFragment, private var _entry: Time hideAllSubviews() binding.pumpingTimeView.visibility = View.VISIBLE val message = defaultPhraseFields( - Phrase.from("{start_date} {start_time} - {end_time}\n{notes}") + Phrase.from("{start_date} {opt_time_range}\n{notes}") ).format().toString().trim { it <= ' ' } binding.pumpingTimeNotes.text = message.trim { it <= ' ' } } @@ -183,7 +186,7 @@ class TimelineEntry(private val fragment: BaseFragment, private var _entry: Time else -> binding.solidFoodImage.visibility = View.VISIBLE } val message = defaultPhraseFields( - Phrase.from("{start_date} {start_time} - {end_time}\n{notes}") + Phrase.from("{start_date} {opt_time_range}\n{notes}") ).format().toString().trim { it <= ' ' } binding.feedingText.text = message.trim { it <= ' ' } } diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/logic/EndAwareContinuousListIntegrator.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/logic/EndAwareContinuousListIntegrator.kt index c6a0ee4..eaef5ac 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/logic/EndAwareContinuousListIntegrator.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/logic/EndAwareContinuousListIntegrator.kt @@ -18,6 +18,10 @@ class EndAwareContinuousListIntegrator : ContinuousListIntegrator() { super.clear() } + fun getItemsCount(className: String): Int { + return itemCounts[className] ?: 0 + } + fun computeValidCount(): Int { val allItems = super.items val itemsByClass = mutableMapOf>() diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/login/LoginFragment.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/login/LoginFragment.kt index 4e84088..a5334d9 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/login/LoginFragment.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/login/LoginFragment.kt @@ -175,6 +175,8 @@ class LoginFragment : BaseFragment() { progressDialog.hide() moveToLoggedIn() } else { + mainActivity.storage.deleteAllData() + val qrCode = QRCode(this, null, true) qrCode.cameraOnInitialized = Runnable { binding.qrCode.isEnabled = qrCode.hasCamera diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/login/Utils.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/login/Utils.kt index 6377b9a..f39b695 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/login/Utils.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/login/Utils.kt @@ -101,4 +101,14 @@ class Utils(val mainActivity: MainActivity) { promise.failed("No app token found.") } } + + companion object { + fun padToLen(s: String, c: Char, length: Int): String { + val sBuilder = StringBuilder(s) + while (sBuilder.length < length) { + sBuilder.insert(0, c) + } + return sBuilder.toString() + } + } } \ No newline at end of file diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/BabyBuddyClient.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/BabyBuddyClient.java index 59a7653..fff00c2 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/BabyBuddyClient.java +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/BabyBuddyClient.java @@ -40,7 +40,6 @@ public class BabyBuddyClient extends StreamReader { public static final String DATE_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ssX"; - public static final String DATE_QUERY_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ss"; public static class ACTIVITIES { public static final String SLEEP = "sleep"; @@ -123,7 +122,7 @@ private static String dateToQueryString(Date date) { if (date == null) { return null; } - final SimpleDateFormat sdf = new SimpleDateFormat(DATE_QUERY_FORMAT_STRING, Locale.ENGLISH); + final SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_STRING, Locale.ENGLISH); sdf.setTimeZone(TimeZone.getTimeZone("UTC")); return sdf.format(date); } diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ChildrenStateTracker.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ChildrenStateTracker.java index 625a72c..881b264 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ChildrenStateTracker.java +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ChildrenStateTracker.java @@ -265,6 +265,10 @@ public boolean isClosed() { return closed || ChildrenStateTracker.this.closed; } + public ChildrenStateTracker getTracker() { + return ChildrenStateTracker.this; + } + private void update() { if (isClosed()) { return; diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/CoordinatedDisconnectDialog.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/CoordinatedDisconnectDialog.kt index 7c0d587..46fa1fb 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/CoordinatedDisconnectDialog.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/CoordinatedDisconnectDialog.kt @@ -4,11 +4,12 @@ import android.app.ProgressDialog import android.content.DialogInterface import androidx.fragment.app.Fragment import androidx.navigation.Navigation.findNavController +import eu.pkgsoftware.babybuddywidgets.BaseFragment import eu.pkgsoftware.babybuddywidgets.CredStore import eu.pkgsoftware.babybuddywidgets.R import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.ConnectingDialogInterface -class CoordinatedDisconnectDialog(val fragment: Fragment, val credStore: CredStore) { +class CoordinatedDisconnectDialog(val fragment: BaseFragment, val credStore: CredStore) { private val dialog = ProgressDialog(fragment.requireContext()) private var uniqueCounter = 1 private val progressTrackers = mutableMapOf() @@ -21,7 +22,7 @@ class CoordinatedDisconnectDialog(val fragment: Fragment, val credStore: CredSto ProgressDialog.BUTTON_NEGATIVE, fragment.resources.getString(R.string.disconnect_dialog_logout) ) { dialogInterface: DialogInterface, i: Int -> - credStore.clearLoginData() + fragment.mainActivity.logout() findNavController(fragment.requireView()).navigate(R.id.logoutOperation) } } diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ServerAccessProviderInterface.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ServerAccessProviderInterface.java index b8d05d3..cce5d12 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ServerAccessProviderInterface.java +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ServerAccessProviderInterface.java @@ -2,7 +2,10 @@ import org.jetbrains.annotations.NotNull; +import java.util.Map; + public interface ServerAccessProviderInterface { @NotNull String getAppToken(); @NotNull String getServerUrl(); + @NotNull Map getAuthCookies(); } diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/Client.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/Client.kt index cf23acb..5ad75ff 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/Client.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/Client.kt @@ -1,5 +1,8 @@ package eu.pkgsoftware.babybuddywidgets.networking.babybuddy +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import eu.pkgsoftware.babybuddywidgets.debugging.GlobalDebugObject import eu.pkgsoftware.babybuddywidgets.networking.RequestCodeFailure import eu.pkgsoftware.babybuddywidgets.networking.ServerAccessProviderInterface @@ -16,9 +19,12 @@ import kotlinx.coroutines.withContext import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response +import okio.Buffer +import okio.BufferedSink import retrofit2.Call import retrofit2.Retrofit import retrofit2.converter.jackson.JacksonConverterFactory +import java.io.ByteArrayOutputStream import java.net.MalformedURLException import java.net.URL import kotlin.random.Random @@ -28,23 +34,53 @@ import kotlin.reflect.KTypeProjection import kotlin.reflect.KVariance import kotlin.reflect.full.createType import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.findParameterByName import kotlin.reflect.full.functions +import kotlin.reflect.full.valueParameters import kotlin.reflect.jvm.javaMethod fun genRequestId(): String { return Random.nextInt(0xFFFFFF + 1).toString(16).padStart(6, '0') } -class AuthInterceptor(private val authToken: String) : Interceptor { +class AuthInterceptor( + private val authToken: String, + private val cookies: Map +) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() val requestWithAuth = originalRequest.newBuilder() .header("Authorization", authToken) + .header("Cookie", cookies.map { "${it.key}=${it.value}" }.joinToString("; ")) .build() return chain.proceed(requestWithAuth) } } +class DebugNetworkInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + + val cookie = request.header("Cookie")?.let { + "Cookie: $it" + } ?: "No cookies" + + val buf = Buffer(); + request.body()?.writeTo(buf) ?: buf.writeUtf8("null"); + val bodyStream = ByteArrayOutputStream(10000) + buf.copyTo(bodyStream) + + GlobalDebugObject.log( + "Raw request: ${request.url()} - " + + "Request body: ${bodyStream} - " + + "Response: ${response.message()} - " + + cookie + ) + return response + } +} + class InvalidBody() : Exception("Invalid body") data class PaginatedResult ( @@ -55,7 +91,8 @@ data class PaginatedResult ( class Client(val credStore: ServerAccessProviderInterface) { val httpClient = OkHttpClient.Builder() - .addInterceptor(AuthInterceptor("Token " + credStore.appToken)) + .addInterceptor(AuthInterceptor("Token " + credStore.appToken, credStore.authCookies)) + .addInterceptor(DebugNetworkInterceptor()) .build() val retrofit = Retrofit.Builder() @@ -197,4 +234,49 @@ class Client(val credStore: ServerAccessProviderInterface) { } return selected } -} \ No newline at end of file + + suspend fun createEntry(itemClass: KClass, item: T): T { + val REQID = genRequestId() + val klass = item.javaClass.kotlin + + val desiredArgumentType = JsonNode::class.createType() + val desiredReturnType = Call::class.createType( + arguments = listOf( + KTypeProjection( + KVariance.INVARIANT, + itemClass.createType() + ) + ) + ) + var selected: KFunction<*>? = null + for (func in ApiInterface::class.functions) { + if (desiredArgumentType == func.findParameterByName("data")?.type) { + if (desiredReturnType == func.returnType) { + selected = func + } + } + } + + if (selected == null) { + throw IncorrectApiConfiguration( + "${REQID} V2Client::createEntry setter for ${klass.qualifiedName} is missing" + ) + } + + val mapper = jacksonObjectMapper() + val node = mapper.valueToTree(item) + node.remove("id") + + GlobalDebugObject.log("${REQID} V2Client::createEntry ${klass.simpleName}") + return withContext(Dispatchers.IO) { + try { + val call: Call = selected.javaMethod!!.invoke(api, node) as Call + return@withContext executeCall(call) + } + catch (e: Exception) { + GlobalDebugObject.log("${REQID} V2Client::createEntry ${klass.simpleName} !exception! ${e.message}") + throw e + } + } + } +} diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/DateHandling.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/DateHandling.kt index 60973bc..49100dd 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/DateHandling.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/DateHandling.kt @@ -1,10 +1,5 @@ package eu.pkgsoftware.babybuddywidgets.networking.babybuddy -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.core.JsonToken -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.deser.std.StdDeserializer -import java.io.IOException import java.text.ParseException import java.text.SimpleDateFormat import java.util.Date @@ -31,6 +26,11 @@ fun parseNullOrDate(s: String, format: String): Date? { } } +fun formatDate(d: Date, format: String): String { + val sdf = SimpleDateFormat(format, Locale.ENGLISH) + return sdf.format(d) +} + fun clientToServerTime(d: Date): Date { return Date(d.time + SystemServerTimeOffset) } @@ -39,39 +39,6 @@ fun serverTimeToClientTime(d: Date): Date { return Date(d.time - SystemServerTimeOffset) } - -class DateTimeDeserializer : StdDeserializer(Date::class.java) { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): Date { - p.text?.let { - parseNullOrDate(it, DATE_TIME_FORMAT_STRING)?.let { - return serverTimeToClientTime(it) - } - } - throw IOException("Invalid date string ${p.text}") - } -} - -class DateOnlyDeserializer : StdDeserializer(Date::class.java) { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): Date { - p.text?.let { - parseNullOrDate(it, DATE_ONLY_FORMAT_STRING)?.let { - return it - } - } - throw IOException("Invalid date string ${p.text}") - } -} - -class AnyDateTimeDeserializer : StdDeserializer(Date::class.java) { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): Date { - p.text?.let { - parseNullOrDate(it, DATE_TIME_FORMAT_STRING)?.let { - return serverTimeToClientTime(it) - } - parseNullOrDate(it, DATE_ONLY_FORMAT_STRING)?.let { - return it - } - } - throw IOException("Invalid date string ${p.text}") - } +fun nowServer(): Date { + return clientToServerTime(Date()) } diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/ApiInterface.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/ApiInterface.kt index 5e19db9..ed2b398 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/ApiInterface.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/ApiInterface.kt @@ -1,8 +1,14 @@ package eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import retrofit2.Call +import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Headers +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query import retrofit2.http.QueryMap @@ -14,7 +20,7 @@ interface ApiInterface { fun getProfile(): Call @ChildKey("id") - @GET("children") + @GET("children/") fun getChildEntries( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -22,7 +28,7 @@ interface ApiInterface { ): Call> @ChildKey("child") - @GET("sleep") + @GET("sleep/") fun getSleepEntries( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -30,7 +36,7 @@ interface ApiInterface { ): Call> @ChildKey("child") - @GET("feedings") + @GET("feedings/") fun getFeedingEntries( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -38,7 +44,7 @@ interface ApiInterface { ): Call> @ChildKey("child") - @GET("tummy-times") + @GET("tummy-times/") fun getTummyTimeEntries( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -46,7 +52,7 @@ interface ApiInterface { ): Call> @ChildKey("child") - @GET("pumping") + @GET("pumping/") fun getPumpingEntries( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -54,7 +60,7 @@ interface ApiInterface { ): Call> @ChildKey("child") - @GET("changes") + @GET("changes/") fun getChangeEntries( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -62,7 +68,7 @@ interface ApiInterface { ): Call> @ChildKey("child") - @GET("notes") + @GET("notes/") fun getNoteEntries( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -70,7 +76,7 @@ interface ApiInterface { ): Call> @ChildKey("child") - @GET("temperature") + @GET("temperature/") fun getTemperatureEnties( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -78,7 +84,7 @@ interface ApiInterface { ): Call> @ChildKey("child") - @GET("weight") + @GET("weight/") fun getWeightEntries( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -86,7 +92,7 @@ interface ApiInterface { ): Call> @ChildKey("child") - @GET("height") + @GET("height/") fun getHeightEntries( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -94,7 +100,7 @@ interface ApiInterface { ): Call> @ChildKey("child") - @GET("head-circumference") + @GET("head-circumference/") fun getHeadCircumferenceEntries( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -102,7 +108,7 @@ interface ApiInterface { ): Call> @ChildKey("child") - @GET("bmi") + @GET("bmi/") fun getBmiEntries( @Query("offset") offset: Int, @Query("limit") limit: Int, @@ -114,4 +120,28 @@ interface ApiInterface { @Path(value = "type", encoded = true) apiPath: String, @Path(value = "id", encoded = true) id: Int, ): Call + + @POST("changes/") + @Headers("Content-Type: application/json") + fun sendChangeEntry(@Body data: JsonNode): Call + + @POST("sleep/") + @Headers("Content-Type: application/json") + fun sendSleepEntry(@Body data: JsonNode): Call + + @POST("tummy-times/") + @Headers("Content-Type: application/json") + fun sendTummyTimeEntry(@Body data: JsonNode): Call + + @POST("notes/") + @Headers("Content-Type: application/json") + fun sendNoteEntry(@Body data: JsonNode): Call + + @POST("feedings/") + @Headers("Content-Type: application/json") + fun sendFeedingEntry(@Body data: JsonNode): Call + + @POST("pumping/") + @Headers("Content-Type: application/json") + fun sendPumpingEntry(@Body data: JsonNode): Call } diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/Child.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/Child.kt index 1082907..89cf6e7 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/Child.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/Child.kt @@ -3,7 +3,7 @@ package eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.AnyDateTimeDeserializer +import eu.pkgsoftware.babybuddywidgets.AnyDateTimeDeserializer import java.util.Date @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/TimeEntries.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/TimeEntries.kt index 93eb13c..b45f0e9 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/TimeEntries.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/models/TimeEntries.kt @@ -1,12 +1,17 @@ package eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models +import com.fasterxml.jackson.annotation.JsonGetter +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonSetter import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient.ACTIVITIES import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient.EVENTS -import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.DateTimeDeserializer -import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.DateOnlyDeserializer +import eu.pkgsoftware.babybuddywidgets.DateTimeDeserializer +import eu.pkgsoftware.babybuddywidgets.DateOnlyDeserializer +import eu.pkgsoftware.babybuddywidgets.DateTimeSerializer import java.util.Date import kotlin.reflect.KClass import kotlin.reflect.full.findAnnotations @@ -16,8 +21,8 @@ annotation class APIPath(val path: String) annotation class UIPath(val path: String) interface TimeEntry { - val type: String - val typeId: Int + val appType: String + val appTypeId: Int val id: Int val childId: Int val start: Date @@ -33,13 +38,19 @@ interface TimeEntry { data class SleepEntry( @JsonProperty("id", required = true) override val id: Int, @JsonProperty("child", required = true) override val childId: Int, - @JsonProperty("start", required = true) @JsonDeserialize(using = DateTimeDeserializer::class) override val start: Date, - @JsonProperty("end", required = true) @JsonDeserialize(using = DateTimeDeserializer::class) override val end: Date, - @JsonProperty("notes", required = false) val _notes: String?, + @JsonDeserialize(using = DateTimeDeserializer::class) + @JsonSerialize(using = DateTimeSerializer::class) + @JsonProperty("start", required = true) + override val start: Date, + @JsonDeserialize(using = DateTimeDeserializer::class) + @JsonSerialize(using = DateTimeSerializer::class) + @JsonProperty("end", required = true) + override val end: Date, + @JsonSetter("notes") val _notes: String?, ) : TimeEntry { - override val type: String = ACTIVITIES.SLEEP - override val typeId: Int = ACTIVITIES.index(type) - override val notes: String = _notes ?: "" + override @JsonIgnore val appType: String = ACTIVITIES.SLEEP + override @JsonIgnore val appTypeId: Int = ACTIVITIES.index(appType) + override @get:JsonGetter("notes") val notes: String = _notes ?: "" } @UIPath("tummy-time") @@ -49,13 +60,22 @@ data class SleepEntry( data class TummyTimeEntry( @JsonProperty("id", required = true) override val id: Int, @JsonProperty("child", required = true) override val childId: Int, - @JsonProperty("start", required = true) @JsonDeserialize(using = DateTimeDeserializer::class) override val start: Date, - @JsonProperty("end", required = true) @JsonDeserialize(using = DateTimeDeserializer::class) override val end: Date, - @JsonProperty("milestone", required = false) val _notes: String?, + + @JsonDeserialize(using = DateTimeDeserializer::class) + @JsonSerialize(using = DateTimeSerializer::class) + @JsonProperty("start", required = true) + override val start: Date, + + @JsonDeserialize(using = DateTimeDeserializer::class) + @JsonSerialize(using = DateTimeSerializer::class) + @JsonProperty("end", required = true) + override val end: Date, + + @JsonSetter("milestone") val _notes: String?, ) : TimeEntry { - override val type: String = ACTIVITIES.TUMMY_TIME - override val typeId: Int = ACTIVITIES.index(type) - override val notes: String = _notes ?: "" + override @JsonIgnore val appType: String = ACTIVITIES.TUMMY_TIME + override @JsonIgnore val appTypeId: Int = ACTIVITIES.index(appType) + override @get:JsonGetter("milestone") val notes: String = _notes ?: "" } @UIPath("feedings") @@ -65,16 +85,25 @@ data class TummyTimeEntry( data class FeedingEntry( @JsonProperty("id", required = true) override val id: Int, @JsonProperty("child", required = true) override val childId: Int, - @JsonProperty("start", required = true) @JsonDeserialize(using = DateTimeDeserializer::class) override val start: Date, - @JsonProperty("end", required = true) @JsonDeserialize(using = DateTimeDeserializer::class) override val end: Date, - @JsonProperty("notes", required = false) val _notes: String?, + + @JsonDeserialize(using = DateTimeDeserializer::class) + @JsonSerialize(using = DateTimeSerializer::class) + @JsonProperty("start", required = true) + override val start: Date, + + @JsonDeserialize(using = DateTimeDeserializer::class) + @JsonSerialize(using = DateTimeSerializer::class) + @JsonProperty("end", required = true) + override val end: Date, + + @JsonSetter("notes") val _notes: String?, @JsonProperty("type", required = true) val feedingType: String, @JsonProperty("method", required = true) val feedingMethod: String, - @JsonProperty("amount", required = true) val amount: Double, + @JsonProperty("amount", required = true) val amount: Double?, ) : TimeEntry { - override val type: String = ACTIVITIES.FEEDING - override val typeId: Int = ACTIVITIES.index(type) - override val notes: String = _notes ?: "" + override @JsonIgnore val appType: String = ACTIVITIES.FEEDING + override @JsonIgnore val appTypeId: Int = ACTIVITIES.index(appType) + override @get:JsonGetter("notes") val notes: String = _notes ?: "" } @UIPath("pumping") @@ -84,17 +113,17 @@ data class FeedingEntry( data class PumpingEntry( @JsonProperty("id", required = true) override val id: Int, @JsonProperty("child", required = true) override val childId: Int, - @JsonProperty("start", required = false) @JsonDeserialize(using = DateTimeDeserializer::class) val _start: Date?, - @JsonProperty("end", required = false) @JsonDeserialize(using = DateTimeDeserializer::class) val _end: Date?, - @JsonProperty("notes", required = false) val _notes: String?, + @JsonSetter("start") @JsonDeserialize(using = DateTimeDeserializer::class) val _start: Date?, + @JsonSetter("end") @JsonDeserialize(using = DateTimeDeserializer::class) val _end: Date?, + @JsonSetter("notes") val _notes: String?, @JsonProperty("amount", required = true) val amount: Double, - @JsonProperty("time", required = false) @JsonDeserialize(using = DateTimeDeserializer::class) private val _legacyTime: Date? + @JsonSetter("time") @JsonDeserialize(using = DateTimeDeserializer::class) private val _legacyTime: Date? ) : TimeEntry { - override val type: String = ACTIVITIES.PUMPING - override val typeId: Int = ACTIVITIES.index(type) - override val start: Date = _start ?: _legacyTime!! - override val end: Date = _end ?: _legacyTime!! - override val notes: String = _notes ?: "" + override @JsonIgnore val appType: String = ACTIVITIES.PUMPING + override @JsonIgnore val appTypeId: Int = ACTIVITIES.index(appType) + override @get:JsonGetter("start") @JsonSerialize(using = DateTimeSerializer::class) val start: Date = _start ?: _legacyTime!! + override @get:JsonGetter("end") @JsonSerialize(using = DateTimeSerializer::class) val end: Date = _end ?: _legacyTime!! + override @get:JsonGetter("notes") val notes: String = _notes ?: "" } @UIPath("changes") @@ -102,19 +131,27 @@ data class PumpingEntry( @ActivityName(EVENTS.CHANGE) @JsonIgnoreProperties(ignoreUnknown = true) data class ChangeEntry( - @JsonProperty("id", required = true) override val id: Int, - @JsonProperty("child", required = true) override val childId: Int, - @JsonProperty("time", required = true) @JsonDeserialize(using = DateTimeDeserializer::class) override val start: Date, - @JsonProperty("notes", required = false) val _notes: String?, + @JsonProperty("id", required = true) + override val id: Int, + + @JsonProperty("child", required = true) + override val childId: Int, + + @JsonProperty("time", required = true) + @JsonDeserialize(using = DateTimeDeserializer::class) + @JsonSerialize(using = DateTimeSerializer::class) + override val start: Date, + + @JsonSetter("notes") val _notes: String?, @JsonProperty("wet", required = true) val wet: Boolean, @JsonProperty("solid", required = true) val solid: Boolean, @JsonProperty("color", required = true) val color: String, @JsonProperty("amount", required = true) val amount: Double?, ) : TimeEntry { - override val type: String = EVENTS.CHANGE - override val typeId: Int = EVENTS.index(type) + override @JsonIgnore val appType: String = EVENTS.CHANGE + override @JsonIgnore val appTypeId: Int = EVENTS.index(appType) override val end: Date = start - override val notes: String = _notes ?: "" + override @get:JsonGetter("notes") val notes: String = _notes ?: "" } @UIPath("notes") @@ -122,13 +159,21 @@ data class ChangeEntry( @ActivityName(EVENTS.NOTE) @JsonIgnoreProperties(ignoreUnknown = true) data class NoteEntry( - @JsonProperty("id", required = true) override val id: Int, - @JsonProperty("child", required = true) override val childId: Int, - @JsonProperty("time", required = true) @JsonDeserialize(using = DateTimeDeserializer::class) override val start: Date, + @JsonProperty("id", required = true) + override val id: Int, + + @JsonProperty("child", required = true) + override val childId: Int, + + @JsonProperty("time", required = true) + @JsonDeserialize(using = DateTimeDeserializer::class) + @JsonSerialize(using = DateTimeSerializer::class) + override val start: Date, + @JsonProperty("note", required = false) val _notes: String?, ) : TimeEntry { - override val type: String = EVENTS.NOTE - override val typeId: Int = EVENTS.index(type) + override @JsonIgnore val appType: String = EVENTS.NOTE + override @JsonIgnore val appTypeId: Int = EVENTS.index(appType) override val end: Date = start override val notes: String = _notes ?: "" } @@ -144,8 +189,8 @@ data class BmiEntry( @JsonProperty("notes", required = false) val _notes: String?, @JsonProperty("bmi", required = true) val bmi: Double, ) : TimeEntry { - override val type: String = EVENTS.BMI - override val typeId: Int = EVENTS.index(type) + override @JsonIgnore val appType: String = EVENTS.BMI + override @JsonIgnore val appTypeId: Int = EVENTS.index(appType) override val end: Date = start override val notes: String = _notes ?: "" } @@ -161,8 +206,8 @@ data class TemperatureEntry( @JsonProperty("notes", required = false) val _notes: String?, @JsonProperty("temperature", required = true) val temperature: Double, ) : TimeEntry { - override val type: String = EVENTS.TEMPERATURE - override val typeId: Int = EVENTS.index(type) + override @JsonIgnore val appType: String = EVENTS.TEMPERATURE + override @JsonIgnore val appTypeId: Int = EVENTS.index(appType) override val end: Date = start override val notes: String = _notes ?: "" } @@ -178,8 +223,8 @@ data class WeightEntry( @JsonProperty("notes", required = false) val _notes: String?, @JsonProperty("weight", required = true) val weight: Double, ) : TimeEntry { - override val type: String = EVENTS.WEIGHT - override val typeId: Int = EVENTS.index(type) + override @JsonIgnore val appType: String = EVENTS.WEIGHT + override @JsonIgnore val appTypeId: Int = EVENTS.index(appType) override val end: Date = start override val notes: String = _notes ?: "" } @@ -195,8 +240,8 @@ data class HeightEntry( @JsonProperty("notes", required = false) val _notes: String?, @JsonProperty("height", required = true) val height: Double, ) : TimeEntry { - override val type: String = EVENTS.HEIGHT - override val typeId: Int = EVENTS.index(type) + override @JsonIgnore val appType: String = EVENTS.HEIGHT + override @JsonIgnore val appTypeId: Int = EVENTS.index(appType) override val end: Date = start override val notes: String = _notes ?: "" } @@ -212,8 +257,8 @@ data class HeadCircumferenceEntry( @JsonProperty("notes", required = false) val _notes: String?, @JsonProperty("head_circumference", required = true) val head_circumference: Double, ) : TimeEntry { - override val type: String = EVENTS.HEAD_CIRCUMFERENCE - override val typeId: Int = EVENTS.index(type) + override @JsonIgnore val appType: String = EVENTS.HEAD_CIRCUMFERENCE + override @JsonIgnore val appTypeId: Int = EVENTS.index(appType) override val end: Date = start override val notes: String = _notes ?: "" } diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/EmptyTimerListProvider.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/EmptyTimerListProvider.kt deleted file mode 100644 index ea02749..0000000 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/EmptyTimerListProvider.kt +++ /dev/null @@ -1,16 +0,0 @@ -package eu.pkgsoftware.babybuddywidgets.timers - -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView - -class EmptyTimerListProvider : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TimerListViewHolder { - throw NotImplementedError() - } - - override fun onBindViewHolder(holder: TimerListViewHolder, position: Int) {} - - override fun getItemCount(): Int { - return 0 - } -} diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/StoreActivityRouter.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/StoreActivityRouter.kt deleted file mode 100644 index 8d00fa5..0000000 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/StoreActivityRouter.kt +++ /dev/null @@ -1,135 +0,0 @@ -package eu.pkgsoftware.babybuddywidgets.timers - -import androidx.core.os.bundleOf -import androidx.navigation.Navigation.findNavController -import eu.pkgsoftware.babybuddywidgets.MainActivity -import eu.pkgsoftware.babybuddywidgets.R -import eu.pkgsoftware.babybuddywidgets.StoreFunction -import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient.ACTIVITIES -import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient.RequestCallback -import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient.Timer -import eu.pkgsoftware.babybuddywidgets.utils.AsyncClientRequest -import eu.pkgsoftware.babybuddywidgets.utils.Promise -import kotlinx.coroutines.launch -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -class InvalidActivityException : java.lang.Exception() {} - -class StoreActivityRouter(val mainActivity: MainActivity) { - private val client = mainActivity.client - - private abstract inner class DeferredStoreFunction( - val activityName: String, - val timer: Timer, - val suspensionScope: Continuation - ) : StoreFunction { - override fun name(): String { - return activityName - } - - override fun timerStopped() { - mainActivity.scope.launch { - try { - AsyncClientRequest.call { - client.deleteTimer(timer.id, it) - } - } catch (e: java.lang.Exception) { - suspensionScope.resumeWithException(e) - return@launch - } - suspensionScope.resume(true) - } - } - - override fun cancel() { - suspensionScope.resume(false) - } - - override fun error(error: java.lang.Exception) { - suspensionScope.resumeWithException(error) - } - - override fun response(response: Boolean?) { - suspensionScope.resume(response ?: true) - } - } - - private interface SimpleStoreFunctionInterface { - fun store(callback: RequestCallback) - } - - private inner class SimpleStoreFunction( - activityName: String, - timer: Timer, - suspensionScope: Continuation, - val func: SimpleStoreFunctionInterface - ) : DeferredStoreFunction(activityName, timer, suspensionScope) { - override fun store(timer: Timer, callback: RequestCallback) { - func.store(callback) - } - } - - suspend fun asyncStore(activity: String, notes: String, timer: Timer): Boolean { - return suspendCoroutine { continuation -> - val storeInterface = when (activity) { - ACTIVITIES.SLEEP -> SimpleStoreFunction( - activity, - timer, - continuation, - object : SimpleStoreFunctionInterface { - override fun store(callback: RequestCallback) { - client.createSleepRecordFromTimer(timer, notes, callback) - } - } - ) - - ACTIVITIES.TUMMY_TIME -> SimpleStoreFunction( - activity, - timer, - continuation, - object : SimpleStoreFunctionInterface { - override fun store(callback: RequestCallback) { - client.createTummyTimeRecordFromTimer(timer, notes, callback) - } - } - ) - - ACTIVITIES.FEEDING -> { - mainActivity.selectedTimer = timer - findNavController(mainActivity.findViewById(R.id.nav_host_fragment_content_main)).navigate( - R.id.action_loggedInFragment2_to_feedingFragment, bundleOf("notes" to notes) - ) - continuation.resume(false) - return@suspendCoroutine - } - - else -> { - continuation.resumeWithException(InvalidActivityException()) - return@suspendCoroutine - } - } - mainActivity.storeActivity(timer, storeInterface) - } - } - - fun store( - activity: String, - notes: String, - timer: Timer, - promise: Promise - ) { - mainActivity.scope.launch { - val result: Boolean - try { - result = asyncStore(activity, notes, timer) - } catch (e: java.lang.Exception) { - promise.failed(e) - return@launch - } - promise.succeeded(result) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControlInterface.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControlInterface.kt index d440f5c..7361934 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControlInterface.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControlInterface.kt @@ -8,15 +8,16 @@ interface TimersUpdatedCallback { fun newTimerListLoaded(timers: Array) } -class TranslatedException(message: String, val originalError: java.lang.Exception?) : Exception(message) { +class TranslatedException(message: String, val originalError: java.lang.Exception?) : + Exception(message) { } interface TimerControlInterface { fun createNewTimer(timer: Timer, cb: Promise) fun startTimer(timer: Timer, cb: Promise) fun stopTimer(timer: Timer, cb: Promise) - fun storeActivity(timer: Timer, activity: String, notes: String, cb: Promise) fun registerTimersUpdatedCallback(callback: TimersUpdatedCallback) + fun unregisterTimersUpdatedCallback(callback: TimersUpdatedCallback) fun getNotes(timer: Timer): Notes fun setNotes(timer: Timer, notes: Notes?) } \ No newline at end of file diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControllersV2.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControllersV2.kt new file mode 100644 index 0000000..1823a78 --- /dev/null +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControllersV2.kt @@ -0,0 +1,1058 @@ +package eu.pkgsoftware.babybuddywidgets.timers + +import android.os.Handler +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.Spinner +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import com.squareup.phrase.Phrase +import eu.pkgsoftware.babybuddywidgets.BaseFragment +import eu.pkgsoftware.babybuddywidgets.Constants.FeedingMethodEnum +import eu.pkgsoftware.babybuddywidgets.Constants.FeedingTypeEnum +import eu.pkgsoftware.babybuddywidgets.Constants.FeedingTypeEnumValues +import eu.pkgsoftware.babybuddywidgets.DialogCallback +import eu.pkgsoftware.babybuddywidgets.R +import eu.pkgsoftware.babybuddywidgets.StoreFunction +import eu.pkgsoftware.babybuddywidgets.databinding.BabyManagerBinding +import eu.pkgsoftware.babybuddywidgets.databinding.DiaperLoggingEntryBinding +import eu.pkgsoftware.babybuddywidgets.databinding.FeedingLoggingEntryBinding +import eu.pkgsoftware.babybuddywidgets.databinding.GenericTimerLoggingEntryBinding +import eu.pkgsoftware.babybuddywidgets.databinding.NoteLoggingEntryBinding +import eu.pkgsoftware.babybuddywidgets.databinding.PumpingLoggingEntryBinding +import eu.pkgsoftware.babybuddywidgets.login.Utils +import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient +import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient.Timer +import eu.pkgsoftware.babybuddywidgets.networking.RequestCodeFailure +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.ChangeEntry +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.FeedingEntry +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.NoteEntry +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.PumpingEntry +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.SleepEntry +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.TimeEntry +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.TummyTimeEntry +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.classActivityName +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.nowServer +import eu.pkgsoftware.babybuddywidgets.utils.AsyncPromise +import eu.pkgsoftware.babybuddywidgets.utils.AsyncPromiseFailure +import eu.pkgsoftware.babybuddywidgets.utils.ConcurrentEventBlocker +import eu.pkgsoftware.babybuddywidgets.utils.Promise +import eu.pkgsoftware.babybuddywidgets.widgets.HorizontalDecIncEditor +import eu.pkgsoftware.babybuddywidgets.widgets.HorizontalNumberPicker +import eu.pkgsoftware.babybuddywidgets.widgets.SwitchButtonLogic +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.launch +import java.io.IOException +import java.util.Date +import kotlin.reflect.KClass + +interface ButtonListCallback { + fun onSelectionChanged(i: Int) +} + +interface FragmentCallbacks { + fun insertControls(view: View) + fun removeControls(view: View) + fun updateTimeline(newEntry: TimeEntry?) +} + +abstract class LoggingControls(val childId: Int) { + abstract val saveButton: ImageButton + abstract val controlsView: View + + abstract fun storeStateForSuspend() + abstract fun reset() + suspend abstract fun save(): TimeEntry? + + open fun updateVisuals() {} + open fun postInit() {} +} + +interface TimerBase { + fun updateTimer(timer: Timer?) +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class GenericTimerRecord( + @JsonProperty("note") val note: String +) + +abstract class GenericLoggingController( + val fragment: BaseFragment, + childId: Int, + val timerControl: TimerControlInterface, + val entryKlass: KClass<*> +) : LoggingControls(childId), TimerBase, StoreFunction { + protected abstract suspend fun createEntry(timer: Timer): TimeEntry + + val bindings = GenericTimerLoggingEntryBinding.inflate(fragment.layoutInflater) + val typeName: String = classActivityName(entryKlass) + + open val uiIconList = bindings.icons.children + open val uiNoteEditor = bindings.noteEditor + open val uiCurrentTimerTime = bindings.currentTimerTime + + override val saveButton: ImageButton = bindings.sendButton + override val controlsView: View = bindings.root + + private var timer: Timer? = null + private var storingPromise: Promise? = null + + override fun postInit() { + fragment.mainActivity.storage.child(childId, typeName)?.let { + uiNoteEditor.setText(it.note) + } + + val children = uiIconList.toList() + for (i in BabyBuddyClient.ACTIVITIES.ALL.indices) { + if (BabyBuddyClient.ACTIVITIES.ALL[i] == typeName) { + children[i].visibility = View.VISIBLE + } else { + children[i].visibility = View.GONE + } + } + + updateVisuals() + } + + override fun storeStateForSuspend() { + fragment.mainActivity.storage.child( + childId, typeName, GenericTimerRecord(uiNoteEditor.text.toString()) + ) + } + + override fun reset() { + uiNoteEditor.setText("") + storeStateForSuspend() + } + + override suspend fun save(): TimeEntry? { + storingPromise?.let { + throw IOException("Already storing activity of type ${typeName}") + } + + timer?.let { timer -> + try { + try { + val result = AsyncPromise.call { promise -> + storingPromise = promise + fragment.mainActivity.storeActivity(timer, this) + } + return result + } + finally { + storingPromise = null + } + } + catch (e: AsyncPromiseFailure) { + fragment.showError( + true, + R.string.activity_store_failure_message, + R.string.activity_store_failure_server_error + ) + } + } + throw IOException("Could not store activity of type ${typeName}") + } + + override fun updateTimer(timer: Timer?) { + this.timer = timer + updateVisuals() + } + + override fun updateVisuals() { + val now = Date() + (timer?.start ?: now).let { + val diff = now.time - it.time + + val seconds = diff.toInt() / 1000 + val minutes = seconds / 60 + val hours = minutes / 60 + + uiCurrentTimerTime.text = "HH:MM:ss" + .replace("HH".toRegex(), "" + hours) + .replace("MM".toRegex(), Utils.padToLen("" + minutes % 60, '0', 2)) + .replace("ss".toRegex(), Utils.padToLen("" + seconds % 60, '0', 2)) + } + } + + override fun store( + timer: Timer, + callback: BabyBuddyClient.RequestCallback + ) { + fragment.mainActivity.scope.launch { + try { + val result = createEntry(timer) + timerControl.stopTimer( + timer, + object : Promise { + override fun succeeded(s: Any?) { + callback.response(result) + } + + override fun failed(f: TranslatedException?) { + callback.error( + f?.originalError + ?: IOException("Failed to stop timer") + ) + } + }) + } + catch (e: Exception) { + callback.error(e) + } + } + } + + override fun name(): String { + return this@GenericLoggingController.typeName + } + + override fun stopTimer(timer: Timer) { + timerControl.stopTimer( + timer, + object : Promise { + override fun succeeded(s: Any?) { + storingPromise!!.succeeded(null) + } + + override fun failed(f: TranslatedException?) { + storingPromise!!.failed(f) + } + }) + } + + override fun cancel() { + storingPromise!!.succeeded(null) + } + + override fun error(error: java.lang.Exception) { + var message = "" + (error.message ?: "") + if ((error is RequestCodeFailure) && (error.hasJSONMessage())) { + message = Phrase.from( + fragment.requireContext(), + R.string.activity_store_failure_server_error + ) + .put("message", "Error while storing activity") + .put("server_message", error.jsonErrorMessages().joinToString(", ")) + .format().toString() + } + + fragment.showQuestion( + true, + fragment.getString(R.string.activity_store_failure_message), + message, + fragment.getString(R.string.activity_store_failure_cancel), + fragment.getString(R.string.activity_store_failure_stop_timer), + object : DialogCallback { + override fun call(b: Boolean) { + if (!b) { + timer?.let { stopTimer(it) } + } else { + storingPromise!!.succeeded(null) + } + } + } + ) + } + + override fun response(response: TimeEntry?) { + storingPromise!!.succeeded(response) + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class DiaperDataRecord( + @JsonProperty("wet") val wet: Boolean, + @JsonProperty("solid") val solid: Boolean, + @JsonProperty("note") val note: String +) + +class DiaperLoggingController(val fragment: BaseFragment, childId: Int) : LoggingControls(childId) { + val bindings = DiaperLoggingEntryBinding.inflate(fragment.layoutInflater) + override val controlsView = bindings.root + override val saveButton = bindings.sendButton + + val wetLogic = SwitchButtonLogic( + bindings.wetDisabledButton, bindings.wetEnabledButton, false + ) + val solidLogic = SwitchButtonLogic( + bindings.solidDisabledButton, bindings.solidEnabledButton, false + ) + val noteEditor = bindings.noteEditor + + init { + wetLogic.addStateListener { _, _ -> + updateSaveEnabledState() + } + solidLogic.addStateListener { _, _ -> + updateSaveEnabledState() + } + + fragment.mainActivity.storage.child(childId, "diaper")?.let { + wetLogic.state = it.wet + solidLogic.state = it.solid + noteEditor.setText(it.note) + } + + updateSaveEnabledState() + } + + fun updateSaveEnabledState() { + saveButton.visibility = if (wetLogic.state || solidLogic.state) { + View.VISIBLE + } else { + View.GONE + } + } + + override fun storeStateForSuspend() { + val ddr = DiaperDataRecord( + wetLogic.state, solidLogic.state, noteEditor.text.toString() + ) + fragment.mainActivity.storage.child(childId, "diaper", ddr) + } + + override fun reset() { + noteEditor.setText("") + wetLogic.state = false + solidLogic.state = false + } + + suspend override fun save(): TimeEntry { + return fragment.mainActivity.client.v2client.createEntry( + ChangeEntry::class, + ChangeEntry( + id = 0, + childId = childId, + start = nowServer(), + _notes = noteEditor.text.toString(), + wet = wetLogic.state, + solid = solidLogic.state, + color = "", + amount = null + ) + ) + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class NotesDataRecord( + @JsonProperty("note") val note: String +) + +class NotesLoggingController(val fragment: BaseFragment, childId: Int) : LoggingControls(childId) { + val bindings = NoteLoggingEntryBinding.inflate(fragment.layoutInflater) + override val controlsView = bindings.root + override val saveButton = bindings.sendButton + + val noteEditor = bindings.noteEditor + + init { + noteEditor.addTextChangedListener { + updateVisuals() + } + fragment.mainActivity.storage.child(childId, "notes")?.let { + noteEditor.setText(it.note) + } + } + + override fun storeStateForSuspend() { + val ddr = NotesDataRecord(noteEditor.text.toString()) + fragment.mainActivity.storage.child(childId, "notes", ddr) + } + + override fun reset() { + noteEditor.setText("") + updateVisuals() + } + + suspend override fun save(): TimeEntry { + return fragment.mainActivity.client.v2client.createEntry( + NoteEntry::class, + NoteEntry( + id = 0, + childId = childId, + start = nowServer(), + _notes = noteEditor.text.toString() + ) + ) + } + + override fun updateVisuals() { + super.updateVisuals() + saveButton.visibility = if (noteEditor.text.isNotEmpty()) { + View.VISIBLE + } else { + View.GONE + } + } +} + +class SleepLoggingController( + fragment: BaseFragment, + childId: Int, + timerControl: TimerControlInterface +) : GenericLoggingController(fragment, childId, timerControl, SleepEntry::class) { + override suspend fun createEntry(timer: Timer): TimeEntry { + return fragment.mainActivity.client.v2client.createEntry( + SleepEntry::class, + SleepEntry( + id = 0, + childId = childId, + start = timer.start, + end = nowServer(), + _notes = bindings.noteEditor.text.toString() + ) + ) + } +} + +class TummyTimeLoggingController( + fragment: BaseFragment, + childId: Int, + timerControl: TimerControlInterface +) : GenericLoggingController(fragment, childId, timerControl, TummyTimeEntry::class) { + override suspend fun createEntry(timer: Timer): TimeEntry { + return fragment.mainActivity.client.v2client.createEntry( + TummyTimeEntry::class, + TummyTimeEntry( + id = 0, + childId = childId, + start = timer.start, + end = nowServer(), + _notes = bindings.noteEditor.text.toString() + ) + ) + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class FeedingRecord( + @JsonProperty("amount") val amount: Double?, + @JsonProperty("note") val note: String, + @JsonProperty("feeding_type") val feedingType: String?, + @JsonProperty("feeding_method") val feedingMethod: String?, +) + +class FeedingLoggingController( + fragment: BaseFragment, + childId: Int, + timerControl: TimerControlInterface +) : GenericLoggingController(fragment, childId, timerControl, TummyTimeEntry::class) { + val feedingBinding = FeedingLoggingEntryBinding.inflate(fragment.layoutInflater) + + override val uiCurrentTimerTime = feedingBinding.currentTimerTime + override val uiNoteEditor = feedingBinding.noteEditor + override val saveButton: ImageButton = feedingBinding.sendButton + override val controlsView: View = feedingBinding.root + + private var assignedMethodButtons: List = emptyList() + private var selectedType: String? = null + private var selectedMethod: String? = null + + override fun postInit() { + super.postInit() + + populateButtonList( + fragment.resources.getTextArray(R.array.feedingTypes), + feedingBinding.feedingTypeButtons, + feedingBinding.feedingTypeSpinner, + object : ButtonListCallback { + override fun onSelectionChanged(i: Int) { + selectedType = FeedingTypeEnumValues[i]!!.post_name + selectedMethod = null + setupFeedingMethodButtons(FeedingTypeEnumValues[i]!!) + feedingBinding.feedingMethodButtons.visibility = View.VISIBLE + feedingBinding.feedingMethodSpinner.visibility = View.GONE + updateVisuals() + } + } + ) + feedingBinding.feedingTypeSpinner.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + val newType = FeedingTypeEnumValues[position]!!.post_name + if (newType == selectedType) return + selectedType = newType + selectedMethod = null + setupFeedingMethodButtons(FeedingTypeEnumValues[position]!!) + feedingBinding.feedingMethodButtons.visibility = View.VISIBLE + feedingBinding.feedingMethodSpinner.visibility = View.GONE + updateVisuals() + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + feedingBinding.feedingMethodSpinner.visibility = View.GONE + feedingBinding.feedingMethodButtons.visibility = View.GONE + } + } + + feedingBinding.amountNumberPicker.value = null + + fragment.mainActivity.storage.child(childId, "feeding")?.let { + feedingBinding.amountNumberPicker.value = it.amount + feedingBinding.noteEditor.setText(it.note) + + selectedType = null + selectedMethod = null + + it.feedingType?.let { + try { + feedingBinding.feedingTypeSpinner.setSelection(FeedingTypeEnum.byPostName(it).value) + selectedType = it + feedingBinding.feedingTypeButtons.visibility = View.GONE + feedingBinding.feedingTypeSpinner.visibility = View.VISIBLE + setupFeedingMethodButtons(FeedingTypeEnum.byPostName(it)) + } + catch (_: NoSuchElementException) { + } + } + it.feedingMethod?.let { + if (selectedType != null) { + try { + assignedMethodButtons.indexOf(FeedingMethodEnum.byPostName(it)).let { + feedingBinding.feedingMethodSpinner.setSelection(it) + } + selectedMethod = it + feedingBinding.feedingMethodButtons.visibility = View.GONE + feedingBinding.feedingMethodSpinner.visibility = View.VISIBLE + } + catch (_: NoSuchElementException) { + } + } + } + } + + updateVisuals() + } + + override fun storeStateForSuspend() { + val fr = FeedingRecord( + feedingBinding.amountNumberPicker.value?.toDouble(), + feedingBinding.noteEditor.text.toString(), + selectedType, + selectedMethod, + ) + fragment.mainActivity.storage.child(childId, "feeding", fr) + } + + override fun reset() { + feedingBinding.amountNumberPicker.value = null + feedingBinding.noteEditor.setText("") + selectedType = null + selectedMethod = null + storeStateForSuspend() + } + + override fun updateVisuals() { + super.updateVisuals() + + val selectedType = selectedType + if (selectedType == null) { + feedingBinding.feedingTypeButtons.visibility = View.VISIBLE + feedingBinding.feedingTypeSpinner.visibility = View.GONE + feedingBinding.feedingMethodButtons.visibility = View.GONE + feedingBinding.feedingMethodSpinner.visibility = View.GONE + } else if (selectedMethod == null) { + setupFeedingMethodButtons(FeedingTypeEnum.byPostName(selectedType)) + feedingBinding.feedingTypeButtons.visibility = View.GONE + feedingBinding.feedingTypeSpinner.visibility = View.VISIBLE + feedingBinding.feedingMethodButtons.visibility = View.VISIBLE + feedingBinding.feedingMethodSpinner.visibility = View.GONE + } else { + feedingBinding.feedingTypeButtons.visibility = View.GONE + feedingBinding.feedingTypeSpinner.visibility = View.VISIBLE + feedingBinding.feedingMethodButtons.visibility = View.GONE + feedingBinding.feedingMethodSpinner.visibility = View.VISIBLE + } + + if (feedingBinding.feedingTypeSpinner.isVisible && feedingBinding.feedingMethodSpinner.isVisible) { + saveButton.visibility = View.VISIBLE + } else { + saveButton.visibility = View.GONE + } + } + + override suspend fun createEntry(timer: Timer): TimeEntry { + return fragment.mainActivity.client.v2client.createEntry( + FeedingEntry::class, + FeedingEntry( + id = 0, + childId = childId, + start = timer.start, + end = nowServer(), + feedingType = selectedType!!, + feedingMethod = selectedMethod!!, + amount = feedingBinding.amountNumberPicker.value?.toDouble(), + _notes = feedingBinding.noteEditor.text.toString() + ) + ) + } + + private fun populateButtonList( + textArray: Array, + buttons: ViewGroup, + spinner: Spinner, + callback: ButtonListCallback + ) { + spinner.visibility = View.GONE + buttons.visibility = View.VISIBLE + buttons.removeAllViewsInLayout() + for (i in textArray.indices) { + val button = Button(fragment.requireContext()) + button.setOnClickListener { + spinner.setSelection(i) + spinner.visibility = View.VISIBLE + buttons.visibility = View.GONE + callback.onSelectionChanged(i) + } + button.text = textArray[i] + button.setLayoutParams( + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + 1f + ) + ) + buttons.addView(button) + } + } + + private fun setupFeedingMethodButtons(type: FeedingTypeEnum) { + when (type) { + FeedingTypeEnum.BREAST_MILK -> { + assignedMethodButtons = listOf( + FeedingMethodEnum.LEFT_BREAST, + FeedingMethodEnum.RIGHT_BREAST, + FeedingMethodEnum.BOTH_BREASTS, + FeedingMethodEnum.BOTTLE, + FeedingMethodEnum.PARENT_FED, + FeedingMethodEnum.SELF_FED, + ) + } + + else -> { + assignedMethodButtons = listOf( + FeedingMethodEnum.BOTTLE, + FeedingMethodEnum.PARENT_FED, + FeedingMethodEnum.SELF_FED, + ) + } + } + + val orgItems: Array = fragment.resources.getTextArray(R.array.feedingMethods) + val textItems: MutableList = ArrayList(10) + for (i in assignedMethodButtons.indices) { + textItems.add(orgItems[assignedMethodButtons.get(i).value]) + } + feedingBinding.feedingMethodSpinner.setAdapter( + ArrayAdapter( + fragment.requireContext(), + android.R.layout.simple_spinner_dropdown_item, + textItems + ) + ) + populateButtonList( + textItems.toTypedArray(), + feedingBinding.feedingMethodButtons, + feedingBinding.feedingMethodSpinner, + object : ButtonListCallback { + override fun onSelectionChanged(i: Int) { + selectedMethod = assignedMethodButtons[i].post_name + updateVisuals() + } + } + ) + feedingBinding.feedingMethodSpinner.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + selectedMethod = assignedMethodButtons[position].post_name + updateVisuals() + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + selectedMethod = null + updateVisuals() + } + } + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PumpingRecord( + @JsonProperty("amount") val amount: Double, + @JsonProperty("note") val note: String, +) + +class PumpingLoggingController( + fragment: BaseFragment, + childId: Int, + timerControl: TimerControlInterface +) : GenericLoggingController(fragment, childId, timerControl, PumpingEntry::class) { + val pumpingBinding = PumpingLoggingEntryBinding.inflate(fragment.layoutInflater) + + override val uiCurrentTimerTime = pumpingBinding.currentTimerTime + override val uiNoteEditor = pumpingBinding.noteEditor + override val saveButton: ImageButton = pumpingBinding.sendButton + override val controlsView: View = pumpingBinding.root + + val amountNumberPicker: HorizontalDecIncEditor = pumpingBinding.amountNumberPicker + + override fun postInit() { + super.postInit() + + amountNumberPicker.allowNull = false + amountNumberPicker.value = 0.0 + + fragment.mainActivity.storage.child(childId, "pumping")?.let { + amountNumberPicker.value = it.amount ?: 0.0 + uiNoteEditor.setText(it.note) + } + + updateVisuals() + } + + override fun storeStateForSuspend() { + val pr = PumpingRecord( + pumpingBinding.amountNumberPicker.value?.toDouble() ?: 0.0, + pumpingBinding.noteEditor.text.toString() + ) + fragment.mainActivity.storage.child(childId, "pumping", pr) + } + + override fun reset() { + pumpingBinding.amountNumberPicker.value = null + pumpingBinding.noteEditor.setText("") + storeStateForSuspend() + } + + override suspend fun createEntry(timer: Timer): TimeEntry { + return fragment.mainActivity.client.v2client.createEntry( + PumpingEntry::class, + PumpingEntry( + id = 0, + childId = childId, + _start = timer.start, + _end = nowServer(), + amount = amountNumberPicker.value!!.toDouble(), + _notes = uiNoteEditor.text.toString(), + _legacyTime = timer.start + ) + ) + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class LoggingButtonControllerStoreState( + @JsonProperty("open_state") val openState: Array, +) + +class LoggingButtonController( + val fragment: BaseFragment, + val bindings: BabyManagerBinding, + val controlsInterface: FragmentCallbacks, + val child: BabyBuddyClient.Child, + val timerControl: TimerControlInterface, +) : TimersUpdatedCallback { + val logicMap = mapOf( + BabyBuddyClient.EVENTS.CHANGE to SwitchButtonLogic( + bindings.diaperDisabledButton, bindings.diaperEnabledButton, false + ), + BabyBuddyClient.EVENTS.NOTE to SwitchButtonLogic( + bindings.notesDisabledButton, bindings.notesEnabledButton, false + ), + BabyBuddyClient.ACTIVITIES.SLEEP to SwitchButtonLogic( + bindings.sleepDisabledButton, bindings.sleepEnabledButton, false + ), + BabyBuddyClient.ACTIVITIES.FEEDING to SwitchButtonLogic( + bindings.feedingDisabledButton, bindings.feedingEnabledButton, false + ), + BabyBuddyClient.ACTIVITIES.TUMMY_TIME to SwitchButtonLogic( + bindings.tummyTimeDisabledButton, bindings.tummyTimeEnabledButton, false + ), + BabyBuddyClient.ACTIVITIES.PUMPING to SwitchButtonLogic( + bindings.pumpingDisabledButton, bindings.pumpingEnabledButton, false + ), + ) + + val loggingControllers: Map = mapOf( + BabyBuddyClient.EVENTS.CHANGE to DiaperLoggingController(fragment, child.id), + BabyBuddyClient.EVENTS.NOTE to NotesLoggingController(fragment, child.id), + BabyBuddyClient.ACTIVITIES.SLEEP to SleepLoggingController( + fragment, child.id, timerControl + ), + BabyBuddyClient.ACTIVITIES.TUMMY_TIME to TummyTimeLoggingController( + fragment, child.id, timerControl + ), + BabyBuddyClient.ACTIVITIES.FEEDING to FeedingLoggingController( + fragment, child.id, timerControl + ), + BabyBuddyClient.ACTIVITIES.PUMPING to PumpingLoggingController( + fragment, child.id, timerControl + ), + ) + + private var timerHandler: Handler? = Handler(fragment.mainActivity.mainLooper) + private var cachedTimers = emptyArray() + private val timerModificationsBlocker = ConcurrentEventBlocker() + + init { + loggingControllers.forEach { (activity, controller) -> + controller.postInit() + + logicMap[activity]?.addStateListener { state, userInduced -> + fragment.mainActivity.scope.launch { + timerModificationsBlocker.wait() + if (state) { + startTimerFromSwitch(controller, userInduced, activity) + } else { + stopTimerFromSwitch(controller, userInduced, activity) + } + } + } + controller.saveButton.setOnClickListener { + fragment.mainActivity.scope.launch { + timerModificationsBlocker.wait() + runSave(activity, controller) + } + } + + timerControl.registerTimersUpdatedCallback(this) + } + + fragment.mainActivity.storage.child( + child.id, "loggingstate" + )?.let { + for ((k, logic) in logicMap.entries) { + if (k !in BabyBuddyClient.EVENTS.ALL) continue + if (k in it.openState) { + logic.state = true + } + } + } + + timerHandler() + } + + private fun startTimerFromSwitch( + controller: LoggingControls, + userInduced: Boolean, + activity: String + ) { + controlsInterface.insertControls(controller.controlsView) + if (userInduced && (controller is TimerBase)) { + cachedTimers.firstOrNull { it.name == activity }?.let { + fragment.mainActivity.scope.launch { + try { + timerModificationsBlocker.register { + val newTimer = AsyncPromise.call { promise -> + timerControl.startTimer(it, promise) + } + controller.updateTimer(newTimer) + } + } + catch (e: AsyncPromiseFailure) { + (e.value as? TranslatedException)?.let { + fragment.showError( + true, + R.string.activity_store_failure_message, + it.message + ) + } + } + } + } + } + } + + private suspend fun stopTimerFromSwitch( + controller: LoggingControls, + userInduced: Boolean, + activity: String + ) { + timerModificationsBlocker.register { + val timer = cachedTimers.firstOrNull { it.name == activity } + if (AsyncPromise.call { promise -> + var defaultSucceed = true + timer?.let { timer -> + if (timer.active && userInduced) { + val timeMs = nowServer().time - timer.start.time + if (timeMs > 10000) { + defaultSucceed = false; + + val message = Phrase.from( + fragment.requireContext(), + R.string.cancel_timer_warning_message + ) + .put("activity", fragment.translateActivityName(activity)) + .format().toString() + + fragment.showQuestion( + true, + fragment.getString(R.string.cancel_timer_warning_title), + message, + fragment.getString(R.string.cancel_timer_warning_stop), + fragment.getString(R.string.cancel_timer_warning_keep), + object : DialogCallback { + override fun call(b: Boolean) { + promise.succeeded(b) + } + } + ); + } + } + } + if (defaultSucceed) { + promise.succeeded(true) + } + }) { + controlsInterface.removeControls(controller.controlsView) + if (userInduced && (controller is TimerBase)) { + timer?.let { + fragment.mainActivity.scope.launch { + try { + controller.updateTimer(null) + AsyncPromise.call { promise -> + timerControl.stopTimer(it, promise) + } + controller.updateTimer(null) + } + catch (e: AsyncPromiseFailure) { + (e.value as? TranslatedException)?.let { + fragment.showError( + true, + R.string.activity_store_failure_message, + it.message + ) + } + } + } + } + } + } + } + } + + private fun timerHandler() { + timerHandler?.let { + it.postDelayed(Runnable { timerHandler() }, 500) + for (c in loggingControllers.values) { + c.updateVisuals() + } + } + } + + suspend fun runSave(activity: String, controller: LoggingControls) { + timerModificationsBlocker.wait() + timerModificationsBlocker.register { + try { + logicMap[activity]?.state = false + val te = controller.save() + controller.reset() + storeStateForSuspend() + controlsInterface.updateTimeline(te) + } + catch (e: RequestCodeFailure) { + fragment.showError( + true, + R.string.activity_store_failure_message, + Phrase.from( + fragment.requireContext(), + R.string.activity_store_failure_server_error + ) + .put( + "message", + fragment.getString(R.string.activity_store_failure_server_error_general) + ) + .put("server_message", e.jsonErrorMessages().joinToString(", ")) + .format().toString() + + ) + } + catch (e: IOException) { + fragment.showError( + true, + R.string.activity_store_failure_message, + R.string.activity_store_failure_server_error_generic_ioerror + ) + } + } + } + + fun storeStateForSuspend() { + val openState = mutableListOf() + for ((name, controller) in loggingControllers) { + controller.storeStateForSuspend() + if (name in BabyBuddyClient.EVENTS.ALL) { + if (logicMap[name]?.state == true) { + openState.add(name) + } + } + } + fragment.mainActivity.storage.child( + child.id, + "loggingstate", + LoggingButtonControllerStoreState(openState.toTypedArray()) + ) + } + + fun destroy() { + storeStateForSuspend() + for (controller in loggingControllers.values) { + controlsInterface.removeControls(controller.controlsView) + } + for (logic in logicMap.values) { + logic.destroy() + } + timerControl.unregisterTimersUpdatedCallback(this) + timerHandler = null + } + + override fun newTimerListLoaded(timers: Array) { + cachedTimers = timers; + if (timerModificationsBlocker.isBlocked) return + + val toDisable = + loggingControllers.filter { it.value is TimerBase }.map { it.key }.toMutableList() + for (timer in timers) { + if (!timer.active) continue + loggingControllers[timer.name]?.let { controller -> + if (controller is TimerBase) { + toDisable.remove(timer.name) + + controller.updateTimer(timer) + controller.updateVisuals() + controlsInterface.insertControls(controller.controlsView) + } + } + logicMap[timer.name]?.let { logic -> + logic.state = true + } + } + for (name in toDisable) { + logicMap[name]?.state = false + loggingControllers[name]?.let { + controlsInterface.removeControls(it.controlsView) + } + } + } +} diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerListProvider.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerListProvider.java deleted file mode 100644 index e0341a5..0000000 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerListProvider.java +++ /dev/null @@ -1,125 +0,0 @@ -package eu.pkgsoftware.babybuddywidgets.timers; - -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import org.jetbrains.annotations.NotNull; - -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import eu.pkgsoftware.babybuddywidgets.BaseFragment; -import eu.pkgsoftware.babybuddywidgets.history.ChildEventHistoryLoader; -import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient; - -public class TimerListProvider extends RecyclerView.Adapter implements TimersUpdatedCallback { - private BabyBuddyClient.Timer[] timers = new BabyBuddyClient.Timer[0]; - - private final BaseFragment baseFragment; - private final List holders = new LinkedList<>(); - private final TimerControlInterface timerControls; - - public TimerListProvider( - @NotNull BaseFragment baseFragment, - @NotNull TimerControlInterface timerControls - ) { - super(); - this.baseFragment = baseFragment; - this.timerControls = timerControls; - this.timerControls.registerTimersUpdatedCallback(this); - } - - private int[] listIds(BabyBuddyClient.Timer[] t) { - int[] result = new int[t.length]; - for (int i = 0; i < t.length; i++) { - result[i] = t[i].id; - } - Arrays.sort(result); - return result; - } - - private boolean compareIds(BabyBuddyClient.Timer[] t1, BabyBuddyClient.Timer[] t2) { - return Arrays.equals(listIds(t1), listIds(t2)); - } - - private TimerListViewHolder findHolderForTimer(BabyBuddyClient.Timer t) { - TimerListViewHolder result = null; - for (TimerListViewHolder h : holders) { - if (h.getTimer().id == t.id) { - if (result != null) { - return null; // Multiple timers - dismiss - } else { - result = h; - } - } - } - return result; - } - - @Override - public TimerListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - eu.pkgsoftware.babybuddywidgets.databinding.QuickTimerEntryBinding entryBinding = eu.pkgsoftware.babybuddywidgets.databinding.QuickTimerEntryBinding.inflate(LayoutInflater.from(parent.getContext())); - return new TimerListViewHolder(baseFragment, entryBinding, timerControls); - } - - @Override - public void onViewAttachedToWindow(@NonNull TimerListViewHolder holder) { - super.onViewAttachedToWindow(holder); - holders.add(holder); - } - - @Override - public void onViewDetachedFromWindow(@NonNull TimerListViewHolder holder) { - super.onViewDetachedFromWindow(holder); - holders.remove(holder); - } - - @Override - public void onBindViewHolder(TimerListViewHolder holder, int position) { - holder.assignTimer(timers[position]); - } - - @Override - public int getItemCount() { - return timers.length; - } - - public void close() { - for (TimerListViewHolder h : holders) { - h.close(); - } - } - - @Override - public void newTimerListLoaded(@NonNull BabyBuddyClient.Timer[] timers) { - timers = timers.clone(); - Arrays.sort(timers, (a, b) -> Integer.compare(a.id, b.id)); - - if (!compareIds(timers, this.timers)) { - this.timers = timers; - notifyDataSetChanged(); - } else { - for (int i = 0; i < timers.length; i++) { - final TimerListViewHolder timerHolder = findHolderForTimer(timers[i]); - if (!this.timers[i].equals(timers[i])) { - BabyBuddyClient.Timer probeTimer = timers[i].clone(); - probeTimer.start = this.timers[i].start; - probeTimer.end = this.timers[i].end; - boolean probeTimerEqual = probeTimer.equals(this.timers[i]); - - this.timers[i] = timers[i]; - if (probeTimerEqual && (timerHolder != null)) { - timerHolder.assignTimer(timers[i]); - } else { - notifyItemChanged(i); - } - } else if (timerHolder != null) { - timerHolder.updateNoChange(); - } - } - } - } -} diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerListViewHolder.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerListViewHolder.java deleted file mode 100644 index 466aebb..0000000 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerListViewHolder.java +++ /dev/null @@ -1,250 +0,0 @@ -package eu.pkgsoftware.babybuddywidgets.timers; - -import android.os.Handler; - -import com.squareup.phrase.Phrase; - -import org.jetbrains.annotations.NotNull; - -import java.util.Date; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import eu.pkgsoftware.babybuddywidgets.BaseFragment; -import eu.pkgsoftware.babybuddywidgets.CredStore; -import eu.pkgsoftware.babybuddywidgets.NotesControl; -import eu.pkgsoftware.babybuddywidgets.NotesEditorLogic; -import eu.pkgsoftware.babybuddywidgets.R; -import eu.pkgsoftware.babybuddywidgets.databinding.NotesEditorBinding; -import eu.pkgsoftware.babybuddywidgets.databinding.QuickTimerEntryBinding; -import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient; -import eu.pkgsoftware.babybuddywidgets.networking.RequestCodeFailure; -import eu.pkgsoftware.babybuddywidgets.utils.Promise; -import eu.pkgsoftware.babybuddywidgets.widgets.SwitchButtonLogic; - -public class TimerListViewHolder extends RecyclerView.ViewHolder { - private final @NotNull QuickTimerEntryBinding binding; - - private final @NotNull BaseFragment baseFragment; - private final BabyBuddyClient client; - private final Handler timerHandler; - private final TimerControlInterface timerControl; - - private final SwitchButtonLogic notesEditorSwitch; - private final NotesEditorLogic notesEditor; - - private SwitchButtonLogic startStopLogic = null; - - private BabyBuddyClient.Timer timer = null; - private Long timerStartTime = null; - - private boolean isClosed = false; - - private String padToLen(String s, char c, int length) { - StringBuilder sBuilder = new StringBuilder(s); - while (sBuilder.length() < length) { - sBuilder.insert(0, c); - } - return sBuilder.toString(); - } - - private boolean newUpdatedPosted = false; - - private void updateTimerTime() { - if (timerStartTime == null) { - binding.currentTimerTime.setText(""); - } else { - long diff = System.currentTimeMillis() + timerStartTime; - - int seconds = (int) diff / 1000; - int minutes = seconds / 60; - int hours = minutes / 60; - - binding.currentTimerTime.setText( - "HH:MM:ss" - .replaceAll("HH", "" + hours) - .replaceAll("MM", padToLen("" + (minutes % 60), '0', 2)) - .replaceAll("ss", padToLen("" + (seconds % 60), '0', 2)) - ); - - if (!newUpdatedPosted) { - timerHandler.postDelayed(() -> { - newUpdatedPosted = false; - if (!isClosed) { - updateTimerTime(); - } - }, 500); - newUpdatedPosted = true; - } - } - } - - public TimerListViewHolder( - BaseFragment baseFragment, - QuickTimerEntryBinding binding, - TimerControlInterface timerControl - ) { - super(binding.getRoot()); - - this.baseFragment = baseFragment; - this.binding = binding; - this.timerControl = timerControl; - - client = baseFragment.getMainActivity().getClient(); - - binding.currentTimerTime.setText(""); - timerHandler = new Handler(baseFragment.getMainActivity().getMainLooper()); - - notesEditorSwitch = new SwitchButtonLogic( - binding.addNoteButton, binding.removeNoteButton, false - ); - NotesEditorBinding notesBinding = NotesEditorBinding.inflate( - baseFragment.getMainActivity().getLayoutInflater() - ); - binding.verticalRoot.addView(notesBinding.getRoot()); - notesEditor = new NotesEditorLogic(notesBinding, false); - notesEditorSwitch.addStateListener((v, userTriggered) -> notesEditor.setVisible(v)); - - startStopLogic = new SwitchButtonLogic( - binding.appStartTimerButton, - binding.appStopTimerButton, - false - ); - startStopLogic.addStateListener( - (active, userClicked) -> { - if (timer == null) { - return; - } - if (!userClicked) { - return; - } - - if (active) { - this.timerControl.startTimer(timer, new Promise<>() { - @Override - public void succeeded(BabyBuddyClient.Timer t) { - timer = t; - updateActiveState(); - } - - @Override - public void failed(TranslatedException s) { - } - }); - } else { - if (BabyBuddyClient.ACTIVITIES.index(timer.name) < 0) { - throw new UnsupportedOperationException("Activity does not exist: " + timer.name); - } - this.timerControl.storeActivity( - timer, - timer.name, - notesEditor.getText(), - new Promise<>() { - @Override - public void succeeded(Boolean stopped) { - if (stopped == null) { - stopped = true; - } - - if (stopped) { - timer.active = false; - updateActiveState(); - - notesEditor.clearText(); - notesEditorSwitch.setState(false); - } - } - - @Override - public void failed(Exception e) { - tryResolveStoreError(e); - } - } - ); - } - } - ); - } - - private void tryResolveStoreError(@NotNull Exception error) { - new ResolveConflicts(baseFragment, timer, timerControl) { - @Override - protected void updateTimerActiveState() { - updateActiveState(); - } - - @Override - protected void finished() { - } - }.tryResolveStoreError(error); - } - - private void updateActiveState() { - startStopLogic.setState(timer.active); - if ((timer == null) || (!timer.active)) { - timerStartTime = null; - } else { - timerStartTime = new Date().getTime() - timer.start.getTime() + client.getServerDateOffsetMillis() - System.currentTimeMillis(); - } - updateTimerTime(); - } - - public void assignTimer(BabyBuddyClient.Timer timer) { - if (isClosed) { - isClosed = false; - updateTimerTime(); - } - - this.timer = timer; - - String name = timer.readableName(); - final int activityIndex = BabyBuddyClient.ACTIVITIES.index(timer.name); - if (activityIndex >= 0) { - final String[] names = baseFragment.getResources().getStringArray(R.array.timerTypeNames); - name = names[activityIndex]; - } - binding.timerName.setText(name); - - updateActiveState(); - - notesEditor.setNotes( - new NotesControl() { - @Override - public void persistChanges() { - baseFragment.getMainActivity().getCredStore().storePrefs(); - } - - @Override - public void setNotes(@NonNull CredStore.Notes notes) { - timerControl.setNotes(timer, notes); - } - - @NonNull - @Override - public CredStore.Notes getNotes() { - return timerControl.getNotes(timer); - } - } - ); - notesEditorSwitch.setState(notesEditor.isVisible()); - } - - /** - * Called if a new data frame was received from the server, but no timer-data was - * changed. - */ - public void updateNoChange() { - // We might have "short circuited" the timer-active state. If this was the case, - // re-enable the timer now! - updateActiveState(); - } - - public BabyBuddyClient.Timer getTimer() { - return timer.clone(); - } - - public void close() { - timerStartTime = null; - isClosed = true; - } -} diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/utils/AmountValuesGenerator.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/utils/AmountValuesGenerator.java new file mode 100644 index 0000000..421dc84 --- /dev/null +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/utils/AmountValuesGenerator.java @@ -0,0 +1,89 @@ +package eu.pkgsoftware.babybuddywidgets.timers.utils; + +import java.text.DecimalFormat; + +import eu.pkgsoftware.babybuddywidgets.widgets.HorizontalNumberPicker; + +public class AmountValuesGenerator implements HorizontalNumberPicker.ValueGenerator { + public static final DecimalFormat FORMAT_VALUE = new DecimalFormat("#.#"); + + public long minValue() { + return -1L; + } + + public long maxValue() { + return 1 + 5 * 9; + } // 5 orders of magnitude + + private long calcBaseValue(long index) { + return (long) Math.round(Math.pow(10, (double) Math.max(0, (index / 9)))); + } + + public String getValue(long index) { + if (index < 0) { + return "None"; + } else { + return FORMAT_VALUE.format(getRawValue(index, 0f)); + } + } + + public Double getRawValue(long index, float offset) { + if (index + offset < -0.001) { + return null; + } else { + if (offset < 0) { + index--; + offset += 1.0f; + if (index < 0) { + index = 0; + offset = 0.0f; + } + } + + if (index == 0) { + return (double) offset; + } + return (double) calcBaseValue(index - 1) * (((index - 1) % 9 + 1) + offset); + } + } + + public long getValueIndex(Double value) { + if (value == null) { + return -1L; + } else { + long exp = (long) Math.max(0, Math.floor(Math.log10(value))); + long base10 = Math.round(Math.pow(10, exp)); + double relativeRest = value / base10; + + long index = (long) Math.floor(relativeRest); + double offset = relativeRest - index; + if (offset >= 0.5) { + offset -= 1.0; + index += 1; + } + index += 9 * exp; + + return Math.max(minValue(), Math.min(maxValue(), index)); + } + } + + public double getValueOffset(Double value) { + if (value == null) { + return 0.0d; + } else { + long exp = (long) Math.max(0, Math.floor(Math.log10(value))); + long base10 = Math.round(Math.pow(10, exp)); + double relativeRest = value / base10; + + long index = (long) Math.floor(relativeRest); + double offset = relativeRest - index; + if (offset >= 0.5) { + offset -= 1.0; + index += 1; + } + index += 9 * exp; + + return Math.max(-0.5, Math.min(0.5, offset)); + } + } +} diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/utils/AsyncPromise.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/utils/AsyncPromise.kt index 16e1738..e218a0a 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/utils/AsyncPromise.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/utils/AsyncPromise.kt @@ -6,6 +6,8 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine +class EmptyException() : Exception() + class AsyncPromiseFailure(val value: Any) : Exception() class AsyncPromise() { diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/utils/ConcurrentEventBlocker.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/utils/ConcurrentEventBlocker.kt new file mode 100644 index 0000000..57c6038 --- /dev/null +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/utils/ConcurrentEventBlocker.kt @@ -0,0 +1,31 @@ +package eu.pkgsoftware.babybuddywidgets.utils + +class ConcurrentEventBlocker { + private var internalBlockCounter = 0 + private val subscribed = mutableListOf>() + + val isBlocked: Boolean + get() = internalBlockCounter > 0 + + suspend fun wait() { + if (internalBlockCounter > 0) { + AsyncPromise.call { promise -> + subscribed.add(promise) + } + } + } + + suspend fun register(body: suspend () -> Unit) { + internalBlockCounter++ + try { + body.invoke() + } + finally { + internalBlockCounter-- + if (internalBlockCounter == 0) { + subscribed.forEach { it.succeeded(Unit) } + subscribed.clear() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/widgets/AutoHGrid.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/widgets/AutoHGrid.kt new file mode 100644 index 0000000..4658b71 --- /dev/null +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/widgets/AutoHGrid.kt @@ -0,0 +1,181 @@ +package eu.pkgsoftware.babybuddywidgets.widgets + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import eu.pkgsoftware.babybuddywidgets.R +import eu.pkgsoftware.babybuddywidgets.Tools + +enum class RowAlign(val value: Int) { + TOP(0), + CENTER(1), + BOTTOM(2) +} + +class AutoHGrid : ViewGroup { + internal data class RowData( + val children: List, + val width: Int, + val maxHeight: Int + ) + + var rowAlign: RowAlign = RowAlign.CENTER + var rowSpacing: Float = 0f + var equalizeRowWidths: Boolean = false + + constructor(context: Context) : super(context) { + init(null, 0) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init(attrs, 0) + } + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + init(attrs, defStyle) + } + + fun init(attrs: AttributeSet?, defStyle: Int) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.AutoHGrid, defStyle, 0) + val rowAlignRaw = typedArray.getInt(R.styleable.AutoHGrid_rowAlign, RowAlign.CENTER.value) + + rowAlign = RowAlign.entries.firstOrNull { it.value == rowAlignRaw } ?: RowAlign.CENTER + rowSpacing = typedArray.getDimension(R.styleable.AutoHGrid_rowSpacing, 0f) + equalizeRowWidths = typedArray.getBoolean(R.styleable.AutoHGrid_equalizeRowWidths, false) + + typedArray.recycle() + } + + private tailrec fun equalizeRowWidths(initialRows: List): List { + val maxWidth = initialRows.maxOf { it.width } + val spacing = Tools.dpToPx(context, rowSpacing) + + val resultRows = initialRows.map { row -> row.children.toMutableList() }.toMutableList() + var rowWidths = resultRows.map { + row -> row.sumOf { it.measuredWidth } + spacing * (row.size - 1) + } + + fun generateRowData(): List { + return resultRows.map { + row -> RowData( + row, + row.sumOf { it.measuredWidth } + spacing * (row.size - 1), + row.maxOf { it.measuredHeight }) + } + } + + while (true) { + var modified = false + for (i in (1 until resultRows.size).reversed()) { + val prevI = i - 1 + if (rowWidths[i] >= rowWidths[prevI]) { + continue + } + var newRowWidth = rowWidths[i] + resultRows[prevI].last().measuredWidth + if (rowWidths[i] > 0) newRowWidth += spacing + if (newRowWidth > maxWidth) { + continue + } + resultRows[i].add(0, resultRows[prevI].removeLast()) + modified = true + break + } + rowWidths = resultRows.map { + row -> row.sumOf { it.measuredWidth } + spacing * (row.size - 1) + } + val newMaxWidth = rowWidths.maxOrNull() ?: 0 + if (newMaxWidth < maxWidth) { + return equalizeRowWidths(generateRowData()) + } + if (!modified || (newMaxWidth > maxWidth)) { + return generateRowData() + } + } + } + + private fun computeRows(width: Int): List { + val placeableChildren = children.filter { it.visibility != View.GONE }.toList() + + val rows = mutableListOf(mutableListOf()) + val rowWidths = mutableListOf() + + val spacing = Tools.dpToPx(context, rowSpacing) + + var currentRowWidth = 0 + for (child in placeableChildren) { + val widthWithSpacing = child.measuredWidth + if (currentRowWidth > 0) spacing else 0 + if (currentRowWidth + widthWithSpacing > width) { + rows.add(mutableListOf()) + rowWidths.add(currentRowWidth) + currentRowWidth = 0 + } + rows.last().add(child) + currentRowWidth += child.measuredWidth + if (currentRowWidth > 0) spacing else 0 + } + rowWidths.add(currentRowWidth) + + val result = mutableListOf() + for (i in rows.indices) { + val row = rows[i] + if (row.isEmpty()) { + continue + } + result.add(RowData(row, rowWidths[i], row.maxOf { it.measuredHeight })) + } + + if (equalizeRowWidths) { + return equalizeRowWidths(result) + } else { + return result + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val width = MeasureSpec.getSize(widthMeasureSpec) + + val placeableChildren = children.filter { it.visibility != View.GONE }.toList() + for (child in placeableChildren) { + measureChild(child, widthMeasureSpec, heightMeasureSpec) + } + + val rows = computeRows(width) + + val height = rows.sumOf { it.maxHeight } + super.onMeasure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) + ) + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + val width = r - l + val spacing = Tools.dpToPx(context, rowSpacing) + + val rows = computeRows(width) + var currentY = 0 + for (row in rows) { + var currentX = (width - row.width) / 2 + for (child in row.children) { + val yOffset = when (rowAlign) { + RowAlign.TOP -> 0 + RowAlign.CENTER -> (row.maxHeight - child.measuredHeight) / 2 + RowAlign.BOTTOM -> row.maxHeight - child.measuredHeight + } + child.layout( + currentX, + currentY + yOffset, + currentX + child.measuredWidth, + currentY + child.measuredHeight + yOffset + ) + currentX += child.measuredWidth + spacing + } + currentY += row.maxHeight + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/widgets/HorizontalDecIncEditor.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/widgets/HorizontalDecIncEditor.kt new file mode 100644 index 0000000..7914c47 --- /dev/null +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/widgets/HorizontalDecIncEditor.kt @@ -0,0 +1,81 @@ +package eu.pkgsoftware.babybuddywidgets.widgets + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import eu.pkgsoftware.babybuddywidgets.databinding.HorizontalDecIncEditorBinding +import kotlin.math.abs +import kotlin.math.floor +import kotlin.math.log10 +import kotlin.math.max +import kotlin.math.pow + +class HorizontalDecIncEditor : LinearLayout { + var value: Double? + get() { + return binding.numberEditor.text.toString().toDoubleOrNull() + } + set(value) { + val v = value.let { + if ((it == null) || (it < 0.0)) { + if (allowNull) { + return@let null + } else { + return@let 0.0 + } + } + it + } + if (v == null) { + binding.numberEditor.setText("") + } else { + binding.numberEditor.setText(v.toString()) + } + } + + var allowNull: Boolean = true + set(value) { + field = value + if (!value && this.value == null) { + this.value = 0.0 + } + } + + val binding = HorizontalDecIncEditorBinding.inflate( + LayoutInflater.from(context), this, true + ) + + private val incrementValue: Int + get() = max(1, (10.0).pow(floor(log10(abs(value?.toDouble() ?: 1.0)))).toInt()) + + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + init() + } + + private fun init() { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + value = null + + binding.decButton.setOnClickListener { + value = (value ?: 0.0) - incrementValue + binding.numberEditor.selectAll() + } + binding.incButton.setOnClickListener { + value = (value ?: 0.0) + incrementValue + binding.numberEditor.selectAll() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/widgets/SwitchButtonLogic.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/widgets/SwitchButtonLogic.java index 0b2cb62..d96a881 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/widgets/SwitchButtonLogic.java +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/widgets/SwitchButtonLogic.java @@ -25,12 +25,20 @@ public SwitchButtonLogic(ImageButton onButton, ImageButton offButton, boolean st updateVisibilityState(); } + public void destroy() { + onButton.setOnClickListener(null); + offButton.setOnClickListener(null); + } + private void updateVisibilityState() { onButton.setVisibility(state ? View.GONE : View.VISIBLE); offButton.setVisibility(state ? View.VISIBLE : View.GONE); } private void setState(boolean b, boolean userInduced) { + if (b == state) { + return; + } state = b; updateVisibilityState(); diff --git a/app/src/main/res/layout/baby_manager.xml b/app/src/main/res/layout/baby_manager.xml index 22596ce..9c6ba6d 100644 --- a/app/src/main/res/layout/baby_manager.xml +++ b/app/src/main/res/layout/baby_manager.xml @@ -2,144 +2,221 @@ - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toTopOf="parent"> - + android:layout_height="wrap_content" + app:equalizeRowWidths="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/diaper_logging_entry.xml b/app/src/main/res/layout/diaper_logging_entry.xml new file mode 100644 index 0000000..9aea449 --- /dev/null +++ b/app/src/main/res/layout/diaper_logging_entry.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/feeding_fragment.xml b/app/src/main/res/layout/feeding_fragment.xml deleted file mode 100644 index 9b42987..0000000 --- a/app/src/main/res/layout/feeding_fragment.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - -