Compare commits

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

1 commit

Author SHA1 Message Date
d2094d937a [android] Revert Android Auto
Signed-off-by: Roman Tsisyk <roman@tsisyk.com>
2023-12-21 18:29:08 +02:00
30 changed files with 0 additions and 3793 deletions

View file

@ -43,9 +43,6 @@
//-->
<uses-permission-sdk-23 android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE"/>
@ -859,15 +856,6 @@
android:label="@string/driving_options_title"/>
<activity
android:name="app.organicmaps.MapPlaceholderActivity"/>
<service
android:name="app.organicmaps.car.CarAppService"
android:foregroundServiceType="location"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.NAVIGATION" />
</intent-filter>
</service>
<service
android:name=".routing.NavigationService"
@ -895,14 +883,5 @@
<!-- Version >= 3.0. Dex Dual Mode support for compatible Samsung devices.
See the documentation: https://developer.samsung.com/samsung-dex/modify-optimizing.html //-->
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true" />
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="5"/>
</application>
</manifest>

View file

@ -1,115 +0,0 @@
package app.organicmaps.car;
import android.app.Notification;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarContext;
import androidx.car.app.Session;
import androidx.car.app.SessionInfo;
import androidx.car.app.notification.CarAppExtender;
import androidx.car.app.notification.CarPendingIntent;
import androidx.car.app.validation.HostValidator;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.BuildConfig;
import app.organicmaps.R;
import app.organicmaps.api.Const;
import app.organicmaps.routing.NavigationService;
public final class CarAppService extends androidx.car.app.CarAppService
{
private static final String CHANNEL_ID = "ANDROID_AUTO";
private static final int NOTIFICATION_ID = CarAppService.class.getSimpleName().hashCode();
public static final String API_CAR_HOST = Const.AUTHORITY + ".car";
public static final String ACTION_SHOW_NAVIGATION_SCREEN = Const.ACTION_PREFIX + ".SHOW_NAVIGATION_SCREEN";
@Nullable
private static NotificationCompat.Extender mCarNotificationExtender;
@NonNull
@Override
public HostValidator createHostValidator()
{
if (BuildConfig.DEBUG)
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR;
return new HostValidator.Builder(getApplicationContext())
.addAllowedHosts(androidx.car.app.R.array.hosts_allowlist_sample)
.build();
}
@NonNull
@Override
public Session onCreateSession(@Nullable SessionInfo sessionInfo)
{
createNotificationChannel();
startForeground(NOTIFICATION_ID, getNotification());
final CarAppSession carAppSession = new CarAppSession(sessionInfo);
carAppSession.getLifecycle().addObserver(new DefaultLifecycleObserver()
{
@Override
public void onDestroy(@NonNull LifecycleOwner owner)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
stopForeground(STOP_FOREGROUND_REMOVE);
else
stopForeground(true);
}
});
return carAppSession;
}
@NonNull
@Override
public Session onCreateSession()
{
return onCreateSession(null);
}
@NonNull
public static NotificationCompat.Extender getCarNotificationExtender(@NonNull CarContext context)
{
if (mCarNotificationExtender != null)
return mCarNotificationExtender;
final Intent intent = new Intent(Intent.ACTION_VIEW)
.setComponent(new ComponentName(context, CarAppService.class))
.setData(Uri.fromParts(Const.API_SCHEME, CarAppService.API_CAR_HOST, CarAppService.ACTION_SHOW_NAVIGATION_SCREEN));
mCarNotificationExtender = new CarAppExtender.Builder()
.setImportance(NotificationManagerCompat.IMPORTANCE_MIN)
.setContentIntent(CarPendingIntent.getCarApp(context, intent.hashCode(), intent, 0))
.build();
return mCarNotificationExtender;
}
private void createNotificationChannel()
{
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
final NotificationChannelCompat notificationChannel = new NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_MIN)
.setName(getString(R.string.car_notification_channel_name))
.setLightsEnabled(false) // less annoying
.setVibrationEnabled(false) // less annoying
.build();
notificationManager.createNotificationChannel(notificationChannel);
}
@NonNull
private Notification getNotification()
{
return NavigationService.getNotificationBuilder(this)
.setChannelId(CHANNEL_ID)
.setContentTitle(getString(R.string.aa_connected_to_car_notification_title))
.build();
}
}

View file

@ -1,282 +0,0 @@
package app.organicmaps.car;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import android.content.Intent;
import android.content.res.Configuration;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
import androidx.car.app.Screen;
import androidx.car.app.ScreenManager;
import androidx.car.app.Session;
import androidx.car.app.SessionInfo;
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;
import app.organicmaps.car.hacks.PopToRootHack;
import app.organicmaps.car.screens.ErrorScreen;
import app.organicmaps.car.screens.MapPlaceholderScreen;
import app.organicmaps.car.screens.MapScreen;
import app.organicmaps.car.screens.PlaceScreen;
import app.organicmaps.car.screens.RequestPermissionsScreen;
import app.organicmaps.car.screens.base.BaseMapScreen;
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;
import app.organicmaps.util.log.Logger;
import app.organicmaps.widget.placepage.PlacePageData;
import java.io.IOException;
public final class CarAppSession extends Session implements DefaultLifecycleObserver,
SensorListener, LocationState.ModeChangeListener, DisplayChangedListener, Framework.PlacePageActivationListener
{
private static final String TAG = CarAppSession.class.getSimpleName();
@Nullable
private final SessionInfo mSessionInfo;
@NonNull
private final SurfaceRenderer mSurfaceRenderer;
@NonNull
private final ScreenManager mScreenManager;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private DisplayManager mDisplayManager;
private boolean mInitFailed = false;
public CarAppSession(@Nullable SessionInfo sessionInfo)
{
getLifecycle().addObserver(this);
mSessionInfo = sessionInfo;
mSurfaceRenderer = new SurfaceRenderer(getCarContext(), getLifecycle());
mScreenManager = getCarContext().getCarService(ScreenManager.class);
}
@Override
public void onCarConfigurationChanged(@NonNull Configuration newConfiguration)
{
Logger.d(TAG, "New configuration: " + newConfiguration);
if (mSurfaceRenderer.isRenderingActive())
{
ThemeUtils.update(getCarContext());
mScreenManager.getTop().invalidate();
}
}
@NonNull
@Override
public Screen onCreateScreen(@NonNull Intent intent)
{
Logger.d(TAG);
Logger.i(TAG, "Session info: " + mSessionInfo);
Logger.i(TAG, "API Level: " + getCarContext().getCarAppApiLevel());
if (mSessionInfo != null)
Logger.i(TAG, "Supported templates: " + mSessionInfo.getSupportedTemplates(getCarContext().getCarAppApiLevel()));
Logger.i(TAG, "Host info: " + getCarContext().getHostInfo());
Logger.i(TAG, "Car configuration: " + getCarContext().getResources().getConfiguration());
final MapScreen mapScreen = new MapScreen(getCarContext(), mSurfaceRenderer);
if (mInitFailed)
return new ErrorScreen.Builder(getCarContext()).setErrorMessage(R.string.dialog_error_storage_message).build();
if (!LocationUtils.checkFineLocationPermission(getCarContext()))
{
mScreenManager.push(mapScreen);
return new RequestPermissionsScreen(getCarContext(), this::onLocationPermissionsGranted);
}
if (mDisplayManager.isDeviceDisplayUsed())
{
mScreenManager.push(mapScreen);
mSurfaceRenderer.disable();
onStop(this);
return new MapPlaceholderScreen(getCarContext());
}
return mapScreen;
}
@Override
public void onNewIntent(@NonNull Intent intent)
{
Logger.d(TAG, intent.toString());
IntentUtils.processIntent(getCarContext(), mSurfaceRenderer, intent);
}
@Override
public void onCreate(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
mDisplayManager = DisplayManager.from(getCarContext());
mDisplayManager.addListener(DisplayType.Car, this);
init();
}
@Override
public void onStart(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
if (mDisplayManager.isCarDisplayUsed())
{
LocationState.nativeSetListener(this);
Framework.nativePlacePageActivationListener(this);
}
SensorHelper.from(getCarContext()).addListener(this);
if (LocationUtils.checkFineLocationPermission(getCarContext()) && !LocationHelper.from(getCarContext()).isActive())
LocationHelper.from(getCarContext()).start();
if (mDisplayManager.isCarDisplayUsed())
{
ThemeUtils.update(getCarContext());
restoreRoute();
}
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
SensorHelper.from(getCarContext()).removeListener(this);
if (mDisplayManager.isCarDisplayUsed())
{
LocationState.nativeRemoveListener();
Framework.nativeRemovePlacePageActivationListener(this);
}
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner)
{
mDisplayManager.removeListener(DisplayType.Car);
}
private void init()
{
mInitFailed = false;
try
{
MwmApplication.from(getCarContext()).init(() -> Config.setFirstStartDialogSeen(getCarContext()));
} catch (IOException e)
{
mInitFailed = true;
Logger.e(TAG, "Failed to initialize the app.");
}
}
@RequiresPermission(ACCESS_FINE_LOCATION)
private void onLocationPermissionsGranted()
{
LocationHelper.from(getCarContext()).start();
}
@Override
public void onMyPositionModeChanged(int newMode)
{
final Screen screen = mScreenManager.getTop();
if (screen instanceof BaseMapScreen)
screen.invalidate();
}
public void onCompassUpdated(double north)
{
Map.onCompassUpdated(north, true);
}
@Override
public void onDisplayChangedToDevice(@NonNull Runnable onTaskFinishedCallback)
{
Logger.d(TAG);
final Screen topScreen = mScreenManager.getTop();
onStop(this);
mSurfaceRenderer.disable();
final MapPlaceholderScreen mapPlaceholderScreen = new MapPlaceholderScreen(getCarContext());
if (isPermissionsOrErrorScreen(topScreen))
mScreenManager.push(mapPlaceholderScreen);
else
mScreenManager.push(new PopToRootHack.Builder(getCarContext()).setScreenToPush(mapPlaceholderScreen).build());
onTaskFinishedCallback.run();
}
@Override
public void onDisplayChangedToCar(@NonNull Runnable onTaskFinishedCallback)
{
Logger.d(TAG);
final Screen topScreen = mScreenManager.getTop();
onStart(this);
mSurfaceRenderer.enable();
// If we have Permissions or Error Screen in Screen Manager (either on the top of the stack or after MapPlaceholderScreen) do nothing
if (isPermissionsOrErrorScreen(topScreen))
return;
mScreenManager.pop();
onTaskFinishedCallback.run();
}
@Override
@SuppressWarnings("unused")
public void onPlacePageActivated(@NonNull PlacePageData data)
{
final MapObject mapObject = (MapObject) data;
// Don't display the PlaceScreen for 'MY_POSITION' or during navigation
// TODO (AndrewShkrob): Implement the 'Add stop' functionality
if (mapObject.isMyPosition() || RoutingController.get().isNavigating())
{
Framework.nativeDeactivatePopup();
return;
}
final PlaceScreen placeScreen = new PlaceScreen.Builder(getCarContext(), mSurfaceRenderer).setMapObject(mapObject).build();
final PopToRootHack hack = new PopToRootHack.Builder(getCarContext()).setScreenToPush(placeScreen).build();
mScreenManager.push(hack);
}
@Override
@SuppressWarnings("unused")
public void onPlacePageDeactivated(boolean switchFullScreenMode)
{
// The function is called when we close the PlaceScreen or when we enter the navigation mode.
// We only need to handle the first case
if (!(mScreenManager.getTop() instanceof PlaceScreen))
return;
RoutingController.get().cancel();
mScreenManager.popToRoot();
}
private void restoreRoute()
{
final RoutingController routingController = RoutingController.get();
if (routingController.isPlanning() || routingController.isNavigating() || routingController.hasSavedRoute())
{
final PlaceScreen placeScreen = new PlaceScreen.Builder(getCarContext(), mSurfaceRenderer).setMapObject(routingController.getEndPoint()).build();
final PopToRootHack hack = new PopToRootHack.Builder(getCarContext()).setScreenToPush(placeScreen).build();
mScreenManager.push(hack);
}
}
private boolean isPermissionsOrErrorScreen(@NonNull Screen screen)
{
return screen instanceof RequestPermissionsScreen || screen instanceof ErrorScreen;
}
}

View file

