Compare commits

...
Sign in to create a new pull request.

6 commits

Author SHA1 Message Date
Alexander Borsuk
30134ff26f [android] Update gradle plugun and androidx deps
Signed-off-by: Alexander Borsuk <me@alex.bio>
2024-05-16 14:29:06 +03:00
Andrew Shkrob
aad60785ba [android-auto] New UI
Signed-off-by: Andrew Shkrob <andrew.shkrob.social@yandex.by>
2024-05-16 14:27:10 +03:00
Andrew Shkrob
2586bdcc8c [android-auto] Use compass and location data from car
Signed-off-by: Andrew Shkrob <andrew.shkrob.social@yandex.by>
2024-05-16 14:26:48 +03:00
d727feb891 some minor fixes
Signed-off-by: kavikhalique <kavikhalique3@gmail.com>
2024-05-16 09:36:58 +03:00
67a8e3f7a2 some left out files and changes
Signed-off-by: kavikhalique <kavikhalique3@gmail.com>
2024-05-16 09:36:58 +03:00
f1510351ef reverts recent track recorded
Signed-off-by: kavikhalique <kavikhalique3@gmail.com>
2024-05-16 09:36:58 +03:00
21 changed files with 615 additions and 45 deletions

View file

@ -22,7 +22,7 @@ buildscript {
googleFirebaseServicesDefault
dependencies {
classpath 'com.android.tools.build:gradle:8.3.2'
classpath 'com.android.tools.build:gradle:8.4.0'
if (googleFirebaseServicesEnabled) {
println('Building with Google Firebase Services')
@ -376,18 +376,19 @@ dependencies {
// > A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable
// We don't use Kotlin, but some dependencies are actively using it.
// See https://stackoverflow.com/a/75719642
implementation 'androidx.core:core:1.12.0'
implementation 'androidx.core:core:1.13.1'
implementation(platform('org.jetbrains.kotlin:kotlin-bom:1.9.23'))
implementation 'androidx.annotation:annotation:1.7.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.car.app:app:1.7.0-alpha02'
implementation 'androidx.car.app:app-projected:1.7.0-alpha02'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.fragment:fragment:1.6.2'
implementation 'androidx.fragment:fragment:1.7.0'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.work:work-runtime:2.9.0'
implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.google.android.material:material:1.12.0'
// Fix for app/organicmaps/util/FileUploadWorker.java:14: error: cannot access ListenableFuture
// https://github.com/organicmaps/organicmaps/issues/6106
implementation 'com.google.guava:guava:33.1.0-android'

View file

@ -451,6 +451,13 @@
android:enabled="true"
android:stopWithTask="false"/>
<service android:name=".location.TrackRecordingService"
android:foregroundServiceType="location"
android:exported="false"
android:enabled="true"
android:stopWithTask="false"
/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${FILE_PROVIDER_PLACEHOLDER}"

View file

@ -334,7 +334,7 @@ public final class Map
}
else
{
nativeSetupWidget(WIDGET_SCALE_FPS_LABEL, UiUtils.dimen(context, R.dimen.margin_base), mHeight - UiUtils.dimen(context, R.dimen.margin_base) * 5, ANCHOR_LEFT_TOP);
nativeSetupWidget(WIDGET_SCALE_FPS_LABEL, (float) mWidth / 2 + UiUtils.dimen(context, R.dimen.margin_base) * 2, UiUtils.dimen(context, R.dimen.margin_base), ANCHOR_LEFT_TOP);
updateCompassOffset(context, mWidth, mCurrentCompassOffsetY, true);
}
}

View file

@ -70,6 +70,7 @@ import app.organicmaps.location.LocationListener;
import app.organicmaps.location.LocationState;
import app.organicmaps.location.SensorHelper;
import app.organicmaps.location.SensorListener;
import app.organicmaps.location.TrackRecordingService;
import app.organicmaps.maplayer.MapButtonsController;
import app.organicmaps.maplayer.MapButtonsViewModel;
import app.organicmaps.maplayer.ToggleMapLayerFragment;

View file

@ -26,8 +26,11 @@ import app.organicmaps.display.DisplayManager;
import app.organicmaps.downloader.CountryItem;
import app.organicmaps.downloader.MapManager;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.location.LocationListener;
import app.organicmaps.location.LocationState;
import app.organicmaps.location.SensorHelper;
import app.organicmaps.location.TrackRecorder;
import app.organicmaps.location.TrackRecordingService;
import app.organicmaps.maplayer.isolines.IsolinesManager;
import app.organicmaps.maplayer.subway.SubwayManager;
import app.organicmaps.maplayer.traffic.TrafficManager;
@ -36,8 +39,10 @@ import app.organicmaps.routing.RoutingController;
import app.organicmaps.search.SearchEngine;
import app.organicmaps.settings.StoragePathManager;
import app.organicmaps.sound.TtsPlayer;
import app.organicmaps.util.AppStateListener;
import app.organicmaps.util.Config;
import app.organicmaps.util.ConnectionState;
import app.organicmaps.util.Listeners;
import app.organicmaps.util.SharedPropertiesUtils;
import app.organicmaps.util.StorageUtils;
import app.organicmaps.util.ThemeSwitcher;
@ -48,6 +53,8 @@ import app.organicmaps.util.log.LogsManager;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class MwmApplication extends Application implements Application.ActivityLifecycleCallbacks
@ -77,6 +84,7 @@ public class MwmApplication extends Application implements Application.ActivityL
private volatile boolean mFrameworkInitialized;
private volatile boolean mPlatformInitialized;
private final Listeners<AppStateListener> mAppStateListeners = new Listeners<>();
private Handler mMainLoopHandler;
private final Object mMainQueueToken = new Object();
@ -157,6 +165,7 @@ public class MwmApplication extends Application implements Application.ActivityL
DownloaderNotifier.createNotificationChannel(this);
NavigationService.createNotificationChannel(this);
TrackRecordingService.createNotificationChannel(this);
registerActivityLifecycleCallbacks(this);
mSubwayManager = new SubwayManager(this);
@ -348,11 +357,25 @@ public class MwmApplication extends Application implements Application.ActivityL
nativeOnTransit(true);
mLocationHelper.resumeLocationInForeground();
Iterator<AppStateListener> listenerIterator = mAppStateListeners.iterator();
while(listenerIterator.hasNext())
{
AppStateListener listener = listenerIterator.next();
listener.onAppForeground();
}
mAppStateListeners.finishIterate();
}
private void onBackground()
{
Iterator<AppStateListener> listenerIterator = mAppStateListeners.iterator();
while(listenerIterator.hasNext())
{
AppStateListener listener = listenerIterator.next();
listener.onAppBackround();
}
mAppStateListeners.finishIterate();
Logger.d(TAG);
nativeOnTransit(false);
@ -365,6 +388,9 @@ public class MwmApplication extends Application implements Application.ActivityL
Logger.i(LOCATION_TAG, "Navigation is in progress, keeping location in the background");
else if (!Map.isEngineCreated() || LocationState.getMode() == LocationState.PENDING_POSITION)
Logger.i(LOCATION_TAG, "PENDING_POSITION mode, keeping location in the background");
else if(TrackRecorder.nativeIsEnabled()){
Logger.i(LOCATION_TAG,"Recent Track Recorder is running in background");
}
else
{
Logger.i(LOCATION_TAG, "Stopping location in the background");
@ -392,4 +418,14 @@ public class MwmApplication extends Application implements Application.ActivityL
@Override
public void onProgress(String countryId, long localSize, long remoteSize) {}
}
public void addListener(AppStateListener listener)
{
mAppStateListeners.register(listener);
}
public void removeListener(AppStateListener listener)
{
mAppStateListeners.unregister(listener);
}
}

View file

@ -13,7 +13,6 @@ import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.Framework;
import app.organicmaps.Map;
import app.organicmaps.MwmApplication;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.MapObject;
@ -25,16 +24,14 @@ import app.organicmaps.car.screens.RequestPermissionsScreen;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.download.DownloadMapsScreenBuilder;
import app.organicmaps.car.screens.download.DownloaderHelpers;
import app.organicmaps.car.util.CarSensorsManager;
import app.organicmaps.car.util.CurrentCountryChangedListener;
import app.organicmaps.car.util.IntentUtils;
import app.organicmaps.car.util.ThemeUtils;
import app.organicmaps.display.DisplayChangedListener;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.display.DisplayType;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.location.LocationState;
import app.organicmaps.location.SensorHelper;
import app.organicmaps.location.SensorListener;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.util.Config;
import app.organicmaps.util.LocationUtils;
@ -46,7 +43,7 @@ import java.util.ArrayList;
import java.util.List;
public final class CarAppSession extends Session implements DefaultLifecycleObserver,
SensorListener, LocationState.ModeChangeListener, DisplayChangedListener, Framework.PlacePageActivationListener
LocationState.ModeChangeListener, DisplayChangedListener, Framework.PlacePageActivationListener
{
private static final String TAG = CarAppSession.class.getSimpleName();
@ -56,6 +53,9 @@ public final class CarAppSession extends Session implements DefaultLifecycleObse
private final SurfaceRenderer mSurfaceRenderer;
@NonNull
private final ScreenManager mScreenManager;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private CarSensorsManager mSensorsManager;
@NonNull
private final CurrentCountryChangedListener mCurrentCountryChangedListener;
@SuppressWarnings("NotNullFieldNotInitialized")
@ -111,6 +111,7 @@ public final class CarAppSession extends Session implements DefaultLifecycleObse
public void onCreate(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
mSensorsManager = new CarSensorsManager(getCarContext());
mDisplayManager = DisplayManager.from(getCarContext());
mDisplayManager.addListener(DisplayType.Car, this);
init();
@ -126,9 +127,8 @@ public final class CarAppSession extends Session implements DefaultLifecycleObse
Framework.nativePlacePageActivationListener(this);
mCurrentCountryChangedListener.onStart(getCarContext());
}
SensorHelper.from(getCarContext()).addListener(this);
if (LocationUtils.checkFineLocationPermission(getCarContext()) && !LocationHelper.from(getCarContext()).isActive())
LocationHelper.from(getCarContext()).start();
if (LocationUtils.checkFineLocationPermission(getCarContext()))
mSensorsManager.onStart();
if (mDisplayManager.isCarDisplayUsed())
{
@ -141,7 +141,7 @@ public final class CarAppSession extends Session implements DefaultLifecycleObse
public void onStop(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
SensorHelper.from(getCarContext()).removeListener(this);
mSensorsManager.onStop();
if (mDisplayManager.isCarDisplayUsed())
{
LocationState.nativeRemoveListener();
@ -182,7 +182,7 @@ public final class CarAppSession extends Session implements DefaultLifecycleObse
screensStack.add(new DownloadMapsScreenBuilder(getCarContext()).setDownloaderType(DownloadMapsScreenBuilder.DownloaderType.FirstLaunch).build());
if (!LocationUtils.checkFineLocationPermission(getCarContext()))
screensStack.add(new RequestPermissionsScreen(getCarContext(), () -> LocationHelper.from(getCarContext()).start()));
screensStack.add(new RequestPermissionsScreen(getCarContext(), mSensorsManager::onStart));
if (mDisplayManager.isDeviceDisplayUsed())
{
@ -205,11 +205,6 @@ public final class CarAppSession extends Session implements DefaultLifecycleObse
screen.invalidate();
}
public void onCompassUpdated(double north)
{
Map.onCompassUpdated(north, true);
}
@Override
public void onDisplayChangedToDevice(@NonNull Runnable onTaskFinishedCallback)
{

View file

@ -7,10 +7,10 @@ import androidx.car.app.CarContext;
import androidx.car.app.constraints.ConstraintManager;
import androidx.car.app.model.Action;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.GridItem;
import androidx.car.app.model.GridTemplate;
import androidx.car.app.model.Header;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.ListTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapWithContentTemplate;
import androidx.core.graphics.drawable.IconCompat;
@ -70,14 +70,14 @@ public class CategoriesScreen extends BaseMapScreen
}
@NonNull
private ListTemplate createCategoriesListTemplate()
private GridTemplate createCategoriesListTemplate()
{
final boolean isNightMode = ThemeUtils.isNightMode(getCarContext());
final ItemList.Builder builder = new ItemList.Builder();
final int categoriesSize = Math.min(CATEGORIES.size(), MAX_CATEGORIES_SIZE);
for (int i = 0; i < categoriesSize; ++i)
{
final Row.Builder itemBuilder = new Row.Builder();
final GridItem.Builder itemBuilder = new GridItem.Builder();
final String title = getCarContext().getString(CATEGORIES.get(i).nameResId);
@DrawableRes final int iconResId = isNightMode ? CATEGORIES.get(i).iconNightResId : CATEGORIES.get(i).iconResId;
@ -86,6 +86,6 @@ public class CategoriesScreen extends BaseMapScreen
itemBuilder.setOnClickListener(() -> getScreenManager().push(new SearchOnMapScreen.Builder(getCarContext(), getSurfaceRenderer()).setCategory(title).build()));
builder.addItem(itemBuilder.build());
}
return new ListTemplate.Builder().setHeader(createHeader()).setSingleList(builder.build()).build();
return new GridTemplate.Builder().setHeader(createHeader()).setSingleList(builder.build()).build();
}
}

View file

@ -5,11 +5,11 @@ import androidx.car.app.CarContext;
import androidx.car.app.model.Action;
import androidx.car.app.model.ActionStrip;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.GridItem;
import androidx.car.app.model.GridTemplate;
import androidx.car.app.model.Header;
import androidx.car.app.model.Item;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.ListTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapWithContentTemplate;
import androidx.core.graphics.drawable.IconCompat;
@ -19,6 +19,7 @@ import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.bookmarks.BookmarkCategoriesScreen;
import app.organicmaps.car.screens.search.SearchScreen;
import app.organicmaps.car.screens.settings.SettingsScreen;
import app.organicmaps.car.util.SuggestionsHelpers;
import app.organicmaps.car.util.UiHelpers;
@ -38,7 +39,7 @@ public class MapScreen extends BaseMapScreen
final MapWithContentTemplate.Builder builder = new MapWithContentTemplate.Builder();
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setActionStrip(createActionStrip());
builder.setContentTemplate(createListTemplate());
builder.setContentTemplate(createGridTemplate());
return builder.build();
}
@ -60,19 +61,19 @@ public class MapScreen extends BaseMapScreen
final ActionStrip.Builder builder = new ActionStrip.Builder();
builder.addAction(freeDriveScreenBuilder.build());
builder.addAction(UiHelpers.createSettingsAction(this, getSurfaceRenderer()));
return builder.build();
}
@NonNull
private ListTemplate createListTemplate()
private GridTemplate createGridTemplate()
{
final ListTemplate.Builder builder = new ListTemplate.Builder();
final GridTemplate.Builder builder = new GridTemplate.Builder();
final ItemList.Builder itemsBuilder = new ItemList.Builder();
itemsBuilder.addItem(createSearchItem());
itemsBuilder.addItem(createCategoriesItem());
itemsBuilder.addItem(createBookmarksItem());
itemsBuilder.addItem(createSettingsItem());
builder.setHeader(createHeader());
builder.setSingleList(itemsBuilder.build());
@ -84,10 +85,9 @@ public class MapScreen extends BaseMapScreen
{
final CarIcon iconSearch = new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_search)).build();
final Row.Builder builder = new Row.Builder();
final GridItem.Builder builder = new GridItem.Builder();
builder.setTitle(getCarContext().getString(R.string.search));
builder.setImage(iconSearch);
builder.setBrowsable(true);
builder.setOnClickListener(this::openSearch);
return builder.build();
}
@ -95,9 +95,11 @@ public class MapScreen extends BaseMapScreen
@NonNull
private Item createCategoriesItem()
{
final Row.Builder builder = new Row.Builder();
final CarIcon iconCategories = new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_address)).build();
final GridItem.Builder builder = new GridItem.Builder();
builder.setImage(iconCategories);
builder.setTitle(getCarContext().getString(R.string.categories));
builder.setBrowsable(true);
builder.setOnClickListener(this::openCategories);
return builder.build();
}
@ -105,13 +107,27 @@ public class MapScreen extends BaseMapScreen
@NonNull
private Item createBookmarksItem()
{
final Row.Builder builder = new Row.Builder();
final CarIcon iconBookmarks = new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_bookmarks)).build();
final GridItem.Builder builder = new GridItem.Builder();
builder.setImage(iconBookmarks);
builder.setTitle(getCarContext().getString(R.string.bookmarks));
builder.setBrowsable(true);
builder.setOnClickListener(this::openBookmarks);
return builder.build();
}
@NonNull
private Item createSettingsItem()
{
final CarIcon iconSettings = new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_settings)).build();
final GridItem.Builder builder = new GridItem.Builder();
builder.setImage(iconSettings);
builder.setTitle(getCarContext().getString(R.string.settings));
builder.setOnClickListener(this::openSettings);
return builder.build();
}
private void openSearch()
{
// Details in UiHelpers.createSettingsAction()
@ -135,4 +151,12 @@ public class MapScreen extends BaseMapScreen
return;
getScreenManager().push(new BookmarkCategoriesScreen(getCarContext(), getSurfaceRenderer()));
}
private void openSettings()
{
// Details in UiHelpers.createSettingsAction()
if (getScreenManager().getTop() != this)
return;
getScreenManager().push(new SettingsScreen(getCarContext(), getSurfaceRenderer()));
}
}

