From 945e5dedcef970d6a6cb945977b345da12e06c0d Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Thu, 2 Mar 2023 21:22:19 +0100 Subject: [PATCH 01/17] Show Data from DB, during loading times --- .../data/repository/NetworkBoundResource.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/NetworkBoundResource.java b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/NetworkBoundResource.java index bfc3fed4..ad67bd41 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/NetworkBoundResource.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/NetworkBoundResource.java @@ -32,11 +32,11 @@ /** * A generic class that can provide a resource backed by both the sqlite database and the network. Copied from the - * official Google architecture-components github-sample under https://github - * .com/android/architecture-components-samples/blob/master/GithubBrowserSample/app/src/main/java/com/android - * /example/github/repository/NetworkBoundResource.kt + * official Google architecture-components github-sample under NetworkBoundResource Source * - * You can read more about it in the [Architecture Guide](https://developer.android.com/arch). + * You can read more about it in the Architecture Guide */ public abstract class NetworkBoundResource extends ExtendedMediatorLiveData> { @@ -60,6 +60,9 @@ private void dbSourceObserver(DbType data, LiveData dbSourceLiveData) { dbObject = data; removeSource(dbSourceLiveData); if (shouldFetch(dbObject)) { + addSource(dbSource, newData -> { + updateValue(Resource.success(convertDbTypeToResultType(newData))); + }); LiveData endpointLiveData = loadEndpoint(); addSource(endpointLiveData, baseEndpoint -> { endpointLiveDataObserver(baseEndpoint, endpointLiveData); @@ -73,9 +76,7 @@ private void dbSourceObserver(DbType data, LiveData dbSourceLiveData) { } private void endpointLiveDataObserver(BaseEndpoint endpoint, LiveData endpointLiveData) { - if (endpoint == null) { - updateValue(Resource.loading(null)); - } else { + if (endpoint != null) { this.endpoint = endpoint; fetchFromNetwork(dbSource); removeSource(endpointLiveData); @@ -85,9 +86,6 @@ private void endpointLiveDataObserver(BaseEndpoint endpoint, LiveData dbSource) { LiveData> apiResponse = loadFromNetwork(); // we re-attach dbSource as a new source, it will dispatch its latest value quickly - addSource(dbSource, newData -> { - updateValue(Resource.loading(convertDbTypeToResultType(newData))); - }); addSource(apiResponse, response -> { removeSource(apiResponse); removeSource(dbSource); From 60055133e976997da54858911e5e94ae39575193 Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Thu, 2 Mar 2023 21:27:24 +0100 Subject: [PATCH 02/17] Show cached lights, while the network is loading --- .../sunriseClock/ui/light/LightsFragment.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/LightsFragment.java b/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/LightsFragment.java index ddeac83b..f44666d7 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/LightsFragment.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/LightsFragment.java @@ -1,5 +1,6 @@ package org.d3kad3nt.sunriseClock.ui.light; +import android.content.Context; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -18,9 +19,9 @@ import org.d3kad3nt.sunriseClock.data.model.endpoint.IEndpointUI; import org.d3kad3nt.sunriseClock.data.model.light.UILight; import org.d3kad3nt.sunriseClock.data.model.resource.Resource; -import org.d3kad3nt.sunriseClock.data.model.resource.Status; import org.d3kad3nt.sunriseClock.databinding.LightsFragmentBinding; import org.d3kad3nt.sunriseClock.ui.MainActivity; +import org.jetbrains.annotations.Contract; import java.util.List; @@ -32,6 +33,8 @@ public class LightsFragment extends Fragment { private Spinner endpointSpinner; private LightsListAdapter adapter; + @NonNull + @Contract(" -> new") public static LightsFragment newInstance() { return new LightsFragment(); } @@ -48,12 +51,17 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c @Override public void onChanged(Resource> listResource) { Log.d(TAG, listResource.getStatus().toString()); - if (listResource.getStatus().equals(Status.SUCCESS) && listResource.getData() != null) { - lightsState.clearError(); + if (listResource.getData() != null) { adapter.submitList(listResource.getData()); - } else if (listResource.getStatus().equals(Status.ERROR)) { - lightsState.setError(getResources().getString(R.string.noLights_title), - listResource.getMessage()); + } + switch (listResource.getStatus()) { + case SUCCESS: + lightsState.clearError(); + break; + case ERROR: + String errorMsg = getResources().getString(R.string.noLights_title); + lightsState.setError(errorMsg, listResource.getMessage()); + break; } } }); @@ -68,11 +76,15 @@ public void onDestroyView() { } private void addEndpointSelector() { - endpointSpinner = new Spinner(getContext()); + Context context = getContext(); + if (context == null) { + throw new IllegalStateException("No Context found"); + } + endpointSpinner = new Spinner(context); EndpointSelectorAdapter adapter = - new EndpointSelectorAdapter(getContext(), android.R.layout.simple_spinner_dropdown_item); + new EndpointSelectorAdapter(context, android.R.layout.simple_spinner_dropdown_item); endpointSpinner.setAdapter(adapter); - endpointSpinner.setOnItemSelectedListener(new EndpointSelectedListener(getContext())); + endpointSpinner.setOnItemSelectedListener(new EndpointSelectedListener(context)); viewModel.getEndpoints().observe(getViewLifecycleOwner(), configList -> { adapter.submitCollection(configList); @@ -90,7 +102,7 @@ private void addEndpointSelector() { Observer> endpointSelector = new Observer>() { @Override - public void onChanged(List endpointConfigs) { + public void onChanged(@NonNull List endpointConfigs) { endpointSpinner.setSelection(endpointConfigs.indexOf(endpointConfig)); } }; From 40242069ffe429ad42c76d4a7a5c69929cd1ab8e Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Thu, 2 Mar 2023 22:30:28 +0100 Subject: [PATCH 03/17] Create DbLightBuilder from existing DbLight --- .../data/model/light/DbLightBuilder.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/data/model/light/DbLightBuilder.java b/app/src/main/java/org/d3kad3nt/sunriseClock/data/model/light/DbLightBuilder.java index c0dd05b5..9828a41d 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/data/model/light/DbLightBuilder.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/data/model/light/DbLightBuilder.java @@ -26,6 +26,24 @@ public class DbLightBuilder { /** * Builder for constructing DbLights. */ + + public static DbLightBuilder from(DbLight dbLight) { + DbLightBuilder builder = new DbLightBuilder(); + builder.setEndpointId(dbLight.getEndpointId()); + builder.setEndpointLightId(dbLight.getEndpointLightId()); + builder.setName(dbLight.getName()); + builder.setIsSwitchable(dbLight.getIsSwitchable()); + builder.setIsOn(dbLight.getIsOn()); + builder.setIsDimmable(dbLight.getIsDimmable()); + builder.setBrightness(dbLight.getBrightness()); + builder.setIsTemperaturable(dbLight.getIsTemperaturable()); + builder.setColorTemperature(dbLight.getColorTemperature()); + builder.setIsColorable(dbLight.getIsColorable()); + builder.setColor(dbLight.getColor()); + builder.setIsReachable(dbLight.getIsReachable()); + return builder; + } + public DbLightBuilder() { } From 245477d205962190c06cdd8b2e7d90c958242647 Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Thu, 2 Mar 2023 23:02:36 +0100 Subject: [PATCH 04/17] Fixed Bug, that isReachable wasn't updated properly --- .../d3kad3nt/sunriseClock/data/local/DbLightDao.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/data/local/DbLightDao.java b/app/src/main/java/org/d3kad3nt/sunriseClock/data/local/DbLightDao.java index fadd6840..50134cf7 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/data/local/DbLightDao.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/data/local/DbLightDao.java @@ -54,12 +54,13 @@ default void upsert(DbLight obj) { // Case 3 // Primary key NOT found in light object. This can happen if (all) light objects are retrieved from the // remote endpoint. - // Rhe lightId primary key is autogenerated by room, therefore it is not known to the remote endpoint. + // The lightId primary key is autogenerated by room, therefore it is not known to the remote endpoint. else if (obj.getEndpointId() != 0L && !(obj.getEndpointLightId().equals(""))) { int rowsUpdated = updateUsingEndpointIdAndEndpointLightId(obj.getEndpointId(), obj.getEndpointLightId(), obj.getName(), obj.getIsSwitchable(), obj.getIsOn(), obj.getIsDimmable(), obj.getBrightness(), - obj.getIsTemperaturable(), obj.getColorTemperature(), obj.getIsColorable(), obj.getColor()); + obj.getIsTemperaturable(), obj.getColorTemperature(), obj.getIsColorable(), obj.getColor(), + obj.getIsReachable()); Log.d(TAG, rowsUpdated + " rows updated by room. Updated DbLight with endpointId: " + obj.getEndpointId() + " and endpointLightId: " + obj.getEndpointLightId()); @@ -100,12 +101,12 @@ else if (obj.getEndpointId() != 0L && !(obj.getEndpointLightId().equals(""))) { */ @Query("UPDATE " + DbLight.TABLENAME + " SET name = :friendlyName, is_switchable = :switchable, is_on = :on, " + "is_dimmable = :dimmable, brightness = :brightness, is_temperaturable = :temperaturable, " + - "colortemperature = :colorTemperature, is_colorable = :colorable, color = :color WHERE endpoint_id = " + - ":endpointId AND endpoint_light_id = :endpointLightId") + "colortemperature = :colorTemperature, is_colorable = :colorable, color = :color, is_reachable = " + + ":is_reachable WHERE endpoint_id = " + ":endpointId AND endpoint_light_id = :endpointLightId") int updateUsingEndpointIdAndEndpointLightId(long endpointId, String endpointLightId, String friendlyName, boolean switchable, boolean on, boolean dimmable, int brightness, boolean temperaturable, int colorTemperature, boolean colorable, - int color); + int color, boolean is_reachable); @Delete() void delete(DbLight obj); From 530f78c8dd539185d924a941f4feca010ae836ef Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Thu, 2 Mar 2023 23:03:25 +0100 Subject: [PATCH 05/17] Improve Way to get isReachable State in ViewModel --- .../ui/light/lightDetail/LightDetailViewModel.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java b/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java index d8f121f6..0be95db8 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java @@ -18,6 +18,7 @@ import org.d3kad3nt.sunriseClock.ui.util.BooleanVisibilityLiveData; import org.d3kad3nt.sunriseClock.ui.util.ResourceVisibilityLiveData; import org.d3kad3nt.sunriseClock.util.LiveDataUtil; +import org.jetbrains.annotations.Contract; public class LightDetailViewModel extends AndroidViewModel { @@ -73,14 +74,16 @@ private LiveData> getLight(long lightID) { return lightRepository.getLight(lightID); } + @NonNull + @Contract(" -> new") private LiveData getIsReachable() { return Transformations.map(light, new Function, Boolean>() { @Override public Boolean apply(final Resource input) { - if (input.getStatus() == Status.SUCCESS) { + if (input.getData() != null) { return input.getData().getIsReachable(); } - return true; + return false; } }); } From 830a2f3783ad6b69a407f889af06ca03e660fd56 Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Thu, 2 Mar 2023 23:03:48 +0100 Subject: [PATCH 06/17] Improve Switch of Class types --- .../sunriseClock/data/repository/NetworkBoundResource.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/NetworkBoundResource.java b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/NetworkBoundResource.java index ad67bd41..0663b363 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/NetworkBoundResource.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/NetworkBoundResource.java @@ -89,8 +89,7 @@ private void fetchFromNetwork(LiveData dbSource) { addSource(apiResponse, response -> { removeSource(apiResponse); removeSource(dbSource); - Class aClass = response.getClass(); - if (ApiSuccessResponse.class.equals(aClass)) { + if (response instanceof ApiSuccessResponse) { ServiceLocator.getExecutor(ExecutorType.IO).execute(() -> { saveResponseToDb(convertRemoteTypeToDbType((ApiSuccessResponse) response)); ServiceLocator.getExecutor(ExecutorType.MainThread).execute(() -> { @@ -103,14 +102,14 @@ private void fetchFromNetwork(LiveData dbSource) { }); }); }); - } else if (ApiEmptyResponse.class.equals(aClass)) { + } else if (response instanceof ApiEmptyResponse) { ServiceLocator.getExecutor(ExecutorType.MainThread).execute(() -> { // reload from disk whatever we had addSource(loadFromDb(), newData -> { updateValue(Resource.success(convertDbTypeToResultType(newData))); }); }); - } else if (ApiErrorResponse.class.equals(aClass)) { + } else if (response instanceof ApiErrorResponse) { onFetchFailed(); addSource(dbSource, newData -> { updateValue(Resource.error(((ApiErrorResponse) response).getErrorMessage(), From 0b4df36f6598bab94a8196b1519912cd97c1cfbe Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Thu, 2 Mar 2023 23:04:32 +0100 Subject: [PATCH 07/17] Add Method to set light to not reachable if network request fails --- .../data/repository/LightRepository.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java index d38df860..bc383937 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java @@ -1,22 +1,27 @@ package org.d3kad3nt.sunriseClock.data.repository; import android.content.Context; +import android.util.Log; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; import androidx.lifecycle.Transformations; import org.d3kad3nt.sunriseClock.data.local.AppDatabase; import org.d3kad3nt.sunriseClock.data.local.DbLightDao; import org.d3kad3nt.sunriseClock.data.model.endpoint.BaseEndpoint; import org.d3kad3nt.sunriseClock.data.model.light.DbLight; +import org.d3kad3nt.sunriseClock.data.model.light.DbLightBuilder; import org.d3kad3nt.sunriseClock.data.model.light.RemoteLight; import org.d3kad3nt.sunriseClock.data.model.light.UILight; import org.d3kad3nt.sunriseClock.data.model.resource.EmptyResource; import org.d3kad3nt.sunriseClock.data.model.resource.Resource; import org.d3kad3nt.sunriseClock.data.remote.common.ApiResponse; import org.d3kad3nt.sunriseClock.data.remote.common.ApiSuccessResponse; +import org.d3kad3nt.sunriseClock.serviceLocator.ExecutorType; +import org.d3kad3nt.sunriseClock.serviceLocator.ServiceLocator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -160,8 +165,29 @@ protected UILight convertDbTypeToResultType(DbLight item) { return UILight.from(item); } + @Override + protected void onFetchFailed() { + //Set Light State to not reachable + LiveData light = dbLightDao.load(lightId); + light.observeForever(new Observer() { + @Override + public void onChanged(final DbLight dbLight) { + if (dbLight == null) { + return; + } + light.removeObserver(this); + ServiceLocator.getExecutor(ExecutorType.IO).execute(() -> { + DbLight updatedLight = DbLightBuilder.from(dbLight).setIsReachable(false).build(); + dbLightDao.upsert(updatedLight); + }); + } + }); + } + @Override protected DbLight convertRemoteTypeToDbType(ApiSuccessResponse response) { + Log.d(TAG, "Convert: Light " + response.getBody().getName() + " is reachable: " + + response.getBody().getIsReachable()); return DbLight.from(response.getBody()); } }; From 81b90d68c82016a1a6564acfb210ece10acb0788 Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Fri, 3 Mar 2023 21:35:48 +0100 Subject: [PATCH 08/17] Add onFetchFailed to getLightsForEndpoint --- .../data/repository/LightRepository.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java index bc383937..c6a3e665 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java @@ -121,6 +121,27 @@ protected List convertRemoteTypeToDbType(ApiSuccessResponse> light = loadFromDb(); + light.observeForever(new Observer>() { + @Override + public void onChanged(final List dbLights) { + if (dbLights == null) { + return; + } + light.removeObserver(this); + ServiceLocator.getExecutor(ExecutorType.IO).execute(() -> { + for (DbLight light : dbLights) { + DbLight updatedLight = DbLightBuilder.from(light).setIsReachable(false).build(); + dbLightDao.upsert(updatedLight); + } + }); + } + }); + } }; } @@ -168,7 +189,7 @@ protected UILight convertDbTypeToResultType(DbLight item) { @Override protected void onFetchFailed() { //Set Light State to not reachable - LiveData light = dbLightDao.load(lightId); + LiveData light = loadFromDb(); light.observeForever(new Observer() { @Override public void onChanged(final DbLight dbLight) { From 40e9e89abc65bca5f877c34170350d78a9d60621 Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Fri, 3 Mar 2023 21:44:14 +0100 Subject: [PATCH 09/17] Add LiveDataUtil.observeUntilNotNull This function observes a livedata until the value of the light is no longer null. When this is true it executes a given action --- .../org/d3kad3nt/sunriseClock/util/Action.java | 6 ++++++ .../sunriseClock/util/LiveDataUtil.java | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java b/app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java new file mode 100644 index 00000000..1d6c6bd2 --- /dev/null +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java @@ -0,0 +1,6 @@ +package org.d3kad3nt.sunriseClock.util; + +public interface Action { + + void execute(T t); +} diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/util/LiveDataUtil.java b/app/src/main/java/org/d3kad3nt/sunriseClock/util/LiveDataUtil.java index f1a03746..b3c2446d 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/util/LiveDataUtil.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/util/LiveDataUtil.java @@ -2,19 +2,20 @@ import android.util.Log; +import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; public class LiveDataUtil { - public static void logChanges(String TAG, LiveData liveData) { + public static void logChanges(final String TAG, @NonNull final LiveData liveData) { liveData.observeForever(t -> { Log.d("LiveDataUtil", "Log Change"); Log.d(TAG, t.toString()); }); } - public static void observeOnce(LiveData liveData, Observer observer) { + public static void observeOnce(@NonNull final LiveData liveData, @NonNull final Observer observer) { liveData.observeForever(new Observer() { @Override public void onChanged(T t) { @@ -23,4 +24,17 @@ public void onChanged(T t) { } }); } + + public static void observeUntilNotNull(@NonNull final LiveData light, @NonNull final Action action) { + light.observeForever(new Observer() { + @Override + public void onChanged(final T t) { + if (t == null) { + return; + } + light.removeObserver(this); + action.execute(t); + } + }); + } } From 1d409aaf95be2f6055633d871415f3b049333c1e Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Fri, 3 Mar 2023 21:44:52 +0100 Subject: [PATCH 10/17] Clean up onFetchFailed methods --- .../data/repository/LightRepository.java | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java index c6a3e665..02b0e225 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java @@ -6,7 +6,6 @@ import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Observer; import androidx.lifecycle.Transformations; import org.d3kad3nt.sunriseClock.data.local.AppDatabase; @@ -22,6 +21,8 @@ import org.d3kad3nt.sunriseClock.data.remote.common.ApiSuccessResponse; import org.d3kad3nt.sunriseClock.serviceLocator.ExecutorType; import org.d3kad3nt.sunriseClock.serviceLocator.ServiceLocator; +import org.d3kad3nt.sunriseClock.util.Action; +import org.d3kad3nt.sunriseClock.util.LiveDataUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -126,13 +127,9 @@ protected List convertRemoteTypeToDbType(ApiSuccessResponse> light = loadFromDb(); - light.observeForever(new Observer>() { + LiveDataUtil.observeUntilNotNull(light, new Action>() { @Override - public void onChanged(final List dbLights) { - if (dbLights == null) { - return; - } - light.removeObserver(this); + public void execute(final List dbLights) { ServiceLocator.getExecutor(ExecutorType.IO).execute(() -> { for (DbLight light : dbLights) { DbLight updatedLight = DbLightBuilder.from(light).setIsReachable(false).build(); @@ -190,13 +187,9 @@ protected UILight convertDbTypeToResultType(DbLight item) { protected void onFetchFailed() { //Set Light State to not reachable LiveData light = loadFromDb(); - light.observeForever(new Observer() { + LiveDataUtil.observeUntilNotNull(light, new Action() { @Override - public void onChanged(final DbLight dbLight) { - if (dbLight == null) { - return; - } - light.removeObserver(this); + public void execute(final DbLight dbLight) { ServiceLocator.getExecutor(ExecutorType.IO).execute(() -> { DbLight updatedLight = DbLightBuilder.from(dbLight).setIsReachable(false).build(); dbLightDao.upsert(updatedLight); From ddfeac9105e70984ac81e703c61ea175432bbcba Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Fri, 3 Mar 2023 21:48:39 +0100 Subject: [PATCH 11/17] Use Action instead of Observer in LiveDataUtil.observeOnce --- .../java/org/d3kad3nt/sunriseClock/util/LiveDataUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/util/LiveDataUtil.java b/app/src/main/java/org/d3kad3nt/sunriseClock/util/LiveDataUtil.java index b3c2446d..11142df0 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/util/LiveDataUtil.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/util/LiveDataUtil.java @@ -15,11 +15,11 @@ public static void logChanges(final String TAG, @NonNull final LiveData l }); } - public static void observeOnce(@NonNull final LiveData liveData, @NonNull final Observer observer) { + public static void observeOnce(@NonNull final LiveData liveData, @NonNull final Action action) { liveData.observeForever(new Observer() { @Override public void onChanged(T t) { - observer.onChanged(t); + action.execute(t); liveData.removeObserver(this); } }); From 55a4d5cd96a584eee9c41db09c3d828081174746 Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Fri, 3 Mar 2023 21:49:56 +0100 Subject: [PATCH 12/17] Add annotations to Action Interface --- .../sunriseClock/data/repository/LightRepository.java | 4 ++-- app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java index 02b0e225..5fcd1f64 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java @@ -129,7 +129,7 @@ protected void onFetchFailed() { LiveData> light = loadFromDb(); LiveDataUtil.observeUntilNotNull(light, new Action>() { @Override - public void execute(final List dbLights) { + public void execute(@NonNull final List dbLights) { ServiceLocator.getExecutor(ExecutorType.IO).execute(() -> { for (DbLight light : dbLights) { DbLight updatedLight = DbLightBuilder.from(light).setIsReachable(false).build(); @@ -189,7 +189,7 @@ protected void onFetchFailed() { LiveData light = loadFromDb(); LiveDataUtil.observeUntilNotNull(light, new Action() { @Override - public void execute(final DbLight dbLight) { + public void execute(@NonNull final DbLight dbLight) { ServiceLocator.getExecutor(ExecutorType.IO).execute(() -> { DbLight updatedLight = DbLightBuilder.from(dbLight).setIsReachable(false).build(); dbLightDao.upsert(updatedLight); diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java b/app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java index 1d6c6bd2..86191c60 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java @@ -1,6 +1,9 @@ package org.d3kad3nt.sunriseClock.util; +import androidx.annotation.NonNull; + +@FunctionalInterface public interface Action { - void execute(T t); + void execute(@NonNull final T t); } From 32f4dd7815c649085707ae6585983cf892a38609 Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Mon, 6 Mar 2023 20:27:46 +0100 Subject: [PATCH 13/17] A Action can take null as an argument This is needed because for example livedata can contain null --- app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java b/app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java index 86191c60..97dfebc0 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/util/Action.java @@ -1,9 +1,7 @@ package org.d3kad3nt.sunriseClock.util; -import androidx.annotation.NonNull; - @FunctionalInterface public interface Action { - void execute(@NonNull final T t); + void execute(final T t); } From db95fe4299749588c64bb17d3f327e33667a28f2 Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Mon, 6 Mar 2023 20:40:37 +0100 Subject: [PATCH 14/17] Use ErrorState instead of light.isReachable if endpoint is not reachable This works, but it is not perfect, that the reachable state is calculated in the LightDetailViewModel. This means that other ViewModels and other places would need to implement the same thing --- .../data/repository/LightRepository.java | 37 ------------------- .../lightDetail/LightDetailViewModel.java | 5 ++- 2 files changed, 4 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java index 5fcd1f64..a5b9d990 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/data/repository/LightRepository.java @@ -12,17 +12,12 @@ import org.d3kad3nt.sunriseClock.data.local.DbLightDao; import org.d3kad3nt.sunriseClock.data.model.endpoint.BaseEndpoint; import org.d3kad3nt.sunriseClock.data.model.light.DbLight; -import org.d3kad3nt.sunriseClock.data.model.light.DbLightBuilder; import org.d3kad3nt.sunriseClock.data.model.light.RemoteLight; import org.d3kad3nt.sunriseClock.data.model.light.UILight; import org.d3kad3nt.sunriseClock.data.model.resource.EmptyResource; import org.d3kad3nt.sunriseClock.data.model.resource.Resource; import org.d3kad3nt.sunriseClock.data.remote.common.ApiResponse; import org.d3kad3nt.sunriseClock.data.remote.common.ApiSuccessResponse; -import org.d3kad3nt.sunriseClock.serviceLocator.ExecutorType; -import org.d3kad3nt.sunriseClock.serviceLocator.ServiceLocator; -import org.d3kad3nt.sunriseClock.util.Action; -import org.d3kad3nt.sunriseClock.util.LiveDataUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -122,23 +117,6 @@ protected List convertRemoteTypeToDbType(ApiSuccessResponse> light = loadFromDb(); - LiveDataUtil.observeUntilNotNull(light, new Action>() { - @Override - public void execute(@NonNull final List dbLights) { - ServiceLocator.getExecutor(ExecutorType.IO).execute(() -> { - for (DbLight light : dbLights) { - DbLight updatedLight = DbLightBuilder.from(light).setIsReachable(false).build(); - dbLightDao.upsert(updatedLight); - } - }); - } - }); - } }; } @@ -183,21 +161,6 @@ protected UILight convertDbTypeToResultType(DbLight item) { return UILight.from(item); } - @Override - protected void onFetchFailed() { - //Set Light State to not reachable - LiveData light = loadFromDb(); - LiveDataUtil.observeUntilNotNull(light, new Action() { - @Override - public void execute(@NonNull final DbLight dbLight) { - ServiceLocator.getExecutor(ExecutorType.IO).execute(() -> { - DbLight updatedLight = DbLightBuilder.from(dbLight).setIsReachable(false).build(); - dbLightDao.upsert(updatedLight); - }); - } - }); - } - @Override protected DbLight convertRemoteTypeToDbType(ApiSuccessResponse response) { Log.d(TAG, "Convert: Light " + response.getBody().getName() + " is reachable: " + diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java b/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java index 0be95db8..aea45f73 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java @@ -77,9 +77,12 @@ private LiveData> getLight(long lightID) { @NonNull @Contract(" -> new") private LiveData getIsReachable() { - return Transformations.map(light, new Function, Boolean>() { + return Transformations.map(light, new Function<>() { @Override public Boolean apply(final Resource input) { + if (input.getStatus() == Status.ERROR) { + return false; + } if (input.getData() != null) { return input.getData().getIsReachable(); } From aff9ef65106c8ebef20b547c2cb2e25cc2a8741e Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Fri, 21 Feb 2025 19:47:00 +0100 Subject: [PATCH 15/17] Start to use new observeOnce Interface --- .../d3kad3nt/sunriseClock/deviceControl/ControlService.java | 6 ++++-- .../ui/light/lightDetail/LightDetailViewModel.java | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/deviceControl/ControlService.java b/app/src/main/java/org/d3kad3nt/sunriseClock/deviceControl/ControlService.java index f743127d..6a057581 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/deviceControl/ControlService.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/deviceControl/ControlService.java @@ -31,6 +31,7 @@ import org.d3kad3nt.sunriseClock.data.model.resource.Status; import org.d3kad3nt.sunriseClock.data.repository.EndpointRepository; import org.d3kad3nt.sunriseClock.data.repository.LightRepository; +import org.d3kad3nt.sunriseClock.util.Action; import org.d3kad3nt.sunriseClock.util.AsyncJoin; import org.d3kad3nt.sunriseClock.util.ExtendedPublisher; import org.d3kad3nt.sunriseClock.util.LiveDataUtil; @@ -78,6 +79,7 @@ public Flow.Publisher createPublisherForAllAvailable() { AsyncJoin asyncHelper = new AsyncJoin(); LiveDataUtil.observeOnce(allEndpoints, new AsyncJoin.Observer<>(asyncHelper) { + @Override public void onChanged(final List endpoints) { for (IEndpointUI endpoint : endpoints) { @@ -308,9 +310,9 @@ private Context getNonNullBaseContext() { } private void initEndpointNames() { - LiveDataUtil.observeOnce(getEndpointRepository().getAllEndpoints(), new Observer<>() { + LiveDataUtil.observeOnce(getEndpointRepository().getAllEndpoints(), new Action<>() { @Override - public void onChanged(final List iEndpointUIS) { + public void execute(final List iEndpointUIS) { for (IEndpointUI endpoint : iEndpointUIS) { endpointNames.put(endpoint.getId(), endpoint.getStringRepresentation()); } diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java b/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java index c4454ade..09b067fb 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/ui/light/lightDetail/LightDetailViewModel.java @@ -18,6 +18,7 @@ import org.d3kad3nt.sunriseClock.ui.util.BooleanVisibilityLiveData; import org.d3kad3nt.sunriseClock.ui.util.ResourceVisibilityLiveData; import org.d3kad3nt.sunriseClock.util.LiveDataUtil; +import org.jetbrains.annotations.Contract; import kotlin.jvm.functions.Function1; From 0a17e8176709c528a84ea2bc2269d8763198f4c7 Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Sun, 23 Feb 2025 21:15:00 +0100 Subject: [PATCH 16/17] Remove Log-Entry for UILight conversion This was done because it had no additional info --- .../java/org/d3kad3nt/sunriseClock/data/model/light/UILight.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/data/model/light/UILight.java b/app/src/main/java/org/d3kad3nt/sunriseClock/data/model/light/UILight.java index a2707f3e..676308bc 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/data/model/light/UILight.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/data/model/light/UILight.java @@ -53,7 +53,6 @@ private UILight(long lightId, long endpointId, String name, boolean isSwitchable @NonNull @Contract("_ -> new") public static UILight from(@NonNull DbLight dbLight) { - Log.d(TAG, "Converting DbLight to UiLight..."); // Place for conversion logic (if UI needs other data types or value ranges). UILight uiLight = new UILight(dbLight.getLightId(), dbLight.getEndpointId(), dbLight.getName(), dbLight.getIsSwitchable(), From e12c702adf32cc82e3ea12d8b72742cd3d0dcf60 Mon Sep 17 00:00:00 2001 From: Christoph Wildhagen Date: Sun, 23 Feb 2025 21:36:17 +0100 Subject: [PATCH 17/17] Fix compile error, but there still is a bug with device controls --- .../d3kad3nt/sunriseClock/deviceControl/ControlService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/d3kad3nt/sunriseClock/deviceControl/ControlService.java b/app/src/main/java/org/d3kad3nt/sunriseClock/deviceControl/ControlService.java index 6a057581..af3585bc 100644 --- a/app/src/main/java/org/d3kad3nt/sunriseClock/deviceControl/ControlService.java +++ b/app/src/main/java/org/d3kad3nt/sunriseClock/deviceControl/ControlService.java @@ -78,8 +78,7 @@ public Flow.Publisher createPublisherForAllAvailable() { LiveData> allEndpoints = getEndpointRepository().getAllEndpoints(); AsyncJoin asyncHelper = new AsyncJoin(); - LiveDataUtil.observeOnce(allEndpoints, new AsyncJoin.Observer<>(asyncHelper) { - + allEndpoints.observeForever(new AsyncJoin.Observer<>(asyncHelper) { @Override public void onChanged(final List endpoints) { for (IEndpointUI endpoint : endpoints) { @@ -108,6 +107,7 @@ public void onChanged(final Resource> listResource) { }); } asyncHelper.removeAsyncTask(this); + allEndpoints.removeObserver(this); } }); asyncHelper.executeWhenJoined(() -> flow.complete());