@ -1,245 +0,0 @@
package app.organicmaps.car;
import static app.organicmaps.display.DisplayType.Car;
import android.graphics.Rect;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.AppManager;
import androidx.car.app.CarContext;
import androidx.car.app.CarToast;
import androidx.car.app.SurfaceCallback;
import androidx.car.app.SurfaceContainer;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.Framework;
import app.organicmaps.Map;
import app.organicmaps.MapRenderingListener;
import app.organicmaps.R;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.settings.UnitLocale;
import app.organicmaps.util.concurrency.UiThread;
import app.organicmaps.util.log.Logger;
public class SurfaceRenderer implements DefaultLifecycleObserver, SurfaceCallback, MapRenderingListener
{
private static final String TAG = SurfaceRenderer.class.getSimpleName();
private final CarContext mCarContext;
private final Map mMap = new Map(Car);
@NonNull
private Rect mVisibleArea = new Rect();
@Nullable
private Surface mSurface = null;
private boolean mIsRunning;
public SurfaceRenderer(@NonNull CarContext carContext, @NonNull Lifecycle lifecycle)
{
Logger.d(TAG, "SurfaceRenderer()");
mCarContext = carContext;
mIsRunning = true;
lifecycle.addObserver(this);
mMap.setMapRenderingListener(this);
}
@Override
public void onSurfaceAvailable(@NonNull SurfaceContainer surfaceContainer)
{
Logger.d(TAG, "Surface available " + surfaceContainer);
if (mSurface != null)
mSurface.release();
mSurface = surfaceContainer.getSurface();
mMap.onSurfaceCreated(
mCarContext,
mSurface,
new Rect(0, 0, surfaceContainer.getWidth(), surfaceContainer.getHeight()),
surfaceContainer.getDpi()
);
mMap.updateBottomWidgetsOffset(mCarContext, -1, -1);
}
@Override
public void onVisibleAreaChanged(@NonNull Rect visibleArea)
{
Logger.d(TAG, "Visible area changed. visibleArea: " + visibleArea);
mVisibleArea = visibleArea;
if (!mVisibleArea.isEmpty())
Framework.nativeSetVisibleRect(mVisibleArea.left, mVisibleArea.top, mVisibleArea.right, mVisibleArea.bottom);
}
@Override
public void onStableAreaChanged(@NonNull Rect stableArea)
{
Logger.d(TAG, "Stable area changed. stableArea: " + stableArea);
if (!stableArea.isEmpty())
Framework.nativeSetVisibleRect(stableArea.left, stableArea.top, stableArea.right, stableArea.bottom);
else if (!mVisibleArea.isEmpty())
Framework.nativeSetVisibleRect(mVisibleArea.left, mVisibleArea.top, mVisibleArea.right, mVisibleArea.bottom);
}
@Override
public void onSurfaceDestroyed(@NonNull SurfaceContainer surfaceContainer)
{
Logger.d(TAG, "Surface destroyed");
if (mSurface != null)
{
mSurface.release();
mSurface = null;
}
mMap.onSurfaceDestroyed(false, true);
}
@Override
public void onCreate(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
mCarContext.getCarService(AppManager.class).setSurfaceCallback(this);
mMap.onCreate(false);
}
@Override
public void onStart(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
mMap.onStart();
mMap.setCallbackUnsupported(this::reportUnsupported);
}
@Override
public void onResume(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
mMap.onResume();
if (DisplayManager.from(mCarContext).isCarDisplayUsed())
UiThread.runLater(() -> mMap.updateMyPositionRoutingOffset(0));
}
@Override
public void onPause(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
mMap.onPause(mCarContext);
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
mMap.onStop();
mMap.setCallbackUnsupported(null);
}
@Override
public void onScroll(float distanceX, float distanceY)
{
Logger.d(TAG, "distanceX: " + distanceX + ", distanceY: " + distanceY);
mMap.onScroll(distanceX, distanceY);
}
@Override
public void onFling(float velocityX, float velocityY)
{
Logger.d(TAG, "velocityX: " + velocityX + ", velocityY: " + velocityY);
}
public void onZoomIn()
{
Map.zoomIn();
}
public void onZoomOut()
{
Map.zoomOut();
}
@Override
public void onScale(float focusX, float focusY, float scaleFactor)
{
Logger.d(TAG, "focusX: " + focusX + ", focusY: " + focusY + ", scaleFactor: " + scaleFactor);
float x = focusX;
float y = focusY;
if (!mVisibleArea.isEmpty())
{
// If a focal point value is negative, use the center point of the visible area.
if (x < 0)
x = mVisibleArea.centerX();
if (y < 0)
y = mVisibleArea.centerY();
}
final boolean animated = Float.compare(scaleFactor, 2f) == 0;
Map.onScale(scaleFactor, x, y, animated);
}
@Override
public void onClick(float x, float y)
{
Logger.d(TAG, "x: " + x + ", y: " + y);
Map.onClick(x, y);
}
public void disable()
{
if (!mIsRunning)
{
Logger.d(TAG, "Already disabled");
return;
}
mCarContext.getCarService(AppManager.class).setSurfaceCallback(null);
mMap.onSurfaceDestroyed(false, true);
mMap.onStop();
mMap.setCallbackUnsupported(null);
mMap.setMapRenderingListener(null);
mIsRunning = false;
}
public void enable()
{
if (mIsRunning)
{
Logger.d(TAG, "Already enabled");
return;
}
mCarContext.getCarService(AppManager.class).setSurfaceCallback(this);
mMap.onStart();
mMap.setCallbackUnsupported(this::reportUnsupported);
mMap.setMapRenderingListener(this);
mMap.updateMyPositionRoutingOffset(0);
mIsRunning = true;
}
public boolean isRenderingActive()
{
return mIsRunning;
}
private void reportUnsupported()
{
String message = mCarContext.getString(R.string.unsupported_phone);
Logger.e(TAG, message);
CarToast.makeText(mCarContext, message, CarToast.LENGTH_LONG).show();
}
@Override
public void onRenderingCreated()
{
UnitLocale.initializeCurrentUnits();
}
}

View file

@ -1,116 +0,0 @@
package app.organicmaps.car.hacks;
import static java.util.Objects.requireNonNull;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarContext;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.car.screens.base.BaseScreen;
/**
* This class is used to provide a working solution for next the two actions:
* <ul>
* <li> {@code popToRoot();}
* <li> {@code push(new Screen());}
* </ul>
* <p>
* When you run the following code:
* <pre>
* {@code
* ScreenManager.popToRoot();
* ScreenManager.push(new Screen());
* }
* </pre>
* the first {@code popToRoot} action won't be applied and a new {@link androidx.car.app.Screen} will be pushed to the top of the stack.
* <p>
* Actually, the {@code popToRoot} action <b>will</b> be applied and the screen stack will be cleared.
* It will contain only two screens: the root and the newly pushed.
* But the steps counter won't be reset after {@code popToRoot}.
* It will be increased by one as if we just push a new screen without {@code popToRoot} action.
* <p>
* To decrease a step counter, it is required to display a previous screen.
* For example we're on step 4 and we're making a {@code popToRoot} action.
* The step counter will be decreased to 1 after {@code RootScreen.onStart()/onResume()} call.
* <p>
* How it works?
* <p>
* In {@link #onStart(LifecycleOwner)} we call {@code getScreenManager().popToRoot();}
* <p>
* The screen ({@link PopToRootHack} class) should begin to destroy - all the lifecycle methods should be called step by step.
* <p>
* In {@link #onStop(LifecycleOwner)} we post {@code getScreenManager().push(mScreenToPush);} to the main thread.
* This mean when the main thread will be free, it will execute our code - it will push a new screen.
* This will happen when {@link PopToRootHack} will be destroyed and the root screen will be displayed.
*/
public final class PopToRootHack extends BaseScreen
{
private static final Handler mHandler = new Handler(Looper.getMainLooper());
private static final RoutePreviewNavigationTemplate mTemplate = new RoutePreviewNavigationTemplate.Builder().setLoading(true).build();
@NonNull
private final BaseScreen mScreenToPush;
private PopToRootHack(@NonNull Builder builder)
{
super(builder.mCarContext);
mScreenToPush = requireNonNull(builder.mScreenToPush);
}
@NonNull
@Override
public Template onGetTemplate()
{
getScreenManager().popToRoot();
return mTemplate;
}
@Override
public void onStart(@NonNull LifecycleOwner owner)
{
getScreenManager().popToRoot();
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
mHandler.post(() -> getScreenManager().push(mScreenToPush));
}
/**
* A builder of {@link PopToRootHack}.
*/
public static final class Builder
{
@NonNull
private final CarContext mCarContext;
@Nullable
private BaseScreen mScreenToPush;
public Builder(@NonNull final CarContext carContext)
{
mCarContext = carContext;
}
@NonNull
public Builder setScreenToPush(@NonNull BaseScreen screenToPush)
{
mScreenToPush = screenToPush;
return this;
}
@NonNull
public PopToRootHack build()
{
if (mScreenToPush == null)
throw new IllegalStateException("You must specify Screen that will be pushed to the ScreenManager after the popToRoot() action");
return new PopToRootHack(this);
}
}
}

View file

@ -1,169 +0,0 @@
package app.organicmaps.car.screens;
import static java.util.Objects.requireNonNull;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.DistanceSpan;
import androidx.car.app.model.ForegroundCarColorSpan;
import androidx.car.app.model.Header;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapTemplate;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.BookmarkCategory;
import app.organicmaps.bookmarks.data.BookmarkInfo;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.util.Colors;
import app.organicmaps.car.util.RoutingHelpers;
import app.organicmaps.car.util.UiHelpers;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.util.Distance;
import app.organicmaps.util.Graphics;
import java.util.ArrayList;
import java.util.List;
public class BookmarksScreen extends BaseMapScreen
{
private final int MAX_CATEGORIES_SIZE;
@Nullable
private BookmarkCategory mBookmarkCategory;
public BookmarksScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext, surfaceRenderer);
final ConstraintManager constraintManager = getCarContext().getCarService(ConstraintManager.class);
MAX_CATEGORIES_SIZE = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST);
}
private BookmarksScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer, @NonNull BookmarkCategory bookmarkCategory)
{
this(carContext, surfaceRenderer);
mBookmarkCategory = bookmarkCategory;
}
@NonNull
@Override
public Template onGetTemplate()
{
final MapTemplate.Builder builder = new MapTemplate.Builder();
builder.setHeader(createHeader());
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setItemList(mBookmarkCategory == null ? createBookmarkCategoriesList() : createBookmarksList());
return builder.build();
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
builder.setTitle(mBookmarkCategory == null ? getCarContext().getString(R.string.bookmarks) : mBookmarkCategory.getName());
return builder.build();
}
@NonNull
private ItemList createBookmarkCategoriesList()
{
final List<BookmarkCategory> bookmarkCategories = getBookmarks();
final int categoriesSize = Math.min(bookmarkCategories.size(), MAX_CATEGORIES_SIZE);
ItemList.Builder builder = new ItemList.Builder();
for (int i = 0; i < categoriesSize; ++i)
{
final BookmarkCategory bookmarkCategory = bookmarkCategories.get(i);
Row.Builder itemBuilder = new Row.Builder();
itemBuilder.setTitle(bookmarkCategory.getName());
itemBuilder.addText(bookmarkCategory.getDescription());
itemBuilder.setOnClickListener(() -> getScreenManager().push(new BookmarksScreen(getCarContext(), getSurfaceRenderer(), bookmarkCategory)));
itemBuilder.setBrowsable(true);
builder.addItem(itemBuilder.build());
}
return builder.build();
}
@NonNull
private ItemList createBookmarksList()
{
final long bookmarkCategoryId = requireNonNull(mBookmarkCategory).getId();
final int bookmarkCategoriesSize = Math.min(mBookmarkCategory.getBookmarksCount(), MAX_CATEGORIES_SIZE);
ItemList.Builder builder = new ItemList.Builder();
for (int i = 0; i < bookmarkCategoriesSize; ++i)
{
final long bookmarkId = BookmarkManager.INSTANCE.getBookmarkIdByPosition(bookmarkCategoryId, i);
final BookmarkInfo bookmarkInfo = new BookmarkInfo(bookmarkCategoryId, bookmarkId);
final Row.Builder itemBuilder = new Row.Builder();
itemBuilder.setTitle(bookmarkInfo.getName());
if (!bookmarkInfo.getAddress().isEmpty())
itemBuilder.addText(bookmarkInfo.getAddress());
final CharSequence description = getDescription(bookmarkInfo);
if (description.length() != 0)
itemBuilder.addText(description);
final Drawable icon = Graphics.drawCircleAndImage(bookmarkInfo.getIcon().argb(),
R.dimen.track_circle_size,
bookmarkInfo.getIcon().getResId(),
R.dimen.bookmark_icon_size,
getCarContext());
itemBuilder.setImage(new CarIcon.Builder(IconCompat.createWithBitmap(Graphics.drawableToBitmap(icon))).build());
itemBuilder.setOnClickListener(() -> BookmarkManager.INSTANCE.showBookmarkOnMap(bookmarkId));
builder.addItem(itemBuilder.build());
}
return builder.build();
}
@NonNull
private CharSequence getDescription(final BookmarkInfo bookmark)
{
final SpannableStringBuilder result = new SpannableStringBuilder(" ");
final Location loc = LocationHelper.from(getCarContext()).getSavedLocation();
if (loc != null)
{
final Distance distance = bookmark.getDistance(loc.getLatitude(), loc.getLongitude(), 0.0);
result.setSpan(DistanceSpan.create(RoutingHelpers.createDistance(distance)), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
result.setSpan(ForegroundCarColorSpan.create(Colors.DISTANCE), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (loc != null && !bookmark.getFeatureType().isEmpty())
{
result.append("");
result.append(bookmark.getFeatureType());
}
return result;
}
@NonNull
private static List<BookmarkCategory> getBookmarks()
{
final List<BookmarkCategory> bookmarkCategories = new ArrayList<>(BookmarkManager.INSTANCE.getCategories());
final List<BookmarkCategory> toRemove = new ArrayList<>();
for (final BookmarkCategory bookmarkCategory : bookmarkCategories)
{
if (bookmarkCategory.getBookmarksCount() == 0 || !bookmarkCategory.isVisible())
toRemove.add(bookmarkCategory);
}
bookmarkCategories.removeAll(toRemove);
return bookmarkCategories;
}
}

View file

@ -1,106 +0,0 @@
package app.organicmaps.car.screens;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
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.Header;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapTemplate;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.R;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.search.SearchOnMapScreen;
import app.organicmaps.car.util.ThemeUtils;
import app.organicmaps.car.util.UiHelpers;
import java.util.Arrays;
import java.util.List;
public class CategoriesScreen extends BaseMapScreen
{
private static class CategoryData
{
@StringRes
public final int nameResId;
@DrawableRes
public final int iconResId;
@DrawableRes
public final int iconNightResId;
public CategoryData(@StringRes int nameResId, @DrawableRes int iconResId, @DrawableRes int iconNightResId)
{
this.nameResId = nameResId;
this.iconResId = iconResId;
this.iconNightResId = iconNightResId;
}
}
// TODO (AndrewShkrob): discuss categories and their priority for this list
private static final List<CategoryData> CATEGORIES = Arrays.asList(
new CategoryData(R.string.fuel, R.drawable.ic_category_fuel, R.drawable.ic_category_fuel_night),
new CategoryData(R.string.parking, R.drawable.ic_category_parking, R.drawable.ic_category_parking_night),
new CategoryData(R.string.eat, R.drawable.ic_category_eat, R.drawable.ic_category_eat_night),
new CategoryData(R.string.food, R.drawable.ic_category_food, R.drawable.ic_category_food_night),
new CategoryData(R.string.hotel, R.drawable.ic_category_hotel, R.drawable.ic_category_hotel_night),
new CategoryData(R.string.toilet, R.drawable.ic_category_toilet, R.drawable.ic_category_toilet_night),
new CategoryData(R.string.rv, R.drawable.ic_category_rv, R.drawable.ic_category_rv_night)
);
private final int MAX_CATEGORIES_SIZE;
public CategoriesScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext, surfaceRenderer);
final ConstraintManager constraintManager = getCarContext().getCarService(ConstraintManager.class);
MAX_CATEGORIES_SIZE = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST);
}
@NonNull
@Override
public Template onGetTemplate()
{
final MapTemplate.Builder builder = new MapTemplate.Builder();
builder.setHeader(createHeader());
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setItemList(createCategoriesList());
return builder.build();
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
builder.setTitle(getCarContext().getString(R.string.categories));
return builder.build();
}
@NonNull
private ItemList createCategoriesList()
{
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 String title = getCarContext().getString(CATEGORIES.get(i).nameResId);
@DrawableRes final int iconResId = isNightMode ? CATEGORIES.get(i).iconNightResId : CATEGORIES.get(i).iconResId;
itemBuilder.setTitle(title);
itemBuilder.setImage(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), iconResId)).build());
itemBuilder.setOnClickListener(() -> getScreenManager().push(new SearchOnMapScreen.Builder(getCarContext(), getSurfaceRenderer()).setCategory(title).build()));
builder.addItem(itemBuilder.build());
}
return builder.build();
}
}