View file

@ -0,0 +1,118 @@
package app.organicmaps.car.util;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresPermission;
import androidx.car.app.CarContext;
import androidx.car.app.hardware.CarHardwareManager;
import androidx.car.app.hardware.common.CarValue;
import androidx.car.app.hardware.info.CarHardwareLocation;
import androidx.car.app.hardware.info.CarSensors;
import androidx.car.app.hardware.info.Compass;
import androidx.core.content.ContextCompat;
import app.organicmaps.Map;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.location.SensorHelper;
import app.organicmaps.util.log.Logger;
import java.util.List;
import java.util.concurrent.Executor;
public class CarSensorsManager
{
private static final String TAG = CarSensorsManager.class.getSimpleName();
@NonNull
private final CarContext mCarContext;
@NonNull
private final CarSensors mCarSensors;
private boolean mIsCarCompassUsed = true;
private boolean mIsCarLocationUsed = true;
public CarSensorsManager(@NonNull final CarContext context)
{
mCarContext = context;
mCarSensors = mCarContext.getCarService(CarHardwareManager.class).getCarSensors();
}
@RequiresPermission(ACCESS_FINE_LOCATION)
public void onStart()
{
final Executor executor = ContextCompat.getMainExecutor(mCarContext);
if (mIsCarCompassUsed)
mCarSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL, executor, this::onCarCompassDataAvailable);
else
SensorHelper.from(mCarContext).addListener(this::onCompassUpdated);
if (!LocationHelper.from(mCarContext).isActive())
LocationHelper.from(mCarContext).start();
if (mIsCarLocationUsed)
mCarSensors.addCarHardwareLocationListener(CarSensors.UPDATE_RATE_FASTEST, executor, this::onCarLocationDataAvailable);
}
public void onStop()
{
if (mIsCarCompassUsed)
mCarSensors.removeCompassListener(this::onCarCompassDataAvailable);
else
SensorHelper.from(mCarContext).removeListener(this::onCompassUpdated);
if (mIsCarLocationUsed)
mCarSensors.removeCarHardwareLocationListener(this::onCarLocationDataAvailable);
}
private void onCarCompassDataAvailable(@NonNull final Compass compass)
{
final CarValue<List<Float>> data = compass.getOrientations();
if (data.getStatus() == CarValue.STATUS_UNIMPLEMENTED)
onCarCompassUnsupported();
else if (data.getStatus() == CarValue.STATUS_SUCCESS)
{
final List<Float> orientations = compass.getOrientations().getValue();
if (orientations == null)
return;
final float azimuth = orientations.get(0);
Map.onCompassUpdated(Math.toRadians(azimuth), true);
}
}
private void onCompassUpdated(double north)
{
Map.onCompassUpdated(north, true);
}
private void onCarLocationDataAvailable(@NonNull final CarHardwareLocation hardwareLocation)
{
final CarValue<Location> location = hardwareLocation.getLocation();
if (location.getStatus() == CarValue.STATUS_UNIMPLEMENTED)
onCarLocationUnsupported();
else if (location.getStatus() == CarValue.STATUS_SUCCESS)
{
final Location loc = location.getValue();
if (loc != null)
LocationHelper.from(mCarContext).onLocationChanged(loc);
}
}
private void onCarLocationUnsupported()
{
Logger.d(TAG);
mIsCarLocationUsed = false;
mCarSensors.removeCarHardwareLocationListener(this::onCarLocationDataAvailable);
}
private void onCarCompassUnsupported()
{
Logger.d(TAG);
mIsCarCompassUsed = false;
mCarSensors.removeCompassListener(this::onCarCompassDataAvailable);
SensorHelper.from(mCarContext).addListener(this::onCompassUpdated);
}
}

