[android] Remove UI from LocationHelper

AlertDialogs are moved from LocationHelper back to parent MwmActivity.
Initial idea of attaching LocationHelper to different activities failed
and MwmActivity was the only one user of LocationHelper.attach().

Location permission checks are extracted from LocationHelper.start() to
callers of LocationHelper.start() to avoid unnecessary inverted callbacks.
LocationHelper.start() now has @RequiresPermission compile-time annotation
to validate that required location permissions are checked before the call.
LocationUtils.isLocationGranted() is replaced with direct calls to
ActitivyCompat.checkSelfPermission() because Android Studio and Android Lint
are not smart enough to detect it for @RequiresPermission check.

Remove harmful Listeners<> abstraction from LocationHelper. Make all things
explicit. Listeners should be removed when the upper levels want to remove
them, not sometime later in the future.

See #4240
Needed for #573 (#4611)

Signed-off-by: Roman Tsisyk <roman@tsisyk.com>
This commit is contained in:
Roman Tsisyk 2023-08-25 08:15:28 +03:00
parent b1235fc407
commit 4300c6f70b
8 changed files with 388 additions and 336 deletions

View file

@ -1,5 +1,7 @@
package app.organicmaps.location;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static app.organicmaps.util.concurrency.UiThread.runLater;
import android.app.PendingIntent;
@ -8,6 +10,7 @@ import android.location.Location;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresPermission;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.ResolvableApiException;
@ -65,9 +68,8 @@ class GoogleFusedLocationProvider extends BaseLocationProvider
mContext = context;
}
@SuppressWarnings("MissingPermission")
// A permission is checked externally
@Override
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
public void start(long interval)
{
Logger.d(TAG);

View file

@ -2,6 +2,8 @@ package app.organicmaps;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
@ -14,12 +16,17 @@ import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.IntentSenderRequest;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.AlertDialog;
import androidx.annotation.UiThread;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
@ -29,6 +36,7 @@ import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
import app.organicmaps.Framework.PlacePageActivationListener;
import app.organicmaps.api.Const;
import app.organicmaps.background.AppBackgroundTracker;
import app.organicmaps.background.Notifier;
import app.organicmaps.base.BaseMwmFragmentActivity;
import app.organicmaps.base.CustomNavigateUpListener;
@ -81,6 +89,7 @@ import app.organicmaps.settings.UnitLocale;
import app.organicmaps.sound.TtsPlayer;
import app.organicmaps.util.Config;
import app.organicmaps.util.Counters;
import app.organicmaps.util.LocationUtils;
import app.organicmaps.util.SharingUtils;
import app.organicmaps.util.ThemeSwitcher;
import app.organicmaps.util.ThemeUtils;
@ -99,6 +108,11 @@ import java.util.ArrayList;
import java.util.Objects;
import java.util.Stack;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static app.organicmaps.location.LocationState.LOCATION_TAG;
public class MwmActivity extends BaseMwmFragmentActivity
implements PlacePageActivationListener,
View.OnTouchListener,
@ -114,7 +128,8 @@ public class MwmActivity extends BaseMwmFragmentActivity
NoConnectionListener,
MenuBottomSheetFragment.MenuBottomSheetInterfaceWithHeader,
PlacePageController.PlacePageRouteSettingsListener,
MapButtonsController.MapButtonClickListener
MapButtonsController.MapButtonClickListener,
AppBackgroundTracker.OnTransitionListener
{
private static final String TAG = MwmActivity.class.getSimpleName();
@ -129,12 +144,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
EditorHostFragment.class.getName(),
ReportFragment.class.getName() };
public static final int REQ_CODE_ERROR_DRIVING_OPTIONS_DIALOG = 5;
public static final int REQ_CODE_DRIVING_OPTIONS = 6;
private static final int REQ_CODE_ISOLINES_ERROR = 8;
public static final String ERROR_DRIVING_OPTIONS_DIALOG_TAG = "error_driving_options_dialog_tag";
private static final String ISOLINES_ERROR_DIALOG_TAG = "isolines_dialog_tag";
private static final String MAIN_MENU_ID = "MAIN_MENU_BOTTOM_SHEET";
private static final String LAYERS_MENU_ID = "LAYERS_MENU_BOTTOM_SHEET";
@ -186,6 +196,17 @@ public class MwmActivity extends BaseMwmFragmentActivity
@Nullable
private WindowInsetsCompat mCurrentWindowInsets;
@Nullable
private Dialog mLocationErrorDialog;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private ActivityResultLauncher<String[]> mLocationPermissionRequest;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private ActivityResultLauncher<IntentSenderRequest> mLocationResolutionRequest;
public interface LeftAnimationTrackListener
{
void onTrackStarted(boolean collapsed);
@ -353,6 +374,9 @@ public class MwmActivity extends BaseMwmFragmentActivity
protected void onSafeCreate(@Nullable Bundle savedInstanceState)
{
super.onSafeCreate(savedInstanceState);
MwmApplication.backgroundTracker(this).addListener(this);
mIsTabletLayout = getResources().getBoolean(R.bool.tabletLayout);
if (!mIsTabletLayout)
@ -377,6 +401,12 @@ public class MwmActivity extends BaseMwmFragmentActivity
initViews(isLaunchByDeepLink);
updateViewsInsets();
// Note: You must call registerForActivityResult() before the fragment or activity is created.
mLocationPermissionRequest = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(),
this::onLocationPermissionsResult);
mLocationResolutionRequest = registerForActivityResult(new ActivityResultContracts.StartIntentSenderForResult(),
this::onLocationResolutionResult);
boolean isConsumed = savedInstanceState == null && processIntent(getIntent());
boolean isFirstLaunch = Counters.isFirstLaunch(this);
// If the map activity is launched by any incoming intent (deeplink, update maps event, etc)
@ -485,7 +515,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
startActivity(new Intent(MwmActivity.this, FeatureCategoryActivity.class));
else
{
new AlertDialog.Builder(this, R.style.MwmTheme_AlertDialog)
new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.message_invalid_feature_position)
.setPositiveButton(R.string.ok, null)
.show();
@ -621,8 +651,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
break;
case myPosition:
LocationState.nativeSwitchToNextMode();
if (!LocationHelper.INSTANCE.isActive())
LocationHelper.INSTANCE.start();
startLocation();
break;
case toggleMapLayer:
toggleMapLayerBottomSheet();
@ -770,8 +799,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
public void startLocationToPoint(final @Nullable MapObject endPoint)
{
closeFloatingPanels();
if (!LocationHelper.INSTANCE.isActive())
LocationHelper.INSTANCE.start();
startLocation();
MapObject startPoint = LocationHelper.INSTANCE.getMyPosition();
RoutingController.get().prepare(startPoint, endPoint);
@ -967,6 +995,17 @@ public class MwmActivity extends BaseMwmFragmentActivity
refreshLightStatusBar();
}
@Override
public void onTransit(boolean foreground)
{
Logger.d(TAG, "foreground = " + foreground);
if (foreground)
resumeLocationInForeground();
else
pauseLocationInBackground();
}
@Override
public void recreate()
{
@ -991,7 +1030,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
if (mOnmapDownloader != null)
mOnmapDownloader.onPause();
mNavigationController.onActivityPaused(this);
LocationHelper.INSTANCE.closeLocationDialog();
pauseLocationInBackground();
super.onPause();
}
@ -1003,7 +1042,6 @@ public class MwmActivity extends BaseMwmFragmentActivity
BookmarkManager.INSTANCE.addLoadingListener(this);
RoutingController.get().attach(this);
IsolinesManager.from(getApplicationContext()).attach(this::onIsolinesStateChanged);
LocationHelper.INSTANCE.attach(this);
LocationState.nativeSetListener(this);
LocationHelper.INSTANCE.addListener(this);
onMyPositionModeChanged(LocationState.nativeGetMode());
@ -1020,7 +1058,6 @@ public class MwmActivity extends BaseMwmFragmentActivity
BookmarkManager.INSTANCE.removeLoadingListener(this);
LocationHelper.INSTANCE.removeListener(this);
LocationState.nativeRemoveListener();
LocationHelper.INSTANCE.detach();
RoutingController.get().detach();
IsolinesManager.from(getApplicationContext()).detach();
mSearchController.detach();
@ -1033,6 +1070,10 @@ public class MwmActivity extends BaseMwmFragmentActivity
{
super.onSafeDestroy();
mNavigationController.destroy();
mLocationPermissionRequest.unregister();
mLocationPermissionRequest = null;
mLocationResolutionRequest.unregister();
mLocationResolutionRequest = null;
}
@Override
@ -1612,16 +1653,33 @@ public class MwmActivity extends BaseMwmFragmentActivity
@Override
public void onMyPositionModeChanged(int newMode)
{
Logger.d(TAG, "location newMode = " + newMode);
Logger.d(LOCATION_TAG, "newMode = " + newMode);
mMapButtonsViewModel.setMyPositionMode(newMode);
RoutingController controller = RoutingController.get();
if (controller.isPlanning())
showAddStartOrFinishFrame(controller, true);
}
/**
* Dismiss location error dialog from the screen, if any.
*/
private void dismissLocationErrorDialog()
{
if (mLocationErrorDialog != null && mLocationErrorDialog.isShowing())
mLocationErrorDialog.dismiss();
mLocationErrorDialog = null;
}
/**
* Called when location is updated.
* @param location new location
*/
@Override
@UiThread
public void onLocationUpdated(@NonNull Location location)
{
dismissLocationErrorDialog();
final RoutingController routing = RoutingController.get();
if (!routing.isNavigating())
return;
@ -1642,13 +1700,256 @@ public class MwmActivity extends BaseMwmFragmentActivity
}
}
/**
* Called when compass data is updated.
* @param north offset from the north
*/
@Override
@UiThread
public void onCompassUpdated(double north)
{
Map.onCompassUpdated(north, false);
mNavigationController.updateNorth();
}
/**
* Start location services when the user presses a button or starts routing.
*/
private void startLocation()
{
Logger.d(LOCATION_TAG);
if (ActivityCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED)
{
Logger.i(LOCATION_TAG, "Permission ACCESS_FINE_LOCATION is granted");
LocationHelper.INSTANCE.start();
return;
}
// Always try to optimistically request FINE permission when the user presses a button or starts routing.
// Android will suppress annoying dialogs and skip directly to onLocationPermissionsResult().
Logger.i(LOCATION_TAG, "Requesting ACCESS_FINE_LOCATION permission");
dismissLocationErrorDialog();
mLocationPermissionRequest.launch(new String[]{
ACCESS_COARSE_LOCATION,
ACCESS_FINE_LOCATION
});
}
/**
* Resume location services when entering the foreground.
*/
private void resumeLocationInForeground()
{
LocationHelper.INSTANCE.setRotation(getWindowManager().getDefaultDisplay().getRotation());
LocationState.nativeSetLocationPendingTimeoutListener(this::onLocationPendingTimeout);
if (LocationState.nativeGetMode() == LocationState.NOT_FOLLOW_NO_POSITION)
{
Logger.i(LOCATION_TAG, "Location updates are stopped by the user manually.");
LocationState.nativeOnLocationError(LocationState.ERROR_GPS_OFF);
LocationHelper.INSTANCE.stop();
}
else if (ActivityCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED)
{
Logger.i(LOCATION_TAG, "Permission ACCESS_FINE_LOCATION is granted");
LocationHelper.INSTANCE.start();
}
else if (ActivityCompat.checkSelfPermission(this, ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED)
{
Logger.i(LOCATION_TAG, "Permission ACCESS_COARSE_LOCATION is granted");
LocationHelper.INSTANCE.start();
}
else
{
Logger.w(LOCATION_TAG, "Permissions ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION are not granted");
LocationState.nativeOnLocationError(LocationState.ERROR_DENIED);
LocationHelper.INSTANCE.stop();
Logger.i(LOCATION_TAG, "Requesting ACCESS_FINE_LOCATION + ACCESS_FINE_LOCATION permissions");
dismissLocationErrorDialog();
mLocationPermissionRequest.launch(new String[]{
ACCESS_COARSE_LOCATION,
ACCESS_FINE_LOCATION
});
}
}
/**
* Pause location services when entering the background.
*/
private void pauseLocationInBackground()
{
dismissLocationErrorDialog();
LocationState.nativeRemoveLocationPendingTimeoutListener();
if (!LocationHelper.INSTANCE.isActive())
return;
Logger.i(LOCATION_TAG);
LocationHelper.INSTANCE.stop();
}
/**
* Called on the result of the system location dialog.
* @param permissions permissions granted or refused.
*/
@UiThread
private void onLocationPermissionsResult(java.util.Map<String, Boolean> permissions)
{
// Print permissions that have been granted or refused.
for (java.util.Map.Entry<String, Boolean> entry : permissions.entrySet())
{
final String permission = entry.getKey().substring(entry.getKey().lastIndexOf('.') + 1);
if (entry.getValue())
Logger.i(LOCATION_TAG, "Permission " + permission + " has been granted");
else
Logger.w(LOCATION_TAG, "Permission " + permission + " has been refused");
}
// Sic: Android Studio requires explicit calls to checkSelfPermission() for @RequiresPermission in start().
if (ActivityCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED)
{
LocationHelper.INSTANCE.start();
return;
}
Logger.w(LOCATION_TAG, "Permissions ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION have been refused");
LocationState.nativeOnLocationError(LocationState.ERROR_DENIED);
LocationHelper.INSTANCE.stop();
if (mLocationErrorDialog != null && mLocationErrorDialog.isShowing())
{
Logger.w(LOCATION_TAG, "Don't show 'location denied' error dialog because another dialog is in progress");
return;
}
mLocationErrorDialog = new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.enable_location_services)
.setMessage(R.string.location_is_disabled_long_text)
.setOnDismissListener(dialog -> mLocationErrorDialog = null)
.setNegativeButton(R.string.close, null)
.show();
}
/**
* Called by GoogleFusedLocationProvider to request to GPS and/or Wi-Fi.
* @param pendingIntent an intent to launch.
*/
@Override
@UiThread
public void onLocationResolutionRequired(@NonNull PendingIntent pendingIntent)
{
Logger.d(LOCATION_TAG);
// Cancel our dialog in favor of system dialog.
dismissLocationErrorDialog();
// Launch system permission resolution dialog.
Logger.i(LOCATION_TAG, "Starting location resolution dialog");
IntentSenderRequest intentSenderRequest = new IntentSenderRequest.Builder(pendingIntent.getIntentSender()).build();
mLocationResolutionRequest.launch(intentSenderRequest);
}
/**
* Triggered by onLocationResolutionRequired().
* @param result invocation result.
*/
@UiThread
private void onLocationResolutionResult(@NonNull ActivityResult result)
{
final int resultCode = result.getResultCode();
Logger.d(LOCATION_TAG, "resultCode = " + resultCode);
if (resultCode != Activity.RESULT_OK)
{
Logger.w(LOCATION_TAG, "Location resolution has been refused");
LocationState.nativeOnLocationError(LocationState.ERROR_GPS_OFF);
LocationHelper.INSTANCE.stop();
return;
}
Logger.i(LOCATION_TAG, "Location resolution has been granted");
LocationHelper.INSTANCE.restart();
}
/**
* Called by AndroidNativeLocationProvider when no suitable location methods are available.
*/
@Override
@UiThread
public void onLocationDisabled()
{
Logger.d(LOCATION_TAG, "settings = " + LocationUtils.areLocationServicesTurnedOn(this));
LocationState.nativeOnLocationError(LocationState.ERROR_GPS_OFF);
LocationHelper.INSTANCE.stop();
if (mLocationErrorDialog != null && mLocationErrorDialog.isShowing())
{
Logger.d(LOCATION_TAG, "Don't show 'location disabled' error dialog because another dialog is in progress");
return;
}
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.enable_location_services)
.setMessage(R.string.location_is_disabled_long_text)
.setOnDismissListener(dialog -> mLocationErrorDialog = null)
.setNegativeButton(R.string.close, null);
final Intent intent = Utils.makeSystemLocationSettingIntent(this);
if (intent != null)
{
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
builder.setPositiveButton(R.string.connection_settings, (dialog, which) -> startActivity(intent));
}
mLocationErrorDialog = builder.show();
}
/**
* Called by the core when location updates were not received after the 30 second deadline.
*/
@UiThread
private void onLocationPendingTimeout()
{
// Sic: the callback can be called after the activity is destroyed because of being queued.
if (isDestroyed())
{
Logger.w(LOCATION_TAG, "Ignore late callback from core because activity is already destroyed");
return;
}
Logger.d(LOCATION_TAG, "services = " + LocationUtils.areLocationServicesTurnedOn(this));
//
// For all cases below we don't stop location provider until user explicitly clicks "Stop" in the dialog.
//
if (mLocationErrorDialog != null && mLocationErrorDialog.isShowing())
{
Logger.d(LOCATION_TAG, "Don't show 'location timeout' error dialog because another dialog is in progress");
return;
}
mLocationErrorDialog = new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.current_location_unknown_title)
.setMessage(R.string.current_location_unknown_message)
.setOnDismissListener(dialog -> mLocationErrorDialog = null)
.setNegativeButton(R.string.current_location_unknown_stop_button, (dialog, which) ->
{
Logger.w(LOCATION_TAG, "Disabled by user");
LocationState.nativeOnLocationError(LocationState.ERROR_GPS_OFF);
LocationHelper.INSTANCE.stop();
})
.setPositiveButton(R.string.current_location_unknown_continue_button, (dialog, which) ->
{
// Do nothing - provider will continue to search location.
})
.show();
}
@Override
public void onUseMyPositionAsStart()
{

View file

@ -1,5 +1,7 @@
package app.organicmaps.location;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static app.organicmaps.util.concurrency.UiThread.runLater;
import android.content.Context;
@ -9,6 +11,7 @@ import android.os.Bundle;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresPermission;
import androidx.core.location.LocationListenerCompat;
import androidx.core.location.LocationManagerCompat;
import androidx.core.location.LocationRequestCompat;
@ -72,9 +75,9 @@ class AndroidNativeProvider extends BaseLocationProvider
throw new IllegalStateException("Can't get LOCATION_SERVICE");
}
@SuppressWarnings("MissingPermission")
// A permission is checked externally
@Override
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
public void start(long interval)
{
Logger.d(TAG);

View file

@ -1,9 +1,13 @@
package app.organicmaps.location;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import android.app.PendingIntent;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresPermission;
import androidx.annotation.UiThread;
abstract class BaseLocationProvider
@ -28,6 +32,7 @@ abstract class BaseLocationProvider
mListener = listener;
}
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
protected abstract void start(long interval);
protected abstract void stop();
}