View file

@ -1,80 +0,0 @@
package app.organicmaps.car.screens;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.car.app.CarContext;
import androidx.car.app.model.Action;
import androidx.car.app.model.MessageTemplate;
import androidx.car.app.model.ParkedOnlyOnClickListener;
import androidx.car.app.model.Template;
import app.organicmaps.R;
import app.organicmaps.car.screens.base.BaseScreen;
import app.organicmaps.car.util.Colors;
public class ErrorScreen extends BaseScreen
{
@StringRes
private final int mErrorMessage;
private final boolean mIsCloseable;
private ErrorScreen(@NonNull Builder builder)
{
super(builder.mCarContext);
mErrorMessage = builder.mErrorMessage;
mIsCloseable = builder.mIsCloseable;
}
@NonNull
@Override
public Template onGetTemplate()
{
final MessageTemplate.Builder builder = new MessageTemplate.Builder(getCarContext().getString(mErrorMessage));
builder.setHeaderAction(Action.APP_ICON);
builder.setTitle(getCarContext().getString(R.string.app_name));
if (mIsCloseable)
{
builder.addAction(new Action.Builder()
.setBackgroundColor(Colors.BUTTON_ACCEPT)
.setTitle(getCarContext().getString(R.string.close))
.setOnClickListener(ParkedOnlyOnClickListener.create(this::finish)).build()
);
}
return builder.build();
}
public static class Builder
{
@NonNull
private final CarContext mCarContext;
@StringRes
private int mErrorMessage;
private boolean mIsCloseable;
public Builder(@NonNull CarContext carContext)
{
mCarContext = carContext;
}
public Builder setErrorMessage(@StringRes int errorMessage)
{
mErrorMessage = errorMessage;
return this;
}
public Builder setCloseable(boolean isCloseable)
{
mIsCloseable = isCloseable;
return this;
}
public ErrorScreen build()
{
return new ErrorScreen(this);
}
}
}

View file

@ -1,36 +0,0 @@
package app.organicmaps.car.screens;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.model.Action;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.MessageTemplate;
import androidx.car.app.model.Template;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.R;
import app.organicmaps.car.screens.base.BaseScreen;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.display.DisplayType;
public class MapPlaceholderScreen extends BaseScreen
{
public MapPlaceholderScreen(@NonNull CarContext carContext)
{
super(carContext);
}
@NonNull
@Override
public Template onGetTemplate()
{
final MessageTemplate.Builder builder = new MessageTemplate.Builder(getCarContext().getString(R.string.aa_used_on_the_phone_screen));
builder.setHeaderAction(Action.APP_ICON);
builder.setTitle(getCarContext().getString(R.string.app_name));
builder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_phone_android)).build());
builder.addAction(new Action.Builder().setTitle(getCarContext().getString(R.string.aa_continue_in_the_car))
.setOnClickListener(() -> DisplayManager.from(getCarContext()).changeDisplay(DisplayType.Car)).build());
return builder.build();
}
}

View file

@ -1,115 +0,0 @@
package app.organicmaps.car.screens;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.model.Action;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Header;
import androidx.car.app.model.Item;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapTemplate;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.R;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.search.SearchScreen;
import app.organicmaps.car.util.UiHelpers;
public class MapScreen extends BaseMapScreen
{
public MapScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext, surfaceRenderer);
}
@NonNull
@Override
public Template onGetTemplate()
{
final MapTemplate.Builder builder = new MapTemplate.Builder();
builder.setHeader(createHeader());
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setActionStrip(UiHelpers.createSettingsActionStrip(this, getSurfaceRenderer()));
builder.setItemList(createList());
return builder.build();
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(new Action.Builder(Action.APP_ICON).build());
builder.setTitle(getCarContext().getString(R.string.app_name));
return builder.build();
}
@NonNull
private ItemList createList()
{
final ItemList.Builder builder = new ItemList.Builder();
builder.addItem(createSearchItem());
builder.addItem(createCategoriesItem());
builder.addItem(createBookmarksItem());
return builder.build();
}
@NonNull
private Item createSearchItem()
{
final CarIcon iconSearch = new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_search)).build();
final Row.Builder builder = new Row.Builder();
builder.setTitle(getCarContext().getString(R.string.search));
builder.setImage(iconSearch);
builder.setBrowsable(true);
builder.setOnClickListener(this::openSearch);
return builder.build();
}
@NonNull
private Item createCategoriesItem()
{
final Row.Builder builder = new Row.Builder();
builder.setTitle(getCarContext().getString(R.string.categories));
builder.setBrowsable(true);
builder.setOnClickListener(this::openCategories);
return builder.build();
}
@NonNull
private Item createBookmarksItem()
{
final Row.Builder builder = new Row.Builder();
builder.setTitle(getCarContext().getString(R.string.bookmarks));
builder.setBrowsable(true);
builder.setOnClickListener(this::openBookmarks);
return builder.build();
}
private void openSearch()
{
// Details in UiHelpers.createSettingsAction()
if (getScreenManager().getTop() != this)
return;
getScreenManager().push(new SearchScreen.Builder(getCarContext(), getSurfaceRenderer()).build());
}
private void openCategories()
{
// Details in UiHelpers.createSettingsAction()
if (getScreenManager().getTop() != this)
return;
getScreenManager().push(new CategoriesScreen(getCarContext(), getSurfaceRenderer()));
}
private void openBookmarks()
{
// Details in UiHelpers.createSettingsAction()
if (getScreenManager().getTop() != this)
return;
getScreenManager().push(new BookmarksScreen(getCarContext(), getSurfaceRenderer()));
}
}

View file

@ -1,248 +0,0 @@
package app.organicmaps.car.screens;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarContext;
import androidx.car.app.CarToast;
import androidx.car.app.model.Action;
import androidx.car.app.model.ActionStrip;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.NavigationManager;
import androidx.car.app.navigation.NavigationManagerCallback;
import androidx.car.app.navigation.model.NavigationTemplate;
import androidx.car.app.navigation.model.Step;
import androidx.car.app.navigation.model.TravelEstimate;
import androidx.car.app.navigation.model.Trip;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.Framework;
import app.organicmaps.R;
import app.organicmaps.car.CarAppService;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.settings.DrivingOptionsScreen;
import app.organicmaps.car.util.Colors;
import app.organicmaps.car.util.RoutingUtils;
import app.organicmaps.car.util.ThemeUtils;
import app.organicmaps.car.util.UiHelpers;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.location.LocationListener;
import app.organicmaps.routing.NavigationService;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.routing.RoutingInfo;
import app.organicmaps.sound.TtsPlayer;
import app.organicmaps.util.LocationUtils;
import app.organicmaps.util.log.Logger;
import java.util.List;
import java.util.Objects;
public class NavigationScreen extends BaseMapScreen implements RoutingController.Container, NavigationManagerCallback
{
private static final String TAG = NavigationScreen.class.getSimpleName();
public static final String MARKER = NavigationScreen.class.getSimpleName();
@NonNull
private final RoutingController mRoutingController;
@NonNull
private final NavigationManager mNavigationManager;
@NonNull
private final LocationListener mLocationListener = (unused) -> updateTrip();
@NonNull
private Trip mTrip = new Trip.Builder().setLoading(true).build();
// This value is used to decide whether to display the "trip finished" toast or not
// False: trip is finished -> show toast
// True: navigation is cancelled by the user or host -> don't show toast
private boolean mNavigationCancelled = false;
private NavigationScreen(@NonNull Builder builder)
{
super(builder.mCarContext, builder.mSurfaceRenderer);
mNavigationManager = builder.mCarContext.getCarService(NavigationManager.class);
mRoutingController = RoutingController.get();
}
@NonNull
@Override
public Template onGetTemplate()
{
final NavigationTemplate.Builder builder = new NavigationTemplate.Builder();
builder.setBackgroundColor(ThemeUtils.isNightMode(getCarContext()) ? Colors.NAVIGATION_TEMPLATE_BACKGROUND_NIGHT : Colors.NAVIGATION_TEMPLATE_BACKGROUND_DAY);
builder.setActionStrip(createActionStrip());
builder.setMapActionStrip(UiHelpers.createMapActionStrip(getCarContext(), getSurfaceRenderer()));
final TravelEstimate destinationTravelEstimate = getDestinationTravelEstimate();
if (destinationTravelEstimate != null)
builder.setDestinationTravelEstimate(getDestinationTravelEstimate());
builder.setNavigationInfo(getNavigationInfo());
return builder.build();
}
@Override
public void onStopNavigation()
{
LocationHelper.from(getCarContext()).removeListener(mLocationListener);
mNavigationCancelled = true;
mRoutingController.cancel();
}
@Override
public void onNavigationCancelled()
{
if (!mNavigationCancelled)
CarToast.makeText(getCarContext(), getCarContext().getString(R.string.trip_finished), CarToast.LENGTH_LONG).show();
finish();
getScreenManager().popToRoot();
}
@Override
public void onCreate(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
mRoutingController.attach(this);
ThemeUtils.update(getCarContext());
mNavigationManager.setNavigationManagerCallback(this);
mNavigationManager.navigationStarted();
LocationHelper.from(getCarContext()).addListener(mLocationListener);
if (LocationUtils.checkFineLocationPermission(getCarContext()))
NavigationService.startForegroundService(getCarContext(), CarAppService.getCarNotificationExtender(getCarContext()));
}
@Override
public void onResume(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
mRoutingController.attach(this);
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner)
{
NavigationService.stopService(getCarContext());
LocationHelper.from(getCarContext()).removeListener(mLocationListener);
if (mRoutingController.isNavigating())
mRoutingController.onSaveState();
mRoutingController.detach();
ThemeUtils.update(getCarContext());
mNavigationManager.navigationEnded();
mNavigationManager.clearNavigationManagerCallback();
}
@NonNull
private ActionStrip createActionStrip()
{
final Action.Builder stopActionBuilder = new Action.Builder();
stopActionBuilder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_close)).build());
stopActionBuilder.setOnClickListener(() -> {
mNavigationCancelled = true;
mRoutingController.cancel();
});
final ActionStrip.Builder builder = new ActionStrip.Builder();
builder.addAction(createTtsAction());
builder.addAction(UiHelpers.createSettingsActionForResult(this, getSurfaceRenderer(), this::onSettingsResult));
builder.addAction(stopActionBuilder.build());
return builder.build();
}
@Nullable
private TravelEstimate getDestinationTravelEstimate()
{
if (mTrip.isLoading())
return null;
List<TravelEstimate> travelEstimates = mTrip.getDestinationTravelEstimates();
if (travelEstimates.size() != 1)
throw new RuntimeException("TravelEstimates size must be 1");
return travelEstimates.get(0);
}
@NonNull
private NavigationTemplate.NavigationInfo getNavigationInfo()
{
final androidx.car.app.navigation.model.RoutingInfo.Builder builder = new androidx.car.app.navigation.model.RoutingInfo.Builder();
if (mTrip.isLoading())
{
builder.setLoading(true);
return builder.build();
}
final List<Step> steps = mTrip.getSteps();
final List<TravelEstimate> stepsEstimates = mTrip.getStepTravelEstimates();
if (steps.isEmpty())
throw new RuntimeException("Steps size must be at least 1");
builder.setCurrentStep(steps.get(0), Objects.requireNonNull(stepsEstimates.get(0).getRemainingDistance()));
if (steps.size() > 1)
builder.setNextStep(steps.get(1));
return builder.build();
}
private void onSettingsResult(@Nullable Object result)
{
if (result == null || result != DrivingOptionsScreen.DRIVING_OPTIONS_RESULT_CHANGED)
return;
// TODO (AndrewShkrob): Need to rebuild the route with updated driving options
Logger.d(TAG, "Driving options changed");
}
@NonNull
private Action createTtsAction()
{
final Action.Builder ttsActionBuilder = new Action.Builder();
@DrawableRes final int imgRes = TtsPlayer.isEnabled() ? R.drawable.ic_voice_on : R.drawable.ic_voice_off;
ttsActionBuilder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), imgRes)).build());
ttsActionBuilder.setOnClickListener(() -> {
TtsPlayer.setEnabled(!TtsPlayer.isEnabled());
invalidate();
});
return ttsActionBuilder.build();
}
private void updateTrip()
{
final RoutingInfo info = Framework.nativeGetRouteFollowingInfo();
mTrip = RoutingUtils.createTrip(getCarContext(), info, RoutingController.get().getEndPoint());
mNavigationManager.updateTrip(mTrip);
invalidate();
}
/**
* A builder of {@link NavigationScreen}.
*/
public static final class Builder
{
@NonNull
private final CarContext mCarContext;
@NonNull
private final SurfaceRenderer mSurfaceRenderer;
public Builder(@NonNull final CarContext carContext, @NonNull final SurfaceRenderer surfaceRenderer)
{
mCarContext = carContext;
mSurfaceRenderer = surfaceRenderer;
}
@NonNull
public NavigationScreen build()
{
final NavigationScreen navigationScreen = new NavigationScreen(this);
navigationScreen.setMarker(MARKER);
return navigationScreen;
}
}
}

