From dc023c78a4c8f9d420a81aff8109a2780a1ef060 Mon Sep 17 00:00:00 2001 From: Andrey Novikov Date: Fri, 2 Feb 2024 21:28:38 +0300 Subject: [PATCH] Correctly process tracking state changes --- .../main/java/mobi/maptrek/Configuration.java | 1 + .../main/java/mobi/maptrek/MainActivity.java | 33 +++------ .../maptrek/location/BaseLocationService.java | 4 ++ .../maptrek/location/LocationService.java | 51 +++++++------ .../mobi/maptrek/util/SingleLiveEvent.java | 72 +++++++++++++++++++ .../maptrek/viewmodels/TrackViewModel.java | 3 +- 6 files changed, 121 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/mobi/maptrek/util/SingleLiveEvent.java diff --git a/app/src/main/java/mobi/maptrek/Configuration.java b/app/src/main/java/mobi/maptrek/Configuration.java index 042c6527..d68eab0b 100644 --- a/app/src/main/java/mobi/maptrek/Configuration.java +++ b/app/src/main/java/mobi/maptrek/Configuration.java @@ -103,6 +103,7 @@ public class Configuration { public static final long ADVICE_MAP_SETTINGS = 0x0000000000000020L; public static final long ADVICE_ADDING_PLACE = 0x0000000000000040L; public static final long ADVICE_RECORD_TRACK = 0x0000000000000080L; + /** @noinspection unused*/ public static final long ADVICE_RECORDED_TRACKS = 0x0000000000000100L; public static final long ADVICE_VIEW_DATA_ITEM = 0x0000000000000200L; public static final long ADVICE_SWITCH_COORDINATES_FORMAT = 0x0000000000000400L; diff --git a/app/src/main/java/mobi/maptrek/MainActivity.java b/app/src/main/java/mobi/maptrek/MainActivity.java index 949ed45e..5200993a 100644 --- a/app/src/main/java/mobi/maptrek/MainActivity.java +++ b/app/src/main/java/mobi/maptrek/MainActivity.java @@ -909,6 +909,7 @@ protected void onStart() { ContextCompat.registerReceiver(this, mBroadcastReceiver, new IntentFilter(MapWorker.BROADCAST_MAP_ADDED), ContextCompat.RECEIVER_NOT_EXPORTED); ContextCompat.registerReceiver(this, mBroadcastReceiver, new IntentFilter(MapWorker.BROADCAST_MAP_REMOVED), ContextCompat.RECEIVER_NOT_EXPORTED); ContextCompat.registerReceiver(this, mBroadcastReceiver, new IntentFilter(MapWorker.BROADCAST_MAP_FAILED), ContextCompat.RECEIVER_NOT_EXPORTED); + ContextCompat.registerReceiver(this, mBroadcastReceiver, new IntentFilter(BaseLocationService.BROADCAST_TRACK_STATE), ContextCompat.RECEIVER_NOT_EXPORTED); ContextCompat.registerReceiver(this, mBroadcastReceiver, new IntentFilter(BaseLocationService.BROADCAST_TRACK_SAVE), ContextCompat.RECEIVER_NOT_EXPORTED); ContextCompat.registerReceiver(this, mBroadcastReceiver, new IntentFilter(NavigationService.BROADCAST_NAVIGATION_STATUS), ContextCompat.RECEIVER_NOT_EXPORTED); ContextCompat.registerReceiver(this, mBroadcastReceiver, new IntentFilter(NavigationService.BROADCAST_NAVIGATION_STATE), ContextCompat.RECEIVER_NOT_EXPORTED); @@ -939,6 +940,8 @@ protected void onResume() { askForPermission(PERMISSIONS_REQUEST_FINE_LOCATION); TRACKING_STATE savedState = TRACKING_STATE.values()[Configuration.getTrackingState()]; + logger.error("Saved tracking state: {}", savedState); + logger.error("Current tracking state: {}", trackViewModel.trackingState.getValue()); if (savedState != trackViewModel.trackingState.getValue()) { if (savedState == TRACKING_STATE.TRACKING) enableTracking(); @@ -951,7 +954,7 @@ else if (savedState == TRACKING_STATE.PAUSED) int recordColor = trackingState == TRACKING_STATE.TRACKING ? mColorAccent : mColorActionIcon; mViews.tracksButton.getDrawable().setTint(recordColor); - if (trackingState == TRACKING_STATE.TRACKING || trackingState == TRACKING_STATE.PAUSED && mCurrentTrackLayer == null) { + if ((trackingState == TRACKING_STATE.TRACKING || trackingState == TRACKING_STATE.PAUSED) && mCurrentTrackLayer == null) { mCurrentTrackLayer = new CurrentTrackLayer(mMap, getApplicationContext(), this); mMap.layers().add(mCurrentTrackLayer, MAP_DATA); mMap.updateMap(true); @@ -965,7 +968,6 @@ else if (savedState == TRACKING_STATE.PAUSED) trackViewModel.currentTrack.setValue(null); mMap.updateMap(true); } - trackViewModel.trackingCommand.setValue(trackingState); // reset command }); trackViewModel.trackingCommand.observe(this, trackingCommand -> { TRACKING_STATE trackingState = trackViewModel.trackingState.getValue(); @@ -1748,6 +1750,7 @@ private void onMapsLongClicked() { menu.findItem(R.id.actionAutoTilt).setChecked(mAutoTilt != -1f); }); showExtendPanel(PANEL_STATE.MAPS, "mapMenu", fragment); + Configuration.setAdviceState(Configuration.ADVICE_MAP_SETTINGS); } private void onMoreClicked() { @@ -2039,6 +2042,7 @@ public void setHighlightedType(int type) { } private void enableTracking() { + logger.error("enableTracking"); Intent intent = new Intent(getApplicationContext(), LocationService.class).setAction(BaseLocationService.ENABLE_TRACK); if (Build.VERSION.SDK_INT >= 26) startForegroundService(intent); @@ -3754,14 +3758,6 @@ public void onFragmentDetached(@NonNull FragmentManager fm, @NonNull Fragment f) mViews.mapsButton, false ); - else if (cls == TrackProperties.class) - HelperUtils.showTargetedAdvice( - MainActivity.this, - Configuration.ADVICE_RECORDED_TRACKS, - R.string.advice_recorded_tracks, - mViews.tracksButton, - false - ); } }; @@ -4069,6 +4065,11 @@ public void onReceive(Context context, Intent intent) { String title = extras != null ? extras.getString(MapWorker.EXTRA_TITLE) : getString(R.string.map); HelperUtils.showError(getString(R.string.msgMapDownloadFailed, title), mViews.coordinatorLayout); } + if (BaseLocationService.BROADCAST_TRACK_STATE.equals(action)) { + int stateOrdinal = intent.getIntExtra("state", TRACKING_STATE.DISABLED.ordinal()); + TRACKING_STATE state = TRACKING_STATE.values()[stateOrdinal]; + trackViewModel.trackingState.setValue(state); + } if (BaseLocationService.BROADCAST_TRACK_SAVE.equals(action)) { final Bundle extras = intent.getExtras(); boolean saved = extras != null && extras.getBoolean("saved"); @@ -4076,13 +4077,6 @@ public void onReceive(Context context, Intent intent) { logger.debug("Track saved: {}", extras.getString("path")); Snackbar.make(mViews.coordinatorLayout, R.string.msgTrackSaved, Snackbar.LENGTH_LONG) .setAction(R.string.actionCustomize, view -> onTrackProperties(extras.getString("path"))) - .addCallback(new BaseTransientBottomBar.BaseCallback(){ - @Override - public void onDismissed(Snackbar snackbar, @DismissEvent int event) { - if (event != DISMISS_EVENT_ACTION) - HelperUtils.showTargetedAdvice(MainActivity.this, Configuration.ADVICE_RECORDED_TRACKS, R.string.advice_recorded_tracks, mViews.tracksButton, false); - } - }) .setAnchorView(mViews.actionPanel) .show(); return; @@ -4512,11 +4506,6 @@ public void onConfigurationChanged(Configuration.ChangedEvent event) { mHillshadeLayer.setBitmapAlpha(1 - transparency * 0.01f); break; } - case Configuration.PREF_TRACKING_STATE: { - int state = Configuration.getTrackingState(); - trackViewModel.trackingState.setValue(TRACKING_STATE.values()[state]); - break; - } } } diff --git a/app/src/main/java/mobi/maptrek/location/BaseLocationService.java b/app/src/main/java/mobi/maptrek/location/BaseLocationService.java index 0553ba76..967d7961 100644 --- a/app/src/main/java/mobi/maptrek/location/BaseLocationService.java +++ b/app/src/main/java/mobi/maptrek/location/BaseLocationService.java @@ -45,6 +45,10 @@ public abstract class BaseLocationService extends Service { * state. */ public static final String DISABLE_BACKGROUND_LOCATIONS = "mobi.maptrek.location.disableBackgroundLocations"; + /** + * Broadcast sent when track recording state changes + */ + public static final String BROADCAST_TRACK_STATE = "mobi.maptrek.location.TrackState"; /** * Broadcast sent when track is about to be saved (or not) */ diff --git a/app/src/main/java/mobi/maptrek/location/LocationService.java b/app/src/main/java/mobi/maptrek/location/LocationService.java index 538204f9..4c83971b 100644 --- a/app/src/main/java/mobi/maptrek/location/LocationService.java +++ b/app/src/main/java/mobi/maptrek/location/LocationService.java @@ -190,24 +190,29 @@ public int onStartCommand(Intent intent, int flags, int startId) { return START_NOT_STICKY; String action = intent.getAction(); - logger.debug("Command: {}", action); - if (action.equals(ENABLE_TRACK) && !mTrackingEnabled) { - mErrorMsg = ""; - mErrorTime = 0; - mTrackingEnabled = true; - mContinuous = false; - mDistanceNotified = 0f; - openDatabase(); - mTrackingStarted = SystemClock.uptimeMillis(); - mTrackStarted = System.currentTimeMillis(); - mForegroundTracking = true; - updateDistanceTracked(); - // https://developer.android.com/training/monitoring-device-state/doze-standby#support_for_other_use_cases - if (!mForegroundLocations) - if (Build.VERSION.SDK_INT < 34) - startForeground(NOTIFICATION_ID, getNotification()); - else - startForeground(NOTIFICATION_ID, getNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION); + logger.error("Command: {}", action); + if (action.equals(ENABLE_TRACK)) { + if (!mTrackingEnabled) { // Command can be sent on activity restart, while service already is running + mErrorMsg = ""; + mErrorTime = 0; + mTrackingEnabled = true; + mContinuous = false; + mDistanceNotified = 0f; + openDatabase(); + mTrackingStarted = SystemClock.uptimeMillis(); + mTrackStarted = System.currentTimeMillis(); + mForegroundTracking = true; + updateDistanceTracked(); + // https://developer.android.com/training/monitoring-device-state/doze-standby#support_for_other_use_cases + if (!mForegroundLocations) + if (Build.VERSION.SDK_INT < 34) + startForeground(NOTIFICATION_ID, getNotification()); + else + startForeground(NOTIFICATION_ID, getNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION); + } + sendBroadcast(new Intent(BROADCAST_TRACK_STATE) + .putExtra("state", TRACKING_STATE.TRACKING.ordinal()) + .setPackage(getPackageName())); Configuration.setTrackingState(TRACKING_STATE.TRACKING.ordinal()); } if (action.equals(DISABLE_TRACK) || action.equals(PAUSE_TRACK) && mTrackingEnabled) { @@ -218,10 +223,16 @@ public int onStartCommand(Intent intent, int flags, int startId) { long trackedTime = (SystemClock.uptimeMillis() - mTrackingStarted) / 60000; Configuration.updateTrackingTime(trackedTime); if (action.equals(DISABLE_TRACK)) { - Configuration.setTrackingState(TRACKING_STATE.DISABLED.ordinal()); + sendBroadcast(new Intent(BROADCAST_TRACK_STATE) + .putExtra("state", TRACKING_STATE.DISABLED.ordinal()) + .setPackage(getPackageName())); + Configuration.setTrackingState(TRACKING_STATE.PAUSED.ordinal()); tryToSaveTrack(); } if (action.equals(PAUSE_TRACK)) { + sendBroadcast(new Intent(BROADCAST_TRACK_STATE) + .putExtra("state", TRACKING_STATE.PAUSED.ordinal()) + .setPackage(getPackageName())); Configuration.setTrackingState(TRACKING_STATE.PAUSED.ordinal()); } if (!mForegroundLocations) { @@ -399,7 +410,7 @@ private Notification getNotification() { iLaunch.addCategory(Intent.CATEGORY_LAUNCHER); iLaunch.setComponent(new ComponentName(getApplicationContext(), MainActivity.class)); iLaunch.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); - PendingIntent piResult = PendingIntent.getActivity(this, 0, iLaunch, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); + PendingIntent piResult = PendingIntent.getActivity(this, 0, iLaunch, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); builder.setWhen(mErrorTime); builder.setSmallIcon(ntfId); diff --git a/app/src/main/java/mobi/maptrek/util/SingleLiveEvent.java b/app/src/main/java/mobi/maptrek/util/SingleLiveEvent.java new file mode 100644 index 00000000..c29bfd38 --- /dev/null +++ b/app/src/main/java/mobi/maptrek/util/SingleLiveEvent.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mobi.maptrek.util; + +import android.util.Log; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A lifecycle-aware observable that sends only new updates after subscription, used for events like + * navigation and Snackbar messages. + *

+ * This avoids a common problem with events: on configuration change (like rotation) an update + * can be emitted if the observer is active. This LiveData only calls the observable if there's an + * explicit call to setValue() or call(). + *

+ * Note that only one observer is going to be notified of changes. + */ +public class SingleLiveEvent extends MutableLiveData { + + private static final String TAG = "SingleLiveEvent"; + + private final AtomicBoolean mPending = new AtomicBoolean(false); + + @MainThread + public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) { + if (hasActiveObservers()) { + Log.w(TAG, "Multiple observers registered but only one will be notified of changes."); + } + // Observe the internal MutableLiveData + super.observe(owner, t -> { + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t); + } + }); + } + + @MainThread + public void setValue(@Nullable T value) { + mPending.set(true); + super.setValue(value); + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + public void call() { + setValue(null); + } +} diff --git a/app/src/main/java/mobi/maptrek/viewmodels/TrackViewModel.java b/app/src/main/java/mobi/maptrek/viewmodels/TrackViewModel.java index 6a91c681..33f349b6 100644 --- a/app/src/main/java/mobi/maptrek/viewmodels/TrackViewModel.java +++ b/app/src/main/java/mobi/maptrek/viewmodels/TrackViewModel.java @@ -21,10 +21,11 @@ import mobi.maptrek.data.Track; import mobi.maptrek.location.BaseLocationService.TRACKING_STATE; +import mobi.maptrek.util.SingleLiveEvent; public class TrackViewModel extends ViewModel { public final MutableLiveData selectedTrack = new MutableLiveData<>(); public final MutableLiveData currentTrack = new MutableLiveData<>(); public final MutableLiveData trackingState = new MutableLiveData<>(TRACKING_STATE.DISABLED); - public final MutableLiveData trackingCommand = new MutableLiveData<>(TRACKING_STATE.DISABLED); + public final SingleLiveEvent trackingCommand = new SingleLiveEvent<>(); }