WIP: [android-auto] Route Planning and other screens #4936

Closed
AndrewShkrob wants to merge 7 commits from android-auto/routing into aa
64 changed files with 3519 additions and 248 deletions

View file

@ -44,6 +44,8 @@
//-->
<uses-permission 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>
@ -719,6 +721,15 @@
<activity
android:name="app.organicmaps.settings.DrivingOptionsActivity"
android:label="@string/driving_options_title"/>
<service
android:name="app.organicmaps.car.NavigationCarAppService"
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>
<!-- Catches app upgraded intent -->
<receiver
@ -744,5 +755,13 @@
<!-- Disable Google's anonymous stats collection -->
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
<!-- For android Auto -->
<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="1"/>
</application>
</manifest>

View file

@ -82,6 +82,9 @@ dependencies {
implementation 'com.google.firebase:firebase-crashlytics-ndk'
}
implementation 'androidx.car.app:app:1.4.0-alpha01'
// Fix for app/organicmaps/util/FileUploadWorker.java:14: error: cannot access ListenableFuture
implementation 'com.google.guava:guava:29.0-android'
// This line is added as a workaround for duplicate classes error caused by some outdated dependency:
// > A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable
// We don't use Kotlin, but some dependencies are actively using it.

View file

@ -1,4 +1,4 @@
propMinSdkVersion=21
propMinSdkVersion=23
propTargetSdkVersion=33
propCompileSdkVersion=33
propBuildToolsVersion=33.0.2

View file

@ -258,6 +258,12 @@ bool Framework::IsDrapeEngineCreated() const
return m_work.IsDrapeEngineCreated();
}
void Framework::UpdateDpi(int dpi)
{
ASSERT_GREATER(dpi, 0, ());
m_work.UpdateVisualScale(dp::VisualScale(dpi));
}
void Framework::Resize(JNIEnv * env, jobject jSurface, int w, int h)
{
if (m_vulkanContextFactory)
@ -477,9 +483,9 @@ void Framework::Scale(double factor, m2::PointD const & pxPoint, bool isAnim)
m_work.Scale(factor, pxPoint, isAnim);
}
void Framework::Move(double factorX, double factorY, bool isAnim)
void Framework::Scroll(double distanceX, double distanceY)
{
m_work.Move(factorX, factorY, isAnim);
m_work.Scroll(distanceX, distanceY);
}
void Framework::Touch(int action, Finger const & f1, Finger const & f2, uint8_t maskedPointer)

View file

@ -92,6 +92,7 @@ namespace android
bool CreateDrapeEngine(JNIEnv * env, jobject jSurface, int densityDpi, bool firstLaunch,
bool launchByDeepLink, uint32_t appVersionCode);
bool IsDrapeEngineCreated() const;
void UpdateDpi(int dpi);
bool DestroySurfaceOnDetach();
void DetachSurface(bool destroySurface);
bool AttachSurface(JNIEnv * env, jobject jSurface);
@ -129,7 +130,7 @@ namespace android
void Scale(double factor, m2::PointD const & pxPoint, bool isAnim);
void Move(double factorX, double factorY, bool isAnim);
void Scroll(double distanceX, double distanceY);
void Touch(int action, Finger const & f1, Finger const & f2, uint8_t maskedPointer);

View file

@ -39,6 +39,12 @@ Java_app_organicmaps_Map_nativeIsEngineCreated(JNIEnv *, jclass)
return g_framework->IsDrapeEngineCreated();
}
JNIEXPORT void JNICALL
Java_app_organicmaps_Map_nativeUpdateEngineDpi(JNIEnv *, jclass, jint dpi)
{
return g_framework->UpdateDpi(dpi);
}
JNIEXPORT jboolean JNICALL
Java_app_organicmaps_Map_nativeShowMapForUrl(JNIEnv * env, jclass, jstring url)
{
@ -130,13 +136,6 @@ Java_app_organicmaps_Map_nativeCompassUpdated(JNIEnv *, jclass, jdouble north, j
g_framework->OnCompassUpdated(info, forceRedraw);
}
JNIEXPORT void JNICALL
Java_app_organicmaps_Map_nativeMove(
JNIEnv *, jclass, jdouble factorX, jdouble factorY, jboolean isAnim)
{
g_framework->Move(factorX, factorY, isAnim);
}
JNIEXPORT void JNICALL
Java_app_organicmaps_Map_nativeScalePlus(JNIEnv *, jclass)
{
@ -150,7 +149,14 @@ Java_app_organicmaps_Map_nativeScaleMinus(JNIEnv *, jclass)
}
JNIEXPORT void JNICALL
Java_app_organicmaps_Map_nativeScale(
Java_app_organicmaps_Map_nativeOnScroll(
JNIEnv *, jclass, jdouble distanceX, jdouble distanceY)
{
g_framework->Scroll(distanceX, distanceY);
}
JNIEXPORT void JNICALL
Java_app_organicmaps_Map_nativeOnScale(
JNIEnv *, jclass, jdouble factor, jdouble focusX, jdouble focusY, jboolean isAnim)
{
g_framework->Scale(factor, {focusX, focusY}, isAnim);

View file

@ -287,7 +287,7 @@ extern "C"
JNIEXPORT void JNICALL Java_app_organicmaps_search_SearchEngine_nativeRunInteractiveSearch(
JNIEnv * env, jclass clazz, jbyteArray bytes, jboolean isCategory,
jstring lang, jlong timestamp, jboolean isMapAndTable)
jstring lang, jlong timestamp, jboolean isMapAndTable, jboolean hasPosition, jdouble lat, jdouble lon)
{
search::ViewportSearchParams vparams{
jni::ToNativeString(env, bytes),
@ -295,7 +295,7 @@ extern "C"
{}, // Default timeout
static_cast<bool>(isCategory),
{}, // Empty m_onStarted callback
{}, // Empty m_onCompleted callback
bind(&OnResults, _1, std::vector<search::ProductInfo>{}, timestamp, isMapAndTable, hasPosition, lat, lon), // Empty m_onCompleted callback
};
// TODO (@alexzatsepin): set up vparams.m_onCompleted here and use
@ -303,20 +303,19 @@ extern "C"
// Don't move vparams here, because it's used below.
g_framework->NativeFramework()->GetSearchAPI().SearchInViewport(vparams);
if (isMapAndTable)
{
search::EverywhereSearchParams eparams{
std::move(vparams.m_query),
std::move(vparams.m_inputLocale),
{}, // default timeout
static_cast<bool>(isCategory),
bind(&OnResults, _1, _2, timestamp, isMapAndTable,
false /* hasPosition */, 0.0 /* lat */, 0.0 /* lon */)
};
if (g_framework->NativeFramework()->GetSearchAPI().SearchEverywhere(std::move(eparams)))
g_queryTimestamp = timestamp;
}
// if (isMapAndTable)
// {
// search::EverywhereSearchParams eparams{
// std::move(vparams.m_query),
// std::move(vparams.m_inputLocale),
// {}, // default timeout
// static_cast<bool>(isCategory),
// bind(&OnResults, _1, _2, timestamp, isMapAndTable, hasPosition, lat, lon)
// };
//
// if (g_framework->NativeFramework()->GetSearchAPI().SearchEverywhere(std::move(eparams)))
// g_queryTimestamp = timestamp;
// }
}
JNIEXPORT void JNICALL Java_app_organicmaps_search_SearchEngine_nativeRunSearchMaps(

View file

@ -0,0 +1,23 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<size
android:width="120dp"
android:height="120dp" />
<solid android:color="@color/base_accent" />
<corners android:radius="16dp" />
</shape>
</item>
<item>
<vector
android:width="15dp"
android:height="15dp"
android:viewportWidth="15"
android:viewportHeight="15">
<path
android:pathData="M13.84,6.852,12.6,5.7,11.5,3.5a1.05,1.05,0,0,0-.9-.5H4.4a1.05,1.05,0,0,0-.9.5L2.4,5.7,1.16,6.852A.5.5,0,0,0,1,7.219V11.5a.5.5,0,0,0,.5.5h2c.2,0,.5-.2.5-.4V11h7v.5c0,.2.2.5.4.5h2.1a.5.5,0,0,0,.5-.5V7.219A.5.5,0,0,0,13.84,6.852ZM4.5,4h6l1,2h-8ZM5,8.6c0,.2-.3.4-.5.4H2.4C2.2,9,2,8.7,2,8.5V7.4c.1-.3.3-.5.6-.4l2,.4c.2,0,.4.3.4.5Zm8-.1c0,.2-.2.5-.4.5H10.5c-.2,0-.5-.2-.5-.4V7.9c0-.2.2-.5.4-.5l2-.4c.3-.1.5.1.6.4Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>
</item>
</layer-list>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M16,1L8,1C6.34,1 5,2.34 5,4v16c0,1.66 1.34,3 3,3h8c1.66,0 3,-1.34 3,-3L19,4c0,-1.66 -1.34,-3 -3,-3zM14,21h-4v-1h4v1zM17.25,18L6.75,18L6.75,4h10.5v14z"/>
</vector>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?windowBackgroundForced"
android:gravity="center"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:src="@drawable/ic_car_connected" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:layout_marginStart="32dp"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/aa_connected"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/aa_used_on_car_screen"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" />
</LinearLayout>
<Button
android:id="@+id/btn_continue"
style="@style/MwmWidget.Button.Accent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:text="@string/aa_continue_on_phone" />
</LinearLayout>

View file

@ -742,6 +742,16 @@
<string name="translated_om_site_url">https://organicmaps.app/ru/</string>
<!-- Link to OSM wiki for Editor, Profile and About pages -->
<string name="osm_wiki_about_url">https://wiki.openstreetmap.org/wiki/RU:О_проекте</string>
<!-- Text on car placeholder screen that maps are shown on the phone screen -->
<string name="aa_used_on_phone_screen">Сейчас Вы используете Organic Maps на экране телефона</string>
<!-- Text on phone placeholder screen that maps are shown on the car screen -->
<string name="aa_used_on_car_screen">Сейчас Вы используете Organic Maps на экране автомобиля</string>
<!-- Android Auto connected -->
<string name="aa_connected">Вы подключены к Android Auto</string>
<!-- Button to show maps on the phone screen instead of a car -->
<string name="aa_continue_on_phone">Продолжить в телефоне</string>
<!-- Button to show maps on the car screen instead of a phone. Must be no more than 18 symbols! -->
<string name="aa_continue_in_car">Продолжить в авто</string>
<!-- SECTION: Types -->
<string name="type.aerialway">Канатная дорога</string>

View file

@ -762,6 +762,16 @@
<string name="translated_om_site_url">https://organicmaps.app/</string>
<!-- Link to OSM wiki for Editor, Profile and About pages -->
<string name="osm_wiki_about_url">https://wiki.openstreetmap.org/wiki/About_OpenStreetMap</string>
<!-- Text on car placeholder screen that maps are shown on the phone screen -->
<string name="aa_used_on_phone_screen">You are now using Organic Maps on the phone screen</string>
<!-- Text on phone placeholder screen that maps are shown on the car screen -->
<string name="aa_used_on_car_screen">You are now using Organic Maps on the car screen</string>
<!-- Android Auto connected -->
<string name="aa_connected">You are connected to Android Auto</string>
<!-- Button to show maps on the phone screen instead of a car -->
<string name="aa_continue_on_phone">Continue on phone</string>
<!-- Button to show maps on the car screen instead of a phone. Must be no more than 18 symbols! -->
<string name="aa_continue_in_car">Continue in car</string>
<!-- SECTION: Types -->
<string name="type.aerialway">Aerialway</string>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
<uses name="template" />
</automotiveApp>

View file

@ -7,6 +7,7 @@ import android.view.Surface;
import androidx.annotation.Nullable;
import app.organicmaps.display.DisplayType;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.util.Config;
import app.organicmaps.util.UiUtils;
@ -50,6 +51,8 @@ public final class Map
public static final int INVALID_POINTER_MASK = 0xFF;
public static final int INVALID_TOUCH_ID = -1;
private final DisplayType mDisplayType;
private int mCurrentCompassOffsetX;
private int mCurrentCompassOffsetY;
private int mBottomWidgetOffsetX;
@ -68,8 +71,11 @@ public final class Map
@Nullable
private CallbackUnsupported mCallbackUnsupported;
public Map()
private static int currentDpi = 0;
public Map(DisplayType mapType)
{
mDisplayType = mapType;
onCreate(false);
}
@ -139,6 +145,12 @@ public final class Map
Logger.d(TAG, "mSurfaceCreated = " + mSurfaceCreated);
if (nativeIsEngineCreated())
{
if (currentDpi != surfaceDpi)
{
currentDpi = surfaceDpi;
nativeUpdateEngineDpi(currentDpi);
setupWidgets(context, surfaceFrame.width(), surfaceFrame.height());
}
if (!nativeAttachSurface(surface))
{
if (mCallbackUnsupported != null)
@ -255,9 +267,9 @@ public final class Map
return mSurfaceCreated;
}
public void onScroll(float distanceX, float distanceY)
public void onScroll(double distanceX, double distanceY)
{
Map.nativeMove(-distanceX / ((float) mWidth), distanceY / ((float) mHeight), false);
Map.nativeOnScroll(distanceX, distanceY);
}
public static void zoomIn()
@ -272,7 +284,7 @@ public final class Map
public static void onScale(double factor, double focusX, double focusY, boolean isAnim)
{
nativeScale(factor, focusX, focusY, isAnim);
nativeOnScale(factor, focusX, focusY, isAnim);
}
public static void onTouch(int actionType, MotionEvent event, int pointerIndex)
@ -289,9 +301,10 @@ public final class Map
}
}
public static void onTouch(float x, float y)
public static void onClick(float x, float y)
{
nativeOnTouch(Map.NATIVE_ACTION_UP, 0, x, y, Map.INVALID_TOUCH_ID, 0, 0, 0);
nativeOnTouch(NATIVE_ACTION_DOWN, 0, x, y, Map.INVALID_TOUCH_ID, 0, 0, 0);
nativeOnTouch(NATIVE_ACTION_UP, 0, x, y, Map.INVALID_TOUCH_ID, 0, 0, 0);
}
public static boolean isEngineCreated()
@ -312,7 +325,9 @@ public final class Map
nativeCleanWidgets();
updateBottomWidgetsOffset(context, mBottomWidgetOffsetX, mBottomWidgetOffsetY);
nativeSetupWidget(WIDGET_SCALE_FPS_LABEL, UiUtils.dimen(context, R.dimen.margin_base), UiUtils.dimen(context, R.dimen.margin_base), ANCHOR_LEFT_TOP);
updateCompassOffset(context, mCurrentCompassOffsetX, mCurrentCompassOffsetY, false);
// Don't show compass on car display
if (mDisplayType == DisplayType.Device)
updateCompassOffset(context, mCurrentCompassOffsetX, mCurrentCompassOffsetY, false);
}
private void updateRulerOffset(final Context context, int offsetX, int offsetY)
@ -346,6 +361,7 @@ public final class Map
boolean isLaunchByDeepLink,
int appVersionCode);
private static native boolean nativeIsEngineCreated();
private static native void nativeUpdateEngineDpi(int dpi);
private static native void nativeSetRenderingInitializationFinishedListener(
@Nullable MapRenderingListener listener);
private static native boolean nativeShowMapForUrl(String url);
@ -366,9 +382,9 @@ public final class Map
private static native void nativeCompassUpdated(double north, boolean forceRedraw);
// Events
private static native void nativeMove(double factorX, double factorY, boolean isAnim);
private static native void nativeScalePlus();
private static native void nativeScaleMinus();
private static native void nativeScale(double factor, double focusX, double focusY, boolean isAnim);
private static native void nativeOnScroll(double distanceX, double distanceY);
private static native void nativeOnScale(double factor, double focusX, double focusY, boolean isAnim);
private static native void nativeOnTouch(int actionType, int id1, float x1, float y1, int id2, float x2, float y2, int maskedPointer);
}