View file

@ -1,333 +0,0 @@
package app.organicmaps.car.screens;
import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
import static android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE;
import android.content.Intent;
import android.net.Uri;
import android.text.SpannableString;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarContext;
import androidx.car.app.CarToast;
import androidx.car.app.model.Action;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.DistanceSpan;
import androidx.car.app.model.DurationSpan;
import androidx.car.app.model.ForegroundCarColorSpan;
import androidx.car.app.model.Header;
import androidx.car.app.model.Pane;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapTemplate;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.Framework;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.bookmarks.data.Metadata;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.settings.DrivingOptionsScreen;
import app.organicmaps.car.util.Colors;
import app.organicmaps.car.util.OnBackPressedCallback;
import app.organicmaps.car.util.RoutingHelpers;
import app.organicmaps.car.util.UiHelpers;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.routing.RoutingInfo;
import app.organicmaps.util.Config;
import app.organicmaps.util.log.Logger;
import java.util.Objects;
public class PlaceScreen extends BaseMapScreen implements OnBackPressedCallback.Callback, RoutingController.Container
{
private static final String TAG = PlaceScreen.class.getSimpleName();
private static final int ROUTER_TYPE = Framework.ROUTER_TYPE_VEHICLE;
@Nullable
private MapObject mMapObject;
private boolean mIsBuildError = false;
@NonNull
private final RoutingController mRoutingController;
@NonNull
private final OnBackPressedCallback mOnBackPressedCallback;
private PlaceScreen(@NonNull Builder builder)
{
super(builder.mCarContext, builder.mSurfaceRenderer);
mMapObject = builder.mMapObject;
mRoutingController = RoutingController.get();
mOnBackPressedCallback = new OnBackPressedCallback(getCarContext(), this);
}
@NonNull
@Override
public Template onGetTemplate()
{
final MapTemplate.Builder builder = new MapTemplate.Builder();
builder.setHeader(createHeader());
builder.setActionStrip(UiHelpers.createSettingsActionStrip(this, getSurfaceRenderer()));
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setPane(createPane());
return builder.build();
}
@Override
public void onCreate(@NonNull LifecycleOwner owner)
{
mRoutingController.restore();
if (mRoutingController.isNavigating() && mRoutingController.getLastRouterType() == ROUTER_TYPE)
{
showNavigation(true);
return;
}
mRoutingController.attach(this);
if (mMapObject == null)
mRoutingController.restoreRoute();
else
{
final boolean hasIncorrectEndPoint = mRoutingController.isPlanning() && (!MapObject.same(mMapObject, mRoutingController.getEndPoint()));
final boolean hasIncorrectRouterType = mRoutingController.getLastRouterType() != ROUTER_TYPE;
final boolean isNotPlanningMode = !mRoutingController.isPlanning();
if (hasIncorrectRouterType)
{
mRoutingController.setRouterType(ROUTER_TYPE);
mRoutingController.rebuildLastRoute();
}
else if (hasIncorrectEndPoint || isNotPlanningMode)
{
mRoutingController.prepare(LocationHelper.from(getCarContext()).getMyPosition(), mMapObject);
}
}
}
@Override
public void onResume(@NonNull LifecycleOwner owner)
{
mRoutingController.attach(this);
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner)
{
if (mRoutingController.isPlanning())
mRoutingController.onSaveState();
if (!mRoutingController.isNavigating())
mRoutingController.detach();
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
getCarContext().getOnBackPressedDispatcher().addCallback(this, mOnBackPressedCallback);
builder.addEndHeaderAction(createDrivingOptionsAction());
return builder.build();
}
@NonNull
private Pane createPane()
{
final Pane.Builder builder = new Pane.Builder();
final RoutingInfo routingInfo = Framework.nativeGetRouteFollowingInfo();
if (routingInfo == null && !mIsBuildError)
{
builder.setLoading(true);
return builder.build();
}
builder.addRow(getPlaceDescription());
if (routingInfo != null)
builder.addRow(getPlaceRouteInfo(routingInfo));
final Row placeOpeningHours = UiHelpers.getPlaceOpeningHoursRow(Objects.requireNonNull(mMapObject), getCarContext());
if (placeOpeningHours != null)
builder.addRow(placeOpeningHours);
createPaneActions(builder);
return builder.build();
}
@NonNull
private Row getPlaceDescription()
{
Objects.requireNonNull(mMapObject);
final Row.Builder builder = new Row.Builder();
builder.setTitle(mMapObject.getTitle());
if (!mMapObject.getSubtitle().isEmpty())
builder.addText(mMapObject.getSubtitle());
String address = mMapObject.getAddress();
if (address.isEmpty())
address = Framework.nativeGetAddress(mMapObject.getLat(), mMapObject.getLon());
if (!address.isEmpty())
builder.addText(address);
return builder.build();
}
@NonNull
private Row getPlaceRouteInfo(@NonNull RoutingInfo routingInfo)
{
final Row.Builder builder = new Row.Builder();
final SpannableString time = new SpannableString(" ");
time.setSpan(DurationSpan.create(routingInfo.totalTimeInSeconds), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
builder.setTitle(time);
final SpannableString distance = new SpannableString(" ");
distance.setSpan(DistanceSpan.create(RoutingHelpers.createDistance(routingInfo.distToTarget)), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
distance.setSpan(ForegroundCarColorSpan.create(Colors.DISTANCE), 0, 1, SPAN_EXCLUSIVE_EXCLUSIVE);
builder.addText(distance);
return builder.build();
}
private void createPaneActions(@NonNull Pane.Builder builder)
{
Objects.requireNonNull(mMapObject);
final String phones = mMapObject.getMetadata(Metadata.MetadataType.FMD_PHONE_NUMBER);
if (!TextUtils.isEmpty(phones))
{
final String phoneNumber = phones.split(";", 1)[0];
final Action.Builder openDialBuilder = new Action.Builder();
openDialBuilder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_phone)).build());
openDialBuilder.setOnClickListener(() -> getCarContext().startCarApp(new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber))));
builder.addAction(openDialBuilder.build());
}
// Don't show `Start` button when build error.
if (mIsBuildError)
return;
final Action.Builder startRouteBuilder = new Action.Builder();
startRouteBuilder.setBackgroundColor(Colors.START_NAVIGATION);
startRouteBuilder.setFlags(Action.FLAG_PRIMARY);
startRouteBuilder.setTitle(getCarContext().getString(R.string.p2p_start));
startRouteBuilder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_follow_and_rotate)).build());
startRouteBuilder.setOnClickListener(() -> {
Config.acceptRoutingDisclaimer();
mRoutingController.start();
});
builder.addAction(startRouteBuilder.build());
}
@NonNull
private Action createDrivingOptionsAction()
{
return new Action.Builder()
.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_settings)).build())
.setOnClickListener(() -> getScreenManager().pushForResult(new DrivingOptionsScreen(getCarContext(), getSurfaceRenderer()), this::onDrivingOptionsResult))
.build();
}
private void onDrivingOptionsResult(@Nullable Object result)
{
if (result == null || result != DrivingOptionsScreen.DRIVING_OPTIONS_RESULT_CHANGED)
return;
// Driving Options changed. Let's rebuild the route
mRoutingController.rebuildLastRoute();
}
@Override
public void onBackPressed()
{
mRoutingController.cancel();
}
@Override
public void showRoutePlan(boolean show, @Nullable Runnable completionListener)
{
if (!show)
return;
if (completionListener != null)
completionListener.run();
}
@Override
public void showNavigation(boolean show)
{
if (show)
{
getScreenManager().popToRoot();
getScreenManager().push(new NavigationScreen.Builder(getCarContext(), getSurfaceRenderer()).build());
}
}
@Override
public void onBuiltRoute()
{
Framework.nativeDeactivatePopup();
mMapObject = mRoutingController.getEndPoint();
invalidate();
}
@Override
public void onPlanningCancelled()
{
Framework.nativeDeactivatePopup();
}
@Override
public void onCommonBuildError(int lastResultCode, @NonNull String[] lastMissingMaps)
{
// TODO(AndrewShkrob): Show the download maps request
Logger.e(TAG, "lastResultCode: " + lastResultCode + ", lastMissingMaps: " + String.join(", ", lastMissingMaps));
CarToast.makeText(getCarContext(), R.string.unable_to_calc_alert_title, CarToast.LENGTH_LONG).show();
mIsBuildError = true;
invalidate();
}
@Override
public void onDrivingOptionsBuildError()
{
onCommonBuildError(-1, new String[0]);
}
/**
* A builder of {@link PlaceScreen}.
*/
public static final class Builder
{
@NonNull
private final CarContext mCarContext;
@NonNull
private final SurfaceRenderer mSurfaceRenderer;
@Nullable
private MapObject mMapObject;
public Builder(@NonNull final CarContext carContext, @NonNull final SurfaceRenderer surfaceRenderer)
{
mCarContext = carContext;
mSurfaceRenderer = surfaceRenderer;
}
public Builder setMapObject(@Nullable MapObject mapObject)
{
mMapObject = mapObject;
return this;
}
@NonNull
public PlaceScreen build()
{
return new PlaceScreen(this);
}
}
}

View file

@ -1,88 +0,0 @@
package app.organicmaps.car.screens;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.model.Action;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.MessageTemplate;
import androidx.car.app.model.ParkedOnlyOnClickListener;
import androidx.car.app.model.Template;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.R;
import app.organicmaps.car.screens.base.BaseScreen;
import app.organicmaps.car.util.Colors;
import app.organicmaps.util.LocationUtils;
import java.util.Arrays;
import java.util.List;
public class RequestPermissionsScreen extends BaseScreen
{
public interface PermissionsGrantedCallback
{
void onPermissionsGranted();
}
private static final List<String> LOCATION_PERMISSIONS = Arrays.asList(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION);
@NonNull
private final PermissionsGrantedCallback mPermissionsGrantedCallback;
public RequestPermissionsScreen(@NonNull CarContext carContext, @NonNull PermissionsGrantedCallback permissionsGrantedCallback)
{
super(carContext);
mPermissionsGrantedCallback = permissionsGrantedCallback;
}
@NonNull
@Override
public Template onGetTemplate()
{
final MessageTemplate.Builder builder = new MessageTemplate.Builder(getCarContext().getString(R.string.aa_location_permissions_request));
final Action grantPermissions = new Action.Builder()
.setTitle(getCarContext().getString(R.string.aa_grant_permissions))
.setBackgroundColor(Colors.BUTTON_ACCEPT)
.setOnClickListener(ParkedOnlyOnClickListener.create(() -> getCarContext().requestPermissions(LOCATION_PERMISSIONS, this::onRequestPermissionsResult)))
.build();
builder.setHeaderAction(Action.APP_ICON);
builder.setTitle(getCarContext().getString(R.string.app_name));
builder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_location_off)).build());
builder.addAction(grantPermissions);
return builder.build();
}
@Override
public void onResume(@NonNull LifecycleOwner owner)
{
// Let's review the permissions once more, as we might enter this function following an ErrorScreen situation
// where the user manually enabled location permissions.
if (LocationUtils.checkFineLocationPermission(getCarContext()))
{
mPermissionsGrantedCallback.onPermissionsGranted();
finish();
}
}
@SuppressWarnings("unused")
private void onRequestPermissionsResult(@NonNull List<String> grantedPermissions, @NonNull List<String> rejectedPermissions)
{
if (grantedPermissions.isEmpty())
{
getScreenManager().push(new ErrorScreen.Builder(getCarContext())
.setErrorMessage(R.string.location_is_disabled_long_text)
.setCloseable(true)
.build()
);
return;
}
mPermissionsGrantedCallback.onPermissionsGranted();
finish();
}
}