View file

@ -2,40 +2,33 @@ package app.organicmaps.location;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import android.app.Activity;
import android.app.Dialog;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationManager;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.IntentSenderRequest;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
import androidx.annotation.UiThread;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import app.organicmaps.Framework;
import app.organicmaps.MwmApplication;
import app.organicmaps.R;
import app.organicmaps.background.AppBackgroundTracker;
import app.organicmaps.base.Initializable;
import app.organicmaps.bookmarks.data.FeatureId;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.util.Config;
import app.organicmaps.util.Listeners;
import app.organicmaps.util.LocationUtils;
import app.organicmaps.util.NetworkPolicy;
import app.organicmaps.util.Utils;
import app.organicmaps.util.log.Logger;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public enum LocationHelper implements Initializable<Context>, AppBackgroundTracker.OnTransitionListener, BaseLocationProvider.Listener
import java.util.LinkedHashSet;
import java.util.Set;
public enum LocationHelper implements Initializable<Context>, BaseLocationProvider.Listener
{
INSTANCE;
@ -54,9 +47,9 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
@NonNull
private Context mContext;
private static final String TAG = LocationHelper.class.getSimpleName();
private static final String TAG = LocationState.LOCATION_TAG;
@NonNull
private final Listeners<LocationListener> mListeners = new Listeners<>();
private final Set<LocationListener> mListeners = new LinkedHashSet<>();
@Nullable
private Location mSavedLocation;
private double mSavedNorth = Double.NaN;
@ -67,17 +60,11 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private BaseLocationProvider mLocationProvider;
@Nullable
private AppCompatActivity mActivity;
private long mInterval;
private boolean mInFirstRun;
private boolean mActive;
@Nullable
private Dialog mErrorDialog;
@Nullable
private ActivityResultLauncher<String[]> mPermissionRequest;
@Nullable
private ActivityResultLauncher<IntentSenderRequest> mResolutionRequest;
private int mRotation = 0;
@Override
public void initialize(@NonNull Context context)
@ -85,7 +72,6 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
mContext = context;
mSensorHelper = new SensorHelper(context);
mLocationProvider = LocationProviderFactory.getProvider(mContext, this);
MwmApplication.backgroundTracker(context).addListener(this);
}
@Override
@ -131,54 +117,17 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
return mActive;
}
@Override
public void onTransit(boolean foreground)
public void setRotation(int rotation)
{
Logger.d(TAG, "foreground = " + foreground + " mode = " + LocationState.nativeGetMode());
if (foreground)
{
if (isActive())
return;
if (LocationState.nativeGetMode() == LocationState.NOT_FOLLOW_NO_POSITION)
{
Logger.d(TAG, "Location updates are stopped by the user manually, so skip provider start"
+ " until the user starts it manually.");
return;
}
Logger.d(TAG, "Starting in foreground");
start();
}
else
{
if (!isActive())
return;
Logger.d(TAG, "Stopping in background");
stop();
}
}
public void closeLocationDialog()
{
if (mErrorDialog != null && mErrorDialog.isShowing())
mErrorDialog.dismiss();
mErrorDialog = null;
Logger.i(TAG, "rotation = " + rotation);
mRotation = rotation;
}
void notifyCompassUpdated(double north)
{
mSavedNorth = north;
if (mActivity != null)
{
int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
mSavedNorth = LocationUtils.correctCompassAngle(rotation, mSavedNorth);
}
mSavedNorth = LocationUtils.correctCompassAngle(mRotation, north);
for (LocationListener listener : mListeners)
listener.onCompassUpdated(mSavedNorth);
mListeners.finishIterate();
}
private void notifyLocationUpdated()
@ -186,11 +135,8 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
if (mSavedLocation == null)
throw new IllegalStateException("No saved location");
closeLocationDialog();
for (LocationListener listener : mListeners)
listener.onLocationUpdated(mSavedLocation);
mListeners.finishIterate();
// If we are still in the first run mode, i.e. user is staying on the first run screens,
// not on the map, we mustn't post location update to the core. Only this preserving allows us
@ -213,11 +159,14 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
@Override
public void onLocationChanged(@NonNull Location location)
{
if (!isActive())
return;
Logger.d(TAG, "provider = " + mLocationProvider.getClass().getSimpleName() + " location = " + location);
if (!isActive())
{
Logger.w(TAG, "Provider is not active");
return;
}
if (!LocationUtils.isAccuracySatisfied(location))
{
Logger.w(TAG, "Unsatisfied accuracy for location = " + location);
@ -242,26 +191,16 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
@UiThread
public void onLocationResolutionRequired(@NonNull PendingIntent pendingIntent)
{
if (!isActive())
return;
Logger.d(TAG);
if (mResolutionRequest == null)
if (!isActive())
{
Logger.d(TAG, "Can't resolve location permissions because UI is not attached");
stop();
LocationState.nativeOnLocationError(LocationState.ERROR_GPS_OFF);
Logger.w(TAG, "Provider is not active");
return;
}
// Cancel our dialog in favor of system dialog.
closeLocationDialog();
// Launch system permission resolution dialog.
IntentSenderRequest intentSenderRequest = new IntentSenderRequest.Builder(pendingIntent.getIntentSender())
.build();
mResolutionRequest.launch(intentSenderRequest);
for (LocationListener listener : mListeners)
listener.onLocationResolutionRequired(pendingIntent);
}
@Override
@ -280,114 +219,13 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
public void onLocationDisabled()
{
Logger.d(TAG, "provider = " + mLocationProvider.getClass().getSimpleName() +
" permissions = " + LocationUtils.isLocationGranted(mContext) +
" settings = " + LocationUtils.areLocationServicesTurnedOn(mContext));
stop();
LocationState.nativeOnLocationError(LocationState.ERROR_GPS_OFF);
if (mActivity == null)
{
Logger.d(TAG, "Don't show 'location disabled' error dialog because Activity is not attached");
return;
}
if (mErrorDialog != null && mErrorDialog.isShowing())
{
Logger.d(TAG, "Don't show 'location disabled' error dialog because another dialog is in progress");
return;
}
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mActivity, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.enable_location_services)
.setMessage(R.string.location_is_disabled_long_text)
.setOnDismissListener(dialog -> mErrorDialog = null)
.setNegativeButton(R.string.close, null);
final Intent intent = Utils.makeSystemLocationSettingIntent(mActivity);
if (intent != null)
{
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
builder.setPositiveButton(R.string.connection_settings, (dialog, which) -> mActivity.startActivity(intent));
}
mErrorDialog = builder.show();
}
@UiThread
private void onLocationDenied()
{
Logger.d(TAG, "provider = " + mLocationProvider.getClass().getSimpleName() +
" permissions = " + LocationUtils.isLocationGranted(mContext) +
" settings = " + LocationUtils.areLocationServicesTurnedOn(mContext));
stop();
LocationState.nativeOnLocationError(LocationState.ERROR_DENIED);
if (mActivity == null)
{
Logger.w(TAG, "Don't show 'location denied' error dialog because Activity is not attached");
return;
}
if (mErrorDialog != null && mErrorDialog.isShowing())
{
Logger.w(TAG, "Don't show 'location denied' error dialog because another dialog is in progress");
return;
}
mErrorDialog = new MaterialAlertDialogBuilder(mActivity, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.enable_location_services)
.setMessage(R.string.location_is_disabled_long_text)
.setOnDismissListener(dialog -> mErrorDialog = null)
.setNegativeButton(R.string.close, null)
.show();
}
@UiThread
private void onLocationPendingTimeout()
{
Logger.d(TAG, " permissions = " + LocationUtils.isLocationGranted(mContext) +
" settings = " + LocationUtils.areLocationServicesTurnedOn(mContext));
//
// For all cases below we don't stop location provider until user explicitly clicks "Stop" in the dialog.
//
if (!isActive())
{
Logger.d(TAG, "Don't show 'location timeout' error dialog because provider is already stopped");
return;
}
if (mActivity == null)
{
Logger.d(TAG, "Don't show 'location timeout' error dialog because Activity is not attached");
return;
}
if (mErrorDialog != null && mErrorDialog.isShowing())
{
Logger.d(TAG, "Don't show 'location timeout' error dialog because another dialog is in progress");
return;
}
final AppCompatActivity activity = mActivity;
mErrorDialog = new MaterialAlertDialogBuilder(activity, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.current_location_unknown_title)
.setMessage(R.string.current_location_unknown_message)
.setOnDismissListener(dialog -> mErrorDialog = null)
.setNegativeButton(R.string.current_location_unknown_stop_button, (dialog, which) ->
{
Logger.w(TAG, "Disabled by user");
LocationState.nativeOnLocationError(LocationState.ERROR_GPS_OFF);
stop();
})
.setPositiveButton(R.string.current_location_unknown_continue_button, (dialog, which) ->
{
// Do nothing - provider will continue to search location.
})
.show();
for (LocationListener listener : mListeners)
listener.onLocationDisabled();
}
/**
@ -398,24 +236,24 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
@UiThread
public void addListener(@NonNull LocationListener listener)
{
Logger.d(TAG, "listener: " + listener + " count was: " + mListeners.getSize());
Logger.d(TAG, "listener: " + listener + " count was: " + mListeners.size());
mListeners.register(listener);
mListeners.add(listener);
if (mSavedLocation != null)
listener.onLocationUpdated(mSavedLocation);
if (!Double.isNaN(mSavedNorth))
listener.onCompassUpdated(mSavedNorth);
}
@UiThread
/**
* Removes given location listener.
* @param listener listener to unregister.
*/
@UiThread
public void removeListener(@NonNull LocationListener listener)
{
Logger.d(TAG, "listener: " + listener + " count was: " + mListeners.getSize());
mListeners.unregister(listener);
Logger.d(TAG, "listener: " + listener + " count was: " + mListeners.size());
mListeners.remove(listener);
}
private void calcLocationUpdatesInterval()
@ -481,40 +319,30 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
{
Logger.d(TAG);
stop();
if (ContextCompat.checkSelfPermission(mContext, ACCESS_COARSE_LOCATION) != PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(mContext, ACCESS_FINE_LOCATION) != PERMISSION_GRANTED)
{
Logger.w(TAG, "Location is not restarted in foreground because of missing permissions");
return;
}
start();
}
/**
* Starts polling location updates.
*/
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
public void start()
{
Logger.d(TAG);
if (isActive())
throw new IllegalStateException("Already started");
if (!LocationUtils.isLocationGranted(mContext))
{
Logger.w(TAG, "Dynamic permissions ACCESS_COARSE_LOCATION and/or ACCESS_FINE_LOCATION are not granted");
Logger.d(TAG, "error mode = " + LocationState.nativeGetMode());
LocationState.nativeOnLocationError(LocationState.ERROR_DENIED);
if (mPermissionRequest == null)
{
Logger.w(TAG, "Don't request location permissions because Activity is not attached");
return;
}
mPermissionRequest.launch(new String[]{
ACCESS_COARSE_LOCATION,
ACCESS_FINE_LOCATION
});
Logger.d(TAG, "Already started");
return;
}
Logger.i(TAG);
checkForAgpsUpdates();
LocationState.nativeSetLocationPendingTimeoutListener(this::onLocationPendingTimeout);
mSensorHelper.start();
final long oldInterval = mInterval;
calcLocationUpdatesInterval();
@ -529,17 +357,15 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
*/
public void stop()
{
Logger.d(TAG);
if (!isActive())
{
Logger.w(TAG, "Already stopped");
Logger.d(TAG, "Already stopped");
return;
}
Logger.i(TAG);
mLocationProvider.stop();
mSensorHelper.stop();
LocationState.nativeRemoveLocationPendingTimeoutListener();
mActive = false;
}
@ -563,83 +389,6 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
manager.sendExtraCommand(LocationManager.GPS_PROVIDER, "force_time_injection", null);
}
/**
* Attach UI to helper.
*/
@UiThread
public void attach(@NonNull AppCompatActivity activity)
{
Logger.d(TAG, "activity = " + activity);
if (mActivity != null)
{
Logger.e(TAG, "Another Activity = " + mActivity + " is already attached");
detach();
}
mActivity = activity;
mPermissionRequest = mActivity.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(),
result -> onRequestPermissionsResult());
mResolutionRequest = mActivity.registerForActivityResult(new ActivityResultContracts.StartIntentSenderForResult(),
result -> onLocationResolutionResult(result.getResultCode()));
}
/**
* Detach UI from helper.
*/
@UiThread
public void detach()
{
Logger.d(TAG, "activity = " + mActivity);
if (mActivity == null)
{
Logger.e(TAG, "Activity is not attached");
return;
}
assert mPermissionRequest != null;
mPermissionRequest.unregister();
mPermissionRequest = null;
assert mResolutionRequest != null;
mResolutionRequest.unregister();
mResolutionRequest = null;
mActivity = null;
}
@UiThread
private void onRequestPermissionsResult()
{
Logger.d(TAG);
if (LocationUtils.isLocationGranted(mContext))
{
Logger.i(TAG, "Permissions have been granted");
if (!isActive())
start();
return;
}
Logger.w(TAG, "Permissions have not been granted");
onLocationDenied();
}
@UiThread
private void onLocationResolutionResult(int resultCode)
{
if (resultCode != Activity.RESULT_OK)
{
Logger.w(TAG, "Resolution has not been granted");
stop();
LocationState.nativeOnLocationError(LocationState.ERROR_GPS_OFF);
return;
}
Logger.i(TAG, "Resolution has been granted");
restart();
}
@UiThread
public boolean isInFirstRun()
{
@ -669,11 +418,7 @@ public enum LocationHelper implements Initializable<Context>, AppBackgroundTrack
notifyLocationUpdated();
Logger.d(TAG, "Current location is available, so play the nice zoom animation");
Framework.nativeRunFirstLaunchAnimation();
return;
}
// Restart location service to show alert dialog if any location error.
restart();
}
public double getSavedNorth()