View file

@ -3,9 +3,11 @@ package app.organicmaps.location;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationManager;
@ -13,6 +15,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
import androidx.annotation.UiThread;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.location.GnssStatusCompat;
import androidx.core.location.LocationManagerCompat;
@ -24,6 +27,7 @@ import app.organicmaps.bookmarks.data.FeatureId;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.routing.JunctionInfo;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.util.AppStateListener;
import app.organicmaps.util.Config;
import app.organicmaps.util.LocationUtils;
import app.organicmaps.util.NetworkPolicy;
@ -32,11 +36,14 @@ import app.organicmaps.util.log.Logger;
import java.util.LinkedHashSet;
import java.util.Set;
public class LocationHelper implements BaseLocationProvider.Listener
public class LocationHelper
implements BaseLocationProvider.Listener,
AppStateListener
{
private static final long INTERVAL_FOLLOW_MS = 0;
private static final long INTERVAL_NOT_FOLLOW_MS = 3000;
private static final long INTERVAL_NAVIGATION_MS = 0;
private static final long INTERVAL_TRACK_RECORDING_BACKGROUND = 10000;
private static final long AGPS_EXPIRATION_TIME_MS = 16 * 60 * 60 * 1000; // 16 hours
@ -79,6 +86,7 @@ public class LocationHelper implements BaseLocationProvider.Listener
@Override
public void onSatelliteStatusChanged(@NonNull GnssStatusCompat status)
{
Logger.d(TAG,"value of interval "+ mInterval);
int used = 0;
boolean fixed = false;
for (int i = 0; i < status.getSatelliteCount(); i++)
@ -103,6 +111,7 @@ public class LocationHelper implements BaseLocationProvider.Listener
{
mContext = context;
mLocationProvider = LocationProviderFactory.getProvider(mContext, this);
MwmApplication.from(mContext).addListener(this);
}
/**
@ -172,6 +181,7 @@ public class LocationHelper implements BaseLocationProvider.Listener
public void onLocationChanged(@NonNull Location location)
{
Logger.d(TAG, "provider = " + mLocationProvider.getClass().getSimpleName() + " location = " + location);
Logger.d(TAG,"value of interval "+ mInterval);
if (!isActive())
{
@ -230,7 +240,7 @@ public class LocationHelper implements BaseLocationProvider.Listener
{
// Try to downgrade to the native provider first and restart the service before notifying the user.
Logger.d(TAG, "provider = " + mLocationProvider.getClass().getSimpleName() + " is not supported," +
" downgrading to use native provider");
" downgrading to use native provider");
mLocationProvider.stop();
mLocationProvider = new AndroidNativeProvider(mContext, this);
mActive = true;
@ -394,7 +404,6 @@ public class LocationHelper implements BaseLocationProvider.Listener
Logger.i(TAG, "Permissions ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION are not granted");
return;
}
start();
}
@ -466,4 +475,29 @@ public class LocationHelper implements BaseLocationProvider.Listener
Framework.nativeRunFirstLaunchAnimation();
}
}
@Override
public void onAppBackround()
{
Logger.i("kavi", "Location helper knows app went in background");
if (!RoutingController.get().isNavigating() && isActive() && TrackRecorder.nativeIsEnabled() && mInterval != INTERVAL_TRACK_RECORDING_BACKGROUND)
{
Logger.i(TAG, "update refresh interval: old = " + mInterval + " new = " + INTERVAL_TRACK_RECORDING_BACKGROUND);
if (LocationUtils.checkLocationPermission(mContext))
{
mLocationProvider.stop();
mInterval = INTERVAL_TRACK_RECORDING_BACKGROUND;
mLocationProvider.start(INTERVAL_TRACK_RECORDING_BACKGROUND);
}
}
Logger.i("kavi","interval is "+ mInterval);
}
@Override
public void onAppForeground()
{
Logger.i("kavi", "Location helper knows app came in foreground");
resumeLocationInForeground();
if(LocationUtils.checkLocationPermission(mContext)) restartWithNewMode();
}
}

View file

@ -0,0 +1,9 @@
package app.organicmaps.location;
public abstract class TrackRecorder
{
public static native void nativeSetEnabled(boolean enable);
public static native boolean nativeIsEnabled();
public static native void nativeSetDuration(int hours);
public static native int nativeGetDuration();
}

View file

@ -0,0 +1,172 @@
package app.organicmaps.location;
import android.app.ForegroundServiceStartNotAllowedException;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import app.organicmaps.MwmActivity;
import app.organicmaps.MwmApplication;
import app.organicmaps.R;
import app.organicmaps.settings.TrackRecordSettingsFragment;
import app.organicmaps.util.LocationUtils;
import app.organicmaps.util.log.Logger;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
public class TrackRecordingService extends Service implements LocationListener
{
public static final String TRACK_REC_CHANNEL_ID = "TRACK RECORDING";
public static final int TRACK_REC_NOTIFICATION_ID = 54321;
private static NotificationCompat.Builder mNotificationBuilder;
private static final String TAG = TrackRecordingService.class.getSimpleName();
@Nullable
@Override
public IBinder onBind(Intent intent)
{
return null;
}
@RequiresPermission(value = ACCESS_FINE_LOCATION)
public static void startForegroundService(@NonNull Context context)
{
ContextCompat.startForegroundService(context, new Intent(context, TrackRecordingService.class));
}
public static void createNotificationChannel(@NonNull Context context)
{
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
final NotificationChannelCompat channel = new NotificationChannelCompat.Builder(TRACK_REC_CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_LOW)
.setName("Track Recording")
.setLightsEnabled(false)
.setVibrationEnabled(false)
.build();
notificationManager.createNotificationChannel(channel);
}
@NonNull
public static NotificationCompat.Builder getNotificationBuilder(@NonNull Context context)
{
if (mNotificationBuilder != null)
return mNotificationBuilder;
final int FLAG_IMMUTABLE = Build.VERSION.SDK_INT < Build.VERSION_CODES.M ? 0 : PendingIntent.FLAG_IMMUTABLE;
final Intent contentIntent = new Intent(context, MwmActivity.class);
final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent,
PendingIntent.FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE);
mNotificationBuilder = new NotificationCompat.Builder(context, TRACK_REC_CHANNEL_ID)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(NotificationManager.IMPORTANCE_LOW)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOngoing(true)
.setShowWhen(true)
.setOnlyAlertOnce(true)
.setSmallIcon(R.drawable.ic_splash)
.setContentTitle("Track Recording is Running")
.setContentText("Recording your traversed tracks in background")
.setContentIntent(pendingIntent)
.setColor(ContextCompat.getColor(context, R.color.notification));
return mNotificationBuilder;
}
@Override
public void onDestroy()
{
mNotificationBuilder = null;
LocationHelper.from(this).removeListener(this);
if(TrackRecorder.nativeIsEnabled()) TrackRecorder.nativeSetEnabled(false);
// The notification is cancelled automatically by the system.
}
@Override
public int onStartCommand(@NonNull Intent intent, int flags, int startId)
{
if (!MwmApplication.from(this).arePlatformAndCoreInitialized())
{
Logger.w(TAG, "Application is not initialized");
stopSelf();
return START_NOT_STICKY; // The service will be stopped by stopSelf().
}
if (!LocationUtils.checkFineLocationPermission(this))
{
// In a hypothetical scenario, the user could revoke location permissions after the app's process crashed,
// but before the service with START_STICKY was restarted by the system.
Logger.w(TAG, "Permission ACCESS_FINE_LOCATION is not granted, skipping TrackRecordingService");
stopSelf();
return START_NOT_STICKY; // The service will be stopped by stopSelf().
}
if(!TrackRecorder.nativeIsEnabled())
{
Logger.i("kavi","Service can't be started because Track Recorder is turned off in settings");
stopSelf();
return START_NOT_STICKY;
}
Logger.i(TAG, "Starting foreground");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
{
try
{
startForeground(TrackRecordingService.TRACK_REC_NOTIFICATION_ID, getNotificationBuilder(this).build());
} catch (ForegroundServiceStartNotAllowedException e)
{
Logger.e(TAG, "Oops! ForegroundService is not allowed", e);
}
}
else
{
startForeground(TrackRecordingService.TRACK_REC_NOTIFICATION_ID, getNotificationBuilder(this).build());
}
final LocationHelper locationHelper = LocationHelper.from(this);
// Subscribe to location updates. This call is idempotent.
locationHelper.addListener(this);
// Restart the location with more frequent refresh interval for Track Recording.
locationHelper.restartWithNewMode();
return START_NOT_STICKY;
}
public static void stopService(@NonNull Context context)
{
Logger.i(TAG);
context.stopService(new Intent(context, TrackRecordingService.class));
}
@Override
public void onLocationUpdated(@NonNull Location location)
{
Logger.i(TAG,"Location is being updated in Track Recording service");
}
@Override
public void onLocationDisabled()
{
LocationListener.super.onLocationDisabled();
TrackRecorder.nativeSetEnabled(false);
stopSelf();
}
}

View file

@ -104,6 +104,10 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment
{
startActivity(new Intent(requireActivity(), HelpActivity.class));
}
else if (key.equals(getString(R.string.pref_track_record)))
{
getSettingsActivity().stackFragment(TrackRecordSettingsFragment.class,"Track Recorder",null);
}
}
return super.onPreferenceTreeClick(preference);
}

View file

@ -0,0 +1,134 @@
package app.organicmaps.settings;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.TwoStatePreference;
import app.organicmaps.MwmApplication;
import app.organicmaps.R;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.location.LocationListener;
import app.organicmaps.location.TrackRecorder;
import app.organicmaps.location.TrackRecordingService;
import app.organicmaps.util.LocationUtils;
import app.organicmaps.util.log.Logger;
import static app.organicmaps.location.TrackRecordingService.stopService;
public class TrackRecordSettingsFragment extends BaseXmlSettingsFragment implements LocationListener
{
private ListPreference mRecordTime;
private TwoStatePreference mRecentTrack;
@Override
protected int getXmlResources()
{
return R.xml.pref_track_record;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState)
{
super.onViewCreated(view, savedInstanceState);
mRecentTrack = getPreference(getString(R.string.pref_recent_track));
mRecordTime = getPreference(getString(R.string.pref_track_record_time));
LocationHelper.from(requireContext()).addListener(this);
}
@Override
public void onStart()
{
super.onStart();
mRecentTrack.setChecked(TrackRecorder.nativeIsEnabled());
mRecordTime.setValue(Integer.toString(TrackRecorder.nativeGetDuration()));
if(TrackRecorder.nativeIsEnabled())
{
mRecordTime.setSummary("Last " + TrackRecorder.nativeGetDuration() + " hours trails will be shown on map");
}
else
{
mRecordTime.setSummary("Enable Recent Track Recorder to select duration for which recorded point will be shown on map");
}
mRecordTime.setEnabled(TrackRecorder.nativeIsEnabled());
mRecentTrack.setOnPreferenceChangeListener(((preference, newValue) -> {
if (newValue == null)
return false;
boolean newVal = (boolean) newValue;
if (newVal)
{
if(!LocationUtils.areLocationServicesTurnedOn(MwmApplication.from(requireContext()))){
Toast.makeText(getActivity(),"Please turn on location",Toast.LENGTH_SHORT).show();
return false;
}
if (!LocationUtils.checkLocationPermission(MwmApplication.from(requireContext())))
{
Toast.makeText(getActivity(),"Please give permission of precise location access",Toast.LENGTH_SHORT).show();
return false;
}
TrackRecordingService.startForegroundService(MwmApplication.from(requireContext()));
mRecordTime.setSummary("Last " + TrackRecorder.nativeGetDuration() + " hour trails will be shown on map");
}
else
{
stopService(MwmApplication.from(requireContext()));
mRecordTime.setSummary("Enable Recent Track Recorder to select duration for which recorded point will be shown on map");
}
mRecentTrack.setChecked(newVal);
mRecordTime.setEnabled(newVal);
TrackRecorder.nativeSetEnabled(newVal);
return true;
}));
mRecordTime.setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue == null)
return false;
String newVal = (String) newValue;
if(newVal.equals(mRecordTime.getValue()))
return false;
mRecordTime.setSummary("Last " + newVal + " hour trails will be shown on map");
int hours = Integer.parseInt(newVal);
TrackRecorder.nativeSetDuration(hours);
return true;
});
}
@Override
public void onDestroyView()
{
super.onDestroyView();
LocationHelper.from(requireContext()).removeListener(this);
}
@Override
public void onLocationUpdated(@NonNull Location location)
{
//No requirement of this method here
}
//This method has been added to make the toggle switch of Record Recent Track alive
@Override
public void onLocationDisabled()
{
LocationListener.super.onLocationDisabled();
Logger.i("kavi","yaha tak aya");
if(mRecentTrack == null || mRecentTrack.getOnPreferenceChangeListener() == null) return;
Logger.i("kavi","yaha tak bhi aya waah");
mRecentTrack.getOnPreferenceChangeListener().onPreferenceChange(getPreference(getString(R.string.pref_recent_track)),false);
// mRecentTrack.setChecked(false);
}
}

View file

@ -0,0 +1,7 @@
package app.organicmaps.util;
public interface AppStateListener
{
void onAppBackround();
void onAppForeground();
}

View file

@ -25,6 +25,10 @@
<string name="pref_tts_info" translatable="false">TtsInfo</string>
<string name="pref_tts_info_link" translatable="false">TtsInfoLink</string>
<string name="pref_tts_speed_cameras" translatable="false">SpeedCameras</string>
<string name="pref_recent_track" translatable="false">RecentTrackRecord</string>
<string name="pref_track_record" translatable="false">TrackRecord</string>
<string name="pref_track_record_switch" translatable="false">TrackRecordSwitch</string>
<string name="pref_track_record_time" translatable="false">TrackRecordTime</string>
<!-- TODO: Move to another domain. -->
<string name="tts_info_link" translatable="false">https://mapsme.zendesk.com/hc/en-us/articles/208628985-How-can-I-check-TTS-settings-on-my-Android-device-</string>
<string name="prefs_routing" translatable="false">RoutingOptions</string>

View file

@ -50,7 +50,6 @@
</string-array>
<string-array name="track_length">
<item>@string/duration_disabled</item>
<item>@string/duration_1_hour</item>
<item>@string/duration_2_hours</item>
<item>@string/duration_6_hours</item>
@ -59,7 +58,6 @@
</string-array>
<string-array name="track_length_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>6</item>

View file

@ -2196,4 +2196,6 @@
<string name="type.amenity.events_venue">Events Venue</string>
<string name="type.shop.auction">Auction</string>
<string name="type.shop.collector">Collectables</string>
<string name="pref_track_record_title">Record Track</string>
<string name="recent_track_help_text">This option allows you to record traveled path for a certain period and see it on the map. Please note: activation of this function causes increased battery usage. The track will be removed automatically from the map after the time interval will expire.</string>
</resources>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat
android:key="@string/pref_recent_track"
android:title="Record Recent Track"
android:order="1"/>
<ListPreference
android:key="@string/pref_track_record_time"
android:title="Record time"
app:singleLineTitle="false"
android:summary="Select duration for which recorded point will be shown on map"
android:entries="@array/track_length"
android:entryValues="@array/track_length_values"
android:order="2"/>
</androidx.preference.PreferenceScreen>

View file

@ -54,6 +54,12 @@
app:singleLineTitle="false"
android:summary="@string/maps_storage_summary"
android:order="8"/>
<Preference
android:key="@string/pref_track_record"
android:title="@string/pref_track_record_title"
app:singleLineTitle="false"
android:summary="This option allows you to record your recent traversed tracks"
android:order="9"/>
<SwitchPreferenceCompat
android:key="@string/pref_enable_logging"
android:title="@string/enable_logging"

View file

@ -1,5 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.3.2' apply false
id 'com.android.library' version '8.3.2' apply false
id 'com.android.application' version '8.4.0' apply false
id 'com.android.library' version '8.4.0' apply false
}