View file

@ -1,24 +0,0 @@
package app.organicmaps.car.screens.base;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import app.organicmaps.car.SurfaceRenderer;
public abstract class BaseMapScreen extends BaseScreen
{
@NonNull
private final SurfaceRenderer mSurfaceRenderer;
public BaseMapScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext);
mSurfaceRenderer = surfaceRenderer;
}
@NonNull
protected SurfaceRenderer getSurfaceRenderer()
{
return mSurfaceRenderer;
}
}

View file

@ -1,15 +0,0 @@
package app.organicmaps.car.screens.base;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.Screen;
import androidx.lifecycle.DefaultLifecycleObserver;
public abstract class BaseScreen extends Screen implements DefaultLifecycleObserver
{
public BaseScreen(@NonNull CarContext carContext)
{
super(carContext);
getLifecycle().addObserver(this);
}
}

View file

@ -1,207 +0,0 @@
package app.organicmaps.car.screens.search;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.Header;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.util.UiHelpers;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.search.NativeSearchListener;
import app.organicmaps.search.SearchEngine;
import app.organicmaps.search.SearchRecents;
import app.organicmaps.search.SearchResult;
import app.organicmaps.util.Language;
public class SearchOnMapScreen extends BaseMapScreen implements NativeSearchListener
{
private final int MAX_RESULTS_SIZE;
@NonNull
private final String mQuery;
@NonNull
private final String mLocale;
private final boolean mIsCategory;
@Nullable
private ItemList mResults = null;
private SearchOnMapScreen(@NonNull Builder builder)
{
super(builder.mCarContext, builder.mSurfaceRenderer);
final ConstraintManager constraintManager = getCarContext().getCarService(ConstraintManager.class);
MAX_RESULTS_SIZE = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST);
mQuery = builder.mQuery;
mLocale = builder.mLocale;
mIsCategory = builder.mIsCategory;
}
@NonNull
@Override
public Template onGetTemplate()
{
final PlaceListNavigationTemplate.Builder builder = new PlaceListNavigationTemplate.Builder();
builder.setHeader(createHeader());
builder.setMapActionStrip(UiHelpers.createMapActionStrip(getCarContext(), getSurfaceRenderer()));
if (mResults == null)
builder.setLoading(true);
else
builder.setItemList(mResults);
return builder.build();
}
@Override
public void onResultsUpdate(@NonNull SearchResult[] results, long timestamp)
{
if (mResults != null)
return;
final ItemList.Builder builder = new ItemList.Builder();
builder.setNoItemsMessage(getCarContext().getString(R.string.search_not_found));
final int resultsSize = Math.min(results.length, MAX_RESULTS_SIZE);
for (int i = 0; i < resultsSize; i++)
builder.addItem(createResultItem(results[i], i));
mResults = builder.build();
invalidate();
}
@NonNull
private Row createResultItem(@NonNull SearchResult result, int resultIndex)
{
final Row.Builder builder = new Row.Builder();
if (result.type == SearchResult.TYPE_RESULT)
{
final String title = result.getTitle(getCarContext());
builder.setTitle(title);
builder.addText(result.getFormattedDescription(getCarContext()));
final CharSequence openingHours = SearchUiHelpers.getOpeningHoursText(getCarContext(), result);
final CharSequence distance = SearchUiHelpers.getDistanceText(result);
final CharSequence openingHoursAndDistanceText = SearchUiHelpers.getOpeningHoursAndDistanceText(openingHours, distance);
if (openingHoursAndDistanceText.length() != 0)
builder.addText(openingHoursAndDistanceText);
if (distance.length() == 0)
{
// All non-browsable rows must have a distance span attached to either its title or texts
builder.setBrowsable(true);
}
builder.setOnClickListener(() -> {
SearchRecents.add(title, getCarContext());
SearchEngine.INSTANCE.cancel();
SearchEngine.INSTANCE.showResult(resultIndex);
getScreenManager().popToRoot();
});
}
else
{
builder.setBrowsable(true);
builder.setTitle(result.suggestion);
builder.setImage(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_search)).build());
builder.setOnClickListener(() -> getScreenManager().push(new Builder(getCarContext(), getSurfaceRenderer()).setQuery(result.suggestion).build()));
}
return builder.build();
}
@Override
public void onStart(@NonNull LifecycleOwner owner)
{
SearchEngine.INSTANCE.addListener(this);
}
@Override
public void onResume(@NonNull LifecycleOwner owner)
{
SearchEngine.INSTANCE.cancel();
final MapObject location = LocationHelper.from(getCarContext()).getMyPosition();
final boolean hasLocation = location != null;
final double lat = hasLocation ? location.getLat() : 0;
final double lon = hasLocation ? location.getLon() : 0;
SearchEngine.INSTANCE.searchInteractive(mQuery, mIsCategory, mLocale, System.nanoTime(), true /* isMapAndTable */, hasLocation, lat, lon);
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
SearchEngine.INSTANCE.removeListener(this);
SearchEngine.INSTANCE.cancel();
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
builder.setTitle(mQuery);
return builder.build();
}
/**
* A builder of {@link SearchOnMapScreen}.
*/
public static final class Builder
{
@NonNull
private final CarContext mCarContext;
@NonNull
private final SurfaceRenderer mSurfaceRenderer;
@NonNull
private String mQuery = "";
@NonNull
private String mLocale;
private boolean mIsCategory;
public Builder(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
mCarContext = carContext;
mSurfaceRenderer = surfaceRenderer;
mLocale = Language.getKeyboardLocale(mCarContext);
}
public Builder setCategory(@NonNull String category)
{
mIsCategory = true;
mQuery = category;
return this;
}
public Builder setQuery(@NonNull String query)
{
mIsCategory = false;
mQuery = query;
return this;
}
public Builder setLocale(@NonNull String locale)
{
mLocale = locale;
return this;
}
@NonNull
public SearchOnMapScreen build()
{
if (mQuery.isEmpty())
throw new IllegalStateException("Search query is empty");
return new SearchOnMapScreen(this);
}
}
}

View file

@ -1,234 +0,0 @@
package app.organicmaps.car.screens.search;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarContext;
import androidx.car.app.constraints.ConstraintManager;
import androidx.car.app.model.Action;
import androidx.car.app.model.ActionStrip;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Row;
import androidx.car.app.model.SearchTemplate;
import androidx.car.app.model.Template;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.search.NativeSearchListener;
import app.organicmaps.search.SearchEngine;
import app.organicmaps.search.SearchRecents;
import app.organicmaps.search.SearchResult;
import app.organicmaps.util.Language;
public class SearchScreen extends BaseMapScreen implements SearchTemplate.SearchCallback, NativeSearchListener
{
private final int MAX_RESULTS_SIZE;
@NonNull
private String mQuery = "";
@NonNull
private String mLocale;
@Nullable
private ItemList mResults = null;
private SearchScreen(@NonNull Builder builder)
{
super(builder.mCarContext, builder.mSurfaceRenderer);
final ConstraintManager constraintManager = getCarContext().getCarService(ConstraintManager.class);
MAX_RESULTS_SIZE = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST);
mLocale = builder.mLocale;
onSearchSubmitted(builder.mQuery);
}
@NonNull
@Override
public Template onGetTemplate()
{
final SearchTemplate.Builder builder = new SearchTemplate.Builder(this);
builder.setHeaderAction(Action.BACK);
builder.setShowKeyboardByDefault(false);
if (mQuery.isEmpty() && mResults == null)
{
if (!loadRecents())
builder.setShowKeyboardByDefault(true);
}
if (!mQuery.isEmpty() && mResults != null && !mResults.getItems().isEmpty())
builder.setActionStrip(createActionStrip());
if (mResults == null)
builder.setLoading(true);
else
builder.setItemList(mResults);
builder.setInitialSearchText(mQuery);
builder.setSearchHint(getCarContext().getString(R.string.search));
return builder.build();
}
@Override
public void onSearchTextChanged(@NonNull String searchText)
{
if (mQuery.equals(searchText))
return;
mQuery = searchText;
mLocale = Language.getKeyboardLocale(getCarContext());
mResults = null;
SearchEngine.INSTANCE.cancel();
if (mQuery.isEmpty())
{
invalidate();
return;
}
final MapObject location = LocationHelper.from(getCarContext()).getMyPosition();
final boolean hasLocation = location != null;
final double lat = hasLocation ? location.getLat() : 0;
final double lon = hasLocation ? location.getLon() : 0;
SearchEngine.INSTANCE.search(getCarContext(), mQuery, false, System.nanoTime(), hasLocation, lat, lon);
invalidate();
}
@Override
public void onSearchSubmitted(@NonNull String searchText)
{
onSearchTextChanged(searchText);
}
@Override
public void onStart(@NonNull LifecycleOwner owner)
{
SearchEngine.INSTANCE.addListener(this);
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
SearchEngine.INSTANCE.removeListener(this);
SearchEngine.INSTANCE.cancel();
}
@Override
public void onResultsUpdate(@NonNull SearchResult[] results, long timestamp)
{
final ItemList.Builder builder = new ItemList.Builder();
builder.setNoItemsMessage(getCarContext().getString(R.string.search_not_found));
final int resultsSize = Math.min(results.length, MAX_RESULTS_SIZE);
for (int i = 0; i < resultsSize; i++)
builder.addItem(createResultItem(results[i], i));
mResults = builder.build();
invalidate();
}
@NonNull
private Row createResultItem(@NonNull SearchResult result, int resultIndex)
{
final Row.Builder builder = new Row.Builder();
if (result.type == SearchResult.TYPE_RESULT)
{
final String title = result.getTitle(getCarContext());
builder.setTitle(title);
builder.addText(result.getFormattedDescription(getCarContext()));
final CharSequence openingHoursAndDistance = SearchUiHelpers.getOpeningHoursAndDistanceText(getCarContext(), result);
if (openingHoursAndDistance.length() != 0)
builder.addText(openingHoursAndDistance);
builder.setOnClickListener(() -> {
SearchRecents.add(title, getCarContext());
SearchEngine.INSTANCE.cancel();
SearchEngine.INSTANCE.showResult(resultIndex);
getScreenManager().popToRoot();
});
}
else
{
builder.setBrowsable(true);
builder.setTitle(result.suggestion);
builder.setImage(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_search)).build());
builder.setOnClickListener(() -> onSearchSubmitted(result.suggestion));
}
return builder.build();
}
private boolean loadRecents()
{
final CarIcon iconRecent = new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_search_recent)).build();
final ItemList.Builder builder = new ItemList.Builder();
builder.setNoItemsMessage(getCarContext().getString(R.string.search_history_text));
SearchRecents.refresh();
final int recentsSize = Math.min(SearchRecents.getSize(), MAX_RESULTS_SIZE);
for (int i = 0; i < recentsSize; ++i)
{
final Row.Builder itemBuilder = new Row.Builder();
final String title = SearchRecents.get(i);
itemBuilder.setTitle(title);
itemBuilder.setImage(iconRecent);
itemBuilder.setOnClickListener(() -> onSearchSubmitted(title));
builder.addItem(itemBuilder.build());
}
mResults = builder.build();
return recentsSize != 0;
}
@NonNull
private ActionStrip createActionStrip()
{
final Action.Builder builder = new Action.Builder();
builder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_show_on_map)).build());
builder.setOnClickListener(() ->
getScreenManager().push(new SearchOnMapScreen.Builder(getCarContext(), getSurfaceRenderer()).setQuery(mQuery).setLocale(mLocale).build()));
return new ActionStrip.Builder().addAction(builder.build()).build();
}
/**
* A builder of {@link SearchScreen}.
*/
public static final class Builder
{
@NonNull
private final CarContext mCarContext;
@NonNull
private final SurfaceRenderer mSurfaceRenderer;
@NonNull
private String mQuery = "";
@NonNull
private String mLocale;
public Builder(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
mCarContext = carContext;
mSurfaceRenderer = surfaceRenderer;
mLocale = Language.getKeyboardLocale(mCarContext);
}
@NonNull
public Builder setQuery(@NonNull String query)
{
mQuery = query;
return this;
}
public Builder setLocale(@NonNull String locale)
{
mLocale = locale;
return this;
}
@NonNull
public SearchScreen build()
{
return new SearchScreen(this);
}
}
}

View file