View file

@ -15,13 +15,14 @@ import androidx.annotation.Nullable;
import androidx.core.content.res.ConfigurationHelper;
import app.organicmaps.base.BaseMwmFragment;
import app.organicmaps.display.DisplayType;
import app.organicmaps.util.log.Logger;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class MapFragment extends BaseMwmFragment implements View.OnTouchListener, SurfaceHolder.Callback
{
private static final String TAG = MapFragment.class.getSimpleName();
private final Map mMap = new Map();
private final Map mMap = new Map(DisplayType.Device);
public void updateCompassOffset(int offsetX, int offsetY)
{
@ -51,6 +52,7 @@ public class MapFragment extends BaseMwmFragment implements View.OnTouchListener
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder)
{
Logger.d(TAG);
int densityDpi;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
@ -64,19 +66,21 @@ public class MapFragment extends BaseMwmFragment implements View.OnTouchListener
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height)
{
Logger.d(TAG);
mMap.onSurfaceChanged(requireContext(), surfaceHolder.getSurface(), surfaceHolder.getSurfaceFrame(), surfaceHolder.isCreating());
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder)
{
Logger.d(TAG, "surfaceDestroyed");
destroySurface();
Logger.d(TAG);
mMap.onSurfaceDestroyed(requireActivity().isChangingConfigurations(), true);
}
@Override
public void onAttach(Context context)
{
Logger.d(TAG);
super.onAttach(context);
mMap.setMapRenderingListener((MapRenderingListener) context);
mMap.setCallbackUnsupported(this::reportUnsupported);
@ -85,6 +89,7 @@ public class MapFragment extends BaseMwmFragment implements View.OnTouchListener
@Override
public void onDetach()
{
Logger.d(TAG);
super.onDetach();
mMap.setMapRenderingListener(null);
mMap.setCallbackUnsupported(null);
@ -93,6 +98,7 @@ public class MapFragment extends BaseMwmFragment implements View.OnTouchListener
@Override
public void onCreate(Bundle b)
{
Logger.d(TAG);
super.onCreate(b);
setRetainInstance(true);
boolean launchByDeepLink = false;
@ -105,28 +111,31 @@ public class MapFragment extends BaseMwmFragment implements View.OnTouchListener
@Override
public void onStart()
{
Logger.d(TAG);
super.onStart();
mMap.onStart();
Logger.d(TAG, "onStart");
}
@Override
public void onStop()
{
Logger.d(TAG);
super.onStop();
mMap.onStop();
Logger.d(TAG, "onStop");
}
@Override
public void onPause()
{
mMap.onPause(requireContext());
Logger.d(TAG);
super.onPause();
mMap.onPause(requireContext());
}
@Override
public void onResume()
{
Logger.d(TAG);
super.onResume();
mMap.onResume();
}
@ -134,7 +143,8 @@ public class MapFragment extends BaseMwmFragment implements View.OnTouchListener
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_map, container, false);
Logger.d(TAG);
final View view = inflater.inflate(R.layout.fragment_map, container, false);
final SurfaceView mSurfaceView = view.findViewById(R.id.map_surfaceview);
mSurfaceView.getHolder().addCallback(this);
return view;

View file

@ -0,0 +1,25 @@
package app.organicmaps;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.base.BaseMwmFragment;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.display.DisplayType;
public class MapPlaceholderFragment extends BaseMwmFragment
{
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
{
final View view = inflater.inflate(R.layout.fragment_map_placeholder, container, false);
view.findViewById(R.id.btn_continue).setOnClickListener((var x) -> DisplayManager.from(requireContext()).changeDisplay(DisplayType.Device));
return view;
}
}

View file

@ -24,9 +24,11 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentFactory;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import app.organicmaps.Framework.PlacePageActivationListener;
import app.organicmaps.api.Const;
import app.organicmaps.background.Notifier;
@ -39,6 +41,8 @@ import app.organicmaps.bookmarks.data.BookmarkInfo;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.bookmarks.data.Track;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.display.DisplayType;
import app.organicmaps.downloader.DownloaderActivity;
import app.organicmaps.downloader.DownloaderFragment;
import app.organicmaps.downloader.MapManager;
@ -197,6 +201,9 @@ public class MwmActivity extends BaseMwmFragmentActivity
@Nullable
private WindowInsetsCompat mCurrentWindowInsets;
@NonNull
private DisplayManager mDisplayManager;
public interface LeftAnimationTrackListener
{
void onTrackStarted(boolean collapsed);
@ -287,7 +294,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
public void showHelp()
{
Intent intent = new Intent(requireActivity(), HelpActivity.class);
Intent intent = new Intent(this, HelpActivity.class);
startActivity(intent);
}
@ -363,6 +370,32 @@ public class MwmActivity extends BaseMwmFragmentActivity
return super.getThemeResourceId(theme);
}
private void replaceMapWithPlaceholder()
{
disableControls();
if (mMapFragment != null)
{
getSupportFragmentManager().beginTransaction().remove(mMapFragment).commitNowAllowingStateLoss();
mMapFragment = null;
}
getSupportFragmentManager().beginTransaction().replace(R.id.map_fragment_container, new MapPlaceholderFragment()).commitNowAllowingStateLoss();
}
private void replacePlaceholderWithMap()
{
initMap(false /* isLaunchByDeepLink */, true /* force */);
enableControls();
}
private void mapDisplayChanged(@NonNull final DisplayType newDisplayType)
{
Logger.d(TAG);
if (newDisplayType == DisplayType.Device)
replacePlaceholderWithMap();
else
replaceMapWithPlaceholder();
}
@SuppressLint("InlinedApi")
@CallSuper
@Override
@ -393,6 +426,10 @@ public class MwmActivity extends BaseMwmFragmentActivity
mSearchController.getToolbar()
.getViewTreeObserver();
mDisplayManager = DisplayManager.from(this);
mDisplayManager.addListener(DisplayType.Device, this::mapDisplayChanged);
getLifecycle().addObserver(mDisplayManager.getObserverFor(DisplayType.Device));
boolean isLaunchByDeepLink = getIntent().getBooleanExtra(EXTRA_LAUNCH_BY_DEEP_LINK, false);
initViews(isLaunchByDeepLink);
updateViewsInsets();
@ -456,7 +493,8 @@ public class MwmActivity extends BaseMwmFragmentActivity
private void initViews(boolean isLaunchByDeeplink)
{
initMap(isLaunchByDeeplink);
if (mDisplayManager.isDeviceDisplayUsed())
initMap(isLaunchByDeeplink, false /* force */);
initNavigationButtons();
if (!mIsTabletLayout)
@ -581,7 +619,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
refreshLightStatusBar();
}
private void initMap(boolean isLaunchByDeepLink)
private void initMap(boolean isLaunchByDeepLink, boolean force)
{
final FragmentManager manager = getSupportFragmentManager();
mMapFragment = (MapFragment) manager.findFragmentByTag(MapFragment.class.getName());
@ -592,10 +630,20 @@ public class MwmActivity extends BaseMwmFragmentActivity
final FragmentFactory factory = manager.getFragmentFactory();
mMapFragment = (MapFragment) factory.instantiate(getClassLoader(), MapFragment.class.getName());
mMapFragment.setArguments(args);
manager
.beginTransaction()
.replace(R.id.map_fragment_container, mMapFragment, MapFragment.class.getName())
.commit();
if (force)
{
manager
.beginTransaction()
.replace(R.id.map_fragment_container, mMapFragment, MapFragment.class.getName())
.commitNowAllowingStateLoss();
}
else
{
manager
.beginTransaction()
.replace(R.id.map_fragment_container, mMapFragment, MapFragment.class.getName())
.commit();
}
}
View container = findViewById(R.id.map_fragment_container);
@ -613,10 +661,10 @@ public class MwmActivity extends BaseMwmFragmentActivity
private void initNavigationButtons()
{
initNavigationButtons(mCurrentLayoutMode);
initNavigationButtons(mCurrentLayoutMode, false /* force */);
}
private void initNavigationButtons(MapButtonsController.LayoutMode layoutMode)
private void initNavigationButtons(MapButtonsController.LayoutMode layoutMode, boolean force)
{
if (mMapButtonsController == null || mMapButtonsController.getLayoutMode() != layoutMode)
{
@ -634,7 +682,10 @@ public class MwmActivity extends BaseMwmFragmentActivity
FragmentTransaction transaction = getSupportFragmentManager()
.beginTransaction().replace(R.id.map_buttons, mMapButtonsController);
transaction.commit();
if (force)
transaction.commitNowAllowingStateLoss();
else
transaction.commit();
}
}
@ -1036,14 +1087,18 @@ public class MwmActivity extends BaseMwmFragmentActivity
protected void onStart()
{
super.onStart();
Framework.nativePlacePageActivationListener(this);
BookmarkManager.INSTANCE.addLoadingListener(this);
RoutingController.get().attach(this);
IsolinesManager.from(getApplicationContext()).attach(this::onIsolinesStateChanged);
if (mDisplayManager.isDeviceDisplayUsed())
{
Logger.d(TAG, "Activate");
RoutingController.get().attach(this);
Framework.nativePlacePageActivationListener(this);
LocationState.nativeSetListener(this);
onMyPositionModeChanged(LocationState.nativeGetMode());
}
LocationHelper.INSTANCE.attach(this);
LocationState.nativeSetListener(this);
LocationHelper.INSTANCE.addListener(this);
onMyPositionModeChanged(LocationState.nativeGetMode());
mSearchController.attach(this);
if (!Config.isScreenSleepEnabled())
Utils.keepScreenOn(true, getWindow());
@ -1053,12 +1108,16 @@ public class MwmActivity extends BaseMwmFragmentActivity
protected void onStop()
{
super.onStop();
Framework.nativeRemovePlacePageActivationListener();
BookmarkManager.INSTANCE.removeLoadingListener(this);
LocationHelper.INSTANCE.removeListener(this);
LocationState.nativeRemoveListener();
if (mDisplayManager.isDeviceDisplayUsed())
{
Logger.d(TAG, "Deactivate");
Framework.nativeRemovePlacePageActivationListener();
LocationState.nativeRemoveListener();
RoutingController.get().detach();
}
LocationHelper.INSTANCE.detach();
RoutingController.get().detach();
IsolinesManager.from(getApplicationContext()).detach();
mSearchController.detach();
Utils.keepScreenOn(false, getWindow());
@ -1077,9 +1136,10 @@ public class MwmActivity extends BaseMwmFragmentActivity
@Override
public void onBackPressed()
{
RoutingController routingController = RoutingController.get();
if (!closeBottomSheet(MAIN_MENU_ID) && !closeBottomSheet(LAYERS_MENU_ID) && !collapseNavMenu() &&
!closePlacePage() &&!closeSearchToolbar(true, true) &&
final boolean isCarDisplayUsed = DisplayManager.from(this).isCarDisplayUsed();
final RoutingController routingController = RoutingController.get();
if (!isCarDisplayUsed && !closeBottomSheet(MAIN_MENU_ID) && !closeBottomSheet(LAYERS_MENU_ID) &&
!collapseNavMenu() && !closePlacePage() && !closeSearchToolbar(true, true) &&
!closeSidePanel() && !closePositionChooser() &&
!routingController.resetToPlanningStateIfNavigating() && !routingController.cancel())
{
@ -1247,13 +1307,6 @@ public class MwmActivity extends BaseMwmFragmentActivity
mMapFragment.updateMyPositionRoutingOffset(offsetY);
}
@Override
@NonNull
public AppCompatActivity requireActivity()
{
return this;
}
@Override
public void showSearch()
{
@ -1508,7 +1561,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
mRoutingPlanInplaceController.hideDrivingOptionsView();
mNavigationController.stop(this);
initNavigationButtons(MapButtonsController.LayoutMode.regular);
initNavigationButtons(MapButtonsController.LayoutMode.regular, false /* force */);
refreshLightStatusBar();
}
@ -1518,7 +1571,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
closeFloatingToolbarsAndPanels(true);
ThemeSwitcher.INSTANCE.restart(isMapRendererActive());
mNavigationController.start(this);
initNavigationButtons(MapButtonsController.LayoutMode.navigation);
initNavigationButtons(MapButtonsController.LayoutMode.navigation, false /* force */);
refreshLightStatusBar();
}
@ -1526,7 +1579,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
public void onPlanningCancelled()
{
closeFloatingToolbarsAndPanels(true);
initNavigationButtons(MapButtonsController.LayoutMode.regular);
initNavigationButtons(MapButtonsController.LayoutMode.regular, false /* force */);
refreshLightStatusBar();
}
@ -1534,7 +1587,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
public void onPlanningStarted()
{
closeFloatingToolbarsAndPanels(true);
initNavigationButtons(MapButtonsController.LayoutMode.planning);
initNavigationButtons(MapButtonsController.LayoutMode.planning, false /* force */);
refreshLightStatusBar();
}
@ -1544,7 +1597,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
closeFloatingToolbarsAndPanels(true);
ThemeSwitcher.INSTANCE.restart(isMapRendererActive());
mNavigationController.stop(this);
initNavigationButtons(MapButtonsController.LayoutMode.planning);
initNavigationButtons(MapButtonsController.LayoutMode.planning, false /* force */);
refreshLightStatusBar();
}
@ -1603,6 +1656,52 @@ public class MwmActivity extends BaseMwmFragmentActivity
.show();
}
@Override
public void onShowDisclaimer()
{
StringBuilder builder = new StringBuilder();
for (int resId : new int[] { R.string.dialog_routing_disclaimer_priority, R.string.dialog_routing_disclaimer_precision,
R.string.dialog_routing_disclaimer_recommendations, R.string.dialog_routing_disclaimer_borders,
R.string.dialog_routing_disclaimer_beware })
builder.append(MwmApplication.from(this).getString(resId)).append("\n\n");
new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.dialog_routing_disclaimer_title)
.setMessage(builder.toString())
.setCancelable(false)
.setNegativeButton(R.string.decline, null)
.setPositiveButton(R.string.accept, (dlg, which) -> {
Config.acceptRoutingDisclaimer();
RoutingController.get().prepare();
})
.show();
}
@Override
public void onSuggestRebuildRoute()
{
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this)
.setMessage(R.string.p2p_reroute_from_current)
.setCancelable(false)
.setNegativeButton(R.string.cancel, null);
TextView titleView = (TextView)View.inflate(this, R.layout.dialog_suggest_reroute_title, null);
titleView.setText(R.string.p2p_only_from_current);
builder.setCustomTitle(titleView);
if (MapObject.isOfType(MapObject.MY_POSITION, RoutingController.get().getEndPoint()))
builder.setPositiveButton(R.string.ok, (dialog, which) -> RoutingController.get().swapPoints());
else
{
if (LocationHelper.INSTANCE.getMyPosition() == null)
builder.setMessage(null).setNegativeButton(null, null);
builder.setPositiveButton(R.string.ok, (dialog, which) -> RoutingController.get().setStartFromMyPosition());
}
builder.show();
}
@Override
public void onMyPositionModeChanged(int newMode)
{
@ -1772,9 +1871,9 @@ public class MwmActivity extends BaseMwmFragmentActivity
public void onSettingsOptionSelected()
{
Intent intent = new Intent(requireActivity(), SettingsActivity.class);
Intent intent = new Intent(this, SettingsActivity.class);
closeFloatingPanels();
requireActivity().startActivity(intent);
startActivity(intent);
}
public void onShareLocationOptionSelected()
@ -1866,4 +1965,51 @@ public class MwmActivity extends BaseMwmFragmentActivity
if (level >= TRIM_MEMORY_RUNNING_LOW)
Framework.nativeMemoryWarning();
}
private void disableControls()
{
initNavigationButtons(MapButtonsController.LayoutMode.regular, true /* force */);
if (mMapButtonsController != null)
mMapButtonsController.showMapButtons(false);
mPlacePageController.close(false);
mMainMenu.show(false);
mNavigationController.show(false);
mRoutingPlanInplaceController.show(false);
closeFloatingToolbarsAndPanels(false);
Logger.d(TAG, "Deactivate");
Framework.nativeRemovePlacePageActivationListener();
if (mOnmapDownloader != null)
mOnmapDownloader.onPause();
RoutingController.get().onSaveState();
RoutingController.get().detach();
}
private void enableControls()
{
RoutingController.get().attach(this);
RoutingController.get().restore();
if (RoutingController.get().isNavigating())
{
showNavigation(true);
initNavigationButtons(MapButtonsController.LayoutMode.navigation, true /* force */);
}
else if (RoutingController.get().isPlanning())
{
mRoutingPlanInplaceController.show(true);
initNavigationButtons(MapButtonsController.LayoutMode.planning, true /* force */);
}
else
{
initNavigationButtons(MapButtonsController.LayoutMode.regular, true /* force */);
if (mMapButtonsController != null)
mMapButtonsController.showMapButtons(true);
}
LocationState.nativeSetListener(this);
Logger.d(TAG, "Activate");
Framework.nativePlacePageActivationListener(this);
onMyPositionModeChanged(LocationState.nativeGetMode());
updateViewsInsets();
if (mOnmapDownloader != null)
mOnmapDownloader.onResume();
}
}

View file

@ -13,6 +13,7 @@ import app.organicmaps.background.NotificationChannelProvider;
import app.organicmaps.background.Notifier;
import app.organicmaps.base.MediaPlayerWrapper;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.downloader.CountryItem;
import app.organicmaps.downloader.MapManager;
import app.organicmaps.editor.Editor;
@ -53,6 +54,9 @@ public class MwmApplication extends Application implements AppBackgroundTracker.
@NonNull
private IsolinesManager mIsolinesManager;
@NonNull
private DisplayManager mDisplayManager;
private volatile boolean mFrameworkInitialized;
private volatile boolean mPlatformInitialized;
@ -74,6 +78,12 @@ public class MwmApplication extends Application implements AppBackgroundTracker.
return mIsolinesManager;
}
@NonNull
public DisplayManager getDisplayManager()
{
return mDisplayManager;
}
public MwmApplication()
{
super();
@ -121,6 +131,7 @@ public class MwmApplication extends Application implements AppBackgroundTracker.
mBackgroundTracker = new AppBackgroundTracker(this);
mSubwayManager = new SubwayManager(this);
mIsolinesManager = new IsolinesManager(this);
mDisplayManager = new DisplayManager();
mPlayer = new MediaPlayerWrapper(this);
}