View file

@ -1,5 +1,6 @@
package app.organicmaps.location;
import android.app.PendingIntent;
import android.location.Location;
import androidx.annotation.NonNull;
@ -12,4 +13,14 @@ public interface LocationListener
{
// No op.
}
default void onLocationDisabled()
{
// No op.
}
default void onLocationResolutionRequired(@NonNull PendingIntent pendingIntent)
{
// No op.
}
}

View file

@ -15,7 +15,7 @@ public final class LocationState
void onMyPositionModeChanged(int newMode);
}
interface PendingTimeoutListener
public interface PendingTimeoutListener
{
void onLocationPendingTimeout();
}
@ -45,10 +45,10 @@ public final class LocationState
public static native void nativeSetListener(@NonNull ModeChangeListener listener);
public static native void nativeRemoveListener();
static native void nativeSetLocationPendingTimeoutListener(@NonNull PendingTimeoutListener listener);
static native void nativeRemoveLocationPendingTimeoutListener();
public static native void nativeSetLocationPendingTimeoutListener(@NonNull PendingTimeoutListener listener);
public static native void nativeRemoveLocationPendingTimeoutListener();
static native void nativeOnLocationError(int errorCode);
public static native void nativeOnLocationError(int errorCode);
static native void nativeLocationUpdated(long time, double lat, double lon, float accuracy,
double altitude, float speed, float bearing);

View file

@ -1,10 +1,6 @@
package app.organicmaps.util;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationManager;
import android.os.Build;
@ -12,7 +8,6 @@ import android.provider.Settings;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
public class LocationUtils
{
@ -116,14 +111,4 @@ public class LocationUtils
return false;
}
}
public static boolean isFineLocationGranted(@NonNull Context context)
{
return ContextCompat.checkSelfPermission(context, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
public static boolean isLocationGranted(@NonNull Context context)
{
return isFineLocationGranted(context) || ContextCompat.checkSelfPermission(context, ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
}