@ -1,95 +0,0 @@
package app.organicmaps.car.screens.search;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.model.CarColor;
import androidx.car.app.model.DistanceSpan;
import androidx.car.app.model.ForegroundCarColorSpan;
import app.organicmaps.R;
import app.organicmaps.car.util.Colors;
import app.organicmaps.car.util.RoutingHelpers;
import app.organicmaps.search.SearchResult;
public final class SearchUiHelpers
{
@NonNull
public static CharSequence getOpeningHoursAndDistanceText(@NonNull CarContext carContext, @NonNull SearchResult searchResult)
{
final CharSequence openingHours = getOpeningHoursText(carContext, searchResult);
final CharSequence distance = getDistanceText(searchResult);
return getOpeningHoursAndDistanceText(openingHours, distance);
}
@NonNull
public static CharSequence getOpeningHoursAndDistanceText(@NonNull CharSequence openingHours, @NonNull CharSequence distance)
{
final SpannableStringBuilder result = new SpannableStringBuilder();
if (openingHours.length() != 0)
result.append(openingHours);
if (result.length() != 0 && distance.length() != 0)
result.append("");
if (distance.length() != 0)
result.append(distance);
return result;
}
@NonNull
public static CharSequence getDistanceText(@NonNull SearchResult searchResult)
{
if (!searchResult.description.distance.isValid())
return "";
final SpannableStringBuilder distance = new SpannableStringBuilder(" ");
distance.setSpan(DistanceSpan.create(RoutingHelpers.createDistance(searchResult.description.distance)), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
distance.setSpan(ForegroundCarColorSpan.create(Colors.DISTANCE), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return distance;
}
@NonNull
public static CharSequence getOpeningHoursText(@NonNull CarContext carContext, @NonNull SearchResult searchResult)
{
final SpannableStringBuilder result = new SpannableStringBuilder();
String text = "";
CarColor color = Colors.DEFAULT;
switch (searchResult.description.openNow)
{
case SearchResult.OPEN_NOW_YES:
if (searchResult.description.minutesUntilClosed < 60) // less than 1 hour
{
final String time = searchResult.description.minutesUntilClosed + " " +
carContext.getString(R.string.minute);
text = carContext.getString(R.string.closes_in, time);
color = Colors.OPENING_HOURS_CLOSES_SOON;
}
else
{
text = carContext.getString(R.string.editor_time_open);
color = Colors.OPENING_HOURS_OPEN;
}
break;
case SearchResult.OPEN_NOW_NO:
if (searchResult.description.minutesUntilOpen < 60) // less than 1 hour
{
final String time = searchResult.description.minutesUntilOpen + " " +
carContext.getString(R.string.minute);
text = carContext.getString(R.string.opens_in, time);
}
else
text = carContext.getString(R.string.closed);
color = Colors.OPENING_HOURS_CLOSED;
break;
}
result.append(text);
result.setSpan(ForegroundCarColorSpan.create(color), 0, result.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
return result;
}
}

View file

@ -1,131 +0,0 @@
package app.organicmaps.car.screens.settings;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.car.app.CarContext;
import androidx.car.app.model.Action;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Header;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapTemplate;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.R;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.util.UiHelpers;
import app.organicmaps.routing.RoutingOptions;
import app.organicmaps.settings.RoadType;
import java.util.HashMap;
import java.util.Map;
public class DrivingOptionsScreen extends BaseMapScreen
{
public static final Object DRIVING_OPTIONS_RESULT_CHANGED = 0x1;
private static class DrivingOption
{
public final RoadType roadType;
@StringRes
public final int text;
public DrivingOption(RoadType roadType, @StringRes int text)
{
this.roadType = roadType;
this.text = text;
}
}
private final DrivingOption[] mDrivingOptions = {
new DrivingOption(RoadType.Toll, R.string.avoid_tolls),
new DrivingOption(RoadType.Dirty, R.string.avoid_unpaved),
new DrivingOption(RoadType.Ferry, R.string.avoid_ferry),
new DrivingOption(RoadType.Motorway, R.string.avoid_motorways)
};
@NonNull
private final CarIcon mCheckboxIcon;
@NonNull
private final CarIcon mCheckboxSelectedIcon;
@NonNull
private final Map<RoadType, Boolean> mInitialDrivingOptionsState = new HashMap<>();
public DrivingOptionsScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext, surfaceRenderer);
mCheckboxIcon = new CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_check_box)).build();
mCheckboxSelectedIcon = new CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_check_box_checked)).build();
initDrivingOptionsState();
}
@NonNull
@Override
public Template onGetTemplate()
{
final MapTemplate.Builder builder = new MapTemplate.Builder();
builder.setHeader(createHeader());
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setItemList(createDrivingOptionsList());
return builder.build();
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
for (final DrivingOption drivingOption : mDrivingOptions)
{
if (Boolean.TRUE.equals(mInitialDrivingOptionsState.get(drivingOption.roadType)) != RoutingOptions.hasOption(drivingOption.roadType))
{
setResult(DRIVING_OPTIONS_RESULT_CHANGED);
return;
}
}
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
builder.setTitle(getCarContext().getString(R.string.driving_options_title));
return builder.build();
}
@NonNull
private ItemList createDrivingOptionsList()
{
final ItemList.Builder builder = new ItemList.Builder();
for (final DrivingOption drivingOption : mDrivingOptions)
builder.addItem(createDrivingOptionCheckbox(drivingOption.roadType, drivingOption.text));
return builder.build();
}
@NonNull
private Row createDrivingOptionCheckbox(RoadType roadType, @StringRes int titleRes)
{
final Row.Builder builder = new Row.Builder();
builder.setTitle(getCarContext().getString(titleRes));
builder.setOnClickListener(() -> {
if (RoutingOptions.hasOption(roadType))
RoutingOptions.removeOption(roadType);
else
RoutingOptions.addOption(roadType);
invalidate();
});
builder.setImage(RoutingOptions.hasOption(roadType) ? mCheckboxSelectedIcon : mCheckboxIcon);
return builder.build();
}
private void initDrivingOptionsState()
{
for (final DrivingOption drivingOption : mDrivingOptions)
mInitialDrivingOptionsState.put(drivingOption.roadType, RoutingOptions.hasOption(drivingOption.roadType));
}
}

View file

@ -1,74 +0,0 @@
package app.organicmaps.car.screens.settings;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.model.Action;
import androidx.car.app.model.Header;
import androidx.car.app.model.Item;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapTemplate;
import app.organicmaps.BuildConfig;
import app.organicmaps.Framework;
import app.organicmaps.R;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.util.UiHelpers;
import app.organicmaps.util.DateUtils;
public class HelpScreen extends BaseMapScreen
{
public HelpScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext, surfaceRenderer);
}
@NonNull
@Override
public Template onGetTemplate()
{
final MapTemplate.Builder builder = new MapTemplate.Builder();
builder.setHeader(createHeader());
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setItemList(createSettingsList());
return builder.build();
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
builder.setTitle(getCarContext().getString(R.string.help));
return builder.build();
}
@NonNull
private ItemList createSettingsList()
{
final ItemList.Builder builder = new ItemList.Builder();
builder.addItem(createVersionInfo());
builder.addItem(createDataVersionInfo());
return builder.build();
}
@NonNull
private Item createVersionInfo()
{
return new Row.Builder()
.setTitle(getCarContext().getString(R.string.app_name))
.addText(BuildConfig.VERSION_NAME)
.build();
}
@NonNull
private Item createDataVersionInfo()
{
return new Row.Builder()
.setTitle(getCarContext().getString(R.string.data_version, ""))
.addText(DateUtils.getShortDateFormatter().format(Framework.getDataVersion()))
.build();
}
}

View file

@ -1,124 +0,0 @@
package app.organicmaps.car.screens.settings;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.car.app.CarContext;
import androidx.car.app.model.Action;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Header;
import androidx.car.app.model.Item;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapTemplate;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.R;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.util.ThemeUtils;
import app.organicmaps.car.util.UiHelpers;
import app.organicmaps.util.Config;
public class SettingsScreen extends BaseMapScreen
{
private interface PrefsGetter
{
boolean get();
}
private interface PrefsSetter
{
void set(boolean newValue);
}
@NonNull
private final CarIcon mCheckboxIcon;
@NonNull
private final CarIcon mCheckboxSelectedIcon;
public SettingsScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext, surfaceRenderer);
mCheckboxIcon = new CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_check_box)).build();
mCheckboxSelectedIcon = new CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_check_box_checked)).build();
}
@NonNull
@Override
public Template onGetTemplate()
{
final MapTemplate.Builder builder = new MapTemplate.Builder();
builder.setHeader(createHeader());
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setItemList(createSettingsList());
return builder.build();
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
builder.setTitle(getCarContext().getString(R.string.settings));
return builder.build();
}
@NonNull
private ItemList createSettingsList()
{
final ItemList.Builder builder = new ItemList.Builder();
builder.addItem(createThemeItem());
builder.addItem(createRoutingOptionsItem());
builder.addItem(createSharedPrefsCheckbox(R.string.big_font, Config::isLargeFontsSize, Config::setLargeFontsSize));
builder.addItem(createSharedPrefsCheckbox(R.string.transliteration_title, Config::isTransliteration, Config::setTransliteration));
builder.addItem(createHelpItem());
return builder.build();
}
@NonNull
private Item createThemeItem()
{
final Row.Builder builder = new Row.Builder();
builder.setTitle(getCarContext().getString(R.string.pref_map_style_title));
builder.addText(getCarContext().getString(ThemeUtils.getThemeMode(getCarContext()).getTitleId()));
builder.setOnClickListener(() -> getScreenManager().push(new ThemeScreen(getCarContext(), getSurfaceRenderer())));
builder.setBrowsable(true);
return builder.build();
}
@NonNull
private Item createRoutingOptionsItem()
{
final Row.Builder builder = new Row.Builder();
builder.setTitle(getCarContext().getString(R.string.driving_options_title));
builder.setOnClickListener(() -> getScreenManager().pushForResult(new DrivingOptionsScreen(getCarContext(), getSurfaceRenderer()), this::setResult));
builder.setBrowsable(true);
return builder.build();
}
@NonNull
private Item createHelpItem()
{
final Row.Builder builder = new Row.Builder();
builder.setTitle(getCarContext().getString(R.string.help));
builder.setOnClickListener(() -> getScreenManager().push(new HelpScreen(getCarContext(), getSurfaceRenderer())));
builder.setBrowsable(true);
return builder.build();
}
@NonNull
private Row createSharedPrefsCheckbox(@StringRes int titleRes, @NonNull PrefsGetter getter, @NonNull PrefsSetter setter)
{
final boolean getterValue = getter.get();
final Row.Builder builder = new Row.Builder();
builder.setTitle(getCarContext().getString(titleRes));
builder.setOnClickListener(() -> {
setter.set(!getterValue);
invalidate();
});
builder.setImage(getterValue ? mCheckboxSelectedIcon : mCheckboxIcon);
return builder.build();
}
}

View file

@ -1,82 +0,0 @@
package app.organicmaps.car.screens.settings;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.model.Action;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Header;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MapTemplate;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.R;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.util.ThemeUtils;
import app.organicmaps.car.util.UiHelpers;
public class ThemeScreen extends BaseMapScreen
{
@NonNull
private final CarIcon mRadioButtonIcon;
@NonNull
private final CarIcon mRadioButtonSelectedIcon;
public ThemeScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext, surfaceRenderer);
mRadioButtonIcon = new CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_radio_button_unchecked)).build();
mRadioButtonSelectedIcon = new CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_radio_button_checked)).build();
}
@NonNull
@Override
public Template onGetTemplate()
{
final MapTemplate.Builder builder = new MapTemplate.Builder();
builder.setHeader(createHeader());
builder.setMapController(UiHelpers.createMapController(getCarContext(), getSurfaceRenderer()));
builder.setItemList(createRadioButtons());
return builder.build();
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
builder.setTitle(getCarContext().getString(R.string.pref_map_style_title));
return builder.build();
}
@NonNull
private ItemList createRadioButtons()
{
final ItemList.Builder builder = new ItemList.Builder();
final ThemeUtils.ThemeMode currentThemeMode = ThemeUtils.getThemeMode(getCarContext());
builder.addItem(createRadioButton(ThemeUtils.ThemeMode.AUTO, currentThemeMode));
builder.addItem(createRadioButton(ThemeUtils.ThemeMode.NIGHT, currentThemeMode));
builder.addItem(createRadioButton(ThemeUtils.ThemeMode.LIGHT, currentThemeMode));
return builder.build();
}
@NonNull
private Row createRadioButton(@NonNull ThemeUtils.ThemeMode themeMode, @NonNull ThemeUtils.ThemeMode currentThemeMode)
{
final Row.Builder builder = new Row.Builder();
builder.setTitle(getCarContext().getString(themeMode.getTitleId()));
builder.setOnClickListener(() -> {
if (themeMode == currentThemeMode)
return;
ThemeUtils.setThemeMode(getCarContext(), themeMode);
invalidate();
});
if (themeMode == currentThemeMode)
builder.setImage(mRadioButtonSelectedIcon);
else
builder.setImage(mRadioButtonIcon);
return builder.build();
}
}

View file

@ -1,19 +0,0 @@
package app.organicmaps.car.util;
import androidx.car.app.model.CarColor;
public final class Colors
{
public static final CarColor DEFAULT = CarColor.DEFAULT;
public static final CarColor DISTANCE = CarColor.BLUE;
public static final CarColor LOCATION_TINT = CarColor.BLUE;
public static final CarColor OPENING_HOURS_OPEN = CarColor.GREEN;
public static final CarColor OPENING_HOURS_CLOSES_SOON = CarColor.YELLOW;
public static final CarColor OPENING_HOURS_CLOSED = CarColor.RED;
public static final CarColor START_NAVIGATION = CarColor.GREEN;
public static final CarColor NAVIGATION_TEMPLATE_BACKGROUND_DAY = CarColor.GREEN;
public static final CarColor NAVIGATION_TEMPLATE_BACKGROUND_NIGHT = CarColor.DEFAULT;
public static final CarColor BUTTON_ACCEPT = CarColor.GREEN;
private Colors() {}
}

View file