View file

@ -265,7 +265,7 @@ public class MapObject implements PopularityProvider, PlacePageData
public boolean hasMetadata()
{
return !mMetadata.isEmpty();
return mMetadata != null && !mMetadata.isEmpty();
}
@MapObjectType

View file

@ -0,0 +1,111 @@
package app.organicmaps.car;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarAppService;
import androidx.car.app.Session;
import androidx.car.app.SessionInfo;
import androidx.car.app.validation.HostValidator;
import androidx.core.app.NotificationCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.BuildConfig;
import app.organicmaps.R;
public final class NavigationCarAppService extends CarAppService
{
/**
* Navigation session channel id.
*/
public static final String CHANNEL_ID = "NavigationSessionChannel";
/**
* The identifier for the notification displayed for the foreground service.
*/
private static final int NOTIFICATION_ID = 97654321;
@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();
// Turn the car app service into a foreground service in order to make sure we can use all
// granted "while-in-use" permissions (e.g. location) in the app's process.
// The "while-in-use" location permission is granted as long as there is a foreground
// service running in a process in which location access takes place. Here, we set this
// service, and not NavigationService (which runs only during navigation), as a
// foreground service because we need location access even when not navigating. If
// location access is needed only during navigation, we can set NavigationService as a
// foreground service instead.
// See https://developer.android.com/reference/com/google/android/libraries/car/app
// /CarAppService#accessing-location for more details.
startForeground(NOTIFICATION_ID, getNotification());
final NavigationSession navigationSession = new NavigationSession(sessionInfo);
navigationSession.getLifecycle().addObserver(new DefaultLifecycleObserver()
{
@Override
public void onDestroy(@NonNull LifecycleOwner owner)
{
stopForeground(true);
}
});
return navigationSession;
}
@NonNull
@Override
public Session onCreateSession()
{
return onCreateSession(null);
}
private void createNotificationChannel()
{
NotificationManager notificationManager = getSystemService(NotificationManager.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
CharSequence name = "Car App Service";
NotificationChannel serviceChannel =
new NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(serviceChannel);
}
}
/**
* Returns the {@link NotificationCompat} used as part of the foreground service.
*/
private Notification getNotification()
{
NotificationCompat.Builder builder =
new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Navigation App")
.setContentText("App is running")
.setSmallIcon(R.mipmap.ic_launcher);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
builder.setChannelId(CHANNEL_ID);
builder.setPriority(NotificationManager.IMPORTANCE_HIGH);
}
return builder.build();
}
}

View file

