WIP: [android-auto] Route Planning and other screens #4936
64 changed files with 3519 additions and 248 deletions
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
propMinSdkVersion=21
|
||||
propMinSdkVersion=23
|
||||
propTargetSdkVersion=33
|
||||
propCompileSdkVersion=33
|
||||
propBuildToolsVersion=33.0.2
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
23
android/res/drawable/ic_car_connected.xml
Normal file
23
android/res/drawable/ic_car_connected.xml
Normal 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>
|
5
android/res/drawable/ic_check_box.xml
Normal file
5
android/res/drawable/ic_check_box.xml
Normal 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>
|
5
android/res/drawable/ic_check_box_checked.xml
Normal file
5
android/res/drawable/ic_check_box_checked.xml
Normal 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>
|
5
android/res/drawable/ic_phone_android.xml
Normal file
5
android/res/drawable/ic_phone_android.xml
Normal 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>
|
49
android/res/layout/fragment_map_placeholder.xml
Normal file
49
android/res/layout/fragment_map_placeholder.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
4
android/res/xml/automotive_app_desc.xml
Normal file
4
android/res/xml/automotive_app_desc.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<automotiveApp>
|
||||
<uses name="template" />
|
||||
</automotiveApp>
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
25
android/src/app/organicmaps/MapPlaceholderFragment.java
Normal file
25
android/src/app/organicmaps/MapPlaceholderFragment.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -265,7 +265,7 @@ public class MapObject implements PopularityProvider, PlacePageData
|
|||
|
||||
public boolean hasMetadata()
|
||||
{
|
||||
return !mMetadata.isEmpty();
|
||||
return mMetadata != null && !mMetadata.isEmpty();
|
||||
}
|
||||
|
||||
@MapObjectType
|
||||
|
|
111
android/src/app/organicmaps/car/NavigationCarAppService.java
Normal file
111
android/src/app/organicmaps/car/NavigationCarAppService.java
Normal 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();
|
||||
}
|
||||
}
|
216
android/src/app/organicmaps/car/NavigationSession.java
Normal file
216
android/src/app/organicmaps/car/NavigationSession.java
Normal 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());
|
||||
|
||||
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)
|
||||
{
|
||||
![]() 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();
|
||||
}
|
||||
}
|
214
android/src/app/organicmaps/car/SurfaceRenderer.java
Normal file
214
android/src/app/organicmaps/car/SurfaceRenderer.java
Normal 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();
|
||||
}
|
||||
}
|
106
android/src/app/organicmaps/car/UiHelpers.java
Normal file
106
android/src/app/organicmaps/car/UiHelpers.java
Normal 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();
|
||||
![]() Это в каком например случае? Это в каком например случае?
|
||||
if (!LocationHelper.INSTANCE.isActive())
|
||||
LocationHelper.INSTANCE.start();
|
||||
});
|
||||
return builder.build();
|
||||
}
|
||||
}
|
138
android/src/app/organicmaps/car/screens/BookmarksScreen.java
Normal file
138
android/src/app/organicmaps/car/screens/BookmarksScreen.java
Normal 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;
|
||||
}
|
||||
}
|
101
android/src/app/organicmaps/car/screens/CategoriesScreen.java
Normal file
101
android/src/app/organicmaps/car/screens/CategoriesScreen.java
Normal 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();
|
||||
}
|
||||
}
|
32
android/src/app/organicmaps/car/screens/ErrorScreen.java
Normal file
32
android/src/app/organicmaps/car/screens/ErrorScreen.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
112
android/src/app/organicmaps/car/screens/MapScreen.java
Normal file
112
android/src/app/organicmaps/car/screens/MapScreen.java
Normal 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()));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
385
android/src/app/organicmaps/car/screens/PlaceScreen.java
Normal file
385
android/src/app/organicmaps/car/screens/PlaceScreen.java
Normal 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");
|
||||
}
|
||||
![]()
```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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
15
android/src/app/organicmaps/car/screens/base/BaseScreen.java
Normal file
15
android/src/app/organicmaps/car/screens/base/BaseScreen.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
![]() 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);
|
||||
![]() ? ?
|
||||
}
|
||||
|
||||
@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(
|
||||
![]() Может в одну строку всё-таки тут и выше? Коряво смотрится. Может в одну строку всё-таки тут и выше? Коряво смотрится.
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
231
android/src/app/organicmaps/car/screens/search/SearchScreen.java
Normal file
231
android/src/app/organicmaps/car/screens/search/SearchScreen.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package app.organicmaps.display;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface DisplayChangedListener
|
||||
{
|
||||
void onDisplayChanged(@NonNull final DisplayType newDisplayType);
|
||||
}
|
359
android/src/app/organicmaps/display/DisplayManager.java
Normal file
359
android/src/app/organicmaps/display/DisplayManager.java
Normal 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;
|
||||
}
|
||||
}
|
7
android/src/app/organicmaps/display/DisplayType.java
Normal file
7
android/src/app/organicmaps/display/DisplayType.java
Normal file
|
@ -0,0 +1,7 @@
|
|||
package app.organicmaps.display;
|
||||
|
||||
public enum DisplayType
|
||||
{
|
||||
Device,
|
||||
Car
|
||||
}
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -17,5 +17,5 @@ public interface NativeSearchListener
|
|||
/**
|
||||
* @param timestamp Timestamp of search request.
|
||||
*/
|
||||
void onResultsEnd(long timestamp);
|
||||
default void onResultsEnd(long timestamp) {}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 = Продолжить в авто
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 */));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -94,6 +94,8 @@ public:
|
|||
|
||||
void Rotated();
|
||||
|
||||
void Scrolled(m2::PointD const & distance);
|
||||
|
||||
void ResetRoutingNotFollowTimer(bool blockTimer = false);
|
||||
void ResetBlockAutoZoomTimer();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
Reference in a new issue
ditto, это нужно вообще?