@ -1,111 +0,0 @@
package app.organicmaps.car.util;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.Screen;
import androidx.car.app.ScreenManager;
import app.organicmaps.Framework;
import app.organicmaps.Map;
import app.organicmaps.MwmActivity;
import app.organicmaps.api.Const;
import app.organicmaps.api.ParsedSearchRequest;
import app.organicmaps.api.RequestType;
import app.organicmaps.car.CarAppService;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.hacks.PopToRootHack;
import app.organicmaps.car.screens.NavigationScreen;
import app.organicmaps.car.screens.search.SearchScreen;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.display.DisplayType;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.util.log.Logger;
public final class IntentUtils
{
private static final String TAG = IntentUtils.class.getSimpleName();
private static final int SEARCH_IN_VIEWPORT_ZOOM = 16;
public static void processIntent(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer, @NonNull Intent intent)
{
final String action = intent.getAction();
if (CarContext.ACTION_NAVIGATE.equals(action))
IntentUtils.processNavigationIntent(carContext, surfaceRenderer, intent);
else if (Intent.ACTION_VIEW.equals(action))
processViewIntent(carContext, intent);
}
// https://developer.android.com/reference/androidx/car/app/CarContext#startCarApp(android.content.Intent)
private static void processNavigationIntent(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer, @NonNull Intent intent)
{
// TODO (AndrewShkrob): This logic will need to be revised when we introduce support for adding stops during navigation or route planning.
// Skip navigation intents during navigation
if (RoutingController.get().isNavigating())
return;
final Uri uri = intent.getData();
if (uri == null)
return;
final ScreenManager screenManager = carContext.getCarService(ScreenManager.class);
switch (Framework.nativeParseAndSetApiUrl(uri.toString()))
{
case RequestType.INCORRECT:
return;
case RequestType.MAP:
screenManager.popToRoot();
Map.executeMapApiRequest();
return;
case RequestType.SEARCH:
screenManager.popToRoot();
final ParsedSearchRequest request = Framework.nativeGetParsedSearchRequest();
final double[] latlon = Framework.nativeGetParsedCenterLatLon();
if (latlon != null)
{
Framework.nativeStopLocationFollow();
Framework.nativeSetViewportCenter(latlon[0], latlon[1], SEARCH_IN_VIEWPORT_ZOOM);
// We need to update viewport for search api manually because of drape engine
// will not notify subscribers when search activity is shown.
if (!request.mIsSearchOnMap)
Framework.nativeSetSearchViewport(latlon[0], latlon[1], SEARCH_IN_VIEWPORT_ZOOM);
}
final SearchScreen.Builder builder = new SearchScreen.Builder(carContext, surfaceRenderer);
builder.setQuery(request.mQuery);
if (request.mLocale != null)
builder.setLocale(request.mLocale);
screenManager.push(new PopToRootHack.Builder(carContext).setScreenToPush(builder.build()).build());
return;
case RequestType.ROUTE:
Logger.e(TAG, "Route API is not supported by Android Auto: " + uri);
return;
case RequestType.CROSSHAIR:
Logger.e(TAG, "Crosshair API is not supported by Android Auto: " + uri);
return;
}
}
private static void processViewIntent(@NonNull CarContext carContext, @NonNull Intent intent)
{
final Uri uri = intent.getData();
if (uri != null
&& Const.API_SCHEME.equals(uri.getScheme())
&& CarAppService.API_CAR_HOST.equals(uri.getSchemeSpecificPart())
&& CarAppService.ACTION_SHOW_NAVIGATION_SCREEN.equals(uri.getFragment()))
{
final ScreenManager screenManager = carContext.getCarService(ScreenManager.class);
final Screen top = screenManager.getTop();
final DisplayManager displayManager = DisplayManager.from(carContext);
if (!displayManager.isCarDisplayUsed())
displayManager.changeDisplay(DisplayType.Car);
if (!(top instanceof NavigationScreen))
screenManager.popTo(NavigationScreen.MARKER);
}
}
private IntentUtils() {}
}

View file

@ -1,137 +0,0 @@
package app.organicmaps.car.util;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import app.organicmaps.R;
import app.organicmaps.routing.SingleLaneInfo;
import java.util.Objects;
public class LanesDrawable extends Drawable
{
@ColorRes
private static final int ACTIVE_LANE_TINT_RES = R.color.white_primary;
@ColorRes
private static final int INACTIVE_LANE_TINT_RES = R.color.icon_tint_light;
@ColorRes
private static final int INACTIVE_LANE_TINT_NIGHT_RES = R.color.icon_tint_light_night;
@DimenRes
private static final int MARGIN_RES = R.dimen.margin_quarter;
private static class TintColorInfo
{
@ColorInt
public final int mActiveLaneTint;
@ColorInt
public final int mInactiveLaneTint;
public TintColorInfo(@ColorInt int activeLaneTint, @ColorInt int inactiveLaneTint)
{
mActiveLaneTint = activeLaneTint;
mInactiveLaneTint = inactiveLaneTint;
}
}
private static class LaneDrawable
{
private final Drawable mDrawable;
private final Rect mRect;
private final int mTintColor;
private LaneDrawable(@NonNull final Context context, @NonNull SingleLaneInfo laneInfo, int horizontalOffset, TintColorInfo colorInfo)
{
mDrawable = Objects.requireNonNull(AppCompatResources.getDrawable(context, laneInfo.mLane[0].mTurnRes));
final int width = mDrawable.getIntrinsicWidth();
final int height = mDrawable.getIntrinsicHeight();
mRect = new Rect(horizontalOffset, 0, horizontalOffset + width, height);
mTintColor = laneInfo.mIsActive ? colorInfo.mActiveLaneTint : colorInfo.mInactiveLaneTint;
}
private void draw(@NonNull final Canvas canvas)
{
mDrawable.setTint(mTintColor);
mDrawable.setBounds(mRect);
mDrawable.draw(canvas);
}
}
@NonNull
private final LaneDrawable[] mLanes;
private final int mWidth;
private final int mHeight;
public LanesDrawable(@NonNull final Context context, @NonNull SingleLaneInfo[] lanes, boolean isDarkMode)
{
final int mMargin = context.getResources().getDimensionPixelSize(MARGIN_RES);
final TintColorInfo tintColorInfo = getTintColorInfo(context, isDarkMode);
mLanes = new LaneDrawable[lanes.length];
int totalWidth = 0;
for (int i = 0; i < lanes.length; ++i)
{
mLanes[i] = new LaneDrawable(context, lanes[i], totalWidth + mMargin, tintColorInfo);
totalWidth += mLanes[i].mRect.width() + mMargin * 2;
}
mWidth = totalWidth;
mHeight = mLanes[0].mRect.height();
}
@Override
public int getIntrinsicWidth()
{
return mWidth;
}
@Override
public int getIntrinsicHeight()
{
return mHeight;
}
@Override
public void draw(@NonNull Canvas canvas)
{
for (final LaneDrawable drawable : mLanes)
drawable.draw(canvas);
}
@Override
public void setAlpha(int alpha) {}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {}
@Override
public int getOpacity()
{
return PixelFormat.UNKNOWN;
}
@NonNull
private static TintColorInfo getTintColorInfo(@NonNull final Context context, boolean isDarkMode)
{
final int activeLaneTint = ContextCompat.getColor(context, ACTIVE_LANE_TINT_RES);
final int inactiveLaneTint = ContextCompat.getColor(context, !isDarkMode ? INACTIVE_LANE_TINT_RES : INACTIVE_LANE_TINT_NIGHT_RES);
return new TintColorInfo(activeLaneTint, inactiveLaneTint);
}
}

View file

@ -1,30 +0,0 @@
package app.organicmaps.car.util;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.ScreenManager;
public class OnBackPressedCallback extends androidx.activity.OnBackPressedCallback
{
public interface Callback
{
void onBackPressed();
}
private final ScreenManager mScreenManager;
private final Callback mCallback;
public OnBackPressedCallback(@NonNull CarContext carContext, @NonNull Callback callback)
{
super(true);
mScreenManager = carContext.getCarService(ScreenManager.class);
mCallback = callback;
}
@Override
public void handleOnBackPressed()
{
mCallback.onBackPressed();
mScreenManager.pop();
}
}

View file

@ -1,138 +0,0 @@
package app.organicmaps.car.util;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Distance;
import androidx.car.app.navigation.model.LaneDirection;
import androidx.car.app.navigation.model.Maneuver;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.routing.RoutingInfo;
import app.organicmaps.routing.SingleLaneInfo;
public final class RoutingHelpers
{
private RoutingHelpers() {}
@NonNull
public static Distance createDistance(@NonNull final app.organicmaps.util.Distance distance)
{
int displayUnit;
switch (distance.mUnits)
{
case Kilometers:
displayUnit = distance.mDistance >= 10.0 ? Distance.UNIT_KILOMETERS : Distance.UNIT_KILOMETERS_P1;
break;
case Feet:
displayUnit = Distance.UNIT_FEET;
break;
case Miles:
displayUnit = distance.mDistance >= 10.0 ? Distance.UNIT_MILES : Distance.UNIT_MILES_P1;
break;
case Meters:
default:
displayUnit = Distance.UNIT_METERS;
break;
}
return Distance.create(distance.mDistance, displayUnit);
}
@NonNull
public static LaneDirection createLaneDirection(@NonNull SingleLaneInfo.LaneWay laneWay, boolean isRecommended)
{
int shape = LaneDirection.SHAPE_UNKNOWN;
switch (laneWay)
{
case REVERSE:
shape = LaneDirection.SHAPE_U_TURN_LEFT;
break;
case SHARP_LEFT:
shape = LaneDirection.SHAPE_SHARP_LEFT;
break;
case LEFT:
shape = LaneDirection.SHAPE_NORMAL_LEFT;
break;
case SLIGHT_LEFT:
case MERGE_TO_LEFT:
shape = LaneDirection.SHAPE_SLIGHT_LEFT;
break;
case SLIGHT_RIGHT:
case MERGE_TO_RIGHT:
shape = LaneDirection.SHAPE_SLIGHT_RIGHT;
break;
case THROUGH:
shape = LaneDirection.SHAPE_STRAIGHT;
break;
case RIGHT:
shape = LaneDirection.SHAPE_NORMAL_RIGHT;
break;
case SHARP_RIGHT:
shape = LaneDirection.SHAPE_SHARP_RIGHT;
break;
}
return LaneDirection.create(shape, isRecommended);
}
@NonNull
public static Maneuver createManeuver(@NonNull final CarContext context, @NonNull RoutingInfo.CarDirection carDirection, int roundaboutExitNum)
{
int maneuverType = Maneuver.TYPE_UNKNOWN;
switch (carDirection)
{
case NO_TURN:
case GO_STRAIGHT:
maneuverType = Maneuver.TYPE_STRAIGHT;
break;
case TURN_RIGHT:
maneuverType = Maneuver.TYPE_TURN_NORMAL_RIGHT;
break;
case TURN_SHARP_RIGHT:
maneuverType = Maneuver.TYPE_TURN_SHARP_RIGHT;
break;
case TURN_SLIGHT_RIGHT:
maneuverType = Maneuver.TYPE_TURN_SLIGHT_RIGHT;
break;
case TURN_LEFT:
maneuverType = Maneuver.TYPE_TURN_NORMAL_LEFT;
break;
case TURN_SHARP_LEFT:
maneuverType = Maneuver.TYPE_TURN_SHARP_LEFT;
break;
case TURN_SLIGHT_LEFT:
maneuverType = Maneuver.TYPE_TURN_SLIGHT_LEFT;
break;
case U_TURN_LEFT:
maneuverType = Maneuver.TYPE_U_TURN_LEFT;
break;
case U_TURN_RIGHT:
maneuverType = Maneuver.TYPE_U_TURN_RIGHT;
break;
// TODO (AndrewShkrob): add support for CW (clockwise) directions
case ENTER_ROUND_ABOUT:
case STAY_ON_ROUND_ABOUT:
case LEAVE_ROUND_ABOUT:
maneuverType = Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW;
break;
case START_AT_THE_END_OF_STREET:
maneuverType = Maneuver.TYPE_DEPART;
break;
case REACHED_YOUR_DESTINATION:
maneuverType = Maneuver.TYPE_DESTINATION;
break;
case EXIT_HIGHWAY_TO_LEFT:
maneuverType = Maneuver.TYPE_OFF_RAMP_SLIGHT_LEFT;
break;
case EXIT_HIGHWAY_TO_RIGHT:
maneuverType = Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT;
break;
}
final Maneuver.Builder builder = new Maneuver.Builder(maneuverType);
if (maneuverType == Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW)
builder.setRoundaboutExitNumber(roundaboutExitNum > 0 ? roundaboutExitNum : 1);
builder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(context, carDirection.getTurnRes())).build());
return builder.build();
}
}

View file