@ -0,0 +1,216 @@
package app.organicmaps.car;
import android.content.Intent;
import android.content.res.Configuration;
import android.location.Location;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.bookmarks.data.MapObject;
import app.organicmaps.car.screens.NavigationScreen;
import app.organicmaps.car.screens.PlaceScreen;
import app.organicmaps.car.screens.hacks.PopToRootHack;
import app.organicmaps.display.DisplayChangedListener;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.display.DisplayType;
import app.organicmaps.MwmApplication;
import app.organicmaps.car.screens.ErrorScreen;
import app.organicmaps.car.screens.MapPlaceholderScreen;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.MapScreen;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.location.LocationListener;
import app.organicmaps.location.LocationState;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.util.log.Logger;
import app.organicmaps.widget.placepage.PlacePageData;
import java.io.IOException;
public final class NavigationSession extends Session implements DefaultLifecycleObserver, LocationListener, LocationState.ModeChangeListener, DisplayChangedListener, Framework.PlacePageActivationListener
{
private static final String TAG = NavigationSession.class.getSimpleName();
@Nullable
private final SessionInfo mSessionInfo;
private final SurfaceRenderer mSurfaceRenderer;
private boolean mInitFailed = false;
public NavigationSession(@Nullable SessionInfo sessionInfo)
{
getLifecycle().addObserver(this);
mSessionInfo = sessionInfo;
mSurfaceRenderer = new SurfaceRenderer(getCarContext(), getLifecycle());
}
@Override
public void onCarConfigurationChanged(@NonNull Configuration newConfiguration)
{
Logger.d(TAG, "New configuration: " + newConfiguration);
}
@NonNull
@Override
public Screen onCreateScreen(@NonNull Intent intent)
{
Logger.d(TAG);
Logger.d(TAG, "Session info: " + mSessionInfo);
Logger.d(TAG, "API Level: " + getCarContext().getCarAppApiLevel());
if (mSessionInfo != null)
Logger.d(TAG, "Supported templates: " + mSessionInfo.getSupportedTemplates(getCarContext().getCarAppApiLevel()));
Logger.d(TAG, "Host info: " + getCarContext().getHostInfo());
Logger.d(TAG, "Car configuration: " + getCarContext().getResources().getConfiguration());
if (mInitFailed)
return new ErrorScreen(getCarContext());
return new MapScreen(getCarContext(), mSurfaceRenderer);
}
@Override
public void onStart(@NonNull LifecycleOwner owner)
{
LocationState.nativeSetListener(this);
LocationHelper.INSTANCE.addListener(this);
LocationHelper.INSTANCE.onTransit(true);
onMyPositionModeChanged(LocationState.nativeGetMode());
Logger.d(TAG, "Activate");
Framework.nativePlacePageActivationListener(this);
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
LocationHelper.INSTANCE.onTransit(false);
LocationHelper.INSTANCE.removeListener(this);
LocationState.nativeRemoveListener();
Logger.d(TAG, "Deactivate");
Framework.nativeRemovePlacePageActivationListener();
}
@Override
public void onCreate(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
final DisplayManager displayManager = DisplayManager.from(getCarContext());
displayManager.addListener(DisplayType.Car, this);
getLifecycle().addObserver(displayManager.getObserverFor(DisplayType.Car));
init();
}
@Override
public void onResume(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
init();
}
@Override
public void onPause(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
}
private void init()
{
mInitFailed = false;
try
{
MwmApplication.from(getCarContext()).init();
} catch (IOException e)
{
mInitFailed = true;
Log.e(TAG, "Failed to initialize the app.");
}
}
@Override
public void onMyPositionModeChanged(int newMode)
{
final Screen screen = getCarContext().getCarService(ScreenManager.class).getTop();
if (screen instanceof BaseMapScreen)
screen.invalidate();
}
@Override
public void onLocationUpdated(@NonNull Location location)
{
}
@Override
public void onCompassUpdated(double north)
{
Map.onCompassUpdated(north, false);
}
@Override
public void onDisplayChanged(@NonNull final DisplayType newDisplayType)
{
Logger.d(TAG);
final ScreenManager screenManager = getCarContext().getCarService(ScreenManager.class);
final boolean isUsedOnDeviceScreenShown = screenManager.getTop() instanceof MapPlaceholderScreen;
if (newDisplayType == DisplayType.Car)
{
LocationState.nativeSetListener(this);
onMyPositionModeChanged(LocationState.nativeGetMode());
biodranik commented 2023-04-08 21:29:25 +00:00 (Migrated from github.com)
Review

ditto, это нужно вообще?

ditto, это нужно вообще?
screenManager.popToRoot();
RoutingController.get().restore();
if (RoutingController.get().isNavigating())
screenManager.push(new NavigationScreen(getCarContext(), mSurfaceRenderer));
else if (RoutingController.get().isPlanning() && RoutingController.get().getEndPoint() != null)
screenManager.push(new PlaceScreen.Builder(getCarContext(), mSurfaceRenderer).setMapObject(RoutingController.get().getEndPoint()).build());
mSurfaceRenderer.enable();
Framework.nativePlacePageActivationListener(this);
}
else if (newDisplayType == DisplayType.Device && !isUsedOnDeviceScreenShown)
{
Framework.nativeRemovePlacePageActivationListener();
mSurfaceRenderer.disable();
final PopToRootHack hack = new PopToRootHack.Builder(getCarContext()).setScreenToPush(new MapPlaceholderScreen(getCarContext())).build();
screenManager.push(hack);
RoutingController.get().onSaveState();
}
}
@Override
public void onPlacePageActivated(@NonNull PlacePageData data)
{
biodranik commented 2023-04-08 21:29:32 +00:00 (Migrated from github.com)
Review

ditto

ditto
final ScreenManager screenManager = getCarContext().getCarService(ScreenManager.class);
if (screenManager.getTop() instanceof PlaceScreen)
{
final PlaceScreen screen = (PlaceScreen) screenManager.getTop();
if (screen.getMapObject().equals(data))
return;
}
final PlaceScreen placeScreen = new PlaceScreen.Builder(getCarContext(), mSurfaceRenderer).setMapObject((MapObject) data).build();
final PopToRootHack hack = new PopToRootHack.Builder(getCarContext()).setScreenToPush(placeScreen).build();
screenManager.push(hack);
}
@Override
public void onPlacePageDeactivated(boolean switchFullScreenMode)
{
final ScreenManager screenManager = getCarContext().getCarService(ScreenManager.class);
if (screenManager.getTop() instanceof PlaceScreen)
screenManager.popToRoot();
}
}

View file

@ -0,0 +1,214 @@
package app.organicmaps.car;
import android.graphics.Rect;
import androidx.annotation.NonNull;
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.R;
import app.organicmaps.util.log.Logger;
import static app.organicmaps.display.DisplayType.Car;
public class SurfaceRenderer implements DefaultLifecycleObserver, SurfaceCallback
{
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();
@NonNull
private Rect mStableArea = new Rect();
private boolean mIsRunning;
public SurfaceRenderer(@NonNull CarContext carContext, @NonNull Lifecycle lifecycle)
{
Logger.d(TAG, "SurfaceRenderer()");
mCarContext = carContext;
mIsRunning = true;
lifecycle.addObserver(this);
}
@Override
public void onSurfaceAvailable(@NonNull SurfaceContainer surfaceContainer)
{
Logger.d(TAG, "Surface available " + surfaceContainer);
mMap.onSurfaceCreated(
mCarContext,
surfaceContainer.getSurface(),
new Rect(0, 0, surfaceContainer.getWidth(), surfaceContainer.getHeight()),
surfaceContainer.getDpi()
);
}
@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);
mStableArea = stableArea;
if (!mStableArea.isEmpty())
Framework.nativeSetVisibleRect(mStableArea.left, mStableArea.top, mStableArea.right, mStableArea.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");
mMap.onSurfaceDestroyed(false, true);
}
@Override
public void onCreate(@NonNull LifecycleOwner owner)
{
Logger.d(TAG, "onCreate");
mCarContext.getCarService(AppManager.class).setSurfaceCallback(this);
// TODO: Properly process deep links from other apps on AA.
boolean launchByDeepLink = false;
mMap.onCreate(launchByDeepLink);
}
@Override
public void onStart(@NonNull LifecycleOwner owner)
{
Logger.d(TAG, "onStart");
mMap.onStart();
mMap.setCallbackUnsupported(this::reportUnsupported);
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
Logger.d(TAG, "onStop");
mMap.onStop();
mMap.setCallbackUnsupported(null);
}
@Override
public void onPause(@NonNull LifecycleOwner owner)
{
Logger.d(TAG, "onPause");
mMap.onPause(mCarContext);
}
@Override
public void onResume(@NonNull LifecycleOwner owner)
{
Logger.d(TAG, "onResume");
mMap.onResume();
}
@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.e(TAG, "Already disabled");
return;
}
mCarContext.getCarService(AppManager.class).setSurfaceCallback(null);
mMap.onSurfaceDestroyed(false, true);
mMap.onStop();
mMap.setCallbackUnsupported(null);
mIsRunning = false;
}
public void enable()
{
if (mIsRunning)
{
Logger.e(TAG, "Already enabled");
return;
}
mCarContext.getCarService(AppManager.class).setSurfaceCallback(this);
mMap.onStart();
mMap.setCallbackUnsupported(this::reportUnsupported);
mIsRunning = true;
}
private void reportUnsupported()
{
String message = mCarContext.getString(R.string.unsupported_phone);
Logger.e(TAG, message);
CarToast.makeText(mCarContext, message, CarToast.LENGTH_LONG).show();
}
}

View file

@ -0,0 +1,106 @@
package app.organicmaps.car;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
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.navigation.model.MapController;
import androidx.core.graphics.drawable.IconCompat;
import app.organicmaps.R;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.settings.SettingsScreen;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.location.LocationState;
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)
{
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
if (mapScreen.getScreenManager().getTop() != mapScreen)
return;
mapScreen.getScreenManager().push(new SettingsScreen(context, surfaceRenderer));
}
).build();
}
@NonNull
private static Action createLocationButton(@NonNull CarContext context)
{
final Action.Builder builder = new Action.Builder();
final int locationMode = LocationState.nativeGetMode();
CarColor tintColor = CarColor.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 = CarColor.BLUE;
break;
case LocationState.FOLLOW_AND_ROTATE:
drawableRes = R.drawable.ic_follow_and_rotate;
tintColor = CarColor.BLUE;
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();
biodranik commented 2023-04-08 21:30:16 +00:00 (Migrated from github.com)
Review

Это в каком например случае?

Это в каком например случае?
if (!LocationHelper.INSTANCE.isActive())
LocationHelper.INSTANCE.start();
});
return builder.build();
}
}

View file

@ -0,0 +1,138 @@
package app.organicmaps.car.screens;
import android.graphics.drawable.Drawable;
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.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.UiHelpers;
import app.organicmaps.car.screens.base.BaseMapScreen;
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.setActionStrip(UiHelpers.createSettingsActionStrip(this, 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()
{
assert mBookmarkCategory != null;
final long bookmarkCategoryId = 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());
if (!bookmarkInfo.getFeatureType().isEmpty())
itemBuilder.addText(bookmarkInfo.getFeatureType());
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 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

@ -0,0 +1,101 @@
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.UiHelpers;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.search.SearchScreen;
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;
public CategoryData(int nameResId, int iconResId)
{
this.nameResId = nameResId;
this.iconResId = iconResId;
}
}
private static final List<CategoryData> CATEGORIES = Arrays.asList(
new CategoryData(R.string.fuel, R.drawable.ic_category_fuel),
new CategoryData(R.string.parking, R.drawable.ic_category_parking),
new CategoryData(R.string.eat, R.drawable.ic_category_eat),
new CategoryData(R.string.food, R.drawable.ic_category_food),
new CategoryData(R.string.hotel, R.drawable.ic_category_hotel),
new CategoryData(R.string.toilet, R.drawable.ic_category_toilet),
new CategoryData(R.string.rv, R.drawable.ic_category_rv)
);
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.setActionStrip(UiHelpers.createSettingsActionStrip(this, 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 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);
itemBuilder.setTitle(title);
itemBuilder.setImage(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), CATEGORIES.get(i).iconResId)).build());
// TODO: replace with this line when implementation of {@link SearchOnMapScreen} will be finished
// itemBuilder.setOnClickListener(() -> getScreenManager().push(new SearchOnMapScreen.Builder(getCarContext(), getSurfaceRenderer()).setCategory(title).build()));
itemBuilder.setOnClickListener(() -> getScreenManager().push(new SearchScreen.Builder(getCarContext(), getSurfaceRenderer()).setCategory(title).build()));
builder.addItem(itemBuilder.build());
}
return builder.build();
}
}

View file

@ -0,0 +1,32 @@
package app.organicmaps.car.screens;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.Screen;
import androidx.car.app.model.LongMessageTemplate;
import androidx.car.app.model.Template;
import app.organicmaps.R;
public class ErrorScreen extends Screen
{
private static final String TAG = ErrorScreen.class.getSimpleName();
public ErrorScreen(@NonNull CarContext carContext)
{
super(carContext);
}
@NonNull
@Override
public Template onGetTemplate()
{
Log.d(TAG, "onGetTemplate");
LongMessageTemplate.Builder builder = new LongMessageTemplate.Builder(getCarContext().getString(R.string.dialog_error_storage_message));
builder.setTitle(getCarContext().getString(R.string.dialog_error_storage_title));
return builder.build();
}
}

View file

@ -0,0 +1,40 @@
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.car.screens.base.BaseScreen;
import app.organicmaps.display.DisplayManager;
import app.organicmaps.display.DisplayType;
import app.organicmaps.R;
import app.organicmaps.util.log.Logger;
public class MapPlaceholderScreen extends BaseScreen
{
private static final String TAG = MapPlaceholderScreen.class.getSimpleName();
public MapPlaceholderScreen(@NonNull CarContext carContext)
{
super(carContext);
}
@NonNull
@Override
public Template onGetTemplate()
{
Logger.d(TAG, "onGetTemplate");
final MessageTemplate.Builder builder = new MessageTemplate.Builder(getCarContext().getString(R.string.aa_used_on_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_car))
.setOnClickListener(() -> DisplayManager.from(getCarContext()).changeDisplay(DisplayType.Car)).build());
return builder.build();
}
}

View file

@ -0,0 +1,112 @@
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.UiHelpers;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.search.SearchScreen;
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()
{
if (getScreenManager().getTop() != this)
return;
getScreenManager().push(new SearchScreen.Builder(getCarContext(), getSurfaceRenderer()).build());
}
private void openCategories()
{
if (getScreenManager().getTop() != this)
return;
getScreenManager().push(new CategoriesScreen(getCarContext(), getSurfaceRenderer()));
}
private void openBookmarks()
{
if (getScreenManager().getTop() != this)
return;
getScreenManager().push(new BookmarksScreen(getCarContext(), getSurfaceRenderer()));
}
}

View file

@ -0,0 +1,51 @@
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.ActionStrip;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.NavigationTemplate;
import app.organicmaps.R;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.UiHelpers;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.routing.RoutingController;
public class NavigationScreen extends BaseMapScreen implements RoutingController.Container
{
private final RoutingController mRoutingController;
public NavigationScreen(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
super(carContext, surfaceRenderer);
mRoutingController = RoutingController.get();
mRoutingController.attach(this);
mRoutingController.restore();
}
@NonNull
@Override
public Template onGetTemplate()
{
final NavigationTemplate.Builder builder = new NavigationTemplate.Builder();
builder.setActionStrip(createActionStrip());
builder.setMapActionStrip(UiHelpers.createMapActionStrip(getCarContext(), getSurfaceRenderer()));
return builder.build();
}
@NonNull
private ActionStrip createActionStrip()
{
final Action.Builder stopActionBuilder = new Action.Builder();
stopActionBuilder.setTitle(getCarContext().getString(R.string.current_location_unknown_stop_button));
stopActionBuilder.setOnClickListener(() -> {
mRoutingController.cancel();
getScreenManager().popToRoot();
});
final ActionStrip.Builder builder = new ActionStrip.Builder();
builder.addAction(UiHelpers.createSettingsAction(this, getSurfaceRenderer()));
builder.addAction(stopActionBuilder.build());
return builder.build();
}
}