@ -1,112 +0,0 @@
package app.organicmaps.car.util;
import android.graphics.Bitmap;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarContext;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.DateTimeWithZone;
import androidx.car.app.navigation.model.Destination;
import androidx.car.app.navigation.model.Lane;
import androidx.car.app.navigation.model.Step;
import androidx.car.app.navigation.model.TravelEstimate;
import androidx.car.app.navigation.model.Trip;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.routing.RoutingInfo;
import app.organicmaps.routing.SingleLaneInfo;
import app.organicmaps.util.Graphics;
import java.util.Calendar;
import java.util.Objects;
public final class RoutingUtils
{
private RoutingUtils() {}
@NonNull
public static Trip createTrip(@NonNull final CarContext context, @Nullable final RoutingInfo info, @Nullable MapObject endPoint)
{
final Trip.Builder builder = new Trip.Builder();
if (info == null || !info.distToTarget.isValid() || !info.distToTurn.isValid())
{
builder.setLoading(true);
return builder.build();
}
builder.setCurrentRoad(info.currentStreet);
// Destination
final Destination.Builder destinationBuilder = new Destination.Builder();
if (endPoint != null)
{
destinationBuilder.setName(endPoint.getName());
destinationBuilder.setAddress(Objects.requireNonNullElse(endPoint.getAddress(), ""));
}
else
destinationBuilder.setName(" ");
builder.addDestination(destinationBuilder.build(), createTravelEstimate(info.distToTarget, info.totalTimeInSeconds));
// TODO (AndrewShkrob): Use real distance and time estimates
builder.addStep(createCurrentStep(context, info), createTravelEstimate(info.distToTurn, 0));
if (!TextUtils.isEmpty(info.nextStreet))
builder.addStep(createNextStep(context, info), createTravelEstimate(app.organicmaps.util.Distance.EMPTY, 0));
return builder.build();
}
@NonNull
private static Step createCurrentStep(@NonNull final CarContext context, @NonNull RoutingInfo info)
{
final Step.Builder builder = new Step.Builder();
builder.setCue(info.currentStreet);
builder.setRoad(info.currentStreet);
builder.setManeuver(RoutingHelpers.createManeuver(context, info.carDirection, info.exitNum));
if (info.lanes != null)
{
for (final SingleLaneInfo laneInfo : info.lanes)
{
final Lane.Builder laneBuilder = new Lane.Builder();
for (final SingleLaneInfo.LaneWay laneWay : laneInfo.mLane)
laneBuilder.addDirection(RoutingHelpers.createLaneDirection(laneWay, laneInfo.mIsActive));
builder.addLane(laneBuilder.build());
}
final LanesDrawable lanesDrawable = new LanesDrawable(context, info.lanes, ThemeUtils.isNightMode(context));
final Bitmap lanesBitmap = Graphics.drawableToBitmap(lanesDrawable);
builder.setLanesImage(new CarIcon.Builder(IconCompat.createWithBitmap(lanesBitmap)).build());
}
return builder.build();
}
@NonNull
private static Step createNextStep(@NonNull final CarContext context, @NonNull RoutingInfo info)
{
final Step.Builder builder = new Step.Builder();
builder.setCue(info.nextStreet);
builder.setManeuver(RoutingHelpers.createManeuver(context, info.nextCarDirection, 0));
return builder.build();
}
@NonNull
private static TravelEstimate createTravelEstimate(@NonNull app.organicmaps.util.Distance distance, int time)
{
final TravelEstimate.Builder builder = new TravelEstimate.Builder(RoutingHelpers.createDistance(distance), createTimeEstimate(time));
builder.setRemainingTimeSeconds(time);
builder.setRemainingDistanceColor(Colors.DISTANCE);
return builder.build();
}
@NonNull
private static DateTimeWithZone createTimeEstimate(int seconds)
{
final Calendar currentTime = Calendar.getInstance();
currentTime.add(Calendar.SECOND, seconds);
return DateTimeWithZone.create(currentTime.getTimeInMillis(), currentTime.getTimeZone());
}
}

View file

@ -1,113 +0,0 @@
package app.organicmaps.car.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import androidx.car.app.CarContext;
import app.organicmaps.Framework;
import app.organicmaps.R;
import app.organicmaps.routing.RoutingController;
public final class ThemeUtils
{
public enum ThemeMode
{
AUTO(R.string.auto, R.string.theme_auto),
LIGHT(R.string.off, R.string.theme_default),
NIGHT(R.string.on, R.string.theme_night);
ThemeMode(@StringRes int titleId, @StringRes int prefsKeyId)
{
mTitleId = titleId;
mPrefsKeyId = prefsKeyId;
}
@StringRes
public int getTitleId()
{
return mTitleId;
}
@StringRes
public int getPrefsKeyId()
{
return mPrefsKeyId;
}
@StringRes
private final int mTitleId;
@StringRes
private final int mPrefsKeyId;
}
private static final String ANDROID_AUTO_PREFERENCES_FILE_KEY = "ANDROID_AUTO_PREFERENCES_FILE_KEY";
private static final String THEME_KEY = "ANDROID_AUTO_THEME_MODE";
@UiThread
public static void update(@NonNull CarContext context)
{
final ThemeMode oldThemeMode = getThemeMode(context);
update(context, oldThemeMode);
}
@UiThread
public static void update(@NonNull CarContext context, @NonNull ThemeMode oldThemeMode)
{
final ThemeMode newThemeMode = oldThemeMode == ThemeMode.AUTO ? (context.isDarkMode() ? ThemeMode.NIGHT : ThemeMode.LIGHT) : oldThemeMode;
@Framework.MapStyle
int newMapStyle;
if (newThemeMode == ThemeMode.NIGHT)
newMapStyle = RoutingController.get().isVehicleNavigation() ? Framework.MAP_STYLE_VEHICLE_DARK : Framework.MAP_STYLE_DARK;
else
newMapStyle = RoutingController.get().isVehicleNavigation() ? Framework.MAP_STYLE_VEHICLE_CLEAR : Framework.MAP_STYLE_CLEAR;
if (Framework.nativeGetMapStyle() != newMapStyle)
Framework.nativeSetMapStyle(newMapStyle);
}
public static boolean isNightMode(@NonNull CarContext context)
{
final ThemeMode themeMode = getThemeMode(context);
return themeMode == ThemeMode.NIGHT || (themeMode == ThemeMode.AUTO && context.isDarkMode());
}
@SuppressLint("ApplySharedPref")
@UiThread
public static void setThemeMode(@NonNull CarContext context, @NonNull ThemeMode themeMode)
{
getSharedPreferences(context).edit().putString(THEME_KEY, context.getString(themeMode.getPrefsKeyId())).commit();
update(context, themeMode);
}
@NonNull
public static ThemeMode getThemeMode(@NonNull CarContext context)
{
final String autoTheme = context.getString(R.string.theme_auto);
final String lightTheme = context.getString(R.string.theme_default);
final String nightTheme = context.getString(R.string.theme_night);
final String themeMode = getSharedPreferences(context).getString(THEME_KEY, autoTheme);
if (themeMode.equals(autoTheme))
return ThemeMode.AUTO;
else if (themeMode.equals(lightTheme))
return ThemeMode.LIGHT;
else if (themeMode.equals(nightTheme))
return ThemeMode.NIGHT;
else
throw new IllegalArgumentException("Unsupported value");
}
@NonNull
private static SharedPreferences getSharedPreferences(@NonNull CarContext context)
{
return context.getSharedPreferences(ANDROID_AUTO_PREFERENCES_FILE_KEY, Context.MODE_PRIVATE);
}
private ThemeUtils() {}
}

View file

@ -1,193 +0,0 @@
package app.organicmaps.car.util;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarContext;
import androidx.car.app.OnScreenResultListener;
import androidx.car.app.Screen;
import androidx.car.app.model.Action;
import androidx.car.app.model.ActionStrip;
import androidx.car.app.model.CarColor;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Row;
import androidx.car.app.navigation.model.MapController;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.Map;
import app.organicmaps.R;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.bookmarks.data.Metadata;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.settings.SettingsScreen;
import app.organicmaps.editor.OpeningHours;
import app.organicmaps.editor.data.Timetable;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.location.LocationState;
import app.organicmaps.util.LocationUtils;
import app.organicmaps.util.Utils;
import java.util.Calendar;
public final class UiHelpers
{
@NonNull
public static ActionStrip createSettingsActionStrip(@NonNull BaseMapScreen mapScreen, @NonNull SurfaceRenderer surfaceRenderer)
{
return new ActionStrip.Builder().addAction(createSettingsAction(mapScreen, surfaceRenderer)).build();
}
@NonNull
public static ActionStrip createMapActionStrip(@NonNull CarContext context, @NonNull SurfaceRenderer surfaceRenderer)
{
final CarIcon iconPlus = new CarIcon.Builder(IconCompat.createWithResource(context, R.drawable.ic_plus)).build();
final CarIcon iconMinus = new CarIcon.Builder(IconCompat.createWithResource(context, R.drawable.ic_minus)).build();
final Action panAction = new Action.Builder(Action.PAN).build();
final Action location = createLocationButton(context);
final Action zoomIn = new Action.Builder().setIcon(iconPlus).setOnClickListener(surfaceRenderer::onZoomIn).build();
final Action zoomOut = new Action.Builder().setIcon(iconMinus).setOnClickListener(surfaceRenderer::onZoomOut).build();
return new ActionStrip.Builder()
.addAction(panAction)
.addAction(zoomIn)
.addAction(zoomOut)
.addAction(location)
.build();
}
@NonNull
public static MapController createMapController(@NonNull CarContext context, @NonNull SurfaceRenderer surfaceRenderer)
{
return new MapController.Builder().setMapActionStrip(createMapActionStrip(context, surfaceRenderer)).build();
}
@NonNull
public static Action createSettingsAction(@NonNull BaseMapScreen mapScreen, @NonNull SurfaceRenderer surfaceRenderer)
{
return createSettingsAction(mapScreen, surfaceRenderer, null);
}
@NonNull
public static Action createSettingsActionForResult(@NonNull BaseMapScreen mapScreen, @NonNull SurfaceRenderer surfaceRenderer, @NonNull OnScreenResultListener onScreenResultListener)
{
return createSettingsAction(mapScreen, surfaceRenderer, onScreenResultListener);
}
@NonNull
private static Action createSettingsAction(@NonNull BaseMapScreen mapScreen, @NonNull SurfaceRenderer surfaceRenderer, @Nullable OnScreenResultListener onScreenResultListener)
{
final CarContext context = mapScreen.getCarContext();
final CarIcon iconSettings = new CarIcon.Builder(IconCompat.createWithResource(context, R.drawable.ic_settings)).build();
return new Action.Builder().setIcon(iconSettings).setOnClickListener(() -> {
// Action.onClickListener for the Screen A maybe called even if the Screen B is shown now.
// We need to check it
// This may happen when we use PopToRootHack:
// * ScreenManager.popToRoot()
// * The root screen (A) is shown for a while
// * User clicks on some action
// * ScreenManager.push(new Screen())
// * New screen (B) is displayed now
// * Action.onClickListener is called for action from root screen (A)
if (mapScreen.getScreenManager().getTop() != mapScreen)
return;
final Screen settingsScreen = new SettingsScreen(context, surfaceRenderer);
if (onScreenResultListener != null)
mapScreen.getScreenManager().pushForResult(settingsScreen, onScreenResultListener);
else
mapScreen.getScreenManager().push(settingsScreen);
}).build();
}
@Nullable
public static Row getPlaceOpeningHoursRow(@NonNull MapObject place, @NonNull CarContext context)
{
final String ohStr = place.getMetadata(Metadata.MetadataType.FMD_OPEN_HOURS);
final Timetable[] timetables = OpeningHours.nativeTimetablesFromString(ohStr);
final boolean isEmptyTT = (timetables == null || timetables.length == 0);
if (ohStr.isEmpty() && isEmptyTT)
return null;
final Row.Builder builder = new Row.Builder();
builder.setImage(new CarIcon.Builder(IconCompat.createWithResource(context, R.drawable.ic_operating_hours)).build());
if (isEmptyTT)
builder.setTitle(ohStr);
else if (timetables[0].isFullWeek())
{
if (timetables[0].isFullday)
builder.setTitle(context.getString(R.string.twentyfour_seven));
else
builder.setTitle(timetables[0].workingTimespan.toWideString());
}
else
{
boolean containsCurrentWeekday = false;
final int currentDay = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
for (final Timetable tt : timetables)
{
if (tt.containsWeekday(currentDay))
{
containsCurrentWeekday = true;
String openTime;
if (tt.isFullday)
openTime = Utils.unCapitalize(context.getString(R.string.editor_time_allday));
else
openTime = tt.workingTimespan.toWideString();
builder.setTitle(openTime);
break;
}
}
// Show that place is closed today.
if (!containsCurrentWeekday)
builder.setTitle(context.getString(R.string.day_off_today));
}
return builder.build();
}
@NonNull
private static Action createLocationButton(@NonNull CarContext context)
{
final Action.Builder builder = new Action.Builder();
final int locationMode = Map.isEngineCreated() ? LocationState.getMode() : LocationState.NOT_FOLLOW_NO_POSITION;
CarColor tintColor = Colors.DEFAULT;
@DrawableRes int drawableRes;
switch (locationMode)
{
case LocationState.PENDING_POSITION:
case LocationState.NOT_FOLLOW_NO_POSITION:
drawableRes = R.drawable.ic_location_off;
break;
case LocationState.NOT_FOLLOW:
drawableRes = R.drawable.ic_not_follow;
break;
case LocationState.FOLLOW:
drawableRes = R.drawable.ic_follow;
tintColor = Colors.LOCATION_TINT;
break;
case LocationState.FOLLOW_AND_ROTATE:
drawableRes = R.drawable.ic_follow_and_rotate;
tintColor = Colors.LOCATION_TINT;
break;
default:
throw new IllegalArgumentException("Invalid button mode: " + locationMode);
}
final CarIcon icon = new CarIcon.Builder(IconCompat.createWithResource(context, drawableRes)).setTint(tintColor).build();
builder.setIcon(icon);
builder.setOnClickListener(() -> {
LocationState.nativeSwitchToNextMode();
final LocationHelper locationHelper = LocationHelper.from(context);
if (!locationHelper.isActive() && LocationUtils.checkFineLocationPermission(context))
locationHelper.start();
});
return builder.build();
}
}