View file

@ -0,0 +1,385 @@
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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.CarContext;
import androidx.car.app.model.Action;
import androidx.car.app.model.CarColor;
import androidx.car.app.model.CarIcon;
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.BookmarkManager;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.bookmarks.data.Metadata;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.UiHelpers;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.car.screens.settings.DrivingOptionsScreen;
import app.organicmaps.car.util.OnBackPressedCallback;
import app.organicmaps.editor.OpeningHours;
import app.organicmaps.editor.data.Timetable;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.routing.RoutingInfo;
import app.organicmaps.util.Config;
import app.organicmaps.util.Utils;
import java.util.Calendar;
public class PlaceScreen extends BaseMapScreen implements RoutingController.Container, OnBackPressedCallback.Callback
{
@NonNull
private final MapObject mMapObject;
@NonNull
private final RoutingController mRoutingController;
@NonNull
private final OnBackPressedCallback mOnBackPressedCallback;
private PlaceScreen(@NonNull Builder builder)
{
super(builder.mCarContext, builder.mSurfaceRenderer);
assert builder.mMapObject != null;
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();
}
@NonNull
public MapObject getMapObject()
{
return mMapObject;
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
getCarContext().getOnBackPressedDispatcher().addCallback(this, mOnBackPressedCallback);
if (!mRoutingController.isBuilding() && !mRoutingController.isBuilt())
builder.addEndHeaderAction(createBookmarkAction());
if (mRoutingController.isBuilt())
{
builder.addEndHeaderAction(
new Action.Builder()
.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_settings)).build())
.setOnClickListener(() -> getScreenManager().push(new DrivingOptionsScreen(getCarContext(), getSurfaceRenderer())))
.build()
);
}
return builder.build();
}
@NonNull
private Pane createPane()
{
final Pane.Builder builder = new Pane.Builder();
if (mRoutingController.isBuilding())
{
builder.setLoading(true);
return builder.build();
}
builder.addRow(getPlaceDescription());
if (mRoutingController.isBuilt())
builder.addRow(getPlaceRouteInfo());
final Row placeOpeningHours = getPlaceOpeningHours();
if (placeOpeningHours != null)
builder.addRow(placeOpeningHours);
createPaneActions(builder);
return builder.build();
}
@NonNull
private Row getPlaceDescription()
{
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()
{
final RoutingInfo routingInfo = RoutingController.get().getCachedRoutingInfo();
if (routingInfo == null)
{
throw new IllegalStateException("routingInfo == null");
}
biodranik commented 2023-04-08 21:32:31 +00:00 (Migrated from github.com)
Review
      throw new IllegalStateException("routingInfo == null");
```suggestion throw new IllegalStateException("routingInfo == null"); ```
final Row.Builder builder = new Row.Builder();
builder.setTitle(routingInfo.distToTarget + " " + routingInfo.targetUnits);
final SpannableString time = new SpannableString(" ");
time.setSpan(DurationSpan.create(routingInfo.totalTimeInSeconds), 0, 1, SPAN_INCLUSIVE_INCLUSIVE);
time.setSpan(ForegroundCarColorSpan.create(CarColor.BLUE), 0, 1, SPAN_EXCLUSIVE_EXCLUSIVE);
builder.addText(time);
return builder.build();
}
@Nullable
private Row getPlaceOpeningHours()
{
if (!mMapObject.hasMetadata())
return null;
final String ohStr = mMapObject.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(getCarContext(), R.drawable.ic_operating_hours)).build());
if (isEmptyTT)
builder.setTitle(ohStr);
else if (timetables[0].isFullWeek())
{
if (timetables[0].isFullday)
builder.setTitle(getCarContext().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(getCarContext().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(getCarContext().getString(R.string.day_off_today));
}
return builder.build();
}
@NonNull
private String getPhoneNumber()
{
if (!mMapObject.hasMetadata())
return "";
final String phones = mMapObject.getMetadata(Metadata.MetadataType.FMD_PHONE_NUMBER);
return phones.split(";", 1)[0];
}
private void createPaneActions(@NonNull Pane.Builder builder)
{
final String phoneNumber = getPhoneNumber();
if (!phoneNumber.isEmpty())
{
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());
}
final Action.Builder startRouteBuilder = new Action.Builder();
startRouteBuilder.setBackgroundColor(CarColor.GREEN);
if (mRoutingController.isNavigating())
{
startRouteBuilder.setFlags(Action.FLAG_PRIMARY);
startRouteBuilder.setTitle(getCarContext().getString(R.string.placepage_add_stop));
startRouteBuilder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_route_via)).build());
// TODO: implement with navigation screen
startRouteBuilder.setOnClickListener(() -> {
});
}
else if (mRoutingController.isBuilt())
{
startRouteBuilder.setFlags(Action.FLAG_DEFAULT);
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(() -> {
mRoutingController.start();
getScreenManager().push(new NavigationScreen(getCarContext(), getSurfaceRenderer()));
});
}
else
{
startRouteBuilder.setFlags(Action.FLAG_PRIMARY);
startRouteBuilder.setTitle(getCarContext().getString(R.string.p2p_to_here));
startRouteBuilder.setIcon(new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_route_to)).build());
startRouteBuilder.setOnClickListener(() -> {
mRoutingController.setRouterType(Framework.ROUTER_TYPE_VEHICLE);
mRoutingController.prepare(LocationHelper.INSTANCE.getMyPosition(), mMapObject);
Framework.nativeDeactivatePopup();
});
}
builder.addAction(startRouteBuilder.build());
}
@NonNull
private Action createBookmarkAction()
{
final Action.Builder builder = new Action.Builder();
final CarIcon.Builder iconBuilder = new CarIcon.Builder(IconCompat.createWithResource(getCarContext(), R.drawable.ic_bookmarks));
if (mMapObject.getMapObjectType() == MapObject.BOOKMARK)
iconBuilder.setTint(CarColor.YELLOW);
builder.setIcon(iconBuilder.build());
builder.setOnClickListener(() -> {
if (MapObject.isOfType(MapObject.BOOKMARK, mMapObject))
Framework.nativeDeleteBookmarkFromMapObject();
else
BookmarkManager.INSTANCE.addNewBookmark(mMapObject.getLat(), mMapObject.getLon());
});
return builder.build();
}
@Override
public void onBuiltRoute()
{
invalidate();
}
@Override
public void onPlanningStarted()
{
invalidate();
}
@Override
public void onPlanningCancelled()
{
invalidate();
}
@Override
public void showRoutePlan(boolean show, @Nullable Runnable completionListener)
{
if (completionListener != null)
completionListener.run();
}
@Override
public void onShowDisclaimer()
{
// TODO: show disclaimer screen or not?
Config.acceptRoutingDisclaimer();
mRoutingController.prepare();
}
@Override
public void onCreate(@NonNull LifecycleOwner owner)
{
mRoutingController.attach(this);
if (mRoutingController.isPlanning() && mRoutingController.getLastRouterType() != Framework.ROUTER_TYPE_VEHICLE)
{
mRoutingController.setRouterType(Framework.ROUTER_TYPE_VEHICLE);
mRoutingController.rebuildLastRoute();
}
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner)
{
mRoutingController.detach();
}
@Override
public void onBackPressed()
{
if (!mRoutingController.isNavigating())
mRoutingController.cancel();
Framework.nativeDeactivatePopup();
}
/**
* 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(@NonNull MapObject mapObject)
{
mMapObject = mapObject;
return this;
}
@NonNull
public PlaceScreen build()
{
if (mMapObject == null)
throw new IllegalStateException("MapObject must be set");
return new PlaceScreen(this);
}
}
}

View file

@ -0,0 +1,24 @@
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

@ -0,0 +1,15 @@
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

@ -0,0 +1,74 @@
package app.organicmaps.car.screens.hacks;
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;
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();
private final BaseScreen mScreenToPush;
private PopToRootHack(@NonNull Builder builder)
{
super(builder.mCarContext);
mScreenToPush = builder.mScreenToPush;
}
@NonNull
@Override
public Template onGetTemplate()
{
return mTemplate;
}
@Override
public void onStart(@NonNull LifecycleOwner owner)
{
mHandler.post(() -> {
getScreenManager().popToRoot();
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

@ -0,0 +1,222 @@
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.CarLocation;
import androidx.car.app.model.Header;
import androidx.car.app.model.Item;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Metadata;
import androidx.car.app.model.Place;
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.UiHelpers;
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.SearchResult;
import app.organicmaps.util.log.Logger;
import java.util.List;
// TODO: finish implementation
biodranik commented 2023-04-08 21:33:44 +00:00 (Migrated from github.com)
Review

nit: можно писать точнее, что надо закончить. Вдруг кто-то другой будет доделывать )

nit: можно писать точнее, что надо закончить. Вдруг кто-то другой будет доделывать )
public class SearchOnMapScreen extends BaseMapScreen implements NativeSearchListener, ItemList.OnItemVisibilityChangedListener
{
private final static String TAG = SearchOnMapScreen.class.getSimpleName();
private final int MAX_RESULTS_SIZE;
private final boolean mIsCategory;
private final String mQuery;
@Nullable
private ItemList mResults = null;
private SearchOnMapScreen(@NonNull Builder builder)
{
super(builder.mCarContext, builder.mSurfaceRenderer);
mIsCategory = builder.mIsCategory;
mQuery = builder.mQuery;
final ConstraintManager constraintManager = getCarContext().getCarService(ConstraintManager.class);
MAX_RESULTS_SIZE = constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST);
}
@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));
builder.setOnItemsVisibilityChangedListener(this);
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();
}
@Override
public void onItemVisibilityChanged(int startIndex, int endIndex)
{
if (mResults == null)
return;
final List<Item> items = mResults.getItems().subList(startIndex, endIndex);
final double[] coordinates = new double[items.size() * 2];
for (int i = 0; i < items.size(); ++i)
{
final Metadata metadata = ((Row) items.get(i)).getMetadata();
assert metadata != null;
assert metadata.getPlace() != null;
final CarLocation location = metadata.getPlace().getLocation();
coordinates[2 * i] = location.getLatitude();
coordinates[2 * i + 1] = location.getLongitude();
}
// Framework.nativeShowPoints(coordinates);
biodranik commented 2023-04-08 21:34:03 +00:00 (Migrated from github.com)
Review

?

?
}
@NonNull
private Row createResultItem(@NonNull SearchResult result, int resultIndex)
{
final Row.Builder builder = new Row.Builder();
builder.setBrowsable(true);
if (result.type == SearchResult.TYPE_RESULT)
{
final Metadata metadata = new Metadata.Builder().setPlace(new Place.Builder(CarLocation.create(result.lat, result.lon)).build()).build();
final String title = result.getTitle(getCarContext());
builder.setTitle(title);
builder.addText(result.getFormattedDescription(getCarContext()));
builder.addText(SearchUiHelpers.getOpeningHoursAndDistanceText(getCarContext(), result));
builder.setMetadata(metadata);
builder.setOnClickListener(() -> {
SearchEngine.INSTANCE.cancel();
SearchEngine.INSTANCE.showResult(resultIndex);
getScreenManager().popToRoot();
});
}
else
{
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)
{
Logger.d(TAG);
SearchEngine.INSTANCE.addListener(this);
}
@Override
public void onResume(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
SearchEngine.INSTANCE.cancel();
final MapObject location = LocationHelper.INSTANCE.getMyPosition();
if (location != null)
SearchEngine.INSTANCE.searchInteractive(
getCarContext(), mQuery, mIsCategory,
System.nanoTime(), true /* isMapAndTable */, true /* hasLocation */,
location.getLat(), location.getLon());
else
SearchEngine.INSTANCE.searchInteractive(
biodranik commented 2023-04-08 21:34:48 +00:00 (Migrated from github.com)
Review

Может в одну строку всё-таки тут и выше? Коряво смотрится.

Может в одну строку всё-таки тут и выше? Коряво смотрится.
getCarContext(), mQuery, mIsCategory,
System.nanoTime(), true /* isMapAndTable */);
SearchEngine.INSTANCE.setQuery(mQuery);
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
SearchEngine.INSTANCE.removeListener(this);
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner)
{
Logger.d(TAG);
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
{
private final CarContext mCarContext;
private final SurfaceRenderer mSurfaceRenderer;
private boolean mIsCategory;
private String mQuery;
public Builder(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
mCarContext = carContext;
mSurfaceRenderer = surfaceRenderer;
}
public Builder setCategory(@NonNull String category)
{
mIsCategory = true;
mQuery = category;
return this;
}
public Builder setQuery(@NonNull String query)
{
mIsCategory = false;
mQuery = query;
return this;
}
@NonNull
public SearchOnMapScreen build()
{
if (mQuery == null)
throw new IllegalStateException("Search query is empty");
return new SearchOnMapScreen(this);
}
}
}

View file

@ -0,0 +1,231 @@
package app.organicmaps.car.screens.search;
import androidx.annotation.NonNull;
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.log.Logger;
public class SearchScreen extends BaseMapScreen implements SearchTemplate.SearchCallback, NativeSearchListener
{
private static final String TAG = SearchScreen.class.getSimpleName();
private final int MAX_RESULTS_SIZE;
private ItemList mResults;
private String mQuery = "";
private boolean mIsCategory;
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);
mIsCategory = builder.mIsCategory;
onSearchSubmitted(builder.mQuery);
}
@NonNull
@Override
public Template onGetTemplate()
{
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)
builder.setActionStrip(createActionStrip());
if (mResults == null)
builder.setLoading(true);
else
builder.setItemList(mResults);
Logger.d(TAG, mQuery);
builder.setInitialSearchText(mQuery);
builder.setSearchHint(getCarContext().getString(R.string.search));
return builder.build();
}
@Override
public void onSearchTextChanged(@NonNull String searchText)
{
Logger.d(TAG, searchText);
if (mQuery.equals(searchText))
return;
if (!mQuery.isEmpty())
mIsCategory = false;
mQuery = searchText;
SearchEngine.INSTANCE.cancel();
final MapObject location = LocationHelper.INSTANCE.getMyPosition();
if (location != null)
SearchEngine.INSTANCE.search(getCarContext(), mQuery, mIsCategory, System.nanoTime(), true, location.getLat(), location.getLon());
else
SearchEngine.INSTANCE.search(getCarContext(), mQuery, mIsCategory, System.nanoTime(), false, 0, 0);
mResults = null;
invalidate();
}
@Override
public void onSearchSubmitted(@NonNull String searchText)
{
Logger.d(TAG, searchText);
onSearchTextChanged(searchText);
}
@Override
public void onResume(@NonNull LifecycleOwner owner)
{
SearchEngine.INSTANCE.addListener(this);
}
@Override
public void onPause(@NonNull LifecycleOwner owner)
{
SearchEngine.INSTANCE.removeListener(this);
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner)
{
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)
{
if (result.description != null)
Logger.d(TAG, result.description.featureType);
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());
// TODO: uncomment when implementation of {@link SearchOnMapScreen} will be finished
// builder.setOnClickListener(() ->
// getScreenManager().push(new SearchOnMapScreen.Builder(getCarContext(), getSurfaceRenderer()).setQuery(mQuery).build()));
return new ActionStrip.Builder().addAction(builder.build()).build();
}
/**
* A builder of {@link SearchScreen}.
*/
public static final class Builder
{
private final CarContext mCarContext;
private final SurfaceRenderer mSurfaceRenderer;
private boolean mIsCategory;
private String mQuery = "";
public Builder(@NonNull CarContext carContext, @NonNull SurfaceRenderer surfaceRenderer)
{
mCarContext = carContext;
mSurfaceRenderer = surfaceRenderer;
}
public Builder setCategory(@NonNull String category)
{
mIsCategory = true;
mQuery = category;
return this;
}
public Builder setQuery(@NonNull String query)
{
mIsCategory = false;
mQuery = query;
return this;
}
@NonNull
public SearchScreen build()
{
return new SearchScreen(this);
}
}
}

View file

@ -0,0 +1,81 @@
package app.organicmaps.car.screens.search;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
import androidx.car.app.model.CarColor;
import androidx.car.app.model.ForegroundCarColorSpan;
import app.organicmaps.R;
import app.organicmaps.search.SearchResult;
public final class SearchUiHelpers
{
@NonNull
public static CharSequence getOpeningHoursAndDistanceText(@NonNull CarContext carContext, @NonNull SearchResult searchResult)
{
final SpannableStringBuilder result = new SpannableStringBuilder();
final Spannable openingHours = getOpeningHours(carContext, searchResult);
if (openingHours.length() != 0)
result.append(openingHours);
if (!TextUtils.isEmpty(searchResult.description.distance))
{
if (result.length() != 0)
result.append("");
final SpannableStringBuilder distance = new SpannableStringBuilder(searchResult.description.distance);
distance.setSpan(ForegroundCarColorSpan.create(CarColor.BLUE), 0, distance.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
result.append(distance);
}
return result;
}
@NonNull
public static Spannable getOpeningHours(@NonNull CarContext carContext, @NonNull SearchResult searchResult)
{
final SpannableStringBuilder result = new SpannableStringBuilder();
String text = "";
CarColor color = CarColor.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 = CarColor.YELLOW;
}
else
{
text = carContext.getString(R.string.editor_time_open);
color = CarColor.GREEN;
}
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 = CarColor.RED;
break;
}
result.append(text);
result.setSpan(ForegroundCarColorSpan.create(color), 0, result.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
return result;
}
}

View file

@ -0,0 +1,82 @@
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 app.organicmaps.R;
import app.organicmaps.car.SurfaceRenderer;
import app.organicmaps.car.UiHelpers;
import app.organicmaps.car.screens.base.BaseMapScreen;
import app.organicmaps.routing.RoutingOptions;
import app.organicmaps.settings.RoadType;
public class DrivingOptionsScreen extends BaseMapScreen
{
@NonNull
private final CarIcon mCheckboxIcon;
@NonNull
private final CarIcon mCheckboxSelectedIcon;
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();
}
@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();
}
@NonNull
private Header createHeader()
{
final Header.Builder builder = new Header.Builder();
builder.setStartHeaderAction(Action.BACK);
builder.setTitle(getCarContext().getString(R.string.driving_options_subheader));
return builder.build();
}
@NonNull
private ItemList createDrivingOptionsList()
{
final ItemList.Builder builder = new ItemList.Builder();
builder.addItem(createDrivingOptionCheckbox(RoadType.Toll, R.string.avoid_tolls));
builder.addItem(createDrivingOptionCheckbox(RoadType.Dirty, R.string.avoid_unpaved));
builder.addItem(createDrivingOptionCheckbox(RoadType.Ferry, R.string.avoid_ferry));
builder.addItem(createDrivingOptionCheckbox(RoadType.Motorway, R.string.avoid_motorways));
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();
}
}

View file

@ -0,0 +1,74 @@
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.UiHelpers;
import app.organicmaps.car.screens.base.BaseMapScreen;
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

@ -0,0 +1,111 @@
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.UiHelpers;
import app.organicmaps.car.screens.base.BaseMapScreen;
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(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 createRoutingOptionsItem()
{
final Row.Builder builder = new Row.Builder();
builder.setTitle(getCarContext().getString(R.string.driving_options_title));
builder.setOnClickListener(() -> getScreenManager().push(new DrivingOptionsScreen(getCarContext(), getSurfaceRenderer())));
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, PrefsGetter getter, 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

@ -0,0 +1,30 @@
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

@ -0,0 +1,8 @@
package app.organicmaps.display;
import androidx.annotation.NonNull;
public interface DisplayChangedListener
{
void onDisplayChanged(@NonNull final DisplayType newDisplayType);
}

View file

@ -0,0 +1,359 @@
package app.organicmaps.display;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import app.organicmaps.MwmApplication;
import app.organicmaps.util.log.Logger;
public class DisplayManager implements Handler.Callback
{
private static final String TAG = DisplayManager.class.getSimpleName();
// See description in updateDisplay()
private static final int MESSAGE_CHANGE_DISPLAY_TO_CAR = 0x01;
private static final int MESSAGE_CHANGE_DISPLAY_TO_DEVICE = 0x02;
private static final int MESSAGE_DELAY = 100; // 0.1s
private class DisplayLifecycleObserver implements LifecycleEventObserver
{
@NonNull
private final DisplayHolder displayHolder;
public DisplayLifecycleObserver(@NonNull DisplayHolder displayHolder)
{
this.displayHolder = displayHolder;
}
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event)
{
Logger.d(TAG, "Source: " + source + " Event: " + event);
displayHolder.lastEvent = event;
updateDisplay();
}
}
private static class DisplayHolder
{
boolean isConnected = false;
boolean notify = false;
Lifecycle.Event lastEvent;
DisplayChangedListener listener;
DisplayLifecycleObserver lifecycleObserver;
public void destroy()
{
isConnected = false;
notify = false;
lastEvent = null;
listener = null;
lifecycleObserver = null;
}
}
@NonNull
private DisplayType mCurrentDisplayType = DisplayType.Device;
@Nullable
private DisplayHolder mDevice;
@Nullable
private DisplayHolder mCar;
private final Handler mCallbackHandler = new Handler(Looper.getMainLooper(), this);
@NonNull
public static DisplayManager from(@NonNull Context context)
{
final MwmApplication app = (MwmApplication) context.getApplicationContext();
return app.getDisplayManager();
}
public boolean isCarConnected()
{
return mCar != null;
}
public boolean isDeviceConnected()
{
return mDevice != null;
}
public boolean isCarDisplayUsed()
{
return mCurrentDisplayType == DisplayType.Car;
}
public boolean isDeviceDisplayUsed()
{
return mCurrentDisplayType == DisplayType.Device;
}
public void addListener(@NonNull final DisplayType displayType, @NonNull final DisplayChangedListener listener)
{
Logger.d(TAG, "displayType = " + displayType + ", listener = " + listener);
if (displayType == DisplayType.Device && mDevice == null)
{
mDevice = new DisplayHolder();
mDevice.isConnected = true;
mDevice.notify = true;
mDevice.listener = listener;
}
else if (displayType == DisplayType.Car && mCar == null)
{
mCar = new DisplayHolder();
mCar.isConnected = true;
mCar.notify = true;
mCar.listener = listener;
}
if (isCarConnected() && !isDeviceConnected())
mCurrentDisplayType = displayType;
}
public LifecycleEventObserver getObserverFor(@NonNull final DisplayType displayType)
{
Logger.d(TAG, "displayType = " + displayType);
if (displayType == DisplayType.Device && mDevice != null)
return mDevice.lifecycleObserver = new DisplayLifecycleObserver(mDevice);
else if (displayType == DisplayType.Car && mCar != null)
return mCar.lifecycleObserver = new DisplayLifecycleObserver(mCar);
throw new RuntimeException("Display holder for " + displayType + " is not initialized");
}
private void updateDisplay()
{
Logger.d(TAG);
final boolean isDeviceDestroyed = mDevice != null && mDevice.lastEvent == Lifecycle.Event.ON_DESTROY;
final boolean isCarDestroyed = mCar != null && mCar.lastEvent == Lifecycle.Event.ON_DESTROY;
// Device is destroyed due to the one of the following reasons:
// 1. Configuration changes (rotation, theme and so on)
// 2. Entering navigation mode
// 3. Application is closed
// When Car is not connected we don't need to change display type.
// If device was destroyed due to reasons 1-2,
// mDevice will be created with the actual information about the new activity after Activity.onCreate() call
if (isDeviceDestroyed)
{
destroyDisplayHolder(mDevice);
// Process configuration change case (also works for navigation case)
if (isDeviceDisplayUsed() && isCarConnected())
// We can't check whether the device changing its configuration or not
// Let's post a delayed message to the handler notifying that display should be changed to car if device is still not connected
mCallbackHandler.sendEmptyMessageDelayed(MESSAGE_CHANGE_DISPLAY_TO_CAR, MESSAGE_DELAY);
return;
}
// This happens when Android Auto is disconnected or when the application is stopped (if only car connected)
if (isCarDestroyed)
{
destroyDisplayHolder(mCar);
if (isDeviceConnected())
{
mDevice.notify = true;
notifyDisplayChange(DisplayType.Device);
}
// We don't care about what's going on next when car and device are disconnected -> application will be stopped
return;
}
// Only device connected. The most common case
if (isDeviceConnected() && !isCarConnected())
{
if (mCurrentDisplayType != DisplayType.Device)
{
mDevice.notify = true;
notifyDisplayChange(DisplayType.Device);
return;
}
}
// Only car connected
if (isCarConnected() && !isDeviceConnected())
{
if (mCurrentDisplayType != DisplayType.Car)
{
mCar.notify = true;
notifyDisplayChange(DisplayType.Car);
return;
}
}
if (isDeviceConnected() && isCarConnected())
{
// 1. Map was used on Car
// 2. Car disconnected
// 3. Show map on Device
// Activity is being recreated when Android Auto disconnected. We have to handle it
if (isCarDisplayUsed() && mDevice.lastEvent == Lifecycle.Event.ON_START)
{
// Let's post a delayed message to the handler notifying that display should be changed to device if car is disconnected
mCallbackHandler.sendEmptyMessageDelayed(MESSAGE_CHANGE_DISPLAY_TO_DEVICE, MESSAGE_DELAY);
return;
}
// 1. Map was shown on Device
// 2. MwmActivity.onStop() called
// 3. Car is launched
// Let's show map on Car because it's the case when we hide application on the phone without closing it
if (isDeviceDisplayUsed() && mDevice.lastEvent == Lifecycle.Event.ON_STOP && mCar.lastEvent == Lifecycle.Event.ON_CREATE)
{
mDevice.notify = true;
mCar.notify = true;
notifyDisplayChange(DisplayType.Car);
return;
}
// Car was connected after Device
// Notify car
if (mCar.lastEvent == Lifecycle.Event.ON_CREATE)
{
mCar.notify = true;
notifyDisplayChange(mCurrentDisplayType);
return;
}
// Device was connected after Car
// Notify device
if (mDevice.lastEvent == Lifecycle.Event.ON_CREATE)
{
mDevice.notify = true;
notifyDisplayChange(mCurrentDisplayType);
return;
}
// 1. Map was shown on Device
// 2. MwmActivity.onStop() called
// 3. Map is shown on Car
// Reason: we need to disable controls on device
if (mDevice.lastEvent == Lifecycle.Event.ON_RESUME && isCarDisplayUsed())
{
mDevice.notify = true;
notifyDisplayChange(mCurrentDisplayType);
}
}
}
private void notifyDisplayChange(@NonNull final DisplayType newDisplayType)
{
Logger.d(TAG, "newDisplayType = " + newDisplayType);
mCurrentDisplayType = newDisplayType;
if (mCurrentDisplayType == DisplayType.Device)
mCallbackHandler.post(this::onDisplayTypeChangedToDevice);
else if (mCurrentDisplayType == DisplayType.Car)
mCallbackHandler.post(this::onDisplayTypeChangedToCar);
}
public void changeDisplay(@NonNull final DisplayType newDisplayType)
{
Logger.d(TAG, "newDisplayType = " + newDisplayType);
if (mCurrentDisplayType == newDisplayType)
return;
if (isCarConnected())
{
assert mCar != null;
mCar.notify = true;
}
if (isDeviceConnected())
{
assert mDevice != null;
mDevice.notify = true;
}
notifyDisplayChange(newDisplayType);
}
private void onDisplayTypeChangedToDevice()
{
Logger.d(TAG);
if (mCar != null && mCar.notify)
{
mCar.listener.onDisplayChanged(DisplayType.Device);
mCar.notify = false;
}
if (mDevice != null && mDevice.notify)
{
mDevice.listener.onDisplayChanged(DisplayType.Device);
mDevice.notify = false;
}
}
private void onDisplayTypeChangedToCar()
{
Logger.d(TAG);
if (mDevice != null && mDevice.notify)
{
mDevice.listener.onDisplayChanged(DisplayType.Car);
mDevice.notify = false;
}
if (mCar != null && mCar.notify)
{
mCar.listener.onDisplayChanged(DisplayType.Car);
mCar.notify = false;
}
}
private void destroyDisplayHolder(DisplayHolder holder)
{
if (holder == null)
return;
Logger.d(TAG, holder == mDevice ? "device" : "car");
holder.destroy();
if (holder == mDevice)
mDevice = null;
else if (holder == mCar)
mCar = null;
}
@Override
public boolean handleMessage(@NonNull Message msg)
{
Logger.d(TAG, "" + msg.what);
if (msg.what == MESSAGE_CHANGE_DISPLAY_TO_CAR)
{
// Device still not connected. It's not a configuration change case. Switch display to car
if (!isDeviceConnected() && isCarConnected())
{
assert mCar != null;
mCar.notify = true;
notifyDisplayChange(DisplayType.Car);
}
return true;
}
else if (msg.what == MESSAGE_CHANGE_DISPLAY_TO_DEVICE)
{
// Car disconnected. Let's switch display to Device
if (isDeviceConnected() && !isCarConnected())
{
assert mDevice != null;
mDevice.notify = true;
notifyDisplayChange(DisplayType.Device);
}
return true;
}
return false;
}
}

View file

@ -0,0 +1,7 @@
package app.organicmaps.display;
public enum DisplayType
{
Device,
Car
}

View file

@ -47,7 +47,7 @@ public class RoutingController implements Initializable<Void>
NAVIGATION
}
enum BuildState
public enum BuildState
{
NONE,
BUILDING,
@ -57,30 +57,31 @@ public class RoutingController implements Initializable<Void>
public interface Container
{
FragmentActivity requireActivity();
void showSearch();
void showRoutePlan(boolean show, @Nullable Runnable completionListener);
void showNavigation(boolean show);
void showDownloader(boolean openDownloaded);
void updateMenu();
void onNavigationCancelled();
void onNavigationStarted();
void onPlanningCancelled();
void onPlanningStarted();
void onAddedStop();
void onRemovedStop();
void onResetToPlanningState();
void onBuiltRoute();
void onDrivingOptionsWarning();
boolean isSubwayEnabled();
void onCommonBuildError(int lastResultCode, @NonNull String[] lastMissingMaps);
void onDrivingOptionsBuildError();
default void showSearch() {}
default void showRoutePlan(boolean show, @Nullable Runnable completionListener) {}
default void showNavigation(boolean show) {}
default void showDownloader(boolean openDownloaded) {}
default void updateMenu() {}
default void onNavigationCancelled() {}
default void onNavigationStarted() {}
default void onPlanningCancelled() {}
default void onPlanningStarted() {}
default void onAddedStop() {}
default void onRemovedStop() {}
default void onResetToPlanningState() {}
default void onBuiltRoute() {}
default void onDrivingOptionsWarning() {}
default boolean isSubwayEnabled() { return false; }
default void onCommonBuildError(int lastResultCode, @NonNull String[] lastMissingMaps) {}
default void onDrivingOptionsBuildError() {}
default void onShowDisclaimer() {}
default void onSuggestRebuildRoute() {}
/**
* @param progress progress to be displayed.
* */
void updateBuildProgress(@IntRange(from = 0, to = 100) int progress, @Framework.RouterType int router);
void onStartRouteBuilding();
default void updateBuildProgress(@IntRange(from = 0, to = 100) int progress, @Framework.RouterType int router) {}
default void onStartRouteBuilding() {}
}
private static final int NO_WAITING_POI_PICK = -1;
@ -331,31 +332,6 @@ public class RoutingController implements Initializable<Void>
Framework.nativeBuildRoute();
}
private void showDisclaimer(final MapObject startPoint, final MapObject endPoint,
final boolean fromApi)
{
if (mContainer == null)
return;
FragmentActivity activity = mContainer.requireActivity();
StringBuilder builder = new StringBuilder();
for (int resId : new int[] { R.string.dialog_routing_disclaimer_priority, R.string.dialog_routing_disclaimer_precision,
R.string.dialog_routing_disclaimer_recommendations, R.string.dialog_routing_disclaimer_borders,
R.string.dialog_routing_disclaimer_beware })
builder.append(MwmApplication.from(activity.getApplicationContext()).getString(resId)).append("\n\n");
new MaterialAlertDialogBuilder(activity, R.style.MwmTheme_AlertDialog)
.setTitle(R.string.dialog_routing_disclaimer_title)
.setMessage(builder.toString())
.setCancelable(false)
.setNegativeButton(R.string.decline, null)
.setPositiveButton(R.string.accept, (dlg, which) -> {
Config.acceptRoutingDisclaimer();
prepare(startPoint, endPoint, fromApi);
})
.show();
}
public void restoreRoute()
{
Framework.nativeLoadRoutePoints();
@ -384,6 +360,11 @@ public class RoutingController implements Initializable<Void>
prepare(getStartPoint(), getEndPoint(), false);
}
public void prepare()
{
prepare(getStartPoint(), getEndPoint());
}
public void prepare(@Nullable MapObject startPoint, @Nullable MapObject endPoint)
{
prepare(startPoint, endPoint, false);
@ -395,7 +376,8 @@ public class RoutingController implements Initializable<Void>
if (!Config.isRoutingDisclaimerAccepted())
{
showDisclaimer(startPoint, endPoint, fromApi);
if (mContainer != null)
mContainer.onShowDisclaimer();
return;
}
@ -455,7 +437,8 @@ public class RoutingController implements Initializable<Void>
if (my == null || !MapObject.isOfType(MapObject.MY_POSITION, getStartPoint()))
{
suggestRebuildRoute();
if (mContainer != null)
mContainer.onSuggestRebuildRoute();
return;
}
@ -534,49 +517,6 @@ public class RoutingController implements Initializable<Void>
return mapObject.getRoutePointInfo() != null;
}
private void suggestRebuildRoute()
{
if (mContainer == null)
return;
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mContainer.requireActivity())
.setMessage(R.string.p2p_reroute_from_current)
.setCancelable(false)
.setNegativeButton(R.string.cancel, null);
TextView titleView = (TextView)View.inflate(mContainer.requireActivity(), R.layout.dialog_suggest_reroute_title, null);
titleView.setText(R.string.p2p_only_from_current);
builder.setCustomTitle(titleView);
if (MapObject.isOfType(MapObject.MY_POSITION, getEndPoint()))
{
builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
swapPoints();
}
});
}
else
{
if (LocationHelper.INSTANCE.getMyPosition() == null)
builder.setMessage(null).setNegativeButton(null, null);
builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
setStartFromMyPosition();
}
});
}
builder.show();
}
private void updatePlan()
{
updateProgress();
@ -720,7 +660,7 @@ public class RoutingController implements Initializable<Void>
return mWaitingPoiPickType != NO_WAITING_POI_PICK;
}
BuildState getBuildState()
public BuildState getBuildState()
{
return mBuildState;
}
@ -771,13 +711,13 @@ public class RoutingController implements Initializable<Void>
}
@Nullable
RoutingInfo getCachedRoutingInfo()
public RoutingInfo getCachedRoutingInfo()
{
return mCachedRoutingInfo;
}
@Nullable
TransitRouteInfo getCachedTransitInfo()
public TransitRouteInfo getCachedTransitInfo()
{
return mCachedTransitRouteInfo;
}
@ -810,7 +750,7 @@ public class RoutingController implements Initializable<Void>
build();
}
private boolean setStartFromMyPosition()
public boolean setStartFromMyPosition()
{
Logger.d(TAG, "setStartFromMyPosition");
@ -958,7 +898,7 @@ public class RoutingController implements Initializable<Void>
return new Pair<>(title, subtitle);
}
private void swapPoints()
public void swapPoints()
{
Logger.d(TAG, "swapPoints");

View file

@ -17,5 +17,5 @@ public interface NativeSearchListener
/**
* @param timestamp Timestamp of search request.
*/
void onResultsEnd(long timestamp);
default void onResultsEnd(long timestamp) {}
}

View file

@ -64,34 +64,10 @@ class SearchAdapter extends RecyclerView.Adapter<SearchAdapter.SearchDataViewHol
{
mResult = result;
mOrder = order;
TextView titleView = getTitleView();
String title = mResult.name;
if (TextUtils.isEmpty(title))
{
SearchResult.Description description = mResult.description;
title = description != null
? Utils.getLocalizedFeatureType(titleView.getContext(), description.featureType)
: "";
}
SpannableStringBuilder builder = new SpannableStringBuilder(title);
if (mResult.highlightRanges != null)
{
final int size = mResult.highlightRanges.length / 2;
int index = 0;
for (int i = 0; i < size; i++)
{
final int start = mResult.highlightRanges[index++];
final int len = mResult.highlightRanges[index++];
builder.setSpan(new StyleSpan(Typeface.BOLD), start, start + len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
final TextView titleView = getTitleView();
if (titleView != null)
titleView.setText(builder);
titleView.setText(mResult.getFormattedTitle(titleView.getContext()));
}
@AttrRes int getTintAttr()
@ -145,44 +121,6 @@ class SearchAdapter extends RecyclerView.Adapter<SearchAdapter.SearchDataViewHol
return 0;
}
// FIXME: Better format based on result type
private CharSequence formatDescription(SearchResult result)
{
String localizedType = Utils.getLocalizedFeatureType(mFrame.getContext(),
result.description.featureType);
final SpannableStringBuilder res = new SpannableStringBuilder(localizedType);
final SpannableStringBuilder tail = new SpannableStringBuilder();
if (!TextUtils.isEmpty(result.description.airportIata))
{
tail.append("").append(result.description.airportIata);
}
else if (!TextUtils.isEmpty(result.description.roadShields))
{
tail.append("").append(result.description.roadShields);
}
else
{
if (!TextUtils.isEmpty(result.description.brand))
{
tail.append("").append(Utils.getLocalizedBrand(mFrame.getContext(), result.description.brand));
}
if (!TextUtils.isEmpty(result.description.cuisine))
{
tail.append("").append(result.description.cuisine);
}
}
if (result.isHotel && result.stars != 0)
{
tail.append("").append("★★★★★★★".substring(0, Math.min(7, result.stars)));
}
res.append(tail);
return res;
}
@NonNull
private CharSequence colorizeString(@NonNull String str, @ColorInt int color)
{
@ -216,7 +154,7 @@ class SearchAdapter extends RecyclerView.Adapter<SearchAdapter.SearchDataViewHol
setBackground();
formatOpeningHours(mResult);
UiUtils.setTextAndHideIfEmpty(mDescription, formatDescription(mResult));
UiUtils.setTextAndHideIfEmpty(mDescription, mResult.getFormattedDescription(mFrame.getContext()));
UiUtils.setTextAndHideIfEmpty(mRegion, mResult.description.region);
UiUtils.setTextAndHideIfEmpty(mDistance, mResult.description.distance);
}

View file

@ -130,10 +130,17 @@ public enum SearchEngine implements NativeSearchListener,
@MainThread
public void searchInteractive(@NonNull String query, boolean isCategory, @NonNull String locale,
long timestamp, boolean isMapAndTable)
long timestamp, boolean isMapAndTable, boolean hasLocation, double lat, double lon)
{
nativeRunInteractiveSearch(query.getBytes(StandardCharsets.UTF_8), isCategory,
locale, timestamp, isMapAndTable);
locale, timestamp, isMapAndTable, hasLocation, lat, lon);
}
@MainThread
public void searchInteractive(@NonNull String query, boolean isCategory, @NonNull String locale,
long timestamp, boolean isMapAndTable)
{
searchInteractive(query, isCategory, locale, timestamp, isMapAndTable, false, 0, 0);
}
@MainThread
@ -143,6 +150,13 @@ public enum SearchEngine implements NativeSearchListener,
searchInteractive(query, isCategory, Language.getKeyboardLocale(context), timestamp, isMapAndTable);
}
@MainThread
public void searchInteractive(@NonNull Context context, @NonNull String query, boolean isCategory,
long timestamp, boolean isMapAndTable, boolean hasLocation, double lat, double lon)
{
searchInteractive(query, isCategory, Language.getKeyboardLocale(context), timestamp, isMapAndTable, hasLocation, lat, lon);
}
@MainThread
public static void searchMaps(@NonNull Context context, String query, long timestamp)
{
@ -227,7 +241,8 @@ public enum SearchEngine implements NativeSearchListener,
*/
private static native void nativeRunInteractiveSearch(byte[] bytes, boolean isCategory,
String language, long timestamp,
boolean isMapAndTable);
boolean isMapAndTable, boolean hasLocation,
double lat, double lon);
/**
* @param bytes utf-8 formatted query bytes

View file

@ -1,8 +1,17 @@
package app.organicmaps.search;
import android.content.Context;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import androidx.annotation.NonNull;
import app.organicmaps.bookmarks.data.FeatureId;
import app.organicmaps.util.Utils;
/**
* Class instances are created from native code.
@ -20,7 +29,7 @@ public class SearchResult implements PopularityProvider
public static final int OPEN_NOW_NO = 2;
public static final SearchResult EMPTY = new SearchResult("", "", 0, 0,
new int[] {});
new int[]{});
public static class Description
{
@ -112,4 +121,79 @@ public class SearchResult implements PopularityProvider
{
return mPopularity;
}
@NonNull
public String getTitle(@NonNull Context context)
{
String title = name;
if (TextUtils.isEmpty(title))
{
title = description != null
? Utils.getLocalizedFeatureType(context, description.featureType)
: "";
}
return title;
}
@NonNull
public Spannable getFormattedTitle(@NonNull Context context)
{
final String title = getTitle(context);
final SpannableStringBuilder builder = new SpannableStringBuilder(title);
if (highlightRanges != null)
{
final int size = highlightRanges.length / 2;
int index = 0;
for (int i = 0; i < size; i++)
{
final int start = highlightRanges[index++];
final int len = highlightRanges[index++];
builder.setSpan(new StyleSpan(Typeface.BOLD), start, start + len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return builder;
}
// FIXME: Better format based on result type
@NonNull
public CharSequence getFormattedDescription(@NonNull Context context)
{
final String localizedType = Utils.getLocalizedFeatureType(context, description.featureType);
final SpannableStringBuilder res = new SpannableStringBuilder(localizedType);
final SpannableStringBuilder tail = new SpannableStringBuilder();
if (!TextUtils.isEmpty(description.airportIata))
{
tail.append("").append(description.airportIata);
}
else if (!TextUtils.isEmpty(description.roadShields))
{
tail.append("").append(description.roadShields);
}
else
{
if (!TextUtils.isEmpty(description.brand))
{
tail.append("").append(Utils.getLocalizedBrand(context, description.brand));
}
if (!TextUtils.isEmpty(description.cuisine))
{
tail.append("").append(description.cuisine);
}
}
if (isHotel && stars != 0)
{
tail.append("").append("★★★★★★★".substring(0, Math.min(7, stars)));
}
res.append(tail);
return res;
}
}

View file

@ -11,6 +11,7 @@ import androidx.annotation.NonNull;
import app.organicmaps.Framework;
import app.organicmaps.R;
import app.organicmaps.util.Utils;
import app.organicmaps.display.DisplayManager;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import java.util.List;
@ -20,6 +21,9 @@ class PlacePageUtils
static void moveViewportUp(@NonNull View placePageView, int viewportMinHeight)
{
placePageView.post(() -> {
// Because of the post(), this lambda is called after the car.SurfaceRenderer.onStableAreaChanged() and breaks the visibleRect configuration
if (DisplayManager.from(placePageView.getContext()).isCarDisplayUsed())
return;
final View coordinatorLayout = (ViewGroup) placePageView.getParent();
final int viewPortWidth = coordinatorLayout.getWidth();
int viewPortHeight = coordinatorLayout.getHeight();

View file

@ -25406,3 +25406,33 @@
ru = https://wiki.openstreetmap.org/wiki/RU:О_проекте
tr = https://wiki.openstreetmap.org/wiki/Tr:About
uk = https://wiki.openstreetmap.org/wiki/Uk:Про_проект
[aa_used_on_phone_screen]
comment = Text on car placeholder screen that maps are shown on the phone screen
tags = android
en = You are now using Organic Maps on the phone screen
ru = Сейчас Вы используете Organic Maps на экране телефона
[aa_used_on_car_screen]
comment = Text on phone placeholder screen that maps are shown on the car screen
tags = android
en = You are now using Organic Maps on the car screen
ru = Сейчас Вы используете Organic Maps на экране автомобиля
[aa_connected]
comment = Android Auto connected
tags = android
en = You are connected to Android Auto
ru = Вы подключены к Android Auto
[aa_continue_on_phone]
comment = Button to show maps on the phone screen instead of a car
tags = android
en = Continue on phone
ru = Продолжить в телефоне
[aa_continue_in_car]
comment = Button to show maps on the car screen instead of a phone. Must be no more than 18 symbols!
tags = android
en = Continue in car
ru = Продолжить в авто

View file

@ -186,8 +186,9 @@ void StipplePenIndex::UploadResources(ref_ptr<dp::GraphicsContext> context, ref_
// Assume that all patterns are initialized when creating texture (ReserveResource) and uploaded once.
// Should provide additional logic like in ColorPalette::UploadResources, if we want multiple uploads.
if (m_uploadCalled)
LOG(LERROR, ("Multiple stipple pen texture uploads are not supported"));
// TODO: https://github.com/organicmaps/organicmaps/issues/4539
// if (m_uploadCalled)
// LOG(LERROR, ("Multiple stipple pen texture uploads are not supported"));
m_uploadCalled = true;
uint32_t height = 0;

View file

@ -199,6 +199,11 @@ void DrapeEngine::Move(double factorX, double factorY, bool isAnim)
AddUserEvent(make_unique_dp<MoveEvent>(factorX, factorY, isAnim));
}
void DrapeEngine::Scroll(double distanceX, double distanceY)
{
AddUserEvent(make_unique_dp<ScrollEvent>(distanceX, distanceY));
}
void DrapeEngine::Rotate(double azimuth, bool isAnim)
{
AddUserEvent(make_unique_dp<RotateEvent>(azimuth, isAnim, nullptr /* parallelAnimCreator */));

View file

@ -129,6 +129,7 @@ public:
void AddTouchEvent(TouchEvent const & event);
void Scale(double factor, m2::PointD const & pxPoint, bool isAnim);
void Move(double factorX, double factorY, bool isAnim);
void Scroll(double distanceX, double distanceY);
void Rotate(double azimuth, bool isAnim);
void ScaleAndSetCenter(m2::PointD const & centerPt, double scaleFactor, bool isAnim,

View file

@ -31,6 +31,7 @@ public:
void OnDragStarted() override {}
void OnDragEnded(m2::PointD const & /* distance */) override {}
void OnRotated() override {}
void OnScrolled(m2::PointD const & distance) override {}
void OnScaleStarted() override {}
void CorrectScalePoint(m2::PointD & pt) const override {}

View file

@ -2078,6 +2078,11 @@ void FrontendRenderer::OnRotated()
m_myPositionController->Rotated();
}
void FrontendRenderer::OnScrolled(m2::PointD const & distance)
{
m_myPositionController->Scrolled(distance);
}
void FrontendRenderer::CorrectScalePoint(m2::PointD & pt) const
{
m_myPositionController->CorrectScalePoint(pt);

View file

@ -222,6 +222,7 @@ private:
void OnScaleStarted() override;
void OnRotated() override;
void OnScrolled(m2::PointD const & distance) override;
void CorrectScalePoint(m2::PointD & pt) const override;
void CorrectScalePoint(m2::PointD & pt1, m2::PointD & pt2) const override;
void CorrectGlobalScalePoint(m2::PointD & pt) const override;

View file

@ -274,6 +274,20 @@ void MyPositionController::Rotated()
m_wasRotationInScaling = true;
}
void MyPositionController::Scrolled(m2::PointD const & distance)
{
if (m_mode == location::PendingPosition)
{
ChangeMode(location::NotFollowNoPosition);
return;
}
if (distance.Length() > 0)
StopLocationFollow();
UpdateViewport(kDoNotChangeZoom);
}
void MyPositionController::ResetRoutingNotFollowTimer(bool blockTimer)
{
if (m_isInRouting)

View file

@ -94,6 +94,8 @@ public:
void Rotated();
void Scrolled(m2::PointD const & distance);
void ResetRoutingNotFollowTimer(bool blockTimer = false);
void ResetBlockAutoZoomTimer();

View file

@ -251,6 +251,13 @@ ScreenBase const & UserEventStream::ProcessEvents(bool & modelViewChanged, bool
breakAnim = OnNewVisibleViewport(viewportEvent);
}
break;
case UserEvent::EventType::Scroll:
{
ref_ptr<ScrollEvent> scrollEvent = make_ref(e);
breakAnim = OnScroll(scrollEvent);
TouchCancel(m_touches);
}
break;
default:
ASSERT(false, ());
@ -457,6 +464,23 @@ bool UserEventStream::OnNewVisibleViewport(ref_ptr<SetVisibleViewportEvent> view
return false;
}
bool UserEventStream::OnScroll(ref_ptr<ScrollEvent> scrollEvent)
{
double const distanceX = scrollEvent->GetDistanceX();
double const distanceY = scrollEvent->GetDistanceY();
ScreenBase screen;
GetTargetScreen(screen);
screen.Move(-distanceX, -distanceY);
ShrinkAndScaleInto(screen, df::GetWorldRect());
if (m_listener)
m_listener->OnScrolled({-distanceX, -distanceY});
return SetScreen(screen, false);
}
bool UserEventStream::SetAngle(double azimuth, bool isAnim, TAnimationCreator const & parallelAnimCreator)
{
ScreenBase screen;

View file

@ -41,7 +41,8 @@ public:
FollowAndRotate,
AutoPerspective,
VisibleViewport,
Move
Move,
Scroll
};
virtual ~UserEvent() = default;
@ -377,6 +378,24 @@ private:
m2::RectD m_rect;
};
class ScrollEvent : public UserEvent
{
public:
ScrollEvent(double distanceX, double distanceY)
: m_distanceX(distanceX)
, m_distanceY(distanceY)
{}
EventType GetType() const override { return UserEvent::EventType::Scroll; }
double GetDistanceX() const { return m_distanceX; }
double GetDistanceY() const { return m_distanceY; }
private:
double m_distanceX;
double m_distanceY;
};
class UserEventStream
{
public:
@ -395,6 +414,7 @@ public:
virtual void OnScaleStarted() = 0;
virtual void OnRotated() = 0;
virtual void OnScrolled(m2::PointD const & distance) = 0;
virtual void CorrectScalePoint(m2::PointD & pt) const = 0;
virtual void CorrectGlobalScalePoint(m2::PointD & pt) const = 0;
virtual void CorrectScalePoint(m2::PointD & pt1, m2::PointD & pt2) const = 0;
@ -455,6 +475,7 @@ private:
bool OnSetCenter(ref_ptr<SetCenterEvent> centerEvent);
bool OnRotate(ref_ptr<RotateEvent> rotateEvent);
bool OnNewVisibleViewport(ref_ptr<SetVisibleViewportEvent> viewportEvent);
bool OnScroll(ref_ptr<ScrollEvent> scrollEvent);
bool SetAngle(double azimuth, bool isAnim, TAnimationCreator const & parallelAnimCreator = nullptr);
bool SetRect(m2::RectD rect, int zoom, bool applyRotation, bool isAnim,

View file

@ -1051,6 +1051,12 @@ void Framework::Move(double factorX, double factorY, bool isAnim)
m_drapeEngine->Move(factorX, factorY, isAnim);
}
void Framework::Scroll(double distanceX, double distanceY)
{
if (m_drapeEngine != nullptr)
m_drapeEngine->Scroll(distanceX, distanceY);
}
void Framework::Rotate(double azimuth, bool isAnim)
{
if (m_drapeEngine != nullptr)

View file

@ -533,6 +533,8 @@ public:
/// factorY = 1.5 moves the map one and a half size up.
void Move(double factorX, double factorY, bool isAnim);
void Scroll(double distanceX, double distanceY);
void Rotate(double azimuth, bool isAnim);
void TouchEvent(df::TouchEvent